import '../../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(); 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(); 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(); 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( , ); 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(); const gallery = widgetRoot('.not-prose'); expect(proseLeaks(gallery)).toEqual([]); }); test('still applies Prose rhythm to content around the widgets', async () => { await renderInProse( <>

First.

Second.

  • One
  • Two
Quoted.
Cell
, ); 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: () => (

Lead paragraph.

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