Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds findAll finder to test utils #53

Merged
merged 1 commit into from
Nov 20, 2024
Merged
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
15 changes: 15 additions & 0 deletions scripts/pluralize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
const pluralizationMap = {
CodeView: "CodeViews",
};

function pluralizeComponentName(componentName) {
if (!(componentName in pluralizationMap)) {
throw new Error(`Could not find the plural case for ${componentName}.`);
}

return pluralizationMap[componentName];
}

export { pluralizeComponentName };
153 changes: 125 additions & 28 deletions scripts/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,120 @@ import path from "node:path";

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

import { pluralizeComponentName } from "./pluralize.js";
import { pascalCase, writeSourceFile } from "./utils.js";

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

function toWrapper(componentClass) {
return `${componentClass}Wrapper`;
}

const configs = {
common: {
buildFinder: ({ componentName, componentNamePlural }) => `
ElementWrapper.prototype.find${componentName} = function(selector) {
const rootSelector = \`.$\{${toWrapper(componentName)}.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, ${toWrapper(componentName)});
};

ElementWrapper.prototype.findAll${componentNamePlural} = function(selector) {
return this.findAllComponents(${toWrapper(componentName)}, selector);
};`,
},
dom: {
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); }`,
buildFinderInterface: ({ componentName, componentNamePlural }) => `
/**
* Returns the wrapper of the first ${componentName} that matches the specified CSS selector.
* If no CSS selector is specified, returns the wrapper of the first ${componentName}.
* If no matching ${componentName} is found, returns \`null\`.
*
* @param {string} [selector] CSS Selector
* @returns {${toWrapper(componentName)} | null}
*/
find${componentName}(selector?: string): ${toWrapper(componentName)} | null;

/**
* Returns an array of ${componentName} wrapper that matches the specified CSS selector.
* If no CSS selector is specified, returns all of the ${componentNamePlural} inside the current wrapper.
* If no matching ${componentName} is found, returns an empty array.
*
* @param {string} [selector] CSS Selector
* @returns {Array<${toWrapper(componentName)}>}
*/
findAll${componentNamePlural}(selector?: string): Array<${toWrapper(componentName)}>;`,
},
selectors: {
defaultExport: `export default function wrapper(root: string = 'body') { return new ElementWrapper(root); }`,
buildFinderInterface: ({ componentName, componentNamePlural }) => `
/**
* Returns a wrapper that matches the ${componentNamePlural} with the specified CSS selector.
* If no CSS selector is specified, returns a wrapper that matches ${componentNamePlural}.
*
* @param {string} [selector] CSS Selector
* @returns {${toWrapper(componentName)}}
*/
find${componentName}(selector?: string): ${toWrapper(componentName)};

/**
* Returns a multi-element wrapper that matches ${componentNamePlural} with the specified CSS selector.
* If no CSS selector is specified, returns a multi-element wrapper that matches ${componentNamePlural}.
*
* @param {string} [selector] CSS Selector
* @returns {MultiElementWrapper<${toWrapper(componentName)}>}
*/
findAll${componentNamePlural}(selector?: string): MultiElementWrapper<${toWrapper(componentName)}>;`,
},
};

function generateTestUtilMetaData() {
const testUtilsSrcDir = path.resolve("src/test-utils");
const metaData = components.reduce((allMetaData, componentFolderName) => {
const absPathComponentFolder = path.resolve(testUtilsSrcDir, componentFolderName);
const relPathTestUtilFile = `./${path.relative(testUtilsSrcDir, absPathComponentFolder)}`;

const componentNameKebab = componentFolderName;
const componentName = pascalCase(componentNameKebab);
const componentNamePlural = pluralizeComponentName(componentName);

const componentMetaData = {
componentName,
componentNamePlural,
relPathTestUtilFile,
};

return allMetaData.concat(componentMetaData);
}, []);

return metaData;
}

function generateFindersInterfaces({ testUtilMetaData, testUtilType, configs }) {
const { buildFinderInterface } = configs[testUtilType];
const findersInterfaces = testUtilMetaData.map(buildFinderInterface);

// we need to redeclare the interface in its original definition, extending a re-export will not work
// https://github.com/microsoft/TypeScript/issues/12607
const interfaces = `declare module '@cloudscape-design/test-utils-core/dist/${testUtilType}' {
interface ElementWrapper {
${findersInterfaces.join("\n")}
}
}`;

return interfaces;
}

function generateFindersImplementations({ testUtilMetaData, configs }) {
const { buildFinder } = configs.common;
const findersImplementations = testUtilMetaData.map(buildFinder);
return findersImplementations.join("\n");
}

generateSelectorUtils();
generateDomIndexFile();
generateSelectorsIndexFile();
Expand All @@ -31,53 +139,42 @@ function generateSelectorUtils() {
function generateDomIndexFile() {
const content = generateIndexFileContent({
testUtilType: "dom",
buildFinderInterface: (componentName) =>
`find${pascalCase(componentName)}(selector?: string): ${pascalCase(componentName)}Wrapper | null;`,
testUtilMetaData: generateTestUtilMetaData(),
});
writeSourceFile("./src/test-utils/dom/index.ts", content);
}

function generateSelectorsIndexFile() {
const content = generateIndexFileContent({
testUtilType: "selectors",
buildFinderInterface: (componentName) =>
`find${pascalCase(componentName)}(selector?: string): ${pascalCase(componentName)}Wrapper;`,
testUtilMetaData: generateTestUtilMetaData(),
});
writeSourceFile("./src/test-utils/selectors/index.ts", content);
}

function generateIndexFileContent({ testUtilType, buildFinderInterface }) {
function generateIndexFileContent({ testUtilType, testUtilMetaData }) {
const config = configs[testUtilType];
if (config === undefined) {
throw new Error("Unknown test util type");
}

return [
// language=TypeScript
`import { ElementWrapper } from '@cloudscape-design/test-utils-core/${testUtilType}';`,
`import '@cloudscape-design/components/test-utils/${testUtilType}';`,
`import { appendSelector } from '@cloudscape-design/test-utils-core/utils';`,
`export { ElementWrapper };`,
...components.map((componentName) => {
const componentImport = `./${componentName}/index`;
...testUtilMetaData.map((metaData) => {
const { componentName, relPathTestUtilFile } = metaData;

return `
import ${pascalCase(componentName)}Wrapper from '${componentImport}';
export { ${pascalCase(componentName)}Wrapper };
import ${toWrapper(componentName)} from '${relPathTestUtilFile}';
export { ${componentName}Wrapper };
`;
}),
// we need to redeclare the interface in its original definition, extending a re-export will not work
// https://github.com/microsoft/TypeScript/issues/12607
`declare module '@cloudscape-design/test-utils-core/dist/${testUtilType}' {
interface ElementWrapper {
${components.map((componentName) => buildFinderInterface(componentName)).join("\n")}
}
}`,
...components.map((componentName) => {
// language=TypeScript
return `ElementWrapper.prototype.find${pascalCase(componentName)} = function(selector) {
const rootSelector = \`.$\{${pascalCase(componentName)}Wrapper.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, ${pascalCase(
componentName,
)}Wrapper);
};`;
}),
`export { createWrapper as default } from '@cloudscape-design/test-utils-core/${testUtilType}';`,
generateFindersInterfaces({ testUtilMetaData, testUtilType, configs }),
generateFindersImplementations({ testUtilMetaData, configs }),
config.defaultExport,
].join("\n");
}

Expand Down
93 changes: 93 additions & 0 deletions src/__tests__/__snapshots__/test-utils-wrappers.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Generate test utils ElementWrapper > 'dom' ElementWrapper matches the snapshot 1`] = `
"import { ElementWrapper } from '@cloudscape-design/test-utils-core/dom';
import '@cloudscape-design/components/test-utils/dom';
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';
export { ElementWrapper };

import CodeViewWrapper from './code-view';
export { CodeViewWrapper };

declare module '@cloudscape-design/test-utils-core/dist/dom' {
interface ElementWrapper {

/**
* Returns the wrapper of the first CodeView that matches the specified CSS selector.
* If no CSS selector is specified, returns the wrapper of the first CodeView.
* If no matching CodeView is found, returns \`null\`.
*
* @param {string} [selector] CSS Selector
* @returns {CodeViewWrapper | null}
*/
findCodeView(selector?: string): CodeViewWrapper | null;

/**
* Returns an array of CodeView wrapper that matches the specified CSS selector.
* If no CSS selector is specified, returns all of the CodeViews inside the current wrapper.
* If no matching CodeView is found, returns an empty array.
*
* @param {string} [selector] CSS Selector
* @returns {Array<CodeViewWrapper>}
*/
findAllCodeViews(selector?: string): Array<CodeViewWrapper>;
}
}

ElementWrapper.prototype.findCodeView = function(selector) {
const rootSelector = \`.\${CodeViewWrapper.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, CodeViewWrapper);
};

ElementWrapper.prototype.findAllCodeViews = function(selector) {
return this.findAllComponents(CodeViewWrapper, selector);
};
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); }"
`;

exports[`Generate test utils ElementWrapper > 'selectors' ElementWrapper matches the snapshot 1`] = `
"import { ElementWrapper } from '@cloudscape-design/test-utils-core/selectors';
import '@cloudscape-design/components/test-utils/selectors';
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';
export { ElementWrapper };

import CodeViewWrapper from './code-view';
export { CodeViewWrapper };

declare module '@cloudscape-design/test-utils-core/dist/selectors' {
interface ElementWrapper {

/**
* Returns a wrapper that matches the CodeViews with the specified CSS selector.
* If no CSS selector is specified, returns a wrapper that matches CodeViews.
*
* @param {string} [selector] CSS Selector
* @returns {CodeViewWrapper}
*/
findCodeView(selector?: string): CodeViewWrapper;

/**
* Returns a multi-element wrapper that matches CodeViews with the specified CSS selector.
* If no CSS selector is specified, returns a multi-element wrapper that matches CodeViews.
*
* @param {string} [selector] CSS Selector
* @returns {MultiElementWrapper<CodeViewWrapper>}
*/
findAllCodeViews(selector?: string): MultiElementWrapper<CodeViewWrapper>;
}
}

ElementWrapper.prototype.findCodeView = function(selector) {
const rootSelector = \`.\${CodeViewWrapper.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, CodeViewWrapper);
};

ElementWrapper.prototype.findAllCodeViews = function(selector) {
return this.findAllComponents(CodeViewWrapper, selector);
};
export default function wrapper(root: string = 'body') { return new ElementWrapper(root); }"
`;
24 changes: 24 additions & 0 deletions src/__tests__/test-utils-wrappers.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import fs from "fs";
import path from "path";
import { describe, expect, test } from "vitest";

describe("Generate test utils ElementWrapper", () => {
const importPaths = [
{
type: "dom",
relativePath: "../test-utils/dom/index.ts",
},
{
type: "selectors",
relativePath: "../test-utils/selectors/index.ts",
},
] as const;

test.each(importPaths)("$type ElementWrapper matches the snapshot", ({ relativePath }) => {
const testUtilsPath = path.join(__dirname, relativePath);
const domWrapper = fs.readFileSync(testUtilsPath, "utf8");
expect(domWrapper).toMatchSnapshot();
});
});
Loading
Loading