Files
upstream-mirror e7da97194a
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
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

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();
});
});
};