Skip to content

Commit d579d77

Browse files
committed
feat: Adds findAll finder function
1 parent 2aa88d5 commit d579d77

File tree

5 files changed

+402
-51
lines changed

5 files changed

+402
-51
lines changed

scripts/pluralize.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
const pluralizationMap = {
4+
CodeView: "CodeViews",
5+
};
6+
7+
function pluralizeComponentName(componentName) {
8+
if (!(componentName in pluralizationMap)) {
9+
throw new Error(`Could not find the plural case for ${componentName}.`);
10+
}
11+
12+
return pluralizationMap[componentName];
13+
}
14+
15+
export { pluralizeComponentName };

scripts/test-utils.js

Lines changed: 137 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,81 +7,167 @@ import path from "node:path";
77

88
import { default as convertToSelectorUtil } from "@cloudscape-design/test-utils-converter";
99

10+
import { pluralizeComponentName } from "./pluralize.js";
1011
import { pascalCase, writeSourceFile } from "./utils.js";
1112

1213
const components = globbySync(["src/test-utils/dom/**/index.ts", "!src/test-utils/dom/index.ts"]).map((fileName) =>
1314
fileName.replace("src/test-utils/dom/", "").replace("/index.ts", ""),
1415
);
1516

16-
generateSelectorUtils();
17-
generateDomIndexFile();
18-
generateSelectorsIndexFile();
19-
compileTypescript();
20-
21-
function generateSelectorUtils() {
22-
components.forEach((componentName) => {
23-
const domFileName = `./src/test-utils/dom/${componentName}/index.ts`;
24-
const domFileContent = fs.readFileSync(domFileName, "utf-8");
25-
const selectorsFileName = `./src/test-utils/selectors/${componentName}/index.ts`;
26-
const selectorsFileContent = convertToSelectorUtil.default(domFileContent);
27-
writeSourceFile(selectorsFileName, selectorsFileContent);
28-
});
17+
function toWrapper(componentClass) {
18+
return `${componentClass}Wrapper`;
2919
}
3020

31-
function generateDomIndexFile() {
32-
const content = generateIndexFileContent({
33-
testUtilType: "dom",
34-
buildFinderInterface: (componentName) =>
35-
`find${pascalCase(componentName)}(selector?: string): ${pascalCase(componentName)}Wrapper | null;`,
36-
});
37-
writeSourceFile("./src/test-utils/dom/index.ts", content);
21+
const testUtilsSrcDir = path.resolve("src/test-utils");
22+
const configs = {
23+
common: {
24+
buildFinder: ({ componentName, componentNamePlural }) => `
25+
ElementWrapper.prototype.find${componentName} = function(selector) {
26+
const rootSelector = \`.$\{${toWrapper(componentName)}.rootSelector}\`;
27+
// casting to 'any' is needed to avoid this issue with generics
28+
// https://github.com/microsoft/TypeScript/issues/29132
29+
return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, ${toWrapper(componentName)});
30+
};
31+
32+
ElementWrapper.prototype.findAll${componentNamePlural} = function(selector) {
33+
return this.findAllComponents(${toWrapper(componentName)}, selector);
34+
};`,
35+
},
36+
dom: {
37+
defaultExport: `export default function wrapper(root: Element = document.body) { if (document && document.body && !document.body.contains(root)) { console.warn('[AwsUi] [test-utils] provided element is not part of the document body, interactions may work incorrectly')}; return new ElementWrapper(root); }`,
38+
buildFinderInterface: ({ componentName, componentNamePlural }) => `
39+
/**
40+
* Returns the wrapper of the first ${componentName} that matches the specified CSS selector.
41+
* If no CSS selector is specified, returns the wrapper of the first ${componentName}.
42+
* If no matching ${componentName} is found, returns \`null\`.
43+
*
44+
* @param {string} [selector] CSS Selector
45+
* @returns {${toWrapper(componentName)} | null}
46+
*/
47+
find${componentName}(selector?: string): ${toWrapper(componentName)} | null;
48+
49+
/**
50+
* Returns an array of ${componentName} wrapper that matches the specified CSS selector.
51+
* If no CSS selector is specified, returns all of the ${componentNamePlural} inside the current wrapper.
52+
* If no matching ${componentName} is found, returns an empty array.
53+
*
54+
* @param {string} [selector] CSS Selector
55+
* @returns {Array<${toWrapper(componentName)}>}
56+
*/
57+
findAll${componentNamePlural}(selector?: string): Array<${toWrapper(componentName)}>;`,
58+
},
59+
selectors: {
60+
defaultExport: `export default function wrapper(root: string = 'body') { return new ElementWrapper(root); }`,
61+
buildFinderInterface: ({ componentName, componentNamePlural }) => `
62+
/**
63+
* Returns a wrapper that matches the ${componentNamePlural} with the specified CSS selector.
64+
* If no CSS selector is specified, returns a wrapper that matches ${componentNamePlural}.
65+
*
66+
* @param {string} [selector] CSS Selector
67+
* @returns {${toWrapper(componentName)}}
68+
*/
69+
find${componentName}(selector?: string): ${toWrapper(componentName)};
70+
71+
/**
72+
* Returns a multi-element wrapper that matches ${componentNamePlural} with the specified CSS selector.
73+
* If no CSS selector is specified, returns a multi-element wrapper that matches ${componentNamePlural}.
74+
*
75+
* @param {string} [selector] CSS Selector
76+
* @returns {MultiElementWrapper<${toWrapper(componentName)}>}
77+
*/
78+
findAll${componentNamePlural}(selector?: string): MultiElementWrapper<${toWrapper(componentName)}>;`,
79+
},
80+
};
81+
82+
function generateFindersInterfaces({ testUtilMetaData, testUtilType, configs }) {
83+
const { buildFinderInterface } = configs[testUtilType];
84+
const findersInterfaces = testUtilMetaData.map(buildFinderInterface);
85+
86+
// we need to redeclare the interface in its original definition, extending a re-export will not work
87+
// https://github.com/microsoft/TypeScript/issues/12607
88+
const interfaces = `declare module '@cloudscape-design/test-utils-core/dist/${testUtilType}' {
89+
interface ElementWrapper {
90+
${findersInterfaces.join("\n")}
91+
}
92+
}`;
93+
94+
return interfaces;
3895
}
3996

40-
function generateSelectorsIndexFile() {
41-
const content = generateIndexFileContent({
42-
testUtilType: "selectors",
43-
buildFinderInterface: (componentName) =>
44-
`find${pascalCase(componentName)}(selector?: string): ${pascalCase(componentName)}Wrapper;`,
45-
});
46-
writeSourceFile("./src/test-utils/selectors/index.ts", content);
97+
function generateFindersImplementations({ testUtilMetaData, configs }) {
98+
const { buildFinder } = configs.common;
99+
const findersImplementations = testUtilMetaData.map(buildFinder);
100+
return findersImplementations.join("\n");
47101
}
48102

49-
function generateIndexFileContent({ testUtilType, buildFinderInterface }) {
103+
function generateIndexFileContent(testUtilType, testUtilMetaData) {
104+
const config = configs[testUtilType];
105+
if (config === undefined) {
106+
throw new Error("Unknown test util type");
107+
}
108+
50109
return [
51110
// language=TypeScript
52111
`import { ElementWrapper } from '@cloudscape-design/test-utils-core/${testUtilType}';`,
53112
`import { appendSelector } from '@cloudscape-design/test-utils-core/utils';`,
54113
`export { ElementWrapper };`,
55-
...components.map((componentName) => {
56-
const componentImport = `./${componentName}/index`;
114+
...testUtilMetaData.map((metaData) => {
115+
const { componentName, relPathtestUtilFile } = metaData;
116+
57117
return `
58-
import ${pascalCase(componentName)}Wrapper from '${componentImport}';
59-
export { ${pascalCase(componentName)}Wrapper };
118+
import ${toWrapper(componentName)} from '${relPathtestUtilFile}';
119+
export { ${componentName}Wrapper };
60120
`;
61121
}),
62-
// we need to redeclare the interface in its original definition, extending a re-export will not work
63-
// https://github.com/microsoft/TypeScript/issues/12607
64-
`declare module '@cloudscape-design/test-utils-core/dist/${testUtilType}' {
65-
interface ElementWrapper {
66-
${components.map((componentName) => buildFinderInterface(componentName)).join("\n")}
67-
}
68-
}`,
69-
...components.map((componentName) => {
70-
// language=TypeScript
71-
return `ElementWrapper.prototype.find${pascalCase(componentName)} = function(selector) {
72-
const rootSelector = \`.$\{${pascalCase(componentName)}Wrapper.rootSelector}\`;
73-
// casting to 'any' is needed to avoid this issue with generics
74-
// https://github.com/microsoft/TypeScript/issues/29132
75-
return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, ${pascalCase(
76-
componentName,
77-
)}Wrapper);
78-
};`;
79-
}),
80-
`export { createWrapper as default } from '@cloudscape-design/test-utils-core/${testUtilType}';`,
122+
generateFindersInterfaces({ testUtilMetaData, testUtilType, configs }),
123+
generateFindersImplementations({ testUtilMetaData, configs }),
124+
config.defaultExport,
81125
].join("\n");
82126
}
83127

128+
function generateTestUtilMetaData() {
129+
const metaData = components.reduce((allMetaData, componentFolderName) => {
130+
const absPathComponentFolder = path.resolve(testUtilsSrcDir, componentFolderName);
131+
const relPathtestUtilFile = `./${path.relative(testUtilsSrcDir, absPathComponentFolder)}`;
132+
133+
const componentNameKebab = componentFolderName;
134+
const componentName = pascalCase(componentNameKebab);
135+
const componentNamePlural = pluralizeComponentName(componentName);
136+
137+
const componentMetaData = {
138+
componentName,
139+
componentNamePlural,
140+
relPathtestUtilFile,
141+
};
142+
143+
return allMetaData.concat(componentMetaData);
144+
}, []);
145+
146+
return metaData;
147+
}
148+
84149
function compileTypescript() {
85150
const config = path.resolve("src/test-utils/tsconfig.json");
86151
execaSync("tsc", ["-p", config, "--sourceMap", "--inlineSources"], { stdio: "inherit" });
87152
}
153+
154+
function generateSelectorUtils() {
155+
components.forEach((componentName) => {
156+
const domFileName = `./src/test-utils/dom/${componentName}/index.ts`;
157+
const domFileContent = fs.readFileSync(domFileName, "utf-8");
158+
const selectorsFileName = `./src/test-utils/selectors/${componentName}/index.ts`;
159+
const selectorsFileContent = convertToSelectorUtil.default(domFileContent);
160+
writeSourceFile(selectorsFileName, selectorsFileContent);
161+
});
162+
}
163+
164+
function generateIndexFile(testUtilType) {
165+
const testUtilMetaData = generateTestUtilMetaData(testUtilType);
166+
const content = generateIndexFileContent(testUtilType, testUtilMetaData);
167+
writeSourceFile(`./src/test-utils/${testUtilType}/index.ts`, content);
168+
}
169+
170+
generateSelectorUtils();
171+
generateIndexFile("dom");
172+
generateIndexFile("selectors");
173+
compileTypescript();
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`Generate test utils ElementWrapper > 'dom' ElementWrapper matches the snapshot 1`] = `
4+
"// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
// SPDX-License-Identifier: Apache-2.0
6+
import { ElementWrapper } from "@cloudscape-design/test-utils-core/dom";
7+
import { appendSelector } from "@cloudscape-design/test-utils-core/utils";
8+
export { ElementWrapper };
9+
10+
import CodeViewWrapper from "./code-view";
11+
export { CodeViewWrapper };
12+
13+
declare module "@cloudscape-design/test-utils-core/dist/dom" {
14+
interface ElementWrapper {
15+
/**
16+
* Returns the wrapper of the first CodeView that matches the specified CSS selector.
17+
* If no CSS selector is specified, returns the wrapper of the first CodeView.
18+
* If no matching CodeView is found, returns \`null\`.
19+
*
20+
* @param {string} [selector] CSS Selector
21+
* @returns {CodeViewWrapper | null}
22+
*/
23+
findCodeView(selector?: string): CodeViewWrapper | null;
24+
25+
/**
26+
* Returns an array of CodeView wrapper that matches the specified CSS selector.
27+
* If no CSS selector is specified, returns all of the CodeViews inside the current wrapper.
28+
* If no matching CodeView is found, returns an empty array.
29+
*
30+
* @param {string} [selector] CSS Selector
31+
* @returns {Array<CodeViewWrapper>}
32+
*/
33+
findAllCodeViews(selector?: string): Array<CodeViewWrapper>;
34+
}
35+
}
36+
37+
ElementWrapper.prototype.findCodeView = function (selector) {
38+
const rootSelector = \`.\${CodeViewWrapper.rootSelector}\`;
39+
// casting to 'any' is needed to avoid this issue with generics
40+
// https://github.com/microsoft/TypeScript/issues/29132
41+
return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, CodeViewWrapper);
42+
};
43+
44+
ElementWrapper.prototype.findAllCodeViews = function (selector) {
45+
return this.findAllComponents(CodeViewWrapper, selector);
46+
};
47+
export default function wrapper(root: Element = document.body) {
48+
if (document && document.body && !document.body.contains(root)) {
49+
console.warn(
50+
"[AwsUi] [test-utils] provided element is not part of the document body, interactions may work incorrectly",
51+
);
52+
}
53+
return new ElementWrapper(root);
54+
}
55+
"
56+
`;
57+
58+
exports[`Generate test utils ElementWrapper > 'selectors' ElementWrapper matches the snapshot 1`] = `
59+
"// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
60+
// SPDX-License-Identifier: Apache-2.0
61+
import { ElementWrapper } from "@cloudscape-design/test-utils-core/selectors";
62+
import { appendSelector } from "@cloudscape-design/test-utils-core/utils";
63+
export { ElementWrapper };
64+
65+
import CodeViewWrapper from "./code-view";
66+
export { CodeViewWrapper };
67+
68+
declare module "@cloudscape-design/test-utils-core/dist/selectors" {
69+
interface ElementWrapper {
70+
/**
71+
* Returns a wrapper that matches the CodeViews with the specified CSS selector.
72+
* If no CSS selector is specified, returns a wrapper that matches CodeViews.
73+
*
74+
* @param {string} [selector] CSS Selector
75+
* @returns {CodeViewWrapper}
76+
*/
77+
findCodeView(selector?: string): CodeViewWrapper;
78+
79+
/**
80+
* Returns a multi-element wrapper that matches CodeViews with the specified CSS selector.
81+
* If no CSS selector is specified, returns a multi-element wrapper that matches CodeViews.
82+
*
83+
* @param {string} [selector] CSS Selector
84+
* @returns {MultiElementWrapper<CodeViewWrapper>}
85+
*/
86+
findAllCodeViews(selector?: string): MultiElementWrapper<CodeViewWrapper>;
87+
}
88+
}
89+
90+
ElementWrapper.prototype.findCodeView = function (selector) {
91+
const rootSelector = \`.\${CodeViewWrapper.rootSelector}\`;
92+
// casting to 'any' is needed to avoid this issue with generics
93+
// https://github.com/microsoft/TypeScript/issues/29132
94+
return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, CodeViewWrapper);
95+
};
96+
97+
ElementWrapper.prototype.findAllCodeViews = function (selector) {
98+
return this.findAllComponents(CodeViewWrapper, selector);
99+
};
100+
export default function wrapper(root = "body") {
101+
return new ElementWrapper(root);
102+
}
103+
"
104+
`;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import fs from "fs";
4+
import path from "path";
5+
import { describe, expect, test } from "vitest";
6+
7+
describe("Generate test utils ElementWrapper", () => {
8+
const importPaths = [
9+
{
10+
type: "dom",
11+
relativePath: "../test-utils/dom/index.ts",
12+
},
13+
{
14+
type: "selectors",
15+
relativePath: "../test-utils/selectors/index.ts",
16+
},
17+
] as const;
18+
19+
test.each(importPaths)("$type ElementWrapper matches the snapshot", ({ relativePath }) => {
20+
const testUtilsPath = path.join(__dirname, relativePath);
21+
const domWrapper = fs.readFileSync(testUtilsPath, "utf8");
22+
expect(domWrapper).toMatchSnapshot();
23+
});
24+
});

0 commit comments

Comments
 (0)