Skip to content

Commit

Permalink
feat: Adds findXAll and findXByTestId selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
orangevolon committed Nov 6, 2024
1 parent 77a8d98 commit 44ecbba
Show file tree
Hide file tree
Showing 6 changed files with 6,044 additions and 793 deletions.
141 changes: 119 additions & 22 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;`,
`/**
* 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 the wrappers of all ${componentNamePlural} that match 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;

function buildComponentInterfaces(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 {
${testUtilMetaData.map(buildComponentInterfaces).join('\n')}
}
}`;

return interfaces;
}

function generateFindersImplementations({ testUtilMetaData, configs }) {
const { noExtraFinders, buildFinder, buildExtraFinders } = configs.common;

function buildComponentImplementations(metadata) {
if (noExtraFinders.includes(metadata.componentName)) {
return buildFinder(metadata);
}

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

return testUtilMetaData.map(buildComponentImplementations).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,39 +156,27 @@ 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');
}

function generateTestUtilMetaData(testUtilType) {
const components = listPublicItems(path.join(testUtilsSrcDir, testUtilType));
console.log(components);

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,
};

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 };
9 changes: 8 additions & 1 deletion package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
"@juggle/resize-observer": "^3.3.1",
"@types/pluralize": "^0.0.33",
"ace-builds": "^1.34.0",
"balanced-match": "^1.0.2",
"clsx": "^1.1.0",
Expand Down
Loading

0 comments on commit 44ecbba

Please sign in to comment.