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(
Visible text
,
);
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(
Same tab
,
);
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();
});
}