Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. ui.lukebennett.dev
Something went wrong. Try again.
12 kB · 341 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342import '../../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, expect, test, vi } from 'vite-plus/test';import { page, userEvent } from 'vite-plus/test/context';import { DocsThemeRoot } from '../theme-controls.js';import { Card, Cards, createMdxHeading, MdxCode, MdxFence, MdxLink, MdxTable,} from './mdx-elements.js';
let container: HTMLElement | undefined;let root: Root | undefined;
afterEach(async () => { if (root) act(() => root?.unmount()); container?.remove(); container = undefined; root = undefined; vi.restoreAllMocks(); vi.useRealTimers(); await page.viewport(1024, 800);});
const H2 = createMdxHeading(2);const H3 = createMdxHeading(3);
test('opens an external link in a new tab without an opener', async () => { await renderMdx(<MdxLink href="https://example.com/docs">External</MdxLink>);
const link = page.getByRole('link', { name: 'External' }); await expect.element(link).toHaveAttribute('href', 'https://example.com/docs'); await expect.element(link).toHaveAttribute('target', '_blank'); await expect.element(link).toHaveAttribute('rel', 'noreferrer noopener');});
test('keeps an internal link, with and without a hash, in the same tab', async () => { await renderMdx( <> <MdxLink href="/components/actions/button">Button</MdxLink> <MdxLink href="/docs/typography#fonts">Fonts</MdxLink> <MdxLink href="#local">Local</MdxLink> </>, );
const expected = [ ['Button', '/components/actions/button'], ['Fonts', '/docs/typography#fonts'], ['Local', '#local'], ] as const; await Promise.all( expected.map(async ([name, href]) => { const link = page.getByRole('link', { name }); await expect.element(link).toHaveAttribute('href', href); expect(link.element().getAttribute('target')).toBeNull(); }), );});
const linkPaths = [ ['an external link', 'https://example.com/docs'], ['a hash-only link', '#local'], ['an internal link', '/docs/typography#fonts'],] as const;
for (const [kind, href] of linkPaths) { test(`keeps the anchor attributes an author writes on ${kind}`, async () => { await renderMdx( <MdxLink aria-controls="panel" aria-expanded="false" aria-haspopup="menu" aria-label="Read more" className="author-class" data-foo="bar" data-testid="the-link" download="file.txt" hrefLang="fr" href={href} id="author-id" title="Link title" type="text/html" > Visible text </MdxLink>, );
const link = page.getByTestId('the-link'); await expect.element(link).toHaveAttribute('href', href); await expect.element(link).toHaveAttribute('title', 'Link title'); await expect.element(link).toHaveAttribute('id', 'author-id'); await expect.element(link).toHaveAttribute('aria-label', 'Read more'); await expect.element(link).toHaveAttribute('download', 'file.txt'); await expect.element(link).toHaveAttribute('data-foo', 'bar'); await expect.element(link).toHaveAttribute('hreflang', 'fr'); await expect.element(link).toHaveAttribute('type', 'text/html'); await expect.element(link).toHaveAttribute('aria-expanded', 'false'); await expect.element(link).toHaveAttribute('aria-controls', 'panel'); await expect.element(link).toHaveAttribute('aria-haspopup', 'menu'); // React Aria Components still owns the class list, so the Link recipe class sits beside the author's. expect(link.element().classList.contains('author-class')).toBe(true); });}
test('merges an author rel into the external link safety tokens', async () => { await renderMdx( <MdxLink href="https://example.com" rel="nofollow noopener"> Merged </MdxLink>, );
const rel = page.getByRole('link', { name: 'Merged' }).element().getAttribute('rel'); expect(rel?.split(' ').sort()).toEqual(['nofollow', 'noopener', 'noreferrer']);});
test('lets an author target and rel override the external link defaults', async () => { await renderMdx( <MdxLink href="https://example.com" rel="author" target="_self"> Same tab </MdxLink>, );
const link = page.getByRole('link', { name: 'Same tab' }); await expect.element(link).toHaveAttribute('target', '_self'); await expect.element(link).toHaveAttribute('rel', 'author');});
test('renders a link without an href as a placeholder anchor that keeps its attributes', async () => { await renderMdx( <MdxLink data-testid="placeholder" id="target" title="Marker"> Marker text </MdxLink>, );
const anchor = page.getByTestId('placeholder'); await expect.element(anchor).toHaveAttribute('id', 'target'); await expect.element(anchor).toHaveAttribute('title', 'Marker'); expect(anchor.element().hasAttribute('href')).toBe(false); expect(page.getByRole('link')).not.toBeInTheDocument();});
test('links a heading to its own anchor and copies the full URL with the hash', 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); await renderMdx(<H2 id="usage">Usage</H2>);
await expect.element(page.getByRole('link', { name: 'Usage' })).toHaveAttribute('href', '#usage');
await act(async () => { await userEvent.click(page.getByRole('button', { name: 'Copy Anchor Link' })); }); const copiedUrl = new URL(writeText.mock.calls[0]?.[0] ?? ''); expect(copiedUrl.hash).toBe('#usage'); expect(copiedUrl.pathname).toBe(window.location.pathname);
await expect.element(page.getByRole('button', { name: 'Copied Anchor Link' })).toBeVisible(); await expect.element(page.getByRole('status')).toHaveTextContent('Copied');
await act(async () => { vi.advanceTimersByTime(1500); }); await expect.element(page.getByRole('button', { name: 'Copy Anchor Link' })).toBeVisible();});
for (const [level, Heading] of [ [2, H2], [3, H3],] as const) { test(`names an h${level} by its text alone while the copy button stays inside it`, async () => { vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); await renderMdx(<Heading id="usage">Usage</Heading>);
const heading = page.getByRole('heading', { exact: true, level, name: 'Usage' }); await expect.element(heading).toBeVisible(); const button = page.getByRole('button', { name: 'Copy Anchor Link' }); expect(heading.element().contains(button.element())).toBe(true);
await userEvent.tab(); await userEvent.tab(); await expect.element(button).toHaveFocus();
// Fake only the revert timer after focus: 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'] }); await act(async () => { await userEvent.keyboard('{Enter}'); }); await expect.element(page.getByRole('status')).toHaveTextContent('Copied'); await expect .element(page.getByRole('heading', { exact: true, level, name: 'Usage' })) .toBeVisible(); await expect.element(page.getByRole('button', { name: 'Copied Anchor Link' })).toBeVisible(); });}
test('keeps the heading id as the deep-link target and gives the anchor a distinct id', async () => { await renderMdx( <> <H2 id="usage">Usage</H2> <H2 id="usage-title">Usage title</H2> </>, );
const ids = [...document.querySelectorAll('[id]')].map((element) => element.id); expect(new Set(ids).size).toBe(ids.length); const heading = page.getByRole('heading', { name: 'Usage', exact: true }).element(); expect(heading.id).toBe('usage'); expect(heading.querySelector('a')?.getAttribute('href')).toBe('#usage');});
test('keeps the anchor button a 24px target', async () => { await renderMdx(<H2 id="usage">Usage</H2>);
const { height, width } = page .getByRole('button', { name: 'Copy Anchor Link' }) .element() .getBoundingClientRect(); expect([width, height]).toEqual([24, 24]);});
test('renders a heading without an id as plain text', async () => { await renderMdx(<H2>Plain</H2>);
await expect.element(page.getByRole('heading', { name: 'Plain', level: 2 })).toBeVisible(); expect(page.getByRole('link')).not.toBeInTheDocument(); expect(page.getByRole('button')).not.toBeInTheDocument();});
test('names a wide table as a focusable region once it overflows', async () => { await page.viewport(400, 800); await renderMdx( <Prose style={{ inlineSize: '12rem' }}> <MdxTable> <tbody> <tr> <td style={{ minInlineSize: '30rem' }}>Wide cell</td> </tr> </tbody> </MdxTable> </Prose>, );
const region = page.getByRole('region', { name: 'Scrollable table' }); await expect.element(region).toBeVisible(); expect(region.element().tabIndex).toBe(0); // Prose makes a plain table scroll itself. The docs table leaves that to its ScrollFade. const scroller = region.element(); const table = scroller.querySelector('table')!; expect(scroller.scrollWidth).toBeGreaterThan(scroller.clientWidth); expect(table.scrollWidth).toBe(table.clientWidth);});
test('leaves fenced code as plain code and styles inline code', async () => { await renderMdx( <> <p> <MdxCode>inline</MdxCode> </p> <MdxFence> <MdxCode>fenced</MdxCode> </MdxFence> </>, );
const inline = page.getByText('inline').element(); const fenced = page.getByText('fenced').element(); // Fence context routes `<code>` to a plain element for the CodeBlock; inline uses Code. expect(inline.closest('pre')).toBeNull(); expect(fenced.closest('pre')).not.toBeNull(); expect(inline.tagName).toBe('CODE'); expect(fenced.tagName).toBe('CODE');});
test('renders a card as a link with a title and its description', async () => { await renderMdx( <Cards> <Card href="/components/layout/stack" title="Stack"> <p>Stack children on the block axis.</p> </Card> </Cards>, );
const link = page.getByRole('link', { name: /Stack/ }); await expect.element(link).toHaveAttribute('href', '/components/layout/stack'); await expect.element(link.getByRole('heading', { name: 'Stack', level: 3 })).toBeVisible(); await expect.element(link.getByText('Stack children on the block axis.')).toBeVisible();});
test('names an external card as opening in a new tab', async () => { await renderMdx( <Cards> <Card href="https://example.com/guide" title="External guide"> Read the external guide. </Card> </Cards>, );
const link = page.getByRole('link', { name: /External guide.*opens in a new tab/i }); await expect.element(link).toHaveAttribute('href', 'https://example.com/guide'); await expect.element(link).toHaveAttribute('target', '_blank'); await expect.element(link).toHaveAttribute('rel', 'noreferrer noopener');});
async function renderMdx(children: ReactNode) { const rootRoute = createRootRoute({ component: () => ( <Provider spritesheetHref={spriteSheetHref}> <DocsThemeRoot>{children}</DocsThemeRoot> </Provider> ), }); const router = createRouter({ history: createMemoryHistory({ initialEntries: ['/docs/example'] }), routeTree: rootRoute, }); container = document.body.appendChild(document.createElement('div')); root = createRoot(container); await act(async () => { root?.render(<RouterProvider router={router} />); await router.load(); });}