Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@typescript-eslint/parser": "^5.30.6",
"css-selector-tokenizer": "^0.8.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.7.0",
"glob": "^7.2.0",
"lodash": "^4.17.21",
"react-dom": "^18.3.1"
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const packages = [
files: ['test-utils-doc', 'dist', '*.js', '*.d.ts', 'NOTICE', 'LICENSE', 'README.md'],
},
packageRoot: path.join(root, './lib/core'),
dependencies: ['css-selector-tokenizer', 'css.escape'],
dependencies: ['css-selector-tokenizer', 'css.escape', 'dom-accessibility-api'],
},
];

Expand Down
19 changes: 16 additions & 3 deletions src/converter/generate-component-finders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ElementWrapper.prototype.find${name} = function(selector) {
const rootSelector = \`.$\{${wrapperName}.rootSelector}\`;
// casting to 'any' is needed to avoid this issue with generics
// https://github.com/microsoft/TypeScript/issues/29132
return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, ${wrapperName});
return (this as any).findComponent(rootSelector, ${wrapperName}, selector);
};

ElementWrapper.prototype.findAll${pluralName} = function(selector) {
Expand All @@ -30,7 +30,7 @@ const componentFindersInterfaces = {
* @param {string} [selector] CSS Selector
* @returns {${wrapperName} | null}
*/
find${name}(selector?: string): ${wrapperName} | null;
find${name}(selector?: Selector): ${wrapperName} | null;

/**
* Returns an array of ${name} wrapper that matches the specified CSS selector.
Expand All @@ -40,7 +40,7 @@ find${name}(selector?: string): ${wrapperName} | null;
* @param {string} [selector] CSS Selector
* @returns {Array<${wrapperName}>}
*/
findAll${pluralName}(selector?: string): Array<${wrapperName}>;`,
findAll${pluralName}(selector?: Selector): Array<${wrapperName}>;`,

selectors: ({ name, pluralName, wrapperName }: ComponentWrapperMetadata) => `
/**
Expand Down Expand Up @@ -87,6 +87,19 @@ export const generateComponentFinders = ({ components, testUtilType }: GenerateF
// SPDX-License-Identifier: Apache-2.0
import { ElementWrapper } from '@cloudscape-design/test-utils-core/${testUtilType}';
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';
${
testUtilType === 'dom'
? `
type Comparator = string | RegExp | ((name: string) => boolean);
type Selector =
| string
| {
name?: Comparator;
description?: Comparator;
textContent?: Comparator;
};`
: ''
}

export { ElementWrapper };
${components.map(componentWrapperImport).join('')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ exports[`index files > dom index file matches the snapshot 1`] = `
import { ElementWrapper } from '@cloudscape-design/test-utils-core/dom';
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';

type Comparator = string | RegExp | ((name: string) => boolean);
type Selector =
| string
| {
name?: Comparator;
description?: Comparator;
textContent?: Comparator;
};

export { ElementWrapper };

import TestComponentAWrapper from './test-component-a';
Expand All @@ -27,7 +36,7 @@ declare module '@cloudscape-design/test-utils-core/dist/dom' {
* @param {string} [selector] CSS Selector
* @returns {TestComponentAWrapper | null}
*/
findTestComponentA(selector?: string): TestComponentAWrapper | null;
findTestComponentA(selector?: Selector): TestComponentAWrapper | null;

/**
* Returns an array of TestComponentA wrapper that matches the specified CSS selector.
Expand All @@ -37,7 +46,7 @@ findTestComponentA(selector?: string): TestComponentAWrapper | null;
* @param {string} [selector] CSS Selector
* @returns {Array<TestComponentAWrapper>}
*/
findAllTestComponentAs(selector?: string): Array<TestComponentAWrapper>;
findAllTestComponentAs(selector?: Selector): Array<TestComponentAWrapper>;
/**
* Returns the wrapper of the first TestComponentB that matches the specified CSS selector.
* If no CSS selector is specified, returns the wrapper of the first TestComponentB.
Expand All @@ -46,7 +55,7 @@ findAllTestComponentAs(selector?: string): Array<TestComponentAWrapper>;
* @param {string} [selector] CSS Selector
* @returns {TestComponentBWrapper | null}
*/
findTestComponentB(selector?: string): TestComponentBWrapper | null;
findTestComponentB(selector?: Selector): TestComponentBWrapper | null;

/**
* Returns an array of TestComponentB wrapper that matches the specified CSS selector.
Expand All @@ -56,7 +65,7 @@ findTestComponentB(selector?: string): TestComponentBWrapper | null;
* @param {string} [selector] CSS Selector
* @returns {Array<TestComponentBWrapper>}
*/
findAllTestComponentBs(selector?: string): Array<TestComponentBWrapper>;
findAllTestComponentBs(selector?: Selector): Array<TestComponentBWrapper>;
}
}

Expand All @@ -65,7 +74,7 @@ ElementWrapper.prototype.findTestComponentA = function(selector) {
const rootSelector = \`.\${TestComponentAWrapper.rootSelector}\`;
// casting to 'any' is needed to avoid this issue with generics
// https://github.com/microsoft/TypeScript/issues/29132
return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TestComponentAWrapper);
return (this as any).findComponent(rootSelector, TestComponentAWrapper, selector);
};

ElementWrapper.prototype.findAllTestComponentAs = function(selector) {
Expand All @@ -75,7 +84,7 @@ ElementWrapper.prototype.findTestComponentB = function(selector) {
const rootSelector = \`.\${TestComponentBWrapper.rootSelector}\`;
// casting to 'any' is needed to avoid this issue with generics
// https://github.com/microsoft/TypeScript/issues/29132
return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TestComponentBWrapper);
return (this as any).findComponent(rootSelector, TestComponentBWrapper, selector);
};

ElementWrapper.prototype.findAllTestComponentBs = function(selector) {
Expand All @@ -99,6 +108,7 @@ exports[`index files > selectors index file matches the snapshot 1`] = `
import { ElementWrapper } from '@cloudscape-design/test-utils-core/selectors';
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';


export { ElementWrapper };

import TestComponentAWrapper from './test-component-a';
Expand Down Expand Up @@ -153,7 +163,7 @@ ElementWrapper.prototype.findTestComponentA = function(selector) {
const rootSelector = \`.\${TestComponentAWrapper.rootSelector}\`;
// casting to 'any' is needed to avoid this issue with generics
// https://github.com/microsoft/TypeScript/issues/29132
return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TestComponentAWrapper);
return (this as any).findComponent(rootSelector, TestComponentAWrapper, selector);
};

ElementWrapper.prototype.findAllTestComponentAs = function(selector) {
Expand All @@ -163,7 +173,7 @@ ElementWrapper.prototype.findTestComponentB = function(selector) {
const rootSelector = \`.\${TestComponentBWrapper.rootSelector}\`;
// casting to 'any' is needed to avoid this issue with generics
// https://github.com/microsoft/TypeScript/issues/29132
return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TestComponentBWrapper);
return (this as any).findComponent(rootSelector, TestComponentBWrapper, selector);
};

ElementWrapper.prototype.findAllTestComponentBs = function(selector) {
Expand Down
8 changes: 4 additions & 4 deletions src/converter/test/generate-component-finders.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ describe(`${generateComponentFinders.name}`, () => {
expect(sourceFileContent).toMatch(`declare module '@cloudscape-design/test-utils-core/dist/${testUtilType}'`);

if (testUtilType === 'dom') {
expect(sourceFileContent).toMatch(`findAlert(selector?: string): AlertWrapper | null`);
expect(sourceFileContent).toMatch(`findAllAlerts(selector?: string): Array<AlertWrapper>`);
expect(sourceFileContent).toMatch(`findStatus(selector?: string): StatusWrapper | null`);
expect(sourceFileContent).toMatch(`findAllStatus(selector?: string): Array<StatusWrapper>`);
expect(sourceFileContent).toMatch(`findAlert(selector?: Selector): AlertWrapper | null`);
expect(sourceFileContent).toMatch(`findAllAlerts(selector?: Selector): Array<AlertWrapper>`);
expect(sourceFileContent).toMatch(`findStatus(selector?: Selector): StatusWrapper | null`);
expect(sourceFileContent).toMatch(`findAllStatus(selector?: Selector): Array<StatusWrapper>`);
} else {
expect(sourceFileContent).toMatch(`findAlert(selector?: string): AlertWrapper`);
expect(sourceFileContent).toMatch(`findAllAlerts(selector?: string): MultiElementWrapper<AlertWrapper>`);
Expand Down
67 changes: 52 additions & 15 deletions src/core/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
/*eslint-env browser*/
import { IElementWrapper } from './interfaces';
import { KeyCode, isScopedSelector, substituteScope, appendSelector } from './utils';
import { KeyCode, isScopedSelector, substituteScope, appendSelector, createComparator, Comparator } from './utils';
import { act } from './utils-dom';
import { computeAccessibleDescription, computeAccessibleName } from 'dom-accessibility-api';

// Original KeyboardEventInit lacks some properties https://github.com/Microsoft/TypeScript/issues/15228
declare global {
Expand Down Expand Up @@ -31,6 +32,14 @@ interface ComponentWrapperClass<Wrapper, ElementType> extends WrapperClass<Wrapp
rootSelector: string;
}

type Selector =
| string
| {
name?: Comparator;
description?: Comparator;
textContent?: Comparator;
};

export class AbstractWrapper<ElementType extends Element>
implements IElementWrapper<ElementType, Array<ElementWrapper<ElementType>>>
{
Expand Down Expand Up @@ -97,26 +106,57 @@ export class AbstractWrapper<ElementType extends Element>
}
}

find<NewElementType extends Element = HTMLElement>(selector: string): ElementWrapper<NewElementType> | null {
return this.findAll<NewElementType>(selector)[0] || null;
find<NewElementType extends Element = HTMLElement>(
rootSelector: string,
selector?: Selector,
): ElementWrapper<NewElementType> | null {
return this.findAll<NewElementType>(rootSelector, selector)[0] || null;
}

matches(selector: string): this | null {
return this.element.matches(selector) ? this : null;
}

findAll<NewElementType extends Element = HTMLElement>(selector: string) {
findAll<NewElementType extends Element = HTMLElement>(rootSelector: string, selector?: Selector) {
let elements: Array<NewElementType>;
if (isScopedSelector(selector)) {
const combinedSelector = typeof selector === 'string' ? appendSelector(rootSelector, selector) : rootSelector;
if (isScopedSelector(combinedSelector)) {
const randomValue = Math.floor(Math.random() * 100000);
const attributeName = `data-awsui-test-scope-${randomValue}`;
const domSelector = substituteScope(selector, `[${attributeName}]`);
const domSelector = substituteScope(combinedSelector, `[${attributeName}]`);
this.getElement().setAttribute(attributeName, '');
elements = Array.prototype.slice.call(this.element.querySelectorAll(domSelector));
this.getElement().removeAttribute(attributeName);
} else {
elements = Array.prototype.slice.call(this.element.querySelectorAll(selector));
elements = Array.prototype.slice.call(this.element.querySelectorAll(combinedSelector));
}

if (selector && typeof selector === 'object') {
// TODO: validate parts of selector
elements = elements.filter(element => {
if (selector.name) {
const comparator = createComparator(selector.name);
if (!comparator(computeAccessibleName(element))) {
return false;
}
}
if (selector.description) {
const comparator = createComparator(selector.description);
if (!comparator(computeAccessibleDescription(element))) {
return false;
}
}
if (selector.textContent) {
const comparator = createComparator(selector.textContent);
if (!comparator(element.textContent || '')) {
return false;
}
}
return true;
});
// TODO: messaging if selector filters out all options?
}

return elements.map(element => new ElementWrapper(element));
}

Expand Down Expand Up @@ -145,10 +185,11 @@ export class AbstractWrapper<ElementType extends Element>
* @returns `Wrapper | null`
*/
findComponent<Wrapper extends ComponentWrapper, ElementType extends HTMLElement>(
selector: string,
rootSelector: string,
ComponentClass: WrapperClass<Wrapper, ElementType>,
selector?: Selector,
): Wrapper | null {
const elementWrapper = this.find<ElementType>(selector);
const elementWrapper = this.find<ElementType>(rootSelector, selector);
return elementWrapper ? new ComponentClass(elementWrapper.getElement()) : null;
}

Expand All @@ -163,14 +204,10 @@ export class AbstractWrapper<ElementType extends Element>
*/
findAllComponents<Wrapper extends ComponentWrapper, ElementType extends HTMLElement>(
ComponentClass: ComponentWrapperClass<Wrapper, ElementType>,
selector?: string,
selector?: Selector,
): Array<Wrapper> {
const componentRootSelector = `.${ComponentClass.rootSelector}`;
const componentCombinedSelector = selector
? appendSelector(componentRootSelector, selector)
: componentRootSelector;

const elementWrappers = this.findAll<ElementType>(componentCombinedSelector);
const elementWrappers = this.findAll<ElementType>(componentRootSelector, selector);
return elementWrappers.map(wrapper => new ComponentClass(wrapper.getElement()));
}
}
Expand Down
Loading
Loading