Skip to content

Commit

Permalink
feat: adds find all selectors to test utils (#3024)
Browse files Browse the repository at this point in the history
  • Loading branch information
orangevolon authored Nov 20, 2024
1 parent 2028cba commit 035ae6b
Show file tree
Hide file tree
Showing 11 changed files with 5,349 additions and 1,380 deletions.
102 changes: 79 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,92 @@ function toWrapper(componentClass) {

const testUtilsSrcDir = path.resolve('src/test-utils');
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 }) =>
`find${componentName}(selector?: string): ${toWrapper(componentName)} | null;`,
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 }) =>
`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)};
/**
* 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 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');
}

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 +116,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 +131,11 @@ function generateTestUtilMetaData(testUtilType) {

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

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

Expand Down
93 changes: 93 additions & 0 deletions build-tools/utils/pluralize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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',
FileDropzone: 'FileDropzones',
FileInput: 'FileInputs',
FileTokenGroup: 'FileTokenGroups',
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 };
1 change: 1 addition & 0 deletions jest.unit.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = mergePresets(cloudscapePreset, {
'environment.js$',
'/internal\\/vendor/',
'<rootDir>/pages',
'test-utils/selectors',
],
coverageThreshold: {
global: {
Expand Down
Loading

0 comments on commit 035ae6b

Please sign in to comment.