Skip to content

Commit 42e494d

Browse files
committed
feat: Add AOM-based selectors to dom utils
1 parent f5e333c commit 42e494d

File tree

10 files changed

+140
-28
lines changed

10 files changed

+140
-28
lines changed

package-lock.json

Lines changed: 13 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@typescript-eslint/parser": "^5.30.6",
2727
"css-selector-tokenizer": "^0.8.0",
2828
"css.escape": "^1.5.1",
29+
"dom-accessibility-api": "^0.7.0",
2930
"glob": "^7.2.0",
3031
"lodash": "^4.17.21",
3132
"react-dom": "^18.3.1"

scripts/generate-package.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const packages = [
2525
files: ['test-utils-doc', 'dist', '*.js', '*.d.ts', 'NOTICE', 'LICENSE', 'README.md'],
2626
},
2727
packageRoot: path.join(root, './lib/core'),
28-
dependencies: ['css-selector-tokenizer', 'css.escape'],
28+
dependencies: ['css-selector-tokenizer', 'css.escape', 'dom-accessibility-api'],
2929
},
3030
];
3131

src/converter/generate-component-finders.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const componentFindersInterfaces = {
3030
* @param {string} [selector] CSS Selector
3131
* @returns {${wrapperName} | null}
3232
*/
33-
find${name}(selector?: string): ${wrapperName} | null;
33+
find${name}(selector?: Selector): ${wrapperName} | null;
3434
3535
/**
3636
* Returns an array of ${name} wrapper that matches the specified CSS selector.
@@ -40,7 +40,7 @@ find${name}(selector?: string): ${wrapperName} | null;
4040
* @param {string} [selector] CSS Selector
4141
* @returns {Array<${wrapperName}>}
4242
*/
43-
findAll${pluralName}(selector?: string): Array<${wrapperName}>;`,
43+
findAll${pluralName}(selector?: Selector): Array<${wrapperName}>;`,
4444

4545
selectors: ({ name, pluralName, wrapperName }: ComponentWrapperMetadata) => `
4646
/**
@@ -88,6 +88,18 @@ export const generateComponentFinders = ({ components, testUtilType }: GenerateF
8888
import { ElementWrapper } from '@cloudscape-design/test-utils-core/${testUtilType}';
8989
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';
9090
91+
${
92+
testUtilType === 'dom'
93+
? `type Selector =
94+
| string
95+
| {
96+
name?: Comparator;
97+
description?: Comparator;
98+
textContent?: Comparator;
99+
};`
100+
: ''
101+
}
102+
91103
export { ElementWrapper };
92104
${components.map(componentWrapperImport).join('')}
93105

src/converter/test/__snapshots__/test-utils-generator-snapshot.test.ts.snap

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ exports[`index files > dom index file matches the snapshot 1`] = `
77
import { ElementWrapper } from '@cloudscape-design/test-utils-core/dom';
88
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';
99
10+
type Selector =
11+
| string
12+
| {
13+
name?: Comparator;
14+
description?: Comparator;
15+
textContent?: Comparator;
16+
};
17+
1018
export { ElementWrapper };
1119
1220
import TestComponentAWrapper from './test-component-a';
@@ -27,7 +35,7 @@ declare module '@cloudscape-design/test-utils-core/dist/dom' {
2735
* @param {string} [selector] CSS Selector
2836
* @returns {TestComponentAWrapper | null}
2937
*/
30-
findTestComponentA(selector?: string): TestComponentAWrapper | null;
38+
findTestComponentA(selector?: Selector): TestComponentAWrapper | null;
3139
3240
/**
3341
* Returns an array of TestComponentA wrapper that matches the specified CSS selector.
@@ -37,7 +45,7 @@ findTestComponentA(selector?: string): TestComponentAWrapper | null;
3745
* @param {string} [selector] CSS Selector
3846
* @returns {Array<TestComponentAWrapper>}
3947
*/
40-
findAllTestComponentAs(selector?: string): Array<TestComponentAWrapper>;
48+
findAllTestComponentAs(selector?: Selector): Array<TestComponentAWrapper>;
4149
/**
4250
* Returns the wrapper of the first TestComponentB that matches the specified CSS selector.
4351
* If no CSS selector is specified, returns the wrapper of the first TestComponentB.
@@ -46,7 +54,7 @@ findAllTestComponentAs(selector?: string): Array<TestComponentAWrapper>;
4654
* @param {string} [selector] CSS Selector
4755
* @returns {TestComponentBWrapper | null}
4856
*/
49-
findTestComponentB(selector?: string): TestComponentBWrapper | null;
57+
findTestComponentB(selector?: Selector): TestComponentBWrapper | null;
5058
5159
/**
5260
* Returns an array of TestComponentB wrapper that matches the specified CSS selector.
@@ -56,7 +64,7 @@ findTestComponentB(selector?: string): TestComponentBWrapper | null;
5664
* @param {string} [selector] CSS Selector
5765
* @returns {Array<TestComponentBWrapper>}
5866
*/
59-
findAllTestComponentBs(selector?: string): Array<TestComponentBWrapper>;
67+
findAllTestComponentBs(selector?: Selector): Array<TestComponentBWrapper>;
6068
}
6169
}
6270
@@ -99,6 +107,8 @@ exports[`index files > selectors index file matches the snapshot 1`] = `
99107
import { ElementWrapper } from '@cloudscape-design/test-utils-core/selectors';
100108
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';
101109
110+
111+
102112
export { ElementWrapper };
103113
104114
import TestComponentAWrapper from './test-component-a';

src/converter/test/generate-component-finders.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ describe(`${generateComponentFinders.name}`, () => {
4040
expect(sourceFileContent).toMatch(`declare module '@cloudscape-design/test-utils-core/dist/${testUtilType}'`);
4141

4242
if (testUtilType === 'dom') {
43-
expect(sourceFileContent).toMatch(`findAlert(selector?: string): AlertWrapper | null`);
44-
expect(sourceFileContent).toMatch(`findAllAlerts(selector?: string): Array<AlertWrapper>`);
45-
expect(sourceFileContent).toMatch(`findStatus(selector?: string): StatusWrapper | null`);
46-
expect(sourceFileContent).toMatch(`findAllStatus(selector?: string): Array<StatusWrapper>`);
43+
expect(sourceFileContent).toMatch(`findAlert(selector?: Selector): AlertWrapper | null`);
44+
expect(sourceFileContent).toMatch(`findAllAlerts(selector?: Selector): Array<AlertWrapper>`);
45+
expect(sourceFileContent).toMatch(`findStatus(selector?: Selector): StatusWrapper | null`);
46+
expect(sourceFileContent).toMatch(`findAllStatus(selector?: Selector): Array<StatusWrapper>`);
4747
} else {
4848
expect(sourceFileContent).toMatch(`findAlert(selector?: string): AlertWrapper`);
4949
expect(sourceFileContent).toMatch(`findAllAlerts(selector?: string): MultiElementWrapper<AlertWrapper>`);

src/core/dom.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
// SPDX-License-Identifier: Apache-2.0
33
/*eslint-env browser*/
44
import { IElementWrapper } from './interfaces';
5-
import { KeyCode, isScopedSelector, substituteScope, appendSelector } from './utils';
5+
import { KeyCode, isScopedSelector, substituteScope, appendSelector, createComparator, Comparator } from './utils';
66
import { act } from './utils-dom';
7+
import { computeAccessibleDescription, computeAccessibleName } from 'dom-accessibility-api';
78

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

35+
type Selector =
36+
| string
37+
| {
38+
name?: Comparator;
39+
description?: Comparator;
40+
textContent?: Comparator;
41+
};
42+
3443
export class AbstractWrapper<ElementType extends Element>
3544
implements IElementWrapper<ElementType, Array<ElementWrapper<ElementType>>>
3645
{
@@ -163,14 +172,38 @@ export class AbstractWrapper<ElementType extends Element>
163172
*/
164173
findAllComponents<Wrapper extends ComponentWrapper, ElementType extends HTMLElement>(
165174
ComponentClass: ComponentWrapperClass<Wrapper, ElementType>,
166-
selector?: string,
175+
selector?: Selector,
167176
): Array<Wrapper> {
168177
const componentRootSelector = `.${ComponentClass.rootSelector}`;
169-
const componentCombinedSelector = selector
170-
? appendSelector(componentRootSelector, selector)
171-
: componentRootSelector;
172-
173-
const elementWrappers = this.findAll<ElementType>(componentCombinedSelector);
178+
const componentCombinedSelector =
179+
typeof selector === 'string' ? appendSelector(componentRootSelector, selector) : componentRootSelector;
180+
181+
let elementWrappers = this.findAll<ElementType>(componentCombinedSelector);
182+
if (selector && typeof selector === 'object') {
183+
// TODO: validate parts of selector
184+
elementWrappers = elementWrappers.filter(wrapper => {
185+
if (selector.name) {
186+
const comparator = createComparator(selector.name);
187+
if (!comparator(computeAccessibleName(wrapper.getElement()))) {
188+
return false;
189+
}
190+
}
191+
if (selector.description) {
192+
const comparator = createComparator(selector.description);
193+
if (!comparator(computeAccessibleDescription(wrapper.getElement()))) {
194+
return false;
195+
}
196+
}
197+
if (selector.textContent) {
198+
const comparator = createComparator(selector.textContent);
199+
if (!comparator(wrapper.getElement().textContent || '')) {
200+
return false;
201+
}
202+
}
203+
return true;
204+
});
205+
// TODO: messaging if selector filters out all options?
206+
}
174207
return elementWrappers.map(wrapper => new ComponentClass(wrapper.getElement()));
175208
}
176209
}

src/core/test/__snapshots__/documenter.test.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ Component's wrapper class",
9898
"isOptional": true,
9999
},
100100
"name": "selector",
101+
"typeName": "Selector",
101102
},
102103
],
103104
"returnType": {
@@ -387,6 +388,7 @@ Component's wrapper class",
387388
"isOptional": true,
388389
},
389390
"name": "selector",
391+
"typeName": "Selector",
390392
},
391393
],
392394
"returnType": {
@@ -706,6 +708,7 @@ Component's wrapper class",
706708
"isOptional": true,
707709
},
708710
"name": "selector",
711+
"typeName": "Selector",
709712
},
710713
],
711714
"returnType": {

src/core/test/dom.test.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('DOM test utils', () => {
99
let node: HTMLElement, wrapper: ElementWrapper;
1010

1111
const CLASS_NAME = 'some-class';
12+
const BUTTON_CLASS_NAME = 'some-class-button';
1213

1314
const LIST_WITH_ITEMS_CLASS_NAME = 'list-with-items';
1415
const LIST_WITHOUT_ITEMS_CLASS_NAME = 'list-without-items';
@@ -26,11 +27,12 @@ describe('DOM test utils', () => {
2627
<a>2</a>
2728
<a>2</a>
2829
</div>
29-
<button class="${CLASS_NAME} active">1</button>
30-
<button class="${CLASS_NAME}">1</button>
30+
<button class="${BUTTON_CLASS_NAME} ${CLASS_NAME} active">1</button>
31+
<button class="${BUTTON_CLASS_NAME} ${CLASS_NAME}">1</button>
3132
<div>
32-
<button>2</button>
33-
<button>2</button>
33+
<button class="${BUTTON_CLASS_NAME}">2</button>
34+
<button class="${BUTTON_CLASS_NAME}">2</button>
35+
<button class="${BUTTON_CLASS_NAME}">12</button>
3436
</div>
3537
<div class="second-type">should not match</div>
3638
<ul class="${LIST_WITH_ITEMS_CLASS_NAME}">
@@ -241,6 +243,9 @@ describe('DOM test utils', () => {
241243
class ListItemWrapper extends ComponentWrapper<HTMLUListElement> {
242244
static rootSelector = LIST_ITEM_CLASS_NAME;
243245
}
246+
class ButtonWrapper extends ComponentWrapper<HTMLButtonElement> {
247+
static rootSelector = BUTTON_CLASS_NAME;
248+
}
244249

245250
it('returns an array of all components matching the wrapper class', () => {
246251
const nodeWithListItemComponents = node.querySelector(`.${LIST_WITH_ITEMS_CLASS_NAME}`)!;
@@ -267,6 +272,32 @@ describe('DOM test utils', () => {
267272

268273
expect(listItems).toHaveLength(0);
269274
});
275+
276+
describe('AOM querying', () => {
277+
describe('name', () => {
278+
it('should filter based on accessible name: exact string match', () => {
279+
const wrapper = createWrapper(node);
280+
const buttons = wrapper.findAllComponents(ButtonWrapper, { name: '1' });
281+
const buttonsContent = buttons.map(wrapper => wrapper.getElement().textContent);
282+
283+
expect(buttonsContent).toEqual(['1', '1']);
284+
});
285+
it('should filter based on accessible name: loose match with regex', () => {
286+
const wrapper = createWrapper(node);
287+
const buttons = wrapper.findAllComponents(ButtonWrapper, { name: /1/ });
288+
const buttonsContent = buttons.map(wrapper => wrapper.getElement().textContent);
289+
290+
expect(buttonsContent).toEqual(['1', '1', '12']);
291+
});
292+
it('should filter based on accessible name: function', () => {
293+
const wrapper = createWrapper(node);
294+
const buttons = wrapper.findAllComponents(ButtonWrapper, { name: name => name === '12' });
295+
const buttonsContent = buttons.map(wrapper => wrapper.getElement().textContent);
296+
297+
expect(buttonsContent).toEqual(['12']);
298+
});
299+
});
300+
});
270301
});
271302
});
272303

src/core/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,18 @@ export enum KeyCode {
8686
alt = 18,
8787
meta = 91,
8888
}
89+
90+
export type Comparator = string | RegExp | ((name: string) => boolean);
91+
92+
export function createComparator(comparator: Comparator) {
93+
switch (typeof comparator) {
94+
case 'string':
95+
return (subject: string) => subject === comparator;
96+
case 'function':
97+
return comparator;
98+
}
99+
if (comparator instanceof RegExp) {
100+
return (subject: string) => comparator.exec(subject);
101+
}
102+
throw new Error(`Invalid condition provided: ${comparator}`);
103+
}

0 commit comments

Comments
 (0)