From accb6cc60628cb6b5bd4d9be2ead41724995e5ae Mon Sep 17 00:00:00 2001 From: Marco Moretti Date: Fri, 11 Dec 2020 15:19:43 +0100 Subject: [PATCH] feat: migrate some files to typescript (#848) Co-authored-by: eps1lon --- package.json | 9 +++-- src/{config.js => config.ts} | 14 +++++-- src/events.js | 15 +++++--- src/{label-helpers.js => label-helpers.ts} | 37 ++++++++++-------- src/{matches.js => matches.ts} | 44 ++++++++++++++++++---- tsconfig.json | 7 ++++ types/config.d.ts | 4 +- types/matches.d.ts | 13 ++++++- types/tsconfig.json | 3 +- 9 files changed, 107 insertions(+), 39 deletions(-) rename src/{config.js => config.ts} (84%) rename src/{label-helpers.js => label-helpers.ts} (61%) rename src/{matches.js => matches.ts} (72%) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index 7559bbde..27747f6e 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "node": ">=10" }, "scripts": { - "build": "kcd-scripts build --ignore \"**/__tests__/**,**/__node_tests__/**,**/__mocks__/**\" && kcd-scripts build --bundle --no-clean", + "build": "kcd-scripts build --no-ts-defs --ignore \"**/__tests__/**,**/__node_tests__/**,**/__mocks__/**\" && kcd-scripts build --no-ts-defs --bundle --no-clean", "lint": "kcd-scripts lint", "setup": "npm install && npm run validate -s", "test": "kcd-scripts test", @@ -53,11 +53,14 @@ "jest-serializer-ansi": "^1.0.3", "jest-watch-select-projects": "^2.0.0", "jsdom": "^16.4.0", - "kcd-scripts": "^7.5.1", + "kcd-scripts": "^7.5.3", "typescript": "^4.1.2" }, "eslintConfig": { - "extends": "./node_modules/kcd-scripts/eslint.js", + "extends": [ + "./node_modules/kcd-scripts/eslint.js", + "plugin:import/typescript" + ], "rules": { "import/prefer-default-export": "off", "import/no-unassigned-import": "off", diff --git a/src/config.js b/src/config.ts similarity index 84% rename from src/config.js rename to src/config.ts index 2472a3fe..d37e46d0 100644 --- a/src/config.js +++ b/src/config.ts @@ -1,9 +1,15 @@ +import {Config, ConfigFn} from '../types/config' import {prettyDOM} from './pretty-dom' +type Callback = () => T +interface InternalConfig extends Config { + _disableExpensiveErrorDiagnostics: boolean +} + // It would be cleaner for this to live inside './queries', but // other parts of the code assume that all exports from // './queries' are query functions. -let config = { +let config: InternalConfig = { testIdAttribute: 'data-testid', asyncUtilTimeout: 1000, // this is to support React's async `act` function. @@ -36,7 +42,9 @@ let config = { } export const DEFAULT_IGNORE_TAGS = 'script, style' -export function runWithExpensiveErrorDiagnosticsDisabled(callback) { +export function runWithExpensiveErrorDiagnosticsDisabled( + callback: Callback, +) { try { config._disableExpensiveErrorDiagnostics = true return callback() @@ -45,7 +53,7 @@ export function runWithExpensiveErrorDiagnosticsDisabled(callback) { } } -export function configure(newConfig) { +export function configure(newConfig: Partial | ConfigFn) { if (typeof newConfig === 'function') { // Pass the existing config out to the provided function // and accept a delta in return diff --git a/src/events.js b/src/events.js index 53ebaabb..57446bf8 100644 --- a/src/events.js +++ b/src/events.js @@ -71,12 +71,15 @@ function createEvent( /* istanbul ignore if */ if (typeof window.DataTransfer === 'function') { Object.defineProperty(event, dataTransferKey, { - value: Object - .getOwnPropertyNames(dataTransferValue) - .reduce((acc, propName) => { - Object.defineProperty(acc, propName, {value: dataTransferValue[propName]}); - return acc; - }, new window.DataTransfer()) + value: Object.getOwnPropertyNames(dataTransferValue).reduce( + (acc, propName) => { + Object.defineProperty(acc, propName, { + value: dataTransferValue[propName], + }) + return acc + }, + new window.DataTransfer(), + ), }) } else { Object.defineProperty(event, dataTransferKey, { diff --git a/src/label-helpers.js b/src/label-helpers.ts similarity index 61% rename from src/label-helpers.js rename to src/label-helpers.ts index d71b4f8e..e8010a1b 100644 --- a/src/label-helpers.js +++ b/src/label-helpers.ts @@ -10,7 +10,9 @@ const labelledNodeNames = [ 'input', ] -function getTextContent(node) { +function getTextContent( + node: Node | Element | HTMLInputElement, +): string | null { if (labelledNodeNames.includes(node.nodeName.toLowerCase())) { return '' } @@ -22,19 +24,22 @@ function getTextContent(node) { .join('') } -function getLabelContent(node) { - let textContent - if (node.tagName.toLowerCase() === 'label') { - textContent = getTextContent(node) +function getLabelContent(element: Element): string | null { + let textContent: string | null + if (element.tagName.toLowerCase() === 'label') { + textContent = getTextContent(element) } else { - textContent = node.value || node.textContent + textContent = (element as HTMLInputElement).value || element.textContent } return textContent } // Based on https://github.com/eps1lon/dom-accessibility-api/pull/352 -function getRealLabels(element) { - if (element.labels !== undefined) return element.labels ?? [] +function getRealLabels(element: Element) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- types are not aware of older browsers that don't implement `labels` + if ((element as HTMLInputElement).labels !== undefined) { + return (element as HTMLInputElement).labels ?? [] + } if (!isLabelable(element)) return [] @@ -42,17 +47,20 @@ function getRealLabels(element) { return Array.from(labels).filter(label => label.control === element) } -function isLabelable(element) { +function isLabelable(element: Element) { return ( - element.tagName.match(/BUTTON|METER|OUTPUT|PROGRESS|SELECT|TEXTAREA/) || + /BUTTON|METER|OUTPUT|PROGRESS|SELECT|TEXTAREA/.test(element.tagName) || (element.tagName === 'INPUT' && element.getAttribute('type') !== 'hidden') ) } -function getLabels(container, element, {selector = '*'} = {}) { - const labelsId = element.getAttribute('aria-labelledby') - ? element.getAttribute('aria-labelledby').split(' ') - : [] +function getLabels( + container: Element, + element: Element, + {selector = '*'} = {}, +) { + const ariaLabelledBy = element.getAttribute('aria-labelledby') + const labelsId = ariaLabelledBy ? ariaLabelledBy.split(' ') : [] return labelsId.length ? labelsId.map(labelId => { const labellingElement = container.querySelector(`[id="${labelId}"]`) @@ -67,7 +75,6 @@ function getLabels(container, element, {selector = '*'} = {}) { const labelledFormControl = Array.from( label.querySelectorAll(formControlSelector), ).filter(formControlElement => formControlElement.matches(selector))[0] - return {content: textToMatch, formControl: labelledFormControl} }) } diff --git a/src/matches.js b/src/matches.ts similarity index 72% rename from src/matches.js rename to src/matches.ts index d9672e42..5aecb416 100644 --- a/src/matches.js +++ b/src/matches.ts @@ -1,19 +1,36 @@ -function assertNotNullOrUndefined(matcher) { - if (matcher == null) { +import { + Matcher, + NormalizerFn, + NormalizerOptions, + DefaultNormalizerOptions, +} from '../types' + +type Nullish = T | null | undefined + +function assertNotNullOrUndefined( + matcher: T, +): asserts matcher is NonNullable { + if (matcher === null || matcher === undefined) { throw new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- implicitly converting `T` to `string` `It looks like ${matcher} was passed instead of a matcher. Did you do something like getByText(${matcher})?`, ) } } -function fuzzyMatches(textToMatch, node, matcher, normalizer) { +function fuzzyMatches( + textToMatch: Nullish, + node: Nullish, + matcher: Nullish, + normalizer: NormalizerFn, +) { if (typeof textToMatch !== 'string') { return false } - assertNotNullOrUndefined(matcher) const normalizedText = normalizer(textToMatch) + if (typeof matcher === 'string') { return normalizedText.toLowerCase().includes(matcher.toLowerCase()) } else if (typeof matcher === 'function') { @@ -23,7 +40,12 @@ function fuzzyMatches(textToMatch, node, matcher, normalizer) { } } -function matches(textToMatch, node, matcher, normalizer) { +function matches( + textToMatch: Nullish, + node: Nullish, + matcher: Nullish, + normalizer: NormalizerFn, +) { if (typeof textToMatch !== 'string') { return false } @@ -40,7 +62,10 @@ function matches(textToMatch, node, matcher, normalizer) { } } -function getDefaultNormalizer({trim = true, collapseWhitespace = true} = {}) { +function getDefaultNormalizer({ + trim = true, + collapseWhitespace = true, +}: DefaultNormalizerOptions = {}): NormalizerFn { return text => { let normalizedText = text normalizedText = trim ? normalizedText.trim() : normalizedText @@ -60,7 +85,12 @@ function getDefaultNormalizer({trim = true, collapseWhitespace = true} = {}) { * @param {Function|undefined} normalizer The user-specified normalizer * @returns {Function} A normalizer */ -function makeNormalizer({trim, collapseWhitespace, normalizer}) { + +function makeNormalizer({ + trim, + collapseWhitespace, + normalizer, +}: NormalizerOptions) { if (normalizer) { // User has specified a custom normalizer if ( diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..17bcf90d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./node_modules/kcd-scripts/shared-tsconfig.json", + "compilerOptions": { + "allowJs": true + }, + "include": ["./src", "./types"] +} diff --git a/types/config.d.ts b/types/config.d.ts index c93237a2..c8e239b6 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -1,13 +1,15 @@ export interface Config { testIdAttribute: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any asyncWrapper(cb: (...args: any[]) => any): Promise + // eslint-disable-next-line @typescript-eslint/no-explicit-any eventWrapper(cb: (...args: any[]) => any): void asyncUtilTimeout: number computedStyleSupportsPseudoElements: boolean defaultHidden: boolean showOriginalStackTrace: boolean throwSuggestions: boolean - getElementError: (message: string, container: HTMLElement) => Error + getElementError: (message: string | null, container: Element) => Error } export interface ConfigFn { diff --git a/types/matches.d.ts b/types/matches.d.ts index d4da667b..03f9a4b8 100644 --- a/types/matches.d.ts +++ b/types/matches.d.ts @@ -1,7 +1,12 @@ import {ARIARole} from 'aria-query' -export type MatcherFunction = (content: string, element: HTMLElement) => boolean -export type Matcher = MatcherFunction | {} +type Nullish = T | null | undefined + +export type MatcherFunction = ( + content: string, + element: Nullish, +) => boolean +export type Matcher = MatcherFunction | RegExp | string // Get autocomplete for ARIARole union types, while still supporting another string // Ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-505826972 @@ -9,6 +14,10 @@ export type ByRoleMatcher = ARIARole | MatcherFunction | {} export type NormalizerFn = (text: string) => string +export interface NormalizerOptions extends DefaultNormalizerOptions { + normalizer?: NormalizerFn +} + export interface MatcherOptions { exact?: boolean /** Use normalizer with getDefaultNormalizer instead */ diff --git a/types/tsconfig.json b/types/tsconfig.json index a7829065..3c43903c 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -1,4 +1,3 @@ { - "extends": "../node_modules/kcd-scripts/shared-tsconfig.json", - "include": ["."] + "extends": "../tsconfig.json" }