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, 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(External); 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( <> Button Fonts Local , ); 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( , ); 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( Merged , ); const rel = page.getByRole('link', { name: 'Merged' }).element().getAttribute('rel'); expect(rel?.split(' ').toSorted()).toEqual(['nofollow', 'noopener', 'noreferrer']); }); test('lets an author target and rel override the external link defaults', async () => { await renderMdx( , ); 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( Marker text , ); 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(

Usage

); 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(Usage); 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( <>

Usage

Usage title

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

Usage

); 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(

Plain

); 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( Wide cell , ); 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( <>

inline

fenced , ); const inline = page.getByText('inline').element(); const fenced = page.getByText('fenced').element(); // Fence context routes `` 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(

Stack children on the block axis.

, ); 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( Read the external guide. , ); 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: () => ( {children} ), }); 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(); await router.load(); }); }