Skip to content
Closed
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
17 changes: 14 additions & 3 deletions src/converter/generate-component-finders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,30 @@ export default function wrapper(root: string = 'body') {
export interface GenerateFindersParams {
components: ComponentWrapperMetadata[];
testUtilType: TestUtilType;
namespace?: string;
}

export const generateComponentFinders = ({ components, testUtilType }: GenerateFindersParams) => `
const DEFAULT_NAMESPACES: Record<TestUtilType, string> = {
dom: '@cloudscape-design/test-utils-core/dist/dom',
selectors: '@cloudscape-design/test-utils-core/dist/selectors',
};

export const generateComponentFinders = ({ components, testUtilType, namespace }: GenerateFindersParams) => {
const declareModuleTarget = namespace ?? DEFAULT_NAMESPACES[testUtilType];
const importPath = namespace ?? `@cloudscape-design/test-utils-core/${testUtilType}`;

return `
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { ElementWrapper } from '@cloudscape-design/test-utils-core/${testUtilType}';
import { ElementWrapper } from '${importPath}';
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';

export { ElementWrapper };
${components.map(componentWrapperImport).join('')}

${components.map(componentWrapperExport).join('')}

declare module '@cloudscape-design/test-utils-core/dist/${testUtilType}' {
declare module '${declareModuleTarget}' {
interface ElementWrapper {
${components.map(componentFindersInterfaces[testUtilType]).join('')}
}
Expand All @@ -122,3 +132,4 @@ ${components.map(componentFinders).join('')}
${testUtilType === 'dom' ? components.map(componentClosestFinder).join('') : ''}
${defaultExport[testUtilType]}
`;
};
14 changes: 9 additions & 5 deletions src/converter/generate-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface GenerateIndexFilesParams extends GenerateTestUtilsParams {
testUtilType: TestUtilType;
}

function generateIndexFile({ testUtilsPath, components, testUtilType }: GenerateIndexFilesParams) {
function generateIndexFile({ testUtilsPath, components, testUtilType, namespace }: GenerateIndexFilesParams) {
const componenWrappersMetadata: ComponentWrapperMetadata[] = components.map(
({ name, pluralName, testUtilsFolderName }) => ({
name,
Expand All @@ -23,7 +23,11 @@ function generateIndexFile({ testUtilsPath, components, testUtilType }: Generate
}),
);

const content = generateComponentFinders({ testUtilType, components: componenWrappersMetadata });
const content = generateComponentFinders({
testUtilType,
components: componenWrappersMetadata,
namespace: namespace?.[testUtilType],
});
const indexFilePath = path.join(testUtilsPath, testUtilType, 'index.ts');
writeSourceFile(indexFilePath, content);
}
Expand Down Expand Up @@ -53,8 +57,8 @@ function generateSelectorUtils(testUtilsPath: string) {
/**
* Generates test utils index files for dom and selector and converts the dom test utils to selectors.
*/
export function generateTestUtils({ components, testUtilsPath }: GenerateTestUtilsParams) {
export function generateTestUtils({ components, testUtilsPath, namespace }: GenerateTestUtilsParams) {
generateSelectorUtils(testUtilsPath);
generateIndexFile({ components, testUtilsPath, testUtilType: 'dom' });
generateIndexFile({ components, testUtilsPath, testUtilType: 'selectors' });
generateIndexFile({ components, testUtilsPath, testUtilType: 'dom', namespace });
generateIndexFile({ components, testUtilsPath, testUtilType: 'selectors', namespace });
}
12 changes: 12 additions & 0 deletions src/converter/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ export interface GenerateTestUtilsParams {
*
*/
testUtilsPath: string;

/**
* Custom namespace for the ElementWrapper module augmentation and import.
* When provided, the generated index files will import ElementWrapper from the custom namespace
* and augment it instead of the default @cloudscape-design/test-utils-core namespace.
* This allows custom component libraries to declare an isolated ElementWrapper that does not
* inherit augmentations from @cloudscape-design/components.
*/
namespace?: {
dom: string;
selectors: string;
};
}

export interface ComponentWrapperMetadata extends ComponentMetadata {
Expand Down
17 changes: 17 additions & 0 deletions src/converter/test/generate-component-finders.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,21 @@
}
});
});

describe.each(testUtilTypes)('%s with custom namespace', testUtilType => {
const customNamespace = `@my-lib/test-utils-core/dist/${testUtilType}`;
const sourceFileContent = generateComponentFinders({ components: mockComponents, testUtilType, namespace: customNamespace });

Check failure on line 79 in src/converter/test/generate-component-finders.test.tsx

View workflow job for this annotation

GitHub Actions / build / build

Replace `·components:·mockComponents,·testUtilType,·namespace:·customNamespace` with `⏎······components:·mockComponents,⏎······testUtilType,⏎······namespace:·customNamespace,⏎···`

Check failure on line 79 in src/converter/test/generate-component-finders.test.tsx

View workflow job for this annotation

GitHub Actions / dry-run / Build test-utils

Replace `·components:·mockComponents,·testUtilType,·namespace:·customNamespace` with `⏎······components:·mockComponents,⏎······testUtilType,⏎······namespace:·customNamespace,⏎···`

test('it imports ElementWrapper from the custom namespace', () => {
expect(sourceFileContent).toMatch(`import { ElementWrapper } from '${customNamespace}'`);
});

test('it augments the custom namespace', () => {
expect(sourceFileContent).toMatch(`declare module '${customNamespace}'`);
});

test('it does not augment the default cloudscape namespace', () => {
expect(sourceFileContent).not.toMatch(`declare module '@cloudscape-design/test-utils-core/dist`);
});
});
});
21 changes: 21 additions & 0 deletions src/converter/test/generate-test-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,25 @@ describe(`${generateTestUtils.name}`, () => {
expect.stringMatching(testUtilsFilePartialContent),
);
});

test.each(testUtilsType)('uses custom namespace in generated %s index file', testUtilType => {
const customNamespace = `@my-lib/test-utils-core/dist/${testUtilType}`;
generateTestUtils({
components: mockComponents,
testUtilsPath: './test/mock-test-utils',
namespace: {
dom: '@my-lib/test-utils-core/dist/dom',
selectors: '@my-lib/test-utils-core/dist/selectors',
},
});

expect(writeFileSync).toHaveBeenCalledWith(
`test/mock-test-utils/${testUtilType}/index.ts`,
expect.stringMatching(`declare module '${customNamespace}'`),
);
expect(writeFileSync).toHaveBeenCalledWith(
`test/mock-test-utils/${testUtilType}/index.ts`,
expect.stringMatching(`from '${customNamespace}'`),
);
});
});
122 changes: 122 additions & 0 deletions src/converter/test/mock-test-utils-custom-namespace/dom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { ElementWrapper } from '@my-lib/test-utils-core/dom';
import { appendSelector } from '@cloudscape-design/test-utils-core/utils';

export { ElementWrapper };

import TestComponentAWrapper from './test-component-a';
import TestComponentBWrapper from './test-component-b';


export { TestComponentAWrapper };
export { TestComponentBWrapper };

declare module '@my-lib/test-utils-core/dist/dom' {
interface ElementWrapper {

/**
* Returns the wrapper of the first TestComponentA that matches the specified CSS selector.
* If no CSS selector is specified, returns the wrapper of the first TestComponentA.
* If no matching TestComponentA is found, returns `null`.
*
* @param {string} [selector] CSS Selector
* @returns {TestComponentAWrapper | null}
*/
findTestComponentA(selector?: string): TestComponentAWrapper | null;

/**
* Returns an array of TestComponentA wrapper that matches the specified CSS selector.
* If no CSS selector is specified, returns all of the TestComponentAs inside the current wrapper.
* If no matching TestComponentA is found, returns an empty array.
*
* @param {string} [selector] CSS Selector
* @returns {Array<TestComponentAWrapper>}
*/
findAllTestComponentAs(selector?: string): Array<TestComponentAWrapper>;

/**
* Returns the wrapper of the closest parent TestComponentA for the current element,
* or the element itself if it is an instance of TestComponentA.
* If no TestComponentA is found, returns `null`.
*
* @returns {TestComponentAWrapper | null}
*/
findClosestTestComponentA(): TestComponentAWrapper | null;
/**
* Returns the wrapper of the first TestComponentB that matches the specified CSS selector.
* If no CSS selector is specified, returns the wrapper of the first TestComponentB.
* If no matching TestComponentB is found, returns `null`.
*
* @param {string} [selector] CSS Selector
* @returns {TestComponentBWrapper | null}
*/
findTestComponentB(selector?: string): TestComponentBWrapper | null;

/**
* Returns an array of TestComponentB wrapper that matches the specified CSS selector.
* If no CSS selector is specified, returns all of the TestComponentBs inside the current wrapper.
* If no matching TestComponentB is found, returns an empty array.
*
* @param {string} [selector] CSS Selector
* @returns {Array<TestComponentBWrapper>}
*/
findAllTestComponentBs(selector?: string): Array<TestComponentBWrapper>;

/**
* Returns the wrapper of the closest parent TestComponentB for the current element,
* or the element itself if it is an instance of TestComponentB.
* If no TestComponentB is found, returns `null`.
*
* @returns {TestComponentBWrapper | null}
*/
findClosestTestComponentB(): TestComponentBWrapper | null;
}
}


ElementWrapper.prototype.findTestComponentA = function(selector) {
let rootSelector = `.${TestComponentAWrapper.rootSelector}`;
if("legacyRootSelector" in TestComponentAWrapper && TestComponentAWrapper.legacyRootSelector){
rootSelector = `:is(.${TestComponentAWrapper.rootSelector}, .${TestComponentAWrapper.legacyRootSelector})`;
}
// 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, TestComponentAWrapper);
};

ElementWrapper.prototype.findAllTestComponentAs = function(selector) {
return this.findAllComponents(TestComponentAWrapper, selector);
};
ElementWrapper.prototype.findTestComponentB = function(selector) {
let rootSelector = `.${TestComponentBWrapper.rootSelector}`;
if("legacyRootSelector" in TestComponentBWrapper && TestComponentBWrapper.legacyRootSelector){
rootSelector = `:is(.${TestComponentBWrapper.rootSelector}, .${TestComponentBWrapper.legacyRootSelector})`;
}
// 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, TestComponentBWrapper);
};

ElementWrapper.prototype.findAllTestComponentBs = function(selector) {
return this.findAllComponents(TestComponentBWrapper, selector);
};

ElementWrapper.prototype.findClosestTestComponentA = function() {
// casting to 'any' is needed to avoid this issue with generics
// https://github.com/microsoft/TypeScript/issues/29132
return (this as any).findClosestComponent(TestComponentAWrapper);
};
ElementWrapper.prototype.findClosestTestComponentB = function() {
// casting to 'any' is needed to avoid this issue with generics
// https://github.com/microsoft/TypeScript/issues/29132
return (this as any).findClosestComponent(TestComponentBWrapper);
};

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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { ComponentWrapper, createWrapper } from '@cloudscape-design/test-utils-core/dom';

export default class TestComponentAWrapper extends ComponentWrapper {
static rootSelector = 'awsui_button_1ueyk_1xee3_5';
static legacyRootSelector = 'awsui_button_2oldf_2oldf_5';

findChild() {
return createWrapper().find('.test-component-a-child');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { ComponentWrapper, createWrapper } from '@cloudscape-design/test-utils-core/dom';

export class ChildWrapper extends ComponentWrapper {
static rootSelector = 'test-component-b-child';

findContent() {
return createWrapper().find('.test-component-b-child-content');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { ComponentWrapper, createWrapper } from '@cloudscape-design/test-utils-core/dom';
import { ChildWrapper } from './child-wrapper';

export default class TestComponentBWrapper extends ComponentWrapper {
static rootSelector = 'test-component-b-root';

findChild(): ChildWrapper {
return createWrapper().find(`.${ChildWrapper.rootSelector}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { BaseElementWrapper, ComponentWrapper, createWrapper } from '@cloudscape-design/test-utils-core/dom';

export { ComponentWrapper, createWrapper };
export class ElementWrapper extends BaseElementWrapper {}
23 changes: 23 additions & 0 deletions src/converter/test/test-utils-generator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,27 @@ describe(`${generateTestUtils.name}`, () => {
expect(container.querySelector(componentBChildSelector).textContent).toBe('First Component B');
});
});

describe('custom namespace generated dom index file', () => {
test('component finders work on the custom ElementWrapper', async () => {
const { default: createWrapper } = await import('./mock-test-utils-custom-namespace/dom');
const container = renderTestNode();
const wrapper = createWrapper(container);

expect(wrapper.findTestComponentA().getElement().textContent).toBe('First Component A');
expect(wrapper.findTestComponentB().getElement().textContent).toBe('First Component B');
});

test('custom ElementWrapper prototype is independent from the cloudscape ElementWrapper prototype', async () => {
const { ElementWrapper: CustomElementWrapper } = await import('./mock-test-utils-custom-namespace/dom');
const { ElementWrapper: CloudscapeElementWrapper } = await import('./mock-test-utils/dom');

// The two ElementWrapper classes are distinct — prototype mutations on one don't affect the other
expect(CustomElementWrapper.prototype).not.toBe(CloudscapeElementWrapper.prototype);
// Both share BaseElementWrapper as their parent, but their own prototypes are separate
expect(Object.getPrototypeOf(CustomElementWrapper.prototype)).toBe(
Object.getPrototypeOf(CloudscapeElementWrapper.prototype),
);
});
});
});
3 changes: 2 additions & 1 deletion src/core/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ export class AbstractWrapper<ElementType extends Element>
}
}

export class ElementWrapper<ElementType extends Element = HTMLElement> extends AbstractWrapper<ElementType> {}
export class BaseElementWrapper<ElementType extends Element = HTMLElement> extends AbstractWrapper<ElementType> {}
export class ElementWrapper<ElementType extends Element = HTMLElement> extends BaseElementWrapper<ElementType> {}
export class ComponentWrapper<ElementType extends Element = HTMLElement> extends AbstractWrapper<ElementType> {}
export function createWrapper(root: Element = document.body) {
if (document && document.body && !document.body.contains(root)) {
Expand Down
3 changes: 2 additions & 1 deletion src/core/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ export class AbstractWrapper implements IElementWrapper<string, MultiElementWrap
}
}

export class ElementWrapper extends AbstractWrapper {}
export class BaseElementWrapper extends AbstractWrapper {}
export class ElementWrapper extends BaseElementWrapper {}

export class ComponentWrapper extends AbstractWrapper {}

Expand Down
Loading
Loading