Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. ui.lukebennett.dev
Something went wrong. Try again.
8.0 kB · 218 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219import '../../styles/app.css';import '@luke-ui/theme-tactile/stylesheet.css';import { Prose } from '@luke-ui/react/prose';import { Provider } from '@luke-ui/react/provider';import spriteSheetHref from '@luke-ui/react/spritesheet.svg?url&no-inline';import { createMemoryHistory, createRootRoute, createRouter, RouterProvider,} from '@tanstack/react-router';import type { ReactNode } from 'react';import { act } from 'react';import type { Root } from 'react-dom/client';import { createRoot } from 'react-dom/client';import { afterEach, assert, expect, test } from 'vite-plus/test';import { CodeBlock } from '../code-block/code-block.js';import { ComponentPropsTable } from '../component-props-table.js';import { ExampleBlock } from '../example-block.js';import { IconGallery } from '../icon-gallery.js';import { DocsThemeRoot } from '../theme-controls.js';
let container: HTMLElement | undefined;let root: Root | undefined;
afterEach(() => { if (root) act(() => root?.unmount()); container?.remove(); container = undefined; root = undefined;});
// Elements Prose styles. A widget root that opts out must not carry these fingerprints.const PROSE_BLOCKS = 'p, ul, ol, li, dl, dt, dd, blockquote, pre, hr, figure, figcaption, table, th, td';
/** Prose-owned values that neither the UA nor typical widget chrome use without Preflight. */function hasProseFingerprint(element: Element): boolean { const style = getComputedStyle(element); if (element.matches('ul, ol')) { return style.paddingInlineStart === '24px'; } if (element.matches('li')) { return style.marginBlockStart === '12px'; } if (element.matches('table')) { // Prose forces `display: block` so a narrow table can scroll; the UA keeps `table`. return style.display === 'block'; } if (element.matches('th, td')) { return style.paddingBlock === '8px' && style.paddingInline === '12px'; } if (element.matches('pre')) { return style.paddingBlock === '12px' && style.paddingInline === '16px'; } if (element.matches('p')) { // Capsize trim boxes are Prose-only on plain paragraphs. const before = getComputedStyle(element, '::before'); return before.display === 'table' && before.content.includes('""'); } if (element.matches('blockquote, figure')) { // Prose sibling gaps (`* + blockquote|figure` → space.40). UA margins differ. return style.marginBlockStart === '40px'; } if (element.matches('hr')) { return style.blockSize === '0px' && style.borderBlockStartStyle === 'solid'; } return false;}
function proseLeaks(widget: Element) { return [...widget.querySelectorAll(PROSE_BLOCKS)] .filter((element) => element.closest('[data-prose-example]') == null) .filter(hasProseFingerprint) .map((element) => element.outerHTML.slice(0, 80));}
function widgetRoot(selector: string) { assert(container, 'expected a container'); const widget = container.querySelector(selector); assert(widget, `expected ${selector}`); return widget;}
test('keeps Prose rhythm off an ExampleBlock and its example', async () => { await renderInProse(<ExampleBlock src="lockup/element" title="Lockup: Element" />); const frame = widgetRoot('.not-prose');
expect(getComputedStyle(frame).marginBlockStart).toBe('16px'); expect(frame.querySelectorAll('li')).toHaveLength(2); expect(proseLeaks(frame)).toEqual([]);});
test('lets a Prose example inside an ExampleBlock keep its own rhythm', async () => { await renderInProse(<ExampleBlock src="prose/lists" title="Prose: Lists" />); const frame = widgetRoot('.not-prose'); const nestedList = frame.querySelector('ul'); assert(nestedList, 'expected a list inside the Prose example');
expect(getComputedStyle(nestedList).listStyleType).toBe('disc'); expect(getComputedStyle(nestedList).paddingInlineStart).toBe('24px'); const items = nestedList.querySelectorAll(':scope > li'); assert(items[1], 'expected a second list item'); expect(getComputedStyle(items[1]).marginBlockStart).not.toBe('0px');});
test('keeps Prose rhythm off a CodeBlock', async () => { await renderInProse(<CodeBlock code={'const a = 1;\nconst b = 2;'} title="Source" />); const figure = widgetRoot('figure');
expect(getComputedStyle(figure).marginBlockStart).toBe('0px'); expect(proseLeaks(figure)).toEqual([]);});
test('keeps Prose rhythm off a component props table', async () => { await renderInProse( <ComponentPropsTable id="props" type={{ isDisabled: { default: 'false', description: 'Disables the control.', type: 'boolean' }, label: { description: 'Visible label.', type: 'string' }, }} />, ); const table = widgetRoot('#props');
expect(table.classList.contains('not-prose')).toBe(true); expect(table.textContent).toContain('Visible label.'); expect(proseLeaks(table)).toEqual([]);});
test('keeps Prose rhythm off the icon gallery', async () => { await renderInProse(<IconGallery />); const gallery = widgetRoot('.not-prose');
expect(proseLeaks(gallery)).toEqual([]);});
test('still applies Prose rhythm to content around the widgets', async () => { await renderInProse( <> <p>First.</p> <p data-testid="second">Second.</p> <ul data-testid="list"> <li>One</li> <li data-testid="second-item">Two</li> </ul> <blockquote data-testid="quote">Quoted.</blockquote> <figure data-testid="figure"> <img alt="" height={1} src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" width={1} /> </figure> <table data-testid="table"> <tbody> <tr> <td data-testid="cell">Cell</td> </tr> </tbody> </table> </>, );
const second = widgetRoot('[data-testid="second"]'); expect(getComputedStyle(second).marginBlockStart).toBe('32px'); expect(getComputedStyle(second, '::before').display).toBe('table'); expect(getComputedStyle(widgetRoot('[data-testid="list"]')).listStyleType).toBe('disc'); expect(getComputedStyle(widgetRoot('[data-testid="list"]')).paddingInlineStart).toBe('24px'); expect(getComputedStyle(widgetRoot('[data-testid="second-item"]')).marginBlockStart).toBe('12px'); expect(getComputedStyle(widgetRoot('[data-testid="quote"]')).marginBlockStart).toBe('40px'); expect(getComputedStyle(widgetRoot('[data-testid="figure"]')).marginBlockStart).toBe('40px'); expect(getComputedStyle(widgetRoot('[data-testid="table"]')).display).toBe('block'); const cell = widgetRoot('[data-testid="cell"]'); expect(getComputedStyle(cell).paddingBlock).toBe('8px'); expect(getComputedStyle(cell).paddingInline).toBe('12px'); // Positive control: fingerprints that proseLeaks must keep detecting, including cells. const leaks = proseLeaks(widgetRoot('[data-testid="article"]')); expect(leaks.some((html) => html.includes('data-testid="cell"'))).toBe(true);});
async function renderInProse(children: ReactNode) { const rootRoute = createRootRoute({ component: () => ( <Provider spritesheetHref={spriteSheetHref}> <DocsThemeRoot> <Prose data-testid="article"> <p>Lead paragraph.</p> {children} </Prose> </DocsThemeRoot> </Provider> ), }); const router = createRouter({ history: createMemoryHistory({ initialEntries: ['/'] }), routeTree: rootRoute, });
container = document.body.appendChild(document.createElement('div')); container.style.inlineSize = '800px'; root = createRoot(container); await act(async () => { root?.render(<RouterProvider router={router} />); await router.load(); }); // Example modules load lazily behind Suspense, so keep wrapping the wait in `act`. // Bound it so a module that never resolves fails instead of hanging the test. const deadline = Date.now() + 10_000; while (container.querySelector('[role="region"][aria-label^="Loading"]') != null) { if (Date.now() > deadline) { throw new Error( `Timed out waiting for the example module to load. Rendered: ${container.textContent ?? ''}`, ); } // oxlint-disable-next-line no-await-in-loop await act(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); }); }}