diff --git a/scripts/pluralize.js b/scripts/pluralize.js new file mode 100644 index 00000000..96bef7a2 --- /dev/null +++ b/scripts/pluralize.js @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +const pluralizationMap = { + Board: "Boards", + BoardItem: "BoardItems", + ItemsPalette: "ItemsPalettes", + PaletteItem: "PaletteItems", +}; + +function pluralizeComponentName(componentName) { + if (!(componentName in pluralizationMap)) { + throw new Error(`Could not find the plural case for ${componentName}.`); + } + + return pluralizationMap[componentName]; +} + +export { pluralizeComponentName }; diff --git a/scripts/test-utils.js b/scripts/test-utils.js index 24cc8a39..f41a0153 100644 --- a/scripts/test-utils.js +++ b/scripts/test-utils.js @@ -1,16 +1,126 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { execaSync } from "execa"; +import { globbySync } from "globby"; import fs from "node:fs"; import path from "node:path"; + import { default as convertToSelectorUtil } from "@cloudscape-design/test-utils-converter"; -import { execaSync } from "execa"; -import { globbySync } from "globby"; + +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(); @@ -29,8 +139,7 @@ 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); } @@ -38,48 +147,38 @@ function generateDomIndexFile() { 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"); } function compileTypescript() { const config = path.resolve("src/test-utils/tsconfig.json"); - execaSync("tsc", ["-p", config, "--sourceMap"], { stdio: "inherit" }); + execaSync("tsc", ["-p", config, "--sourceMap", "--inlineSources"], { stdio: "inherit" }); } diff --git a/src/__tests__/__snapshots__/test-utils-wrappers.test.tsx.snap b/src/__tests__/__snapshots__/test-utils-wrappers.test.tsx.snap new file mode 100644 index 00000000..bda0c9dd --- /dev/null +++ b/src/__tests__/__snapshots__/test-utils-wrappers.test.tsx.snap @@ -0,0 +1,297 @@ +// 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 BoardWrapper from './board'; + export { BoardWrapper }; + + + import BoardItemWrapper from './board-item'; + export { BoardItemWrapper }; + + + import ItemsPaletteWrapper from './items-palette'; + export { ItemsPaletteWrapper }; + + + import PaletteItemWrapper from './palette-item'; + export { PaletteItemWrapper }; + +declare module '@cloudscape-design/test-utils-core/dist/dom' { + interface ElementWrapper { + + /** + * Returns the wrapper of the first Board that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first Board. + * If no matching Board is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {BoardWrapper | null} + */ + findBoard(selector?: string): BoardWrapper | null; + + /** + * Returns an array of Board wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the Boards inside the current wrapper. + * If no matching Board is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ + findAllBoards(selector?: string): Array; + + /** + * Returns the wrapper of the first BoardItem that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first BoardItem. + * If no matching BoardItem is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {BoardItemWrapper | null} + */ + findBoardItem(selector?: string): BoardItemWrapper | null; + + /** + * Returns an array of BoardItem wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the BoardItems inside the current wrapper. + * If no matching BoardItem is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ + findAllBoardItems(selector?: string): Array; + + /** + * Returns the wrapper of the first ItemsPalette that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first ItemsPalette. + * If no matching ItemsPalette is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {ItemsPaletteWrapper | null} + */ + findItemsPalette(selector?: string): ItemsPaletteWrapper | null; + + /** + * Returns an array of ItemsPalette wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the ItemsPalettes inside the current wrapper. + * If no matching ItemsPalette is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ + findAllItemsPalettes(selector?: string): Array; + + /** + * Returns the wrapper of the first PaletteItem that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first PaletteItem. + * If no matching PaletteItem is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {PaletteItemWrapper | null} + */ + findPaletteItem(selector?: string): PaletteItemWrapper | null; + + /** + * Returns an array of PaletteItem wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the PaletteItems inside the current wrapper. + * If no matching PaletteItem is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ + findAllPaletteItems(selector?: string): Array; + } + } + + ElementWrapper.prototype.findBoard = function(selector) { + const rootSelector = \`.\${BoardWrapper.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, BoardWrapper); + }; + + ElementWrapper.prototype.findAllBoards = function(selector) { + return this.findAllComponents(BoardWrapper, selector); + }; + + ElementWrapper.prototype.findBoardItem = function(selector) { + const rootSelector = \`.\${BoardItemWrapper.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, BoardItemWrapper); + }; + + ElementWrapper.prototype.findAllBoardItems = function(selector) { + return this.findAllComponents(BoardItemWrapper, selector); + }; + + ElementWrapper.prototype.findItemsPalette = function(selector) { + const rootSelector = \`.\${ItemsPaletteWrapper.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, ItemsPaletteWrapper); + }; + + ElementWrapper.prototype.findAllItemsPalettes = function(selector) { + return this.findAllComponents(ItemsPaletteWrapper, selector); + }; + + ElementWrapper.prototype.findPaletteItem = function(selector) { + const rootSelector = \`.\${PaletteItemWrapper.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, PaletteItemWrapper); + }; + + ElementWrapper.prototype.findAllPaletteItems = function(selector) { + return this.findAllComponents(PaletteItemWrapper, 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 BoardWrapper from './board'; + export { BoardWrapper }; + + + import BoardItemWrapper from './board-item'; + export { BoardItemWrapper }; + + + import ItemsPaletteWrapper from './items-palette'; + export { ItemsPaletteWrapper }; + + + import PaletteItemWrapper from './palette-item'; + export { PaletteItemWrapper }; + +declare module '@cloudscape-design/test-utils-core/dist/selectors' { + interface ElementWrapper { + + /** + * Returns a wrapper that matches the Boards with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches Boards. + * + * @param {string} [selector] CSS Selector + * @returns {BoardWrapper} + */ + findBoard(selector?: string): BoardWrapper; + + /** + * Returns a multi-element wrapper that matches Boards with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches Boards. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ + findAllBoards(selector?: string): MultiElementWrapper; + + /** + * Returns a wrapper that matches the BoardItems with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches BoardItems. + * + * @param {string} [selector] CSS Selector + * @returns {BoardItemWrapper} + */ + findBoardItem(selector?: string): BoardItemWrapper; + + /** + * Returns a multi-element wrapper that matches BoardItems with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches BoardItems. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ + findAllBoardItems(selector?: string): MultiElementWrapper; + + /** + * Returns a wrapper that matches the ItemsPalettes with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches ItemsPalettes. + * + * @param {string} [selector] CSS Selector + * @returns {ItemsPaletteWrapper} + */ + findItemsPalette(selector?: string): ItemsPaletteWrapper; + + /** + * Returns a multi-element wrapper that matches ItemsPalettes with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches ItemsPalettes. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ + findAllItemsPalettes(selector?: string): MultiElementWrapper; + + /** + * Returns a wrapper that matches the PaletteItems with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches PaletteItems. + * + * @param {string} [selector] CSS Selector + * @returns {PaletteItemWrapper} + */ + findPaletteItem(selector?: string): PaletteItemWrapper; + + /** + * Returns a multi-element wrapper that matches PaletteItems with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches PaletteItems. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ + findAllPaletteItems(selector?: string): MultiElementWrapper; + } + } + + ElementWrapper.prototype.findBoard = function(selector) { + const rootSelector = \`.\${BoardWrapper.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, BoardWrapper); + }; + + ElementWrapper.prototype.findAllBoards = function(selector) { + return this.findAllComponents(BoardWrapper, selector); + }; + + ElementWrapper.prototype.findBoardItem = function(selector) { + const rootSelector = \`.\${BoardItemWrapper.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, BoardItemWrapper); + }; + + ElementWrapper.prototype.findAllBoardItems = function(selector) { + return this.findAllComponents(BoardItemWrapper, selector); + }; + + ElementWrapper.prototype.findItemsPalette = function(selector) { + const rootSelector = \`.\${ItemsPaletteWrapper.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, ItemsPaletteWrapper); + }; + + ElementWrapper.prototype.findAllItemsPalettes = function(selector) { + return this.findAllComponents(ItemsPaletteWrapper, selector); + }; + + ElementWrapper.prototype.findPaletteItem = function(selector) { + const rootSelector = \`.\${PaletteItemWrapper.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, PaletteItemWrapper); + }; + + ElementWrapper.prototype.findAllPaletteItems = function(selector) { + return this.findAllComponents(PaletteItemWrapper, selector); + }; +export default function wrapper(root: string = 'body') { return new ElementWrapper(root); }" +`; diff --git a/src/__tests__/test-utils-wrappers.test.tsx b/src/__tests__/test-utils-wrappers.test.tsx new file mode 100644 index 00000000..c0e31419 --- /dev/null +++ b/src/__tests__/test-utils-wrappers.test.tsx @@ -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(); + }); +}); diff --git a/src/__tests__/test-utils.test.tsx b/src/__tests__/test-utils.test.tsx new file mode 100644 index 00000000..6e7501a5 --- /dev/null +++ b/src/__tests__/test-utils.test.tsx @@ -0,0 +1,174 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ComponentType } from "react"; +import { render } from "@testing-library/react"; +import { paramCase, pascalCase } from "change-case"; +import { describe, expect, test } from "vitest"; + +import * as components from "../../lib/components"; +import createWrapperDom, { ElementWrapper as DomElementWrapper } from "../../lib/components/test-utils/dom"; +import createWrapperSelectors from "../../lib/components/test-utils/selectors"; +import { ItemContextWrapper } from "../board-item/__tests__/board-item-wrapper"; +import { defaultProps } from "./default-props"; + +const componentWithMultipleRootElements = ["Board", "ItemsPalette"]; +const componentNames = Object.keys(components).filter( + (component) => !componentWithMultipleRootElements.includes(component), +); + +const RENDER_COMPONENTS_DEFAULT_PROPS: Record[] = [ + { + "data-testid": "first-item", + "data-name": "first item", + }, + { + "data-testid": "second-item", + "data-name": "second item", + }, +]; + +function renderComponents(componentName: string, props = RENDER_COMPONENTS_DEFAULT_PROPS) { + const Component = components[componentName as keyof typeof components] as ComponentType; + const componentDefaultProps = defaultProps[paramCase(componentName) as keyof typeof defaultProps]; + return render( +
+ {props.map(({ ...customProps }, index) => ( + + ))} +
, + { wrapper: ItemContextWrapper }, + ); +} + +function getComponentSelectors(componentName: string) { + const componentNamePascalCase = pascalCase(componentName); + const findAllRegex = new RegExp(`findAll${componentNamePascalCase}.*`); + + // The same set of selector functions are present in both dom and selectors. + // For this reason, looking into DOM is representative of both groups. + const wrapperPropsList = Object.keys(DomElementWrapper.prototype); + + // Every component has the same set of selector functions. + // For this reason, casting the function names into the Board component. + const findName = `find${componentNamePascalCase}` as "findBoard"; + const findAllName = wrapperPropsList.find((selector) => findAllRegex.test(selector)) as "findAllBoards"; + + return { findName, findAllName }; +} + +describe.each(componentNames)("ElementWrapper selectors for %s component", (componentName) => { + const { findName, findAllName } = getComponentSelectors(componentName); + + describe("dom wrapper", () => { + test(`${findName} returns the first ${componentName}`, () => { + const { container } = renderComponents(componentName); + const wrapper = createWrapperDom(container); + const element = wrapper[findName]()!.getElement(); + + expect(element).toHaveAttribute("data-name", "first item"); + }); + + test(`${findAllName} returns all of the ${componentName} components`, () => { + const { container } = renderComponents(componentName); + const wrapper = createWrapperDom(container); + const elementNameAttributes = wrapper[findAllName]().map((component) => + component!.getElement().getAttribute("data-name"), + ); + + expect(elementNameAttributes).toEqual(["first item", "second item"]); + }); + + test(`${findAllName} returns only the matching ${componentName} components, when a selector is specified`, () => { + const { container } = renderComponents(componentName, [ + { "data-type": "first-type", "data-name": "first item" }, + { "data-type": "second-type", "data-name": "second item" }, + { "data-type": "second-type", "data-name": "third item" }, + ]); + const wrapper = createWrapperDom(container); + const elementNameAttributes = wrapper[findAllName]("[data-type=second-type]").map((component) => + component!.getElement().getAttribute("data-name"), + ); + + expect(elementNameAttributes).toEqual(["second item", "third item"]); + }); + }); + + describe("selectors wrapper", () => { + test(`${findName} returns a selector that matches the ${componentName}`, () => { + const { container } = renderComponents(componentName); + const wrapper = createWrapperSelectors(); + const selector = wrapper[findName]().toSelector(); + const element = container.querySelector(selector); + + expect(element).toHaveAttribute("data-name", "first item"); + }); + + test(`${findAllName} returns a selector that matches the ${componentName} with nth-child index`, () => { + const { container } = renderComponents(componentName); + const wrapper = createWrapperSelectors(); + const selector = wrapper[findAllName]().get(2).toSelector(); + const element = container.querySelector(selector); + + expect(element).toHaveAttribute("data-name", "second item"); + }); + + test(`${findAllName} appends the specified selector to the default ${componentName} selectors`, () => { + const { container } = renderComponents(componentName, [ + { "data-type": "first-type", "data-name": "first item" }, + { "data-type": "second-type", "data-name": "second item" }, + ]); + const wrapper = createWrapperSelectors(); + const firstElement = container.querySelector(wrapper[findAllName]("[data-type=second-type]").get(1).toSelector()); + const secondElement = container.querySelector( + wrapper[findAllName]("[data-type=second-type]").get(2).toSelector(), + ); + + expect(firstElement).toBeFalsy(); + expect(secondElement).toBeTruthy(); + }); + }); +}); + +describe.each(componentWithMultipleRootElements)("ElementWrapper selectors for %s component", (componentName) => { + const { findName, findAllName } = getComponentSelectors(componentName); + + describe("dom wrapper", () => { + test(`${findName} returns the first ${componentName}`, () => { + const { container } = renderComponents(componentName); + const wrapper = createWrapperDom(container); + const element = wrapper[findName]()!.getElement(); + + expect(element.closest("[data-name]")).toHaveAttribute("data-name", "first item"); + }); + + test(`${findAllName} returns all of the ${componentName} components`, () => { + const { container } = renderComponents(componentName); + const wrapper = createWrapperDom(container); + const elementNameAttributes = wrapper[findAllName]().map((component) => + component!.getElement()!.closest("[data-name]")!.getAttribute("data-name"), + ); + + expect(elementNameAttributes).toEqual(["first item", "second item"]); + }); + }); + + describe("selectors wrapper", () => { + test(`${findName} returns a selector that matches the ${componentName}`, () => { + const { container } = renderComponents(componentName); + const wrapper = createWrapperSelectors(); + const selector = wrapper[findName]().toSelector(); + const element = container.querySelector(selector); + + expect(element!.closest("[data-name]")).toHaveAttribute("data-name", "first item"); + }); + + test(`${findAllName} returns a selector that matches the ${componentName}`, () => { + const { container } = renderComponents(componentName); + const wrapper = createWrapperSelectors(); + const selector = wrapper[findAllName]().toSelector(); + const element = container.querySelector(selector); + + expect(element!.closest("[data-name]")).toHaveAttribute("data-name", "first item"); + }); + }); +});