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 extra selectors to the test utils #2932

Merged
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
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'],
Copy link
Contributor Author

@orangevolon orangevolon Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit more context on this: The "root" element of these two components behave differently. For example for the top navigation:

<div data-testid="assiged-by-user">
  <div>
    <div class="awsui-hashed-root-class">
      ...
    </div>
  </div>
</div>

So data-testid is the rootClass has are not assigned to the same element. This means findTopNavigationByTestId will never return any result and might make the users confused.

Since these two components are also not meant to be multiple in a single app, we decided to keep these two as exceptions. They won't receive findAllXX and findXXByTestId finders.

We'll address them separately later as it might need a change to the component itself.

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 = {
orangevolon marked this conversation as resolved.
Show resolved Hide resolved
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.

Loading