Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. ui.lukebennett.dev
Something went wrong. Try again.
15 kB · 375 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376import '../../styles/app.css';import '@luke-ui/theme-tactile/stylesheet.css';import axe from 'axe-core';import type { ReactNode } from 'react';import { act, createRef } from 'react';import type { Root } from 'react-dom/client';import { createRoot } from 'react-dom/client';import { afterEach, expect, test, vi } from 'vite-plus/test';import { page, userEvent } from 'vite-plus/test/context';import { StoryWrapper } from '../../lib/story-wrapper';import { CodeBlock } from './code-block';
let container: HTMLElement | undefined;let root: Root | undefined;
afterEach(() => { if (root) act(() => root?.unmount()); container?.remove(); container = undefined; root = undefined; vi.restoreAllMocks(); vi.useRealTimers();});
test('copies plain source including leading and trailing whitespace', async () => { // Fake only the revert timer: a real 1.5s timeout can fire before `expect.element` starts // polling on a slow runner, so "Copied" is never observed. vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }); const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); const source = ' pnpm add @luke-ui/react ';
renderCodeBlock(<CodeBlock code={source} title="Terminal" />);
await act(async () => { await userEvent.click(page.getByRole('button', { name: 'Copy' })); });
expect(writeText).toHaveBeenCalledWith(source); await expect.element(page.getByRole('button', { name: 'Copied' })).toBeVisible(); await expect.element(page.getByRole('status')).toMatchTextContent('Copied');});
test('announces clipboard failure without claiming Copied', async () => { vi.spyOn(navigator.clipboard, 'writeText').mockRejectedValue(new Error('denied'));
renderCodeBlock(<CodeBlock code="const value = 1;" title="Source" />);
await act(async () => { await userEvent.click(page.getByRole('button', { name: 'Copy' })); });
await expect.element(page.getByRole('status')).toMatchTextContent('Could not copy code'); await expect.element(page.getByRole('button', { name: 'Copy' })).toBeVisible(); expect(page.getByRole('button', { name: 'Copied' })).not.toBeInTheDocument();});
test('copies copyText instead of rendered highlighted text', async () => { const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); const source = 'const value = 1;\n'; const html = '<code class="shiki"><span>rendered</span></code>';
renderCodeBlock(<CodeBlock copyText={source} html={html} />);
await act(async () => { await userEvent.click(page.getByRole('button', { name: 'Copy' })); });
expect(writeText).toHaveBeenCalledWith(source); expect(page.getByText('rendered').element()).toBeTruthy();});
test('updates scroll accessibility when the mounted source and label change', async () => { await page.viewport(400, 800); renderCodeBlock(<CodeBlock allowCopy={false} code="short" viewportLabel="Source" />); const source = container?.querySelector('pre'); assert(source != null, 'Expected a source element'); const viewport = source.parentElement; assert(viewport != null, 'Expected a scroll viewport'); expect(page.getByRole('region').query()).toBeNull();
rerenderCodeBlock(<CodeBlock allowCopy={false} code={'x'.repeat(200)} viewportLabel="Source" />); await expect.element(page.getByRole('region', { name: 'Source' })).toBeVisible(); expect(viewport).toHaveAttribute('tabindex', '0'); expect(container?.querySelector('pre')).toBe(source);
rerenderCodeBlock( <CodeBlock allowCopy={false} code={'x'.repeat(200)} viewportLabel="Updated source" />, ); await expect.element(page.getByRole('region', { name: 'Updated source' })).toBeVisible(); expect(page.getByRole('region', { name: 'Source', exact: true }).query()).toBeNull();
rerenderCodeBlock(<CodeBlock allowCopy={false} code="short" viewportLabel="Updated source" />); await expect.poll(() => page.getByRole('region').query()).toBeNull(); expect(viewport).not.toHaveAttribute('tabindex'); expect(viewport).not.toHaveAttribute('aria-label');});
test('a tall standalone fence grows to its natural height and only scrolls sideways', async () => { await page.viewport(400, 600); const lines = Array.from({ length: 80 }, (_, index) => `const value${index} = ${index};`); lines[40] = `const long = '${'x'.repeat(200)}';`; renderCodeBlock(<CodeBlock allowCopy={false} code={lines.join('\n')} title="Tall" />); const viewport = container?.querySelector('pre')?.parentElement; assert(viewport != null, 'Expected a scroll viewport');
const region = page.getByRole('region', { name: 'Tall' }); await expect.element(region).toBeVisible(); expect(viewport.getBoundingClientRect().height).toBeGreaterThan(window.innerHeight); expect(viewport.clientHeight).toBe(viewport.scrollHeight); expect(viewport.scrollWidth).toBeGreaterThan(viewport.clientWidth); expect(viewport).toHaveAttribute('tabindex', '0');
region.element().focus(); await userEvent.keyboard('{ArrowRight}'); await expect.poll(() => viewport.scrollLeft).toBeGreaterThan(0); expect(viewport.scrollTop).toBe(0);});
test('keeps the focus ring visible on a standalone horizontally scrollable fence', async () => { await page.viewport(320, 720); renderCodeBlock(<CodeBlock allowCopy={false} code={'x'.repeat(200)} title="Source" />); const viewport = page.getByRole('region', { name: 'Source' }).element(); expect(viewport.scrollWidth).toBeGreaterThan(viewport.clientWidth);
// Keyboard input makes programmatic focus show its ring. await userEvent.keyboard('{Shift}'); viewport.focus(); expectFocusRingUnclipped(viewport);});
test('updates scroll accessibility when the viewport narrows and widens', async () => { await page.viewport(1000, 800); renderCodeBlock(<CodeBlock allowCopy={false} code={'x'.repeat(80)} title="Source" />); expect(page.getByRole('region').query()).toBeNull();
await page.viewport(320, 800); const viewport = page.getByRole('region', { name: 'Source' }); await expect.element(viewport).toBeVisible(); expect(viewport).toHaveAttribute('tabindex', '0');
await page.viewport(1000, 800); await expect.poll(() => viewport.query()).toBeNull();});
test('composes source refs through content changes, ref replacement, and unmount', () => { const cleanup = vi.fn<() => void>(); const callbackRef = vi.fn<(node: HTMLPreElement | null) => (() => void) | void>((node) => { if (node) return cleanup; }); const figureRef = createRef<HTMLElement>(); const objectRef = createRef<HTMLPreElement>(); renderCodeBlock(<CodeBlock code="first" ref={figureRef} sourceRef={callbackRef} />); const source = container?.querySelector('pre'); assert(source != null, 'Expected a source element'); expect(callbackRef).toHaveBeenCalledExactlyOnceWith(source); expect(figureRef.current).toBe(source.closest('figure'));
rerenderCodeBlock( <CodeBlock html="<code>second</code>" ref={figureRef} sourceRef={callbackRef} />, ); expect(source).toHaveTextContent('second'); expect(callbackRef).toHaveBeenCalledTimes(1); expect(cleanup).not.toHaveBeenCalled();
rerenderCodeBlock(<CodeBlock code="third" ref={figureRef} sourceRef={objectRef} />); expect(cleanup).toHaveBeenCalledTimes(1); expect(objectRef.current).toBe(source); expect(source).toHaveTextContent('third');
act(() => root?.unmount()); root = undefined; expect(objectRef.current).toBeNull(); expect(figureRef.current).toBeNull();});
test('copies child source while excluding ignored spans', async () => { const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); renderCodeBlock( <CodeBlock> <code> first<span className="nd-copy-ignore">annotation</span>second </code> </CodeBlock>, );
await act(async () => { await userEvent.click(page.getByRole('button', { name: 'Copy' })); }); expect(writeText).toHaveBeenCalledWith('first\nsecond');});
test('scrolls Shiki line spans across the full figure width under overlay copy', async () => { await page.viewport(320, 720);
const longImport = `import { Box } from '@luke-ui/react/box';`; const longBox = `<Box maxInlineSize="42rem" padding="sp24">`; const html = [ '<code class="shiki">', `<span class="line">${longImport}</span>`, `<span class="line">${longBox}</span>`, '<span class="line">{children}</span>', '<span class="line"></Box>;</span>', '</code>', ].join('');
renderCodeBlock(<CodeBlock copyText={`${longImport}\n${longBox}`} html={html} />);
const viewport = page.getByRole('region', { name: 'Code' }).element(); expect(viewport.scrollWidth).toBeGreaterThan(viewport.clientWidth);
const figure = viewport.closest('figure'); assert(figure != null, 'Expected a figure ancestor'); // Overlay copy must not carve out a flex/grid side column. expect(viewport.getBoundingClientRect().width).toBeGreaterThan(figure.clientWidth * 0.9);
const pre = viewport.querySelector('pre'); assert(pre != null, 'Expected a pre element'); expect(pre.getBoundingClientRect().width).toBeGreaterThan(viewport.clientWidth);});
test('keeps empty Shiki line spans one line tall', () => { // The highlight plugin drops the `shiki` class, so Fumadocs' `.line:empty` rule does not apply. const html = [ '<code>', '<span class="line">import a from "a";</span>', '<span class="line"></span>', '<span class="line">export default a;</span>', '</code>', ].join('');
renderCodeBlock(<CodeBlock copyText={'import a from "a";\n\nexport default a;'} html={html} />);
const lines = container?.querySelectorAll('.line'); assert(lines != null && lines.length === 3, 'Expected three line spans'); const [first, empty, last] = [...lines].map((line) => line.getBoundingClientRect()); assert(first != null && empty != null && last != null, 'Expected line boxes');
expect(empty.height).toBeGreaterThan(0); expect(empty.height).toBeGreaterThanOrEqual(first.height - 1); expect(last.top - first.top).toBeGreaterThanOrEqual(first.height + empty.height - 1);});
test('hides the copy control when allowCopy is false', () => { renderCodeBlock(<CodeBlock allowCopy={false} code="secret" />);
expect(page.getByRole('button', { name: 'Copy' })).not.toBeInTheDocument();});
test('centres overlay copy on the first line of a one-line fence', () => { renderCodeBlock(<CodeBlock code="pnpm add @luke-ui/react react-aria-components" />);
const copyButton = page.getByRole('button', { name: 'Copy' }).element(); const figure = copyButton.closest('figure'); assert(figure != null, 'Expected a figure ancestor'); const line = figure.querySelector('.line') ?? figure.querySelector('pre'); assert(line != null, 'Expected a code line');
const buttonBox = copyButton.getBoundingClientRect(); const lineBox = line.getBoundingClientRect(); const buttonMidY = (buttonBox.top + buttonBox.bottom) / 2; const lineMidY = (lineBox.top + lineBox.bottom) / 2; expect(Math.abs(buttonMidY - lineMidY)).toBeLessThanOrEqual(2);});
test('opts out of prose inline-code chrome on the fence code element', () => { renderCodeBlock( <div className="prose"> <CodeBlock code={'const value = 1;'} /> </div>, );
const figure = page.getByRole('button', { name: 'Copy' }).element().closest('figure'); assert(figure != null, 'Expected a figure ancestor'); expect(figure.classList.contains('not-prose')).toBe(true);
const code = figure.querySelector('pre code'); assert(code != null, 'Expected a code element'); const styles = getComputedStyle(code); expect(styles.borderWidth).toBe('0px'); expect(styles.padding).toBe('0px'); expect(styles.backgroundColor).toBe('rgba(0, 0, 0, 0)');});
for (const copyCase of [ { name: 'untitled overlay copy', title: undefined as string | undefined }, { name: 'titled header copy', title: 'Source' },]) { test(`scrolls horizontally and keeps ${copyCase.name} on the physical right in RTL docs`, async () => { await page.viewport(320, 720); document.documentElement.dir = 'rtl';
try { renderCodeBlock(<CodeBlock code={'x'.repeat(200)} title={copyCase.title} />);
const regionName = copyCase.title ?? 'Code'; const viewport = page.getByRole('region', { name: regionName }).element(); expect(viewport.scrollWidth).toBeGreaterThan(viewport.clientWidth); expect(viewport.tabIndex).toBe(0);
const copyButton = page.getByRole('button', { name: 'Copy' }).element(); const figure = copyButton.closest('figure'); assert(figure != null, 'Expected a figure ancestor');
// Figure forces LTR like Fumadocs so copy stays on the physical right. expect(figure.getAttribute('dir')).toBe('ltr'); expect(getComputedStyle(figure).direction).toBe('ltr');
const figureBox = figure.getBoundingClientRect(); const buttonBox = copyButton.getBoundingClientRect(); const buttonMidX = (buttonBox.left + buttonBox.right) / 2; const figureMidX = (figureBox.left + figureBox.right) / 2; expect(buttonMidX).toBeGreaterThan(figureMidX);
// Full-width scrollport — not a side column beside the button. expect(viewport.getBoundingClientRect().width).toBeGreaterThan(figure.clientWidth * 0.9); } finally { document.documentElement.dir = 'ltr'; } });}
test('the CodeBlock scene has no axe violations', async () => { renderCodeBlock(<CodeBlock code={'const example = "hello";'} title="Example" />);
const results = await axe.run(container!); expect( results.violations.map((violation) => ({ id: violation.id, nodes: violation.nodes.map((node) => node.html), })), ).toEqual([]);});
function renderCodeBlock(node: ReactNode) { container = document.body.appendChild(document.createElement('div')); root = createRoot(container); rerenderCodeBlock(node);}
function rerenderCodeBlock(node: ReactNode) { act(() => { root?.render(<StoryWrapper>{node}</StoryWrapper>); });}
function assert(condition: boolean, message: string): asserts condition { if (!condition) throw new Error(message);}
/** Asserts the focus ring is drawn and no overflow ancestor clips it. */function expectFocusRingUnclipped(element: HTMLElement | SVGElement) { expect(element).toHaveFocus(); expect(element.matches(':focus-visible')).toBe(true); const { outlineOffset, outlineWidth } = getComputedStyle(element); const extent = Math.max(0, Number.parseFloat(outlineOffset) + Number.parseFloat(outlineWidth)); const rect = element.getBoundingClientRect(); // Room between the ring and each overflow clip edge. Negative means the ring is cut off. const gaps: Array<number> = []; for ( let ancestor = element.parentElement; ancestor && ancestor !== container; ancestor = ancestor.parentElement ) { const { overflowX, overflowY } = getComputedStyle(ancestor); const clip = ancestor.getBoundingClientRect(); if (overflowX !== 'visible') { gaps.push(rect.left - extent - clip.left, clip.right - (rect.right + extent)); } if (overflowY !== 'visible') { gaps.push(rect.top - extent - clip.top, clip.bottom - (rect.bottom + extent)); } } expect(gaps.length).toBeGreaterThan(0); expect(Math.min(...gaps)).toBeGreaterThanOrEqual(-0.5);}