Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
252 lines
7.1 KiB
TypeScript
252 lines
7.1 KiB
TypeScript
import { render, screen } from '@testing-library/react';
|
|
import {
|
|
cloneElement,
|
|
createElement,
|
|
createRef,
|
|
type HTMLAttributes,
|
|
type ReactElement,
|
|
} from 'react';
|
|
import { renderToString } from 'react-dom/server';
|
|
import {
|
|
afterEach,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
type MockInstance,
|
|
vi,
|
|
} from 'vitest';
|
|
|
|
import { ThemeProvider } from '@ui/theme-constants/ThemeProvider';
|
|
import { isDefined } from '@ui/utilities/utils/isDefined';
|
|
|
|
import { type ComponentConformanceCase } from './types/ComponentConformanceCase';
|
|
import { type ComponentConformanceOptions } from './types/ComponentConformanceOptions';
|
|
import { hideGlobals } from './utils/hideGlobals';
|
|
|
|
const CONFORMANCE_TEST_ID = 'conformance-root';
|
|
const CONFORMANCE_CLASS_NAME = 'conformance-consumer-class';
|
|
const CONFORMANCE_RENDER_CLASS_NAME = 'conformance-render-class';
|
|
const CONFORMANCE_DATA_ATTRIBUTE_VALUE = 'conformance-data-value';
|
|
const CONFORMANCE_ARIA_LABEL = 'conformance-aria-label';
|
|
const CONFORMANCE_STYLE = { marginTop: '7px' };
|
|
const SERVER_HIDDEN_GLOBALS = [
|
|
'window',
|
|
'document',
|
|
'navigator',
|
|
'CSS',
|
|
'Node',
|
|
'Element',
|
|
'HTMLElement',
|
|
] as const;
|
|
|
|
export const runComponentConformance = ({
|
|
name,
|
|
element,
|
|
refInstanceOf,
|
|
wrapper: Wrapper,
|
|
ownClassName,
|
|
renderPropTagName = 'span',
|
|
skip = [],
|
|
}: ComponentConformanceOptions) => {
|
|
const compose = (probeProps: Record<string, unknown>): ReactElement => {
|
|
const probe = cloneElement(element, {
|
|
'data-testid': CONFORMANCE_TEST_ID,
|
|
...probeProps,
|
|
});
|
|
|
|
return (
|
|
<ThemeProvider colorScheme="light">
|
|
{isDefined(Wrapper) ? <Wrapper>{probe}</Wrapper> : probe}
|
|
</ThemeProvider>
|
|
);
|
|
};
|
|
|
|
const itUnlessSkipped = (
|
|
conformanceCase: ComponentConformanceCase,
|
|
title: string,
|
|
run: () => void,
|
|
) => {
|
|
const register = skip.includes(conformanceCase) ? it.skip : it;
|
|
|
|
register(title, run);
|
|
};
|
|
|
|
describe(`${name} conformance`, () => {
|
|
let consoleErrorSpy: MockInstance;
|
|
let consoleWarnSpy: MockInstance;
|
|
|
|
beforeEach(() => {
|
|
consoleErrorSpy = vi
|
|
.spyOn(console, 'error')
|
|
.mockImplementation(() => undefined);
|
|
consoleWarnSpy = vi
|
|
.spyOn(console, 'warn')
|
|
.mockImplementation(() => undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
consoleErrorSpy.mockRestore();
|
|
consoleWarnSpy.mockRestore();
|
|
});
|
|
|
|
itUnlessSkipped(
|
|
'consoleErrorsAndWarnings',
|
|
'renders without console errors or warnings',
|
|
() => {
|
|
render(compose({}));
|
|
|
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
itUnlessSkipped('ref', 'forwards ref to the root DOM node', () => {
|
|
const ref = createRef<Element>();
|
|
|
|
render(compose({ ref }));
|
|
|
|
expect(ref.current).toBeInstanceOf(refInstanceOf);
|
|
expect(ref.current).toBe(screen.getByTestId(CONFORMANCE_TEST_ID));
|
|
});
|
|
|
|
itUnlessSkipped('ref', 'cleans up callback refs on unmount', () => {
|
|
const cleanup = vi.fn();
|
|
const ref = vi.fn(() => cleanup);
|
|
const { unmount } = render(compose({ ref }));
|
|
|
|
expect(ref).toHaveBeenCalledWith(screen.getByTestId(CONFORMANCE_TEST_ID));
|
|
expect(cleanup).toHaveBeenCalledTimes(ref.mock.calls.length - 1);
|
|
|
|
unmount();
|
|
|
|
expect(cleanup).toHaveBeenCalledTimes(ref.mock.calls.length);
|
|
});
|
|
|
|
itUnlessSkipped('dataAttributes', 'passes data attributes through', () => {
|
|
render(compose({ 'data-conformance': CONFORMANCE_DATA_ATTRIBUTE_VALUE }));
|
|
|
|
expect(screen.getByTestId(CONFORMANCE_TEST_ID)).toHaveAttribute(
|
|
'data-conformance',
|
|
CONFORMANCE_DATA_ATTRIBUTE_VALUE,
|
|
);
|
|
});
|
|
|
|
itUnlessSkipped('ariaAttributes', 'passes aria attributes through', () => {
|
|
render(compose({ 'aria-label': CONFORMANCE_ARIA_LABEL }));
|
|
|
|
expect(screen.getByTestId(CONFORMANCE_TEST_ID)).toHaveAttribute(
|
|
'aria-label',
|
|
CONFORMANCE_ARIA_LABEL,
|
|
);
|
|
});
|
|
|
|
itUnlessSkipped('className', 'merges the consumer className', () => {
|
|
render(compose({ className: CONFORMANCE_CLASS_NAME }));
|
|
|
|
const rootNode = screen.getByTestId(CONFORMANCE_TEST_ID);
|
|
|
|
expect(rootNode).toHaveClass(CONFORMANCE_CLASS_NAME);
|
|
expect(rootNode.className.split(' ').pop()).toBe(CONFORMANCE_CLASS_NAME);
|
|
|
|
if (isDefined(ownClassName)) {
|
|
expect(rootNode).toHaveClass(ownClassName);
|
|
}
|
|
});
|
|
|
|
itUnlessSkipped(
|
|
'style',
|
|
'applies the consumer style to the root node',
|
|
() => {
|
|
render(compose({ style: CONFORMANCE_STYLE }));
|
|
|
|
expect(screen.getByTestId(CONFORMANCE_TEST_ID)).toHaveStyle(
|
|
CONFORMANCE_STYLE,
|
|
);
|
|
},
|
|
);
|
|
|
|
itUnlessSkipped('renderProp', 'renders the element given to render', () => {
|
|
const ref = createRef<Element>();
|
|
|
|
render(
|
|
compose({
|
|
ref,
|
|
className: CONFORMANCE_CLASS_NAME,
|
|
render: createElement(renderPropTagName, {
|
|
className: CONFORMANCE_RENDER_CLASS_NAME,
|
|
'data-conformance-render': CONFORMANCE_DATA_ATTRIBUTE_VALUE,
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const rootNode = screen.getByTestId(CONFORMANCE_TEST_ID);
|
|
|
|
expect(rootNode.localName).toBe(renderPropTagName);
|
|
expect(rootNode).toHaveAttribute(
|
|
'data-conformance-render',
|
|
CONFORMANCE_DATA_ATTRIBUTE_VALUE,
|
|
);
|
|
expect(rootNode).toHaveClass(
|
|
CONFORMANCE_CLASS_NAME,
|
|
CONFORMANCE_RENDER_CLASS_NAME,
|
|
);
|
|
expect(ref.current).toBe(rootNode);
|
|
|
|
if (isDefined(ownClassName)) {
|
|
expect(rootNode).toHaveClass(ownClassName);
|
|
}
|
|
});
|
|
|
|
itUnlessSkipped(
|
|
'renderProp',
|
|
'renders the element returned by the render function',
|
|
() => {
|
|
const ref = createRef<Element>();
|
|
|
|
render(
|
|
compose({
|
|
ref,
|
|
className: CONFORMANCE_CLASS_NAME,
|
|
render: (renderProps: HTMLAttributes<HTMLElement>) =>
|
|
createElement(renderPropTagName, {
|
|
...renderProps,
|
|
'data-conformance-render': CONFORMANCE_DATA_ATTRIBUTE_VALUE,
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const rootNode = screen.getByTestId(CONFORMANCE_TEST_ID);
|
|
|
|
expect(rootNode.localName).toBe(renderPropTagName);
|
|
expect(rootNode).toHaveAttribute(
|
|
'data-conformance-render',
|
|
CONFORMANCE_DATA_ATTRIBUTE_VALUE,
|
|
);
|
|
expect(rootNode).toHaveClass(CONFORMANCE_CLASS_NAME);
|
|
expect(ref.current).toBe(rootNode);
|
|
},
|
|
);
|
|
|
|
itUnlessSkipped('unmount', 'unmounts cleanly', () => {
|
|
const { unmount } = render(compose({}));
|
|
|
|
expect(() => unmount()).not.toThrow();
|
|
expect(screen.queryByTestId(CONFORMANCE_TEST_ID)).toBeNull();
|
|
});
|
|
|
|
itUnlessSkipped('serverRender', 'renders to a string without a DOM', () => {
|
|
const restoreGlobals = hideGlobals(SERVER_HIDDEN_GLOBALS);
|
|
|
|
try {
|
|
expect(() => renderToString(compose({}))).not.toThrow();
|
|
} finally {
|
|
restoreGlobals();
|
|
}
|
|
|
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
};
|