Skip to content

Commit

Permalink
Merge branch 'main' into line_wrap
Browse files Browse the repository at this point in the history
  • Loading branch information
Alice Koreman committed Dec 2, 2024
2 parents 5b7e6e6 + f12b6cc commit 713e49a
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 28 deletions.
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

0 comments on commit 713e49a

Please sign in to comment.