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): ReactElement => { const probe = cloneElement(element, { 'data-testid': CONFORMANCE_TEST_ID, ...probeProps, }); return ( {isDefined(Wrapper) ? {probe} : probe} ); }; 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(); 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(); 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(); render( compose({ ref, className: CONFORMANCE_CLASS_NAME, render: (renderProps: HTMLAttributes) => 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(); }); }); };