import '../../styles/app.css'; import '@luke-ui/react/themes/tactile/stylesheet.css'; import { themeClassName as tactileThemeClassName } from '@luke-ui/react/themes/tactile'; import axe from 'axe-core'; 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 { 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(); }); test('copies plain source including leading and trailing whitespace', async () => { const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); const source = ' pnpm add @luke-ui/react '; renderCodeBlock(); 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(); 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 = 'rendered'; renderCodeBlock(); await act(async () => { await userEvent.click(page.getByRole('button', { name: 'Copy' })); }); expect(writeText).toHaveBeenCalledWith(source); expect(page.getByText('rendered').element()).toBeTruthy(); }); 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 = ``; const html = [ '', `${longImport}`, `${longBox}`, '{children}', ';', '', ].join(''); renderCodeBlock(); 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('hides the copy control when allowCopy is false', () => { renderCodeBlock(); expect(page.getByRole('button', { name: 'Copy' })).not.toBeInTheDocument(); }); test('centres overlay copy on the first line of a one-line fence', () => { renderCodeBlock(); 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(
, ); 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(); 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(); 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')); container.className = `luke-ui-theme ${tactileThemeClassName}`; root = createRoot(container); act(() => { root?.render({node}); }); } function assert(condition: boolean, message: string): asserts condition { if (!condition) throw new Error(message); }