diff --git a/apps/docs/src/components/page-actions.browser.test.tsx b/apps/docs/src/components/page-actions.browser.test.tsx index 1943650f..1e361f02 100644 --- a/apps/docs/src/components/page-actions.browser.test.tsx +++ b/apps/docs/src/components/page-actions.browser.test.tsx @@ -63,15 +63,6 @@ test('omits the Storybook, React Aria, and Source pills when the page has none', await expect.element(page.getByRole('link', { name: 'Edit on GitHub' })).toBeVisible(); }); -test('the Source and Edit on GitHub pills stay distinguishable, even though both use the GitHub mark', async () => { - renderActions({ githubUrl, markdownUrl, reactAriaUrl: null, sourceUrl, storybookUrl: null }); - - const sourceLink = page.getByRole('link', { name: 'Source' }); - const editLink = page.getByRole('link', { name: 'Edit on GitHub' }); - await expect.element(sourceLink).toHaveAttribute('href', sourceUrl); - await expect.element(editLink).toHaveAttribute('href', githubUrl); -}); - test('reports the copied state after Copy Markdown succeeds', async () => { vi.stubGlobal( 'fetch', diff --git a/apps/docs/src/components/playground/editor-skeleton.browser.test.tsx b/apps/docs/src/components/playground/editor-skeleton.browser.test.tsx index 0e6e1308..b618c2d5 100644 --- a/apps/docs/src/components/playground/editor-skeleton.browser.test.tsx +++ b/apps/docs/src/components/playground/editor-skeleton.browser.test.tsx @@ -1,7 +1,5 @@ import '../../styles/app.css'; -import '@luke-ui/react/themes/paper/stylesheet.css'; import '@luke-ui/react/themes/tactile/stylesheet.css'; -import { themeClassName as paperThemeClassName } from '@luke-ui/react/themes/paper'; import { themeClassName as tactileThemeClassName } from '@luke-ui/react/themes/tactile'; import { act } from 'react'; import type { Root } from 'react-dom/client'; @@ -20,23 +18,16 @@ afterEach(() => { root = undefined; }); -for (const [themeName, themeClassName] of [ - ['Tactile', tactileThemeClassName], - ['Paper', paperThemeClassName], -] as const) { - for (const mode of ['light', 'dark'] as const) { - test(`uses an opaque ${themeName} ${mode} popover surface behind its loading pill`, () => { - renderSkeleton(themeClassName, mode); - - const label = page.getByText('Loading editor', { exact: true }).nth(1).element(); - const pillBackground = getComputedStyle(label.parentElement as HTMLElement).backgroundColor; - const expectedBackground = getSemanticSurfaceBackground(); - - expect(pillBackground).toBe(expectedBackground); - expect(pillBackground).not.toBe('rgba(0, 0, 0, 0)'); - }); - } -} +test('uses an opaque popover surface behind its loading pill', () => { + renderSkeleton(tactileThemeClassName, 'light'); + + const label = page.getByText('Loading editor', { exact: true }).nth(1).element(); + const pillBackground = getComputedStyle(label.parentElement as HTMLElement).backgroundColor; + const expectedBackground = getSemanticSurfaceBackground(); + + expect(pillBackground).toBe(expectedBackground); + expect(pillBackground).not.toBe('rgba(0, 0, 0, 0)'); +}); function renderSkeleton(themeClassName: string, mode: 'light' | 'dark') { container = document.body.appendChild(document.createElement('div')); diff --git a/apps/docs/src/components/playground/playground-layout.browser.test.ts b/apps/docs/src/components/playground/playground-layout.browser.test.ts index 83d453fd..d777c27b 100644 --- a/apps/docs/src/components/playground/playground-layout.browser.test.ts +++ b/apps/docs/src/components/playground/playground-layout.browser.test.ts @@ -17,8 +17,6 @@ test('uses the desktop panel layout before JavaScript corrects the server orient expect(getComputedStyle(group as HTMLDivElement).flexDirection).toBe('row'); expect(getComputedStyle(separator).inlineSize).toBe('1px'); expect(getComputedStyle(separator).blockSize).toBe('400px'); - expect(getComputedStyle(separator, '::after').width).toBe('6px'); - expect(getComputedStyle(separator, '::after').height).toBe('64px'); }); test('uses the mobile panel layout independently of the JavaScript orientation', async () => { @@ -28,8 +26,6 @@ test('uses the mobile panel layout independently of the JavaScript orientation', expect(getComputedStyle(group as HTMLDivElement).flexDirection).toBe('column'); expect(getComputedStyle(separator).inlineSize).toBe('390px'); expect(getComputedStyle(separator).blockSize).toBe('1px'); - expect(getComputedStyle(separator, '::after').width).toBe('64px'); - expect(getComputedStyle(separator, '::after').height).toBe('6px'); }); function renderPanelLayout(inlineDirection: 'column' | 'row') { diff --git a/apps/docs/src/components/site-nav.browser.test.tsx b/apps/docs/src/components/site-nav.browser.test.tsx index a97d79c5..dcdf73f1 100644 --- a/apps/docs/src/components/site-nav.browser.test.tsx +++ b/apps/docs/src/components/site-nav.browser.test.tsx @@ -42,34 +42,11 @@ test('marks only the current destination on desktop and keeps search available', await expect.element(page.getByRole('button', { name: /Search/ })).toBeVisible(); }); -test('marks the components destination active on a component page', async () => { - await page.viewport(1024, 800); - await renderAt('/components/actions/button', ); - - await expect - .element(page.getByRole('link', { name: 'Components' })) - .toHaveAttribute('aria-current', 'page'); - expect(getCurrentLinks()).toHaveLength(1); -}); - -test('marks the docs destination active on a docs page', async () => { - await page.viewport(1024, 800); - await renderAt('/docs/installation', ); - - await expect - .element(page.getByRole('link', { name: 'Docs' })) - .toHaveAttribute('aria-current', 'page'); - expect(getCurrentLinks()).toHaveLength(1); -}); - test('offers search and theme controls from the mobile bar with no destination active on the landing page', async () => { await page.viewport(390, 800); await renderAt('/', ); expect(getCurrentLinks()).toHaveLength(0); - - const wordmark = page.getByRole('link', { name: 'Luke UI' }).element(); - expect(wordmark.scrollWidth).toBeLessThanOrEqual(wordmark.clientWidth); await expect.element(page.getByRole('button', { name: 'Open Search' })).toBeVisible(); const themeTrigger = page.getByRole('button', { name: 'Theme' }); @@ -86,17 +63,6 @@ test('leaves every destination inactive on the 404 page', async () => { expect(getCurrentLinks()).toHaveLength(0); }); -test('links to the repository with an accessible name', async () => { - await page.viewport(1024, 800); - await renderAt('/', ); - - const repositoryLink = page.getByRole('link', { name: 'GitHub repository' }); - await expect.element(repositoryLink).toBeVisible(); - await expect - .element(repositoryLink) - .toHaveAttribute('href', 'https://github.com/lukebennett88/luke-ui'); -}); - async function renderAt(pathname: string, children: ReactNode) { const rootRoute = createRootRoute({ component: () => ( diff --git a/apps/docs/src/components/theme-controls.browser.test.tsx b/apps/docs/src/components/theme-controls.browser.test.tsx index be017d4e..0251b1d3 100644 --- a/apps/docs/src/components/theme-controls.browser.test.tsx +++ b/apps/docs/src/components/theme-controls.browser.test.tsx @@ -62,28 +62,6 @@ test('persists theme identity and colour mode independently', async () => { await expect.poll(() => getThemeRoot().dataset.colorMode).toBe('dark'); }); -test('theme profile is labelled and operable with arrow keys', async () => { - renderTheme(); - - const group = page.getByRole('radiogroup', { name: 'Theme profile' }); - const tactileProfile = page.getByRole('radio', { name: 'Tactile' }); - const paperProfile = page.getByRole('radio', { name: 'Paper' }); - - expect(group.element()).toBeInTheDocument(); - expect(tactileProfile).toBeChecked(); - - await userEvent.tab(); - expect(tactileProfile.element()).toHaveFocus(); - - await userEvent.keyboard('{ArrowRight}'); - expect(paperProfile.element()).toHaveFocus(); - - await userEvent.keyboard('{Enter}'); - - expect(paperProfile).toBeChecked(); - expect(document.documentElement).toHaveClass(paperThemeClassName); -}); - test('system colour mode follows the platform preference and drives the docs chrome', async () => { await emulateColorScheme('dark'); renderTheme(, { defaultTheme: 'system', enableSystem: true }); diff --git a/apps/docs/src/components/theme-dom-guard.browser.test.tsx b/apps/docs/src/components/theme-dom-guard.browser.test.tsx deleted file mode 100644 index d03aa69e..00000000 --- a/apps/docs/src/components/theme-dom-guard.browser.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import '../styles/app.css'; -import '@luke-ui/react/themes/tactile/stylesheet.css'; -import { rootClassName } from '@luke-ui/react/theme'; -import { themeClassName as tactileThemeClassName } from '@luke-ui/react/themes/tactile'; -import { cx } from '@luke-ui/react/utils'; -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 } from 'vite-plus/test'; -import ColorModeOverride from '../examples/theming/color-mode-override'; -import SemanticVariables from '../examples/theming/semantic-variables'; - -let container: HTMLElement | undefined; -let root: Root | undefined; - -afterEach(() => { - if (root) act(() => root?.unmount()); - container?.remove(); - document.documentElement.removeAttribute('class'); - container = undefined; - root = undefined; -}); - -function renderInDocsThemeRoot(children: ReactNode) { - container = document.body.appendChild(document.createElement('div')); - container.className = `luke-ui-theme ${tactileThemeClassName}`; - root = createRoot(container); - act(() => { - root?.render(children); - }); -} - -/** - * Every identity class starts with this prefix, emitted by the theme layer's internal - * `themeClassName` helper. It is the marker for a supported identity boundary, so a nested - * identity is caught without depending on any single bundled theme name or on that helper, which - * is not part of the public API. - */ -const IDENTITY_CLASS_PREFIX = 'luke-ui-theme-'; - -// A literal identity class rather than an import: the same shape `themeClassName('product')` -// used to produce, kept as a string now that helper is internal to the theme package. -const NESTED_IDENTITY_CLASS_NAME = 'luke-ui-theme-product'; - -function nestedIdentityElementsWithin(themeRoot: Element): Array { - const nested: Array = []; - for (const element of themeRoot.querySelectorAll('*')) { - if (Array.from(element.classList).some((token) => token.startsWith(IDENTITY_CLASS_PREFIX))) { - nested.push(element); - } - } - return nested; -} - -test('detects a supported identity class nested beneath the docs identity root', () => { - renderInDocsThemeRoot( -
- Docs themed example -
- Nested application root -
-
, - ); - - const host = container; - if (!host) throw new Error('Expected the docs theme root'); - - const nested = nestedIdentityElementsWithin(host); - expect(nested).toHaveLength(1); - expect(nested[0]?.className).toContain(NESTED_IDENTITY_CLASS_NAME); -}); - -test('detects a nested identity when the docs identity sits on ', () => { - // Mirrors `DocsThemeRoot`'s topology: the docs identity now lives on ``, not on a `div` - // the docs own. An application root rendered anywhere in the document with its own identity - // class is nested beneath it, and is now structurally easier to hit. - document.documentElement.classList.add(tactileThemeClassName); - - container = document.body.appendChild(document.createElement('div')); - root = createRoot(container); - act(() => { - root?.render( -
- Nested application root -
, - ); - }); - - const nested = nestedIdentityElementsWithin(document.documentElement); - expect(nested).toHaveLength(1); - expect(nested[0]?.className).toContain(NESTED_IDENTITY_CLASS_NAME); -}); - -test('rendering the theming examples under the docs identity root adds no nested identity', () => { - renderInDocsThemeRoot( - <> - - - , - ); - - const host = container; - if (!host) throw new Error('Expected the docs theme root'); - - expect(nestedIdentityElementsWithin(host)).toEqual([]); -}); diff --git a/apps/docs/src/components/token-explorer.browser.test.tsx b/apps/docs/src/components/token-explorer.browser.test.tsx deleted file mode 100644 index 39139fe6..00000000 --- a/apps/docs/src/components/token-explorer.browser.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import '../styles/app.css'; -import '@luke-ui/react/themes/tactile/stylesheet.css'; -import { - createMemoryHistory, - createRootRoute, - createRouter, - RouterProvider, -} from '@tanstack/react-router'; -import { act } from 'react'; -import type { Root } from 'react-dom/client'; -import { createRoot } from 'react-dom/client'; -import { afterEach, expect, test } from 'vite-plus/test'; -import { page } from 'vite-plus/test/context'; -import { StoryWrapper } from '../lib/story-wrapper.js'; -import { tokenPurposeGroups } from '../lib/token-purpose-groups.js'; -import { TokenExplorer } from './token-explorer.js'; - -let container: HTMLElement | undefined; -let root: Root | undefined; - -afterEach(() => { - if (root) act(() => root?.unmount()); - container?.remove(); - container = undefined; - root = undefined; -}); - -test('expands every purpose and omits mixed sample treatments', async () => { - await renderExplorer(); - - for (const group of tokenPurposeGroups) { - const trigger = page - .getByRole('button', { name: `${group.title} ${group.tokens.length}` }) - .element(); - expect(trigger.getAttribute('aria-expanded')).toBe('true'); - } - - const typographyTrigger = page - .getByRole('button', { - name: `Typography ${tokenPurposeGroups.find((group) => group.id === 'typography')?.tokens.length}`, - }) - .element(); - const panelId = typographyTrigger.getAttribute('aria-controls'); - if (panelId === null) throw new Error('Typography trigger has no disclosure panel.'); - - const headers = Array.from(document.getElementById(panelId)?.querySelectorAll('th') ?? []).map( - (header) => header.textContent?.trim(), - ); - expect(headers).toEqual(['vars path', 'CSS variable']); -}); - -async function renderExplorer() { - const rootRoute = createRootRoute({ - component: () => ( - - - - ), - }); - const router = createRouter({ - history: createMemoryHistory({ initialEntries: ['/token-reference'] }), - routeTree: rootRoute, - }); - - container = document.body.appendChild(document.createElement('div')); - root = createRoot(container); - await act(async () => { - root?.render(); - await router.load(); - }); -} diff --git a/apps/docs/src/lib/component-page-tree.test.ts b/apps/docs/src/lib/component-page-tree.test.ts index 8b88d2df..47694e32 100644 --- a/apps/docs/src/lib/component-page-tree.test.ts +++ b/apps/docs/src/lib/component-page-tree.test.ts @@ -1,5 +1,5 @@ import { searchPath } from 'fumadocs-core/breadcrumb'; -import type { Item, Node, Root } from 'fumadocs-core/page-tree'; +import type { Node, Root } from 'fumadocs-core/page-tree'; import { loader } from 'fumadocs-core/source'; import { expect, test } from 'vite-plus/test'; import { docs } from '../../.source/server.js'; @@ -15,50 +15,6 @@ function allNodes(tree: Root): ReadonlyArray { return tree.fallback ? [...tree.children, ...tree.fallback.children] : tree.children; } -/** Find a clickable node by URL. Display names repeat across groups (Actions Button vs Primitives Button). */ -function findClickableNode(nodes: ReadonlyArray, url: string): Item | undefined { - for (const node of nodes) { - if (node.type === 'page' && node.url === url) return node; - if (node.type === 'folder') { - if (node.index?.url === url) return node.index; - const found = findClickableNode(node.children, url); - if (found !== undefined) return found; - } - } - return undefined; -} - -function findAnyNodeWithUrl(nodes: ReadonlyArray, url: string): Node | undefined { - for (const node of nodes) { - if (node.type === 'page' && node.url === url) return node; - if (node.type === 'folder') { - if (node.index?.url === url) return node; - const found = findAnyNodeWithUrl(node.children, url); - if (found !== undefined) return found; - } - } - return undefined; -} - -test('guides live under /docs, not the docs content root', () => { - const tree = source.getPageTree(); - const nodes = allNodes(tree); - - expect(findClickableNode(nodes, '/docs/installation')?.url).toBe('/docs/installation'); - expect(findAnyNodeWithUrl(nodes, '/installation')).toBeUndefined(); - expect(source.getPage(['docs', 'installation'])).toBeDefined(); - expect(source.getPage(['installation'])).toBeUndefined(); -}); - -const components: ReadonlyArray<{ group: string; name: string }> = [ - { group: 'actions', name: 'button' }, - { group: 'actions', name: 'icon-button' }, - { group: 'forms', name: 'text-field' }, - { group: 'layout', name: 'box' }, - { group: 'typography', name: 'heading' }, - { group: 'primitives', name: 'visually-hidden' }, -]; - /** The root folder in a `searchPath` result, per `getBreadcrumbItemsFromPath` in `fumadocs-core/breadcrumb`. */ function findRootFolder(path: ReadonlyArray) { return path.find((node) => node.type === 'folder' && node.root); @@ -86,24 +42,3 @@ test('remapping a Props URL to its Guide URL keeps tree matching under the Compo expect(findRootFolder(remappedPath ?? [])?.name).toBe('Components'); expect(findRootFolder(remappedPath ?? [])?.name).toBe(findRootFolder(guidePath ?? [])?.name); }); - -for (const { group, name } of components) { - test(`the sidebar entry for /components/${group}/${name} is clickable and hides its Props page`, () => { - const tree = source.getPageTree(); - const nodes = allNodes(tree); - const guideUrl = `/components/${group}/${name}`; - const propsUrl = `${guideUrl}/props`; - - const clickable = findClickableNode(nodes, guideUrl); - expect(clickable, `expected a clickable node for ${guideUrl}`).toBeDefined(); - expect(clickable?.url).toBe(guideUrl); - - expect( - findAnyNodeWithUrl(nodes, propsUrl), - `${propsUrl} should not appear in the page tree`, - ).toBeUndefined(); - - expect(source.getPage(['components', group, name])).toBeDefined(); - expect(source.getPage(['components', group, name, 'props'])).toBeDefined(); - }); -} diff --git a/apps/docs/src/lib/theming-docs.test.ts b/apps/docs/src/lib/theming-docs.test.ts index 2db06cce..ec597b7f 100644 --- a/apps/docs/src/lib/theming-docs.test.ts +++ b/apps/docs/src/lib/theming-docs.test.ts @@ -2,42 +2,9 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { extname, resolve } from 'node:path'; import { expect, test } from 'vite-plus/test'; -const contentDir = resolve(import.meta.dirname, '../../content/docs/docs'); const allDocsContentDir = resolve(import.meta.dirname, '../../content/docs'); const examplesDir = resolve(import.meta.dirname, '../examples'); -const SETTLED_THEMING_PAGES = [ - 'theming', - 'applying-a-theme', - 'color', - 'authoring-a-theme', - 'token-reference', -]; - -const SUPERSEDED_THEMING_PAGES = [ - 'theme-system', - 'theme', - 'color-mode', - 'spacing', - 'radius', - 'shadow', -]; - -function docsPagePath(slug: string): string { - return resolve(contentDir, `${slug}.mdx`); -} - -function readDocsPage(slug: string): string { - return readFileSync(docsPagePath(slug), 'utf8'); -} - -function docsMetaPages(): Array { - const meta = JSON.parse(readFileSync(resolve(contentDir, 'meta.json'), 'utf8')) as { - pages?: Array; - }; - return meta.pages ?? []; -} - function findAllMdxFiles(directory: string): Array { return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const path = resolve(directory, entry.name); @@ -46,37 +13,6 @@ function findAllMdxFiles(directory: string): Array { }); } -test('the settled theming pages exist', () => { - for (const slug of SETTLED_THEMING_PAGES) { - expect(existsSync(docsPagePath(slug))).toBe(true); - } -}); - -test('the superseded theming pages no longer exist', () => { - for (const slug of SUPERSEDED_THEMING_PAGES) { - expect(existsSync(docsPagePath(slug))).toBe(false); - } -}); - -test('the Theming navigation lists only the settled pages', () => { - const pages = docsMetaPages(); - const start = pages.indexOf('---Theming---'); - const end = pages.indexOf('---Guides---'); - - expect(start).toBeGreaterThanOrEqual(0); - expect(end).toBeGreaterThan(start); - expect(pages.slice(start + 1, end)).toEqual(SETTLED_THEMING_PAGES); -}); - -test('spacing, radius, and shadow live on the token reference', () => { - const tokenReference = readDocsPage('token-reference'); - - expect(tokenReference).toContain('## Choosing a scale'); - for (const example of ['overview/spacing-scale', 'overview/radius-roles', 'overview/depth']) { - expect(tokenReference).toContain(`src="${example}"`); - } -}); - test('no rendered example applies a theme identity class', () => { // `themeClassName` is the export name every per-theme entrypoint // (`@luke-ui/react/themes/paper`, `@luke-ui/react/themes/tactile`) uses for its identity class, diff --git a/docs/TESTING.md b/docs/TESTING.md index 5299a2f4..e93effdc 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -51,8 +51,8 @@ composition, prop plumbing, styling hooks, and behaviour that Luke UI adds or de Each component opts into the applicable tier in the component test manifest: - **Universal**: documented element, `ref`, `className`, `id`, and `data-*` forwarding. -- **Field-shaped**: `inputRef`, native `name`/form participation, `onBlur`, and label, description, - and error association. +- **Field-shaped**: object and callback `inputRef`, native `name`/form participation, `onBlur`, and + label, description, and error association. - **None**: an explicit exception for a component that does not satisfy either contract. The shared conformance helpers test these contracts once. Do not repeat them in individual tests. diff --git a/packages/@luke-ui/react/src/box/box.browser.test.tsx b/packages/@luke-ui/react/src/box/box.browser.test.tsx index f15f2c43..31a7c7d3 100644 --- a/packages/@luke-ui/react/src/box/box.browser.test.tsx +++ b/packages/@luke-ui/react/src/box/box.browser.test.tsx @@ -37,24 +37,20 @@ test('renders a responsive layout at the retained breakpoints', async () => { await page.viewport(640, 800); expect(getComputedStyle(box).flexDirection).toBe('column'); - expect(getComputedStyle(box).gap).toBe('8px'); await page.viewport(768, 800); expect(getComputedStyle(box).flexDirection).toBe('row'); - expect(getComputedStyle(box).gap).toBe('24px'); }); test('renders semantic and consumer-owned elements with resolved props', () => { const semanticResult = render( - + Account summary content , ); const section = semanticResult.locator.getByRole('region', { name: 'Account summary' }); expect(section.element().tagName).toBe('SECTION'); - expect(getComputedStyle(section.element()).display).toBe('flex'); - expect(getComputedStyle(section.element()).padding).toBe('16px'); let refElement: HTMLElement | null = null; let receivedAriaLabel = false; diff --git a/packages/@luke-ui/react/src/checkbox/checkbox.browser.test.tsx b/packages/@luke-ui/react/src/checkbox/checkbox.browser.test.tsx index 74a65614..6a6d1767 100644 --- a/packages/@luke-ui/react/src/checkbox/checkbox.browser.test.tsx +++ b/packages/@luke-ui/react/src/checkbox/checkbox.browser.test.tsx @@ -1,7 +1,5 @@ -import { createRef } from 'react'; import { expect, test } from 'vite-plus/test'; -import type { Locator } from 'vite-plus/test/context'; -import { cdp, page, userEvent } from 'vite-plus/test/context'; +import { cdp, page } from 'vite-plus/test/context'; import { testFieldShapedConformance, testIntegration } from '../conformance/helpers.js'; import { render } from '../test-utils/render.js'; import { componentTestRegistration } from './component-test-registration.js'; @@ -100,102 +98,6 @@ test('the icon indicator stays out of the accessible name', async () => { expect(axNode.name?.value).toBe('Invalid'); }); -// `inputRef` must land on the hidden ``, not on the wrapper -// `
` React Aria's own `ref` targets. -test('resolves an inputRef object to the checkbox input, not a wrapper', async () => { - const ref = createRef(); - render( - - Terms - , - ); - - const checkbox = page.getByRole('checkbox', { name: 'Terms' }); - await expect.element(checkbox).toBeVisible(); - - expect(ref.current).toBeInstanceOf(HTMLInputElement); - expect(ref.current).toBe(checkbox.element()); -}); - -// React Aria types its own `inputRef` as a ref object, so this is the case our -// widened prop plus the `useObjectRef` bridge exists for: React Hook Form's -// `field.ref` is a callback. -test('resolves a callback inputRef to the checkbox input', async () => { - const resolved: Array = []; - render( - { - resolved.push(node); - }} - name="terms" - > - Terms - , - ); - - const checkbox = page.getByRole('checkbox', { name: 'Terms' }); - await expect.element(checkbox).toBeVisible(); - - expect(resolved.at(-1)).toBeInstanceOf(HTMLInputElement); - expect(resolved.at(-1)).toBe(checkbox.element()); -}); - -test('forwards name to the input so a native form submit collects it', async () => { - const { locator: scene } = render( -
- Terms -
, - ); - - const checkbox = page.getByRole('checkbox', { name: 'Terms' }); - await expect.element(checkbox).toHaveAttribute('name', 'terms'); - - const form = scene.element().querySelector('form'); - if (form == null) throw new Error('Expected the form element.'); - expect(new FormData(form).get('terms')).toBe(null); - - // The input itself is visually hidden behind the indicator, so the clickable - // content label is the only hit target — same as every other test in this file. - await userEvent.click(contentFor(checkbox)); - expect(new FormData(form).get('terms')).toBe('on'); -}); - -test('forwards onBlur to the input', async () => { - const blurs: Array = []; - render( - <> - { - blurs.push('terms'); - }} - > - Terms - - - , - ); - - const checkbox = page.getByRole('checkbox', { name: 'Terms' }); - await expect.element(checkbox).toBeVisible(); - - // Tabbed rather than clicked: the input is visually hidden behind the indicator, - // so it is not its own hit target. - await userEvent.tab(); - await expect.element(checkbox).toHaveFocus(); - expect(blurs).toEqual([]); - - await userEvent.click(page.getByRole('button', { name: 'Next' })); - expect(blurs).toEqual(['terms']); -}); - -/** The clickable `