diff --git a/src/queries/alt-text.ts b/src/queries/alt-text.ts index 6be73277..a7c84031 100644 --- a/src/queries/alt-text.ts +++ b/src/queries/alt-text.ts @@ -18,16 +18,15 @@ const queryAllByAltText: AllByBoundAttribute = ( ) } -const getMultipleError: GetErrorFunction = (c, alt) => +const getMultipleError: GetErrorFunction<[unknown]> = (c, alt) => `Found multiple elements with the alt text: ${alt}` -const getMissingError: GetErrorFunction = (c, alt) => +const getMissingError: GetErrorFunction<[unknown]> = (c, alt) => `Unable to find an element with the alt text: ${alt}` -const queryAllByAltTextWithSuggestions = wrapAllByQueryWithSuggestion( - queryAllByAltText, - queryAllByAltText.name, - 'queryAll', -) +const queryAllByAltTextWithSuggestions = wrapAllByQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [altText: Matcher, options?: SelectorMatcherOptions] +>(queryAllByAltText, queryAllByAltText.name, 'queryAll') const [ queryByAltText, getAllByAltText, diff --git a/src/queries/display-value.ts b/src/queries/display-value.ts index bab8a136..7177a84a 100644 --- a/src/queries/display-value.ts +++ b/src/queries/display-value.ts @@ -1,6 +1,11 @@ import {wrapAllByQueryWithSuggestion} from '../query-helpers' import {checkContainerType} from '../helpers' -import {AllByBoundAttribute, GetErrorFunction} from '../../types' +import { + AllByBoundAttribute, + GetErrorFunction, + Matcher, + MatcherOptions, +} from '../../types' import { getNodeText, matches, @@ -38,16 +43,15 @@ const queryAllByDisplayValue: AllByBoundAttribute = ( }) } -const getMultipleError: GetErrorFunction = (c, value) => +const getMultipleError: GetErrorFunction<[unknown]> = (c, value) => `Found multiple elements with the display value: ${value}.` -const getMissingError: GetErrorFunction = (c, value) => +const getMissingError: GetErrorFunction<[unknown]> = (c, value) => `Unable to find an element with the display value: ${value}.` -const queryAllByDisplayValueWithSuggestions = wrapAllByQueryWithSuggestion( - queryAllByDisplayValue, - queryAllByDisplayValue.name, - 'queryAll', -) +const queryAllByDisplayValueWithSuggestions = wrapAllByQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [value: Matcher, options?: MatcherOptions] +>(queryAllByDisplayValue, queryAllByDisplayValue.name, 'queryAll') const [ queryByDisplayValue, diff --git a/src/queries/label-text.ts b/src/queries/label-text.ts index 0b59d253..39e766d5 100644 --- a/src/queries/label-text.ts +++ b/src/queries/label-text.ts @@ -1,7 +1,13 @@ import {getConfig} from '../config' import {checkContainerType} from '../helpers' import {getLabels, getRealLabels, getLabelContent} from '../label-helpers' -import {AllByText, GetErrorFunction} from '../../types' +import { + AllByText, + GetErrorFunction, + Matcher, + MatcherOptions, + SelectorMatcherOptions, +} from '../../types' import { fuzzyMatches, matches, @@ -100,9 +106,6 @@ const queryAllByLabelText: AllByText = ( return labelledElements }, []) .concat( - // TODO: Remove ignore after `queryAllByAttribute` will be moved to TS - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error queryAllByAttribute('aria-label', container, text, { exact, normalizer: matchNormalizer, @@ -171,9 +174,12 @@ function getTagNameOfElementAssociatedWithLabelViaFor( } // the reason mentioned above is the same reason we're not using buildQueries -const getMultipleError: GetErrorFunction = (c, text) => +const getMultipleError: GetErrorFunction<[unknown]> = (c, text) => `Found multiple elements with the text of: ${text}` -const queryByLabelText = wrapSingleQueryWithSuggestion( +const queryByLabelText = wrapSingleQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [labelText: Matcher, options?: SelectorMatcherOptions] +>( makeSingleQuery(queryAllByLabelText, getMultipleError), queryAllByLabelText.name, 'query', @@ -181,32 +187,31 @@ const queryByLabelText = wrapSingleQueryWithSuggestion( const getByLabelText = makeSingleQuery(getAllByLabelText, getMultipleError) const findAllByLabelText = makeFindQuery( - wrapAllByQueryWithSuggestion( - getAllByLabelText, - getAllByLabelText.name, - 'findAll', - ), + wrapAllByQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [labelText: Matcher, options?: SelectorMatcherOptions] + >(getAllByLabelText, getAllByLabelText.name, 'findAll'), ) const findByLabelText = makeFindQuery( - wrapSingleQueryWithSuggestion(getByLabelText, getAllByLabelText.name, 'find'), -) - -const getAllByLabelTextWithSuggestions = wrapAllByQueryWithSuggestion( - getAllByLabelText, - getAllByLabelText.name, - 'getAll', -) -const getByLabelTextWithSuggestions = wrapSingleQueryWithSuggestion( - getByLabelText, - getAllByLabelText.name, - 'get', + wrapSingleQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [labelText: Matcher, options?: SelectorMatcherOptions] + >(getByLabelText, getAllByLabelText.name, 'find'), ) -const queryAllByLabelTextWithSuggestions = wrapAllByQueryWithSuggestion( - queryAllByLabelText, - queryAllByLabelText.name, - 'queryAll', -) +const getAllByLabelTextWithSuggestions = wrapAllByQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [labelText: Matcher, options?: MatcherOptions] +>(getAllByLabelText, getAllByLabelText.name, 'getAll') +const getByLabelTextWithSuggestions = wrapSingleQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [labelText: Matcher, options?: SelectorMatcherOptions] +>(getByLabelText, getAllByLabelText.name, 'get') + +const queryAllByLabelTextWithSuggestions = wrapAllByQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [labelText: Matcher, options?: SelectorMatcherOptions] +>(queryAllByLabelText, queryAllByLabelText.name, 'queryAll') export { queryAllByLabelTextWithSuggestions as queryAllByLabelText, diff --git a/src/queries/placeholder-text.ts b/src/queries/placeholder-text.ts index 9c07c137..42559954 100644 --- a/src/queries/placeholder-text.ts +++ b/src/queries/placeholder-text.ts @@ -5,21 +5,17 @@ import {queryAllByAttribute, buildQueries} from './all-utils' const queryAllByPlaceholderText: AllByBoundAttribute = (...args) => { checkContainerType(args[0]) - // TODO: Remove ignore after `queryAllByAttribute` will be moved to TS - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error return queryAllByAttribute('placeholder', ...args) } -const getMultipleError: GetErrorFunction = (c, text) => +const getMultipleError: GetErrorFunction<[unknown]> = (c, text) => `Found multiple elements with the placeholder text of: ${text}` -const getMissingError: GetErrorFunction = (c, text) => +const getMissingError: GetErrorFunction<[unknown]> = (c, text) => `Unable to find an element with the placeholder text of: ${text}` -const queryAllByPlaceholderTextWithSuggestions = wrapAllByQueryWithSuggestion( - queryAllByPlaceholderText, - queryAllByPlaceholderText.name, - 'queryAll', -) +const queryAllByPlaceholderTextWithSuggestions = wrapAllByQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [placeholderText: Matcher, options?: MatcherOptions] +>(queryAllByPlaceholderText, queryAllByPlaceholderText.name, 'queryAll') const [ queryByPlaceholderText, diff --git a/src/queries/test-id.ts b/src/queries/test-id.ts index 6a9c9c81..563f29ab 100644 --- a/src/queries/test-id.ts +++ b/src/queries/test-id.ts @@ -7,22 +7,18 @@ const getTestIdAttribute = () => getConfig().testIdAttribute const queryAllByTestId: AllByBoundAttribute = (...args) => { checkContainerType(args[0]) - // TODO: Remove ignore after `queryAllByAttribute` will be moved to TS - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error return queryAllByAttribute(getTestIdAttribute(), ...args) } -const getMultipleError: GetErrorFunction = (c, id) => +const getMultipleError: GetErrorFunction<[unknown]> = (c, id) => `Found multiple elements by: [${getTestIdAttribute()}="${id}"]` -const getMissingError: GetErrorFunction = (c, id) => +const getMissingError: GetErrorFunction<[unknown]> = (c, id) => `Unable to find an element by: [${getTestIdAttribute()}="${id}"]` -const queryAllByTestIdWithSuggestions = wrapAllByQueryWithSuggestion( - queryAllByTestId, - queryAllByTestId.name, - 'queryAll', -) +const queryAllByTestIdWithSuggestions = wrapAllByQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [testId: Matcher, options?: MatcherOptions] +>(queryAllByTestId, queryAllByTestId.name, 'queryAll') const [ queryByTestId, diff --git a/src/queries/text.ts b/src/queries/text.ts index 950d9f84..0e6ac3f7 100644 --- a/src/queries/text.ts +++ b/src/queries/text.ts @@ -40,16 +40,15 @@ const queryAllByText: AllByText = ( ) } -const getMultipleError: GetErrorFunction = (c, text) => +const getMultipleError: GetErrorFunction<[unknown]> = (c, text) => `Found multiple elements with the text: ${text}` -const getMissingError: GetErrorFunction = (c, text) => +const getMissingError: GetErrorFunction<[unknown]> = (c, text) => `Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.` -const queryAllByTextWithSuggestions = wrapAllByQueryWithSuggestion( - queryAllByText, - queryAllByText.name, - 'queryAll', -) +const queryAllByTextWithSuggestions = wrapAllByQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [text: Matcher, options?: MatcherOptions] +>(queryAllByText, queryAllByText.name, 'queryAll') const [queryByText, getAllByText, getByText, findAllByText, findByText] = buildQueries(queryAllByText, getMultipleError, getMissingError) diff --git a/src/queries/title.ts b/src/queries/title.ts index 116d89d0..7366855f 100644 --- a/src/queries/title.ts +++ b/src/queries/title.ts @@ -1,6 +1,11 @@ import {wrapAllByQueryWithSuggestion} from '../query-helpers' import {checkContainerType} from '../helpers' -import {AllByBoundAttribute, GetErrorFunction} from '../../types' +import { + AllByBoundAttribute, + GetErrorFunction, + Matcher, + MatcherOptions, +} from '../../types' import { fuzzyMatches, matches, @@ -31,16 +36,15 @@ const queryAllByTitle: AllByBoundAttribute = ( ) } -const getMultipleError: GetErrorFunction = (c, title) => +const getMultipleError: GetErrorFunction<[unknown]> = (c, title) => `Found multiple elements with the title: ${title}.` -const getMissingError: GetErrorFunction = (c, title) => +const getMissingError: GetErrorFunction<[unknown]> = (c, title) => `Unable to find an element with the title: ${title}.` -const queryAllByTitleWithSuggestions = wrapAllByQueryWithSuggestion( - queryAllByTitle, - queryAllByTitle.name, - 'queryAll', -) +const queryAllByTitleWithSuggestions = wrapAllByQueryWithSuggestion< + // @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment + [title: Matcher, options?: MatcherOptions] +>(queryAllByTitle, queryAllByTitle.name, 'queryAll') const [queryByTitle, getAllByTitle, getByTitle, findAllByTitle, findByTitle] = buildQueries(queryAllByTitle, getMultipleError, getMissingError) diff --git a/src/query-helpers.js b/src/query-helpers.ts similarity index 55% rename from src/query-helpers.js rename to src/query-helpers.ts index 52b7888d..155210e1 100644 --- a/src/query-helpers.js +++ b/src/query-helpers.ts @@ -1,13 +1,25 @@ +import type { + GetErrorFunction, + Matcher, + MatcherOptions, + QueryMethod, + Variant, + waitForOptions as WaitForOptions, + WithSuggest, +} from '../types' import {getSuggestedQuery} from './suggestions' import {fuzzyMatches, matches, makeNormalizer} from './matches' import {waitFor} from './wait-for' import {getConfig} from './config' -function getElementError(message, container) { +function getElementError(message: string | null, container: HTMLElement) { return getConfig().getElementError(message, container) } -function getMultipleElementsFoundError(message, container) { +function getMultipleElementsFoundError( + message: string, + container: HTMLElement, +) { return getElementError( `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)).`, container, @@ -15,20 +27,27 @@ function getMultipleElementsFoundError(message, container) { } function queryAllByAttribute( - attribute, - container, - text, - {exact = true, collapseWhitespace, trim, normalizer} = {}, -) { + attribute: string, + container: HTMLElement, + text: Matcher, + {exact = true, collapseWhitespace, trim, normalizer}: MatcherOptions = {}, +): HTMLElement[] { const matcher = exact ? matches : fuzzyMatches const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer}) - return Array.from(container.querySelectorAll(`[${attribute}]`)).filter(node => + return Array.from( + container.querySelectorAll(`[${attribute}]`), + ).filter(node => matcher(node.getAttribute(attribute), node, text, matchNormalizer), ) } -function queryByAttribute(attribute, container, text, ...args) { - const els = queryAllByAttribute(attribute, container, text, ...args) +function queryByAttribute( + attribute: string, + container: HTMLElement, + text: Matcher, + options?: MatcherOptions, +) { + const els = queryAllByAttribute(attribute, container, text, options) if (els.length > 1) { throw getMultipleElementsFoundError( `Found multiple elements by [${attribute}=${text}]`, @@ -41,8 +60,11 @@ function queryByAttribute(attribute, container, text, ...args) { // this accepts a query function and returns a function which throws an error // if more than one elements is returned, otherwise it returns the first // element or null -function makeSingleQuery(allQuery, getMultipleError) { - return (container, ...args) => { +function makeSingleQuery( + allQuery: QueryMethod, + getMultipleError: GetErrorFunction, +) { + return (container: HTMLElement, ...args: Arguments) => { const els = allQuery(container, ...args) if (els.length > 1) { const elementStrings = els @@ -62,7 +84,10 @@ ${elementStrings}`, } } -function getSuggestionError(suggestion, container) { +function getSuggestionError( + suggestion: {toString(): string}, + container: HTMLElement, +) { return getConfig().getElementError( `A better query is available, try this: ${suggestion.toString()} @@ -73,8 +98,11 @@ ${suggestion.toString()} // this accepts a query function and returns a function which throws an error // if an empty list of elements is returned -function makeGetAllQuery(allQuery, getMissingError) { - return (container, ...args) => { +function makeGetAllQuery( + allQuery: (container: HTMLElement, ...args: Arguments) => HTMLElement[], + getMissingError: GetErrorFunction, +) { + return (container: HTMLElement, ...args: Arguments) => { const els = allQuery(container, ...args) if (!els.length) { throw getConfig().getElementError( @@ -89,8 +117,19 @@ function makeGetAllQuery(allQuery, getMissingError) { // this accepts a getter query function and returns a function which calls // waitFor and passing a function which invokes the getter. -function makeFindQuery(getter) { - return (container, text, options, waitForOptions) => { +function makeFindQuery( + getter: ( + container: HTMLElement, + text: Matcher, + options: MatcherOptions, + ) => QueryFor, +) { + return ( + container: HTMLElement, + text: Matcher, + options: MatcherOptions, + waitForOptions: WaitForOptions, + ) => { return waitFor( () => { return getter(container, text, options) @@ -101,13 +140,22 @@ function makeFindQuery(getter) { } const wrapSingleQueryWithSuggestion = - (query, queryAllByName, variant) => - (container, ...args) => { + ( + query: (container: HTMLElement, ...args: Arguments) => HTMLElement | null, + queryAllByName: string, + variant: Variant, + ) => + (container: HTMLElement, ...args: Arguments) => { const element = query(container, ...args) - const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1) + const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1) as [ + WithSuggest, + ] if (element && suggest) { const suggestion = getSuggestedQuery(element, variant) - if (suggestion && !queryAllByName.endsWith(suggestion.queryName)) { + if ( + suggestion && + !queryAllByName.endsWith(suggestion.queryName as string) + ) { throw getSuggestionError(suggestion.toString(), container) } } @@ -116,24 +164,40 @@ const wrapSingleQueryWithSuggestion = } const wrapAllByQueryWithSuggestion = - (query, queryAllByName, variant) => - (container, ...args) => { + < + // We actually want `Arguments extends [args: ...unknown[], options?: Options]` + // But that's not supported by TS so we have to `@ts-expect-error` every callsite + Arguments extends [...unknown[], WithSuggest], + >( + query: (container: HTMLElement, ...args: Arguments) => HTMLElement[], + queryAllByName: string, + variant: Variant, + ) => + (container: HTMLElement, ...args: Arguments) => { const els = query(container, ...args) - const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1) + const [{suggest = getConfig().throwSuggestions} = {}] = args.slice(-1) as [ + WithSuggest, + ] if (els.length && suggest) { // get a unique list of all suggestion messages. We are only going to make a suggestion if // all the suggestions are the same const uniqueSuggestionMessages = [ ...new Set( - els.map(element => getSuggestedQuery(element, variant)?.toString()), + els.map( + element => + getSuggestedQuery(element, variant)?.toString() as string, + ), ), ] if ( // only want to suggest if all the els have the same suggestion. uniqueSuggestionMessages.length === 1 && - !queryAllByName.endsWith(getSuggestedQuery(els[0], variant).queryName) + !queryAllByName.endsWith( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- TODO: Can this be null at runtime? + getSuggestedQuery(els[0], variant)!.queryName as string, + ) ) { throw getSuggestionError(uniqueSuggestionMessages[0], container) } @@ -142,7 +206,21 @@ const wrapAllByQueryWithSuggestion = return els } -function buildQueries(queryAllBy, getMultipleError, getMissingError) { +// TODO: This deviates from the published declarations +// However, the implementation always required a dyadic (after `container`) not variadic `queryAllBy` considering the implementation of `makeFindQuery` +// This is at least statically true and can be verified by accepting `QueryMethod` +function buildQueries( + queryAllBy: QueryMethod< + [matcher: Matcher, options: MatcherOptions], + HTMLElement[] + >, + getMultipleError: GetErrorFunction< + [matcher: Matcher, options: MatcherOptions] + >, + getMissingError: GetErrorFunction< + [matcher: Matcher, options: MatcherOptions] + >, +) { const queryBy = wrapSingleQueryWithSuggestion( makeSingleQuery(queryAllBy, getMultipleError), queryAllBy.name, diff --git a/tsconfig.json b/tsconfig.json index 17bcf90d..7a60d8d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "./node_modules/kcd-scripts/shared-tsconfig.json", "compilerOptions": { - "allowJs": true + "allowJs": true, + "downlevelIteration": true }, "include": ["./src", "./types"] } diff --git a/types/query-helpers.d.ts b/types/query-helpers.d.ts index 28db0a31..2f5b9188 100644 --- a/types/query-helpers.d.ts +++ b/types/query-helpers.d.ts @@ -1,7 +1,12 @@ import {Matcher, MatcherOptions} from './matches' import {waitForOptions} from './wait-for' -export type GetErrorFunction = (c: Element | null, alt: string) => string +export type WithSuggest = {suggest?: boolean} + +export type GetErrorFunction = ( + c: Element | null, + ...args: Arguments +) => string export interface SelectorMatcherOptions extends MatcherOptions { selector?: string @@ -24,7 +29,10 @@ export type AllByAttribute = ( export const queryByAttribute: QueryByAttribute export const queryAllByAttribute: AllByAttribute -export function getElementError(message: string, container: HTMLElement): Error +export function getElementError( + message: string | null, + container: HTMLElement, +): Error /** * query methods have a common call signature. Only the return type differs. @@ -58,8 +66,9 @@ export type BuiltQueryMethods = [ FindAllBy, FindBy, ] + export function buildQueries( - queryByAll: GetAllBy, - getMultipleError: (container: HTMLElement, ...args: Arguments) => string, - getMissingError: (container: HTMLElement, ...args: Arguments) => string, + queryAllBy: GetAllBy, + getMultipleError: GetErrorFunction, + getMissingError: GetErrorFunction, ): BuiltQueryMethods