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: test utils API improvements (Collective PR) #2985

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
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
142 changes: 119 additions & 23 deletions build-tools/tasks/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { pascalCase } = require('change-case');
const { default: convertToSelectorUtil } = require('@cloudscape-design/test-utils-converter');
const { through, task } = require('../utils/gulp-utils');
const { writeFile, listPublicItems } = require('../utils/files');
const { pluralizeComponentName } = require('../utils/pluralize');
const themes = require('../utils/themes');

function toWrapper(componentClass) {
Expand All @@ -15,24 +16,132 @@ function toWrapper(componentClass) {

const testUtilsSrcDir = path.resolve('src/test-utils');
const configs = {
common: {
// These components are not meant to be present in multiple instances in a single app.
// For this reason no findAll and findByTestId finders will be generated for them.
noExtraFinders: ['AppLayout', 'TopNavigation'],
buildFinder: ({ componentName }) => `
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)});
};`,
buildExtraFinders: ({ componentName, componentNamePlural }) => `
ElementWrapper.prototype.findAll${componentNamePlural} = function(selector) {
return this.findAllComponents(${toWrapper(componentName)}, selector);
};

ElementWrapper.prototype.find${componentName}ByTestId = function(testId) {
const selector = \`.\${${toWrapper(componentName)}.rootSelector}[data-testid="\${CSS.escape(testId)}"]\`;
return this.findComponent(selector, ${toWrapper(componentName)});
};`,
},
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 }) =>
`find${componentName}(selector?: string): ${toWrapper(componentName)} | null;`,
buildFinderInterface: ({ componentName }) => `
/**
* 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;`,
buildExtraFinderInterfaces: ({ componentName, componentNamePlural }) => `
/**
* 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)}>;
/**
* Returns the first ${componentName} that matches the specified test ID.
* Looks for the \`data-testid\` attribute assigned to the component.
* If no matching ${componentName} is found, returns \`null\`.
*
* @param {string} testId
* @returns {${toWrapper(componentName)} | null}
*/
find${componentName}ByTestId(testId: string): ${toWrapper(componentName)} | null;`,
},
selectors: {
defaultExport: `export default function wrapper(root: string = 'body') { return new ElementWrapper(root); }`,
buildFinderInterface: ({ componentName }) =>
`find${componentName}(selector?: string): ${toWrapper(componentName)};`,
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)};`,
buildExtraFinderInterfaces: ({ componentName, componentNamePlural }) => `
/**
* 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)}>;
/**
* Returns a wrapper that matches the first ${componentName} with the specified test ID.
* Looks for the \`data-testid\` attribute assigned to the component.
*
* @param {string} testId
* @returns {${toWrapper(componentName)}}
*/
find${componentName}ByTestId(testId: string): ${toWrapper(componentName)};`,
},
};

function generateFindersInterfaces({ testUtilMetaData, testUtilType, configs }) {
const { buildFinderInterface, buildExtraFinderInterfaces } = configs[testUtilType];
const { noExtraFinders } = configs.common;

const findersInterfaces = testUtilMetaData.map(metadata => {
if (noExtraFinders.includes(metadata.componentName)) {
return buildFinderInterface(metadata);
}

return [buildFinderInterface(metadata), buildExtraFinderInterfaces(metadata)].join('\n');
});

// 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 { noExtraFinders, buildFinder, buildExtraFinders } = configs.common;

const findersImplementations = testUtilMetaData.map(metadata => {
if (noExtraFinders.includes(metadata.componentName)) {
return buildFinder(metadata);
}

return [buildFinder(metadata), buildExtraFinders(metadata)].join('\n');
});

return findersImplementations.join('\n');
}

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

return [
// language=TypeScript
Expand All @@ -47,24 +156,9 @@ function generateIndexFileContent(testUtilType, testUtilMetaData) {
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 {
${testUtilMetaData.map(metaData => buildFinderInterface(metaData)).join('\n')}
}
}`,
...testUtilMetaData.map(({ componentName }) => {
const wrapperName = toWrapper(componentName);
// language=TypeScript
return `ElementWrapper.prototype.find${componentName} = 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});
};`;
}),
defaultExport,
generateFindersInterfaces({ testUtilMetaData, testUtilType, configs }),
generateFindersImplementations({ testUtilMetaData, configs }),
config.defaultExport,
].join('\n');
}

Expand All @@ -77,9 +171,11 @@ function generateTestUtilMetaData(testUtilType) {

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

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

Expand Down
90 changes: 90 additions & 0 deletions build-tools/utils/pluralize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
const pluralizationMap = {
Alert: 'Alerts',
AnchorNavigation: 'AnchorNavigations',
Annotation: 'Annotations',
AppLayout: 'AppLayouts',
AreaChart: 'AreaCharts',
AttributeEditor: 'AttributeEditors',
Autosuggest: 'Autosuggests',
Badge: 'Badges',
BarChart: 'BarCharts',
Box: 'Boxes',
BreadcrumbGroup: 'BreadcrumbGroups',
Button: 'Buttons',
ButtonDropdown: 'ButtonDropdowns',
ButtonGroup: 'ButtonGroups',
Calendar: 'Calendars',
Cards: 'Cards',
Checkbox: 'Checkboxes',
CodeEditor: 'CodeEditors',
CollectionPreferences: 'CollectionPreferences',
ColumnLayout: 'ColumnLayouts',
Container: 'Containers',
ContentLayout: 'ContentLayouts',
CopyToClipboard: 'CopyToClipboards',
DateInput: 'DateInputs',
DatePicker: 'DatePickers',
DateRangePicker: 'DateRangePickers',
Drawer: 'Drawers',
ExpandableSection: 'ExpandableSections',
FileUpload: 'FileUploads',
Flashbar: 'Flashbars',
Form: 'Forms',
FormField: 'FormFields',
Grid: 'Grids',
Header: 'Headers',
HelpPanel: 'HelpPanels',
Hotspot: 'Hotspots',
Icon: 'Icons',
Input: 'Inputs',
KeyValuePairs: 'KeyValuePairs',
LineChart: 'LineCharts',
Link: 'Links',
LiveRegion: 'LiveRegions',
MixedLineBarChart: 'MixedLineBarCharts',
Modal: 'Modals',
Multiselect: 'Multiselects',
Pagination: 'Paginations',
PieChart: 'PieCharts',
Popover: 'Popovers',
ProgressBar: 'ProgressBars',
PromptInput: 'PromptInputs',
PropertyFilter: 'PropertyFilters',
RadioGroup: 'RadioGroups',
S3ResourceSelector: 'S3ResourceSelectors',
SegmentedControl: 'SegmentedControls',
Select: 'Selects',
SideNavigation: 'SideNavigations',
Slider: 'Sliders',
SpaceBetween: 'SpaceBetweens',
Spinner: 'Spinners',
SplitPanel: 'SplitPanels',
StatusIndicator: 'StatusIndicators',
Steps: 'Steps',
Table: 'Tables',
Tabs: 'Tabs',
TagEditor: 'TagEditors',
TextContent: 'TextContents',
TextFilter: 'TextFilters',
Textarea: 'Textareas',
Tiles: 'Tiles',
TimeInput: 'TimeInputs',
Toggle: 'Toggles',
ToggleButton: 'ToggleButtons',
TokenGroup: 'TokenGroups',
TopNavigation: 'TopNavigations',
TutorialPanel: 'TutorialPanels',
Wizard: 'Wizards',
};

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

return pluralizationMap[componentName];
}

module.exports = { pluralizeComponentName };
3 changes: 2 additions & 1 deletion package-lock.json

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

20 changes: 10 additions & 10 deletions pages/app-layout/refresh-content-width.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,75 +32,75 @@ export default function () {
breadcrumbs={<Breadcrumbs />}
maxContentWidth={maxContentWidth}
content={
<div className={styles.highlightBorder} data-test-id="content">
<div className={styles.highlightBorder} data-testid="content">
<Box variant="h1">Demo page for layout types in visual refresh</Box>
<SpaceBetween size={'l'}>
<Button
data-test-id="button_width-400"
data-testid="button_width-400"
onClick={() => {
setMaxContentWidth(400);
}}
>
Set content width to 400
</Button>
<Button
data-test-id="button_width-number-max_value"
data-testid="button_width-number-max_value"
onClick={() => {
setMaxContentWidth(Number.MAX_VALUE);
}}
>
Set content width to Number.MAX_VALUE
</Button>
<Button
data-test-id="button_width-undef"
data-testid="button_width-undef"
onClick={() => {
setMaxContentWidth(undefined);
}}
>
Set content width to undef
</Button>
<Button
data-test-id="button_type-default"
data-testid="button_type-default"
onClick={() => {
setContentType('default');
}}
>
Set content type to default
</Button>
<Button
data-test-id="button_type-cards"
data-testid="button_type-cards"
onClick={() => {
setContentType('cards');
}}
>
Set content type to cards
</Button>
<Button
data-test-id="button_type-table"
data-testid="button_type-table"
onClick={() => {
setContentType('table');
}}
>
Set content type to table
</Button>
<Button
data-test-id="button_type-form"
data-testid="button_type-form"
onClick={() => {
setContentType('form');
}}
>
Set content type to form
</Button>
<Button
data-test-id="button_type-wizard"
data-testid="button_type-wizard"
onClick={() => {
setContentType('wizard');
}}
>
Set content type to wizard
</Button>
<Button
data-test-id="button_set-drawers-open"
data-testid="button_set-drawers-open"
onClick={() => {
setDrawersOpen(true);
}}
Expand Down
2 changes: 1 addition & 1 deletion pages/app-layout/with-drawers.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function WithDrawers() {
breadcrumbs={<Breadcrumbs />}
content={
<ContentLayout
data-test-id="content"
data-testid="content"
header={
<SpaceBetween size="m">
<Header variant="h1" description="Sometimes you need custom drawers to get the job done.">
Expand Down
2 changes: 1 addition & 1 deletion pages/app-layout/with-one-open-drawer.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function WithDrawers() {
navigationHide={true}
content={
<ContentLayout
data-test-id="content"
data-testid="content"
header={
<Header variant="h1" description="Sometimes you need custom drawers to get the job done.">
One drawer opened
Expand Down
Loading