` DOM props.
- exportName: 'CodeProps',
- name: 'CodeProps',
- path: 'packages/@luke-ui/react/src/core/code/code.tsx',
- props: ['lineClamp', 'textWrap'],
- },
- {
- exportName: 'LoadingSpinnerProps',
- name: 'LoadingSpinnerProps',
- path: 'packages/@luke-ui/react/src/core/loading-spinner/loading-spinner.tsx',
- props: ['aria-label', 'children', 'color', 'isLoading', 'size'],
- },
- {
- exportName: 'InputGroupInputProps',
- name: 'InputGroupInputProps',
- path: 'packages/@luke-ui/react/src/core/primitives/input-group/input-group.tsx',
- props: [
- 'aria-label',
- 'className',
- 'inputMode',
- 'onHoverChange',
- 'onHoverEnd',
- 'onHoverStart',
- 'placeholder',
- 'ref',
- 'render',
- 'size',
- ],
- },
- {
- // The five button-shaped types below all inherit `AriaBaseButtonProps` (react-aria's
- // `useButton`), which declares `ARIA_BASE_BUTTON_LONG_TAIL` directly alongside genuinely
- // documented siblings on the same interface body — see the comment above that list.
exportName: 'ButtonProps',
name: 'core ButtonProps',
path: 'packages/@luke-ui/react/src/core/button/button.tsx',
@@ -901,11 +655,14 @@ const PINNED_VISIBLE_PROPS: ReadonlyArray<{
},
];
-test.each(PINNED_VISIBLE_PROPS)(
- '$name shows exactly its documented props',
- async ({ exportName, path, props }) => {
- const names = await visiblePropNames(path, exportName);
- expect([...names].sort()).toEqual([...props].sort());
- },
- TS_MORPH_TEST_TIMEOUT,
-);
+for (const pinned of PINNED_VISIBLE_PROPS) {
+ test(
+ `${pinned.name} shows exactly its documented props`,
+ async () => {
+ const { exportName, path, props } = pinned;
+ const names = await visiblePropNames(path, exportName);
+ expect([...names].sort()).toEqual([...props].sort());
+ },
+ TS_MORPH_TEST_TIMEOUT,
+ );
+}
diff --git a/apps/docs/src/lib/docs-prose-rules.test.ts b/apps/docs/src/lib/docs-prose-rules.test.ts
index bded7868..16f50c7f 100644
--- a/apps/docs/src/lib/docs-prose-rules.test.ts
+++ b/apps/docs/src/lib/docs-prose-rules.test.ts
@@ -59,39 +59,45 @@ test('"us" stays case-sensitive so it does not match inside another word', () =>
expect(findProseRuleLabels('This uses the default configuration.')).toEqual([]);
});
-test('extractProseForRules strips an HTML entity so — is not read as an unspaced em dash', () => {
- const prose = extractProseForRules('Use the — entity here.');
- expect(findProseRuleLabels(prose)).toEqual([]);
-});
-
-test('extractProseForRules strips inline code so a banned word inside it is not reported', () => {
- const prose = extractProseForRules('Call `we.simply()` to configure it.');
- expect(findProseRuleLabels(prose)).toEqual([]);
-});
-
-test('extractProseForRules strips a JSX attribute value so a banned word inside it is not reported', () => {
- const prose = extractProseForRules(' ');
- expect(findProseRuleLabels(prose)).toEqual([]);
-});
-
-test('extractProseForRules strips a single-line import line so a banned word in a binding name is not reported', () => {
- const prose = extractProseForRules("import { we, us } from './data';\n\nConfigure it.");
- expect(findProseRuleLabels(prose)).toEqual([]);
-});
-
-test('extractProseForRules strips fenced code so a banned word inside a code sample is not reported', () => {
- const prose = extractProseForRules("```tsx\nconst users = ['we', 'us'];\n```\n\nConfigure it.");
- expect(findProseRuleLabels(prose)).toEqual([]);
-});
-
-test('extractProseForRules strips frontmatter so a banned word in a title is not reported', () => {
- const prose = extractProseForRules('---\ntitle: We simply note that\n---\n\nConfigure it.');
- expect(findProseRuleLabels(prose)).toEqual([]);
-});
-
-test('extractProseForRules leaves ordinary prose outside code, tags, and imports intact', () => {
- const prose = extractProseForRules(
- "import { Foo } from './foo';\n\nThis prose keeps its semicolon; it should still be reported.\n\n ",
- );
- expect(findProseRuleLabels(prose)).toEqual(['prose semicolon']);
-});
+for (const proseCase of [
+ {
+ expected: [] as ReadonlyArray,
+ name: 'strips an HTML entity so — is not read as an unspaced em dash',
+ source: 'Use the — entity here.',
+ },
+ {
+ expected: [],
+ name: 'strips inline code so a banned word inside it is not reported',
+ source: 'Call `we.simply()` to configure it.',
+ },
+ {
+ expected: [],
+ name: 'strips a JSX attribute value so a banned word inside it is not reported',
+ source: ' ',
+ },
+ {
+ expected: [],
+ name: 'strips a single-line import line so a banned word in a binding name is not reported',
+ source: "import { we, us } from './data';\n\nConfigure it.",
+ },
+ {
+ expected: [],
+ name: 'strips fenced code so a banned word inside a code sample is not reported',
+ source: "```tsx\nconst users = ['we', 'us'];\n```\n\nConfigure it.",
+ },
+ {
+ expected: [],
+ name: 'strips frontmatter so a banned word in a title is not reported',
+ source: '---\ntitle: We simply note that\n---\n\nConfigure it.',
+ },
+ {
+ expected: ['prose semicolon'],
+ name: 'leaves ordinary prose outside code, tags, and imports intact',
+ source:
+ "import { Foo } from './foo';\n\nThis prose keeps its semicolon; it should still be reported.\n\n ",
+ },
+]) {
+ test(`extractProseForRules ${proseCase.name}`, () => {
+ expect(findProseRuleLabels(extractProseForRules(proseCase.source))).toEqual(proseCase.expected);
+ });
+}
diff --git a/apps/docs/src/lib/guide-prop-audit.test.ts b/apps/docs/src/lib/guide-prop-audit.test.ts
index 1c057e9e..8b99c2b2 100644
--- a/apps/docs/src/lib/guide-prop-audit.test.ts
+++ b/apps/docs/src/lib/guide-prop-audit.test.ts
@@ -66,19 +66,20 @@ test('every authored guide/table pair has curated taught-prop metadata', () => {
}).toEqual({ missing: [], stale: [] });
});
-test.each(
- authoredTables.flatMap((table) => {
- const props = GUIDE_TAUGHT_PROPS[guideTableKey(table.guide, table.path, table.name)];
- if (props === undefined || props.length === 0) return [];
- return [{ ...table, props }];
- }),
-)(
- '$guide teaches documented props on $name',
- async ({ guide, name, path, props }) => {
- const names = await visiblePropNames(path, name);
- for (const prop of props) {
- expect(names, `${guide} teaches ${prop} on ${name}`).toContain(prop);
- }
- },
- TS_MORPH_TEST_TIMEOUT,
-);
+for (const table of authoredTables.flatMap((table) => {
+ const props = GUIDE_TAUGHT_PROPS[guideTableKey(table.guide, table.path, table.name)];
+ if (props === undefined || props.length === 0) return [];
+ return [{ ...table, props }];
+})) {
+ test(
+ `${table.guide} teaches documented props on ${table.name}`,
+ async () => {
+ const { guide, name, path, props } = table;
+ const names = await visiblePropNames(path, name);
+ for (const prop of props) {
+ expect(names, `${guide} teaches ${prop} on ${name}`).toContain(prop);
+ }
+ },
+ TS_MORPH_TEST_TIMEOUT,
+ );
+}
diff --git a/apps/docs/src/lib/home-content.test.ts b/apps/docs/src/lib/home-content.test.ts
deleted file mode 100644
index 196420a4..00000000
--- a/apps/docs/src/lib/home-content.test.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { expect, test } from 'vite-plus/test';
-import { HOME_INTRO, homeMarkdown } from './home-content.js';
-
-test('opens with the Luke UI heading and the lead sentence', () => {
- expect(homeMarkdown().startsWith(`# Luke UI\n\n${HOME_INTRO}\n`)).toBe(true);
-});
-
-test('links to Installation', () => {
- expect(homeMarkdown()).toContain('- [Installation](/docs/installation)');
-});
diff --git a/apps/docs/src/lib/story-wrapper.browser.test.tsx b/apps/docs/src/lib/story-wrapper.browser.test.tsx
index 9cb0084d..3a773dc6 100644
--- a/apps/docs/src/lib/story-wrapper.browser.test.tsx
+++ b/apps/docs/src/lib/story-wrapper.browser.test.tsx
@@ -10,8 +10,6 @@ import type { Root } from 'react-dom/client';
import { createRoot } from 'react-dom/client';
import { afterEach, expect, test } from 'vite-plus/test';
import { Comparison, ComparisonItem } from '#docs';
-import AutoGridResponsive from '../examples/auto-grid/responsive.js';
-import GridResponsive from '../examples/grid/responsive.js';
import { StoryWrapper } from './story-wrapper.js';
const mounted: Array<{ container: HTMLElement; root: Root }> = [];
@@ -128,28 +126,26 @@ test('flow retains deliberately narrow example sizing', () => {
expect(getComputedStyle(autoGrid).gridTemplateColumns.split(' ').filter(Boolean).length).toBe(1);
});
-test('Comparison is compact and centred in flow', () => {
- const { available, exampleRoot, storyRoot } = renderInWrapper(comparisonFixture());
- const storyBox = storyRoot.getBoundingClientRect();
- const comparisonBox = exampleRoot.getBoundingClientRect();
-
- expect(comparisonBox.width).toBeLessThan(available);
- expect(
- Math.abs(comparisonBox.left - storyBox.left - (storyBox.right - comparisonBox.right)),
- ).toBeLessThanOrEqual(1);
-});
+for (const comparisonCase of [
+ { containerWidth: undefined as number | undefined, expectCompact: true, name: 'wide' },
+ { containerWidth: 280, expectCompact: false, name: 'narrow' },
+]) {
+ test(`Comparison stays within the ${comparisonCase.name} flow surface and centred`, () => {
+ const { available, exampleRoot, storyRoot } = renderInWrapper(comparisonFixture(), {
+ containerWidth: comparisonCase.containerWidth,
+ });
+ const storyBox = storyRoot.getBoundingClientRect();
+ const comparisonBox = exampleRoot.getBoundingClientRect();
-test('Comparison caps its width to the narrow flow surface', () => {
- const { available, exampleRoot, storyRoot } = renderInWrapper(comparisonFixture(), {
- containerWidth: 280,
+ expect(comparisonBox.width <= available).toBe(true);
+ expect(comparisonBox.width < available || !comparisonCase.expectCompact).toBe(true);
+ expect(comparisonBox.left).toBeGreaterThanOrEqual(storyBox.left - 1);
+ expect(comparisonBox.right).toBeLessThanOrEqual(storyBox.right + 1);
+ expect(
+ Math.abs(comparisonBox.left - storyBox.left - (storyBox.right - comparisonBox.right)),
+ ).toBeLessThanOrEqual(1);
});
- const storyBox = storyRoot.getBoundingClientRect();
- const comparisonBox = exampleRoot.getBoundingClientRect();
-
- expect(comparisonBox.width).toBeLessThanOrEqual(available);
- expect(comparisonBox.left).toBeGreaterThanOrEqual(storyBox.left);
- expect(comparisonBox.right).toBeLessThanOrEqual(storyBox.right);
-});
+}
test('full-bleed has no inset padding or minimum height', () => {
const { exampleRoot, storyRoot } = renderInWrapper(
@@ -165,23 +161,6 @@ test('full-bleed has no inset padding or minimum height', () => {
);
});
-test('responsive layout examples resolve against the preview container width', () => {
- for (const [width, gridColumns, autoGridColumns] of [
- [900, 4, 3],
- [400, 2, 2],
- ] as const) {
- const grid = renderInWrapper( , { containerWidth: width });
- expect(
- getComputedStyle(grid.exampleRoot).gridTemplateColumns.split(' ').filter(Boolean).length,
- ).toBe(gridColumns);
-
- const autoGrid = renderInWrapper( , { containerWidth: width });
- expect(
- getComputedStyle(autoGrid.exampleRoot).gridTemplateColumns.split(' ').filter(Boolean).length,
- ).toBe(autoGridColumns);
- }
-});
-
function comparisonFixture() {
return (
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 3eef85a5..3188bcde 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -20,6 +20,17 @@ Assert observable public behaviour. Prefer roles and accessible names, `userEven
Do not test private functions, implementation details, or computed appearance. Use computed styles
only when layout is the contract.
+Do not use `test.each`, `it.each`, or `describe.each`. Parameterise with a `for…of` loop that calls
+`test()` (or `describe()`) inside, and put the distinguishing value in the title:
+
+```ts
+for (const ratio of ratios) {
+ test(`locks the frame to ${ratio}`, () => {
+ // …
+ });
+}
+```
+
Shared assertions take concrete elements and values. Keep the test, fixture, and contract choice in
the component's test file.
diff --git a/packages/@luke-ui/playground-core/src/compiler.test.ts b/packages/@luke-ui/playground-core/src/compiler.test.ts
index 7e6aa56f..be8399af 100644
--- a/packages/@luke-ui/playground-core/src/compiler.test.ts
+++ b/packages/@luke-ui/playground-core/src/compiler.test.ts
@@ -16,7 +16,7 @@ test('compileComponent default-exports a function component from scope', () => {
expect(typeof Component).toBe('function');
});
-test.each([
+const acceptedComponents = [
[
'memo',
[
@@ -40,20 +40,28 @@ test.each([
'export default lazy(() => Promise.resolve({ default: () => null }));',
].join('\n'),
],
-])('compileComponent accepts a %s component', (_name, code) => {
- const { compileComponent } = createPlaygroundCompiler(reactScope);
- const Component = compileComponent(code);
- expect(Component).not.toBeNull();
- expect(typeof Component).toBe('object');
-});
+] as const;
+
+for (const [name, code] of acceptedComponents) {
+ test(`compileComponent accepts a ${name} component`, () => {
+ const { compileComponent } = createPlaygroundCompiler(reactScope);
+ const Component = compileComponent(code);
+ expect(Component).not.toBeNull();
+ expect(typeof Component).toBe('object');
+ });
+}
-test.each([
+const rejectedExports = [
['no default export', 'export const value = 1;'],
['a default export of null', 'export default null;'],
-])('compileComponent rejects %s', (_name, code) => {
- const { compileComponent } = createPlaygroundCompiler({});
- expect(() => compileComponent(code)).toThrow(/default-export a React component/);
-});
+] as const;
+
+for (const [name, code] of rejectedExports) {
+ test(`compileComponent rejects ${name}`, () => {
+ const { compileComponent } = createPlaygroundCompiler({});
+ expect(() => compileComponent(code)).toThrow(/default-export a React component/);
+ });
+}
test('createPlaygroundCompiler throws for unknown specifiers', () => {
const { compileComponent } = createPlaygroundCompiler({ react: {} });
diff --git a/packages/@luke-ui/playground-core/src/protocol.test.ts b/packages/@luke-ui/playground-core/src/protocol.test.ts
index 314ce216..adbbc456 100644
--- a/packages/@luke-ui/playground-core/src/protocol.test.ts
+++ b/packages/@luke-ui/playground-core/src/protocol.test.ts
@@ -161,44 +161,52 @@ test('a trusted message after a resync-before-ready unblocks later postCode call
const COMPILE_ERROR = 'Playground code must default-export a React component.';
-test.each([
+const onResultCases = [
['a success', () => ({ ok: true }) as const, { type: 'success' } as const],
[
'a compilation error',
() => ({ message: COMPILE_ERROR, ok: false }) as const,
{ message: COMPILE_ERROR, type: 'error' } as const,
],
-])('%s reaches onResult', (_name, compile, expected) => {
- const bus = createMessageBus();
- const results: Array = [];
- attachFakePreview(bus, compile);
- const session = createPlaygroundPageSession({
- getCode: () => VALID_CODE,
- getPorts: () => bus.ports,
- onResult: (result) => results.push(result),
+] as const;
+
+for (const [name, compile, expected] of onResultCases) {
+ test(`${name} reaches onResult`, () => {
+ const bus = createMessageBus();
+ const results: Array = [];
+ attachFakePreview(bus, compile);
+ const session = createPlaygroundPageSession({
+ getCode: () => VALID_CODE,
+ getPorts: () => bus.ports,
+ onResult: (result) => results.push(result),
+ });
+ bus.listenPage((event) => session.handleMessage(event));
+ session.resync();
+ expect(results).toEqual([expected]);
});
- bus.listenPage((event) => session.handleMessage(event));
- session.resync();
- expect(results).toEqual([expected]);
-});
+}
-test.each([
+const ignoredMessageCases = [
['an untrusted origin', { origin: 'https://other.test', sourceIsPreview: true }],
[
'a same-origin message from a source that is not the preview',
{ origin: ORIGIN, sourceIsPreview: false },
],
-])('ignores %s', (_name, { origin, sourceIsPreview }) => {
- const { previewWindow, results, session } = createRecordingSession();
+] as const;
- session.handleMessage({
- data: { type: 'playground:success' },
- origin,
- source: sourceIsPreview ? previewWindow : { role: 'other' },
- });
+for (const [name, { origin, sourceIsPreview }] of ignoredMessageCases) {
+ test(`ignores ${name}`, () => {
+ const { previewWindow, results, session } = createRecordingSession();
- expect(results).toEqual([]);
-});
+ session.handleMessage({
+ data: { type: 'playground:success' },
+ origin,
+ source: sourceIsPreview ? previewWindow : { role: 'other' },
+ });
+
+ expect(results).toEqual([]);
+ });
+}
test('a missing preview window is a no-op for postCode and resync', () => {
const session = createPlaygroundPageSession({
diff --git a/packages/@luke-ui/react/src/core/aspect-ratio/aspect-ratio.browser.test.tsx b/packages/@luke-ui/react/src/core/aspect-ratio/aspect-ratio.browser.test.tsx
index c095b3e5..ebb3dfac 100644
--- a/packages/@luke-ui/react/src/core/aspect-ratio/aspect-ratio.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/aspect-ratio/aspect-ratio.browser.test.tsx
@@ -1,27 +1,9 @@
import { AspectRatio } from '@luke-ui/react/aspect-ratio';
import { vars } from '@luke-ui/react/theme';
-import { createRef } from 'react';
import { test, expect } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisualAppearance } from '../test-utils/visual.js';
-test('AspectRatio forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- Content
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected AspectRatio element.');
-
- expectForwardsDomProps(target, ref);
-});
-
const ratios = ['1 / 1', '4 / 3', '3 / 2', '16 / 9', '21 / 9'] as const;
const blankPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
@@ -92,29 +74,6 @@ test('applies an explicit objectFit value to the media child', () => {
expect(getComputedStyle(media).objectFit).toBe('contain');
});
-test('applies root layout props and ignores unsupported Box utilities from an object spread', () => {
- const props = {
- borderStyle: 'solid',
- borderWidth: 'thick',
- inlineSize: '10rem',
- ratio: '16 / 9',
- } as const;
- const { locator } = render(
-
-
- ,
- );
- const parent = locator.getByTestId('parent').element();
- const element = locator.getByTestId('ratio').element();
- if (!(parent instanceof HTMLElement) || !(element instanceof HTMLElement)) {
- throw new Error('Expected AspectRatio elements.');
- }
-
- expect(element.getBoundingClientRect().width).toBe(160);
- expect(element.getBoundingClientRect().width).toBeLessThan(parent.getBoundingClientRect().width);
- expect(element.offsetWidth).toBe(element.clientWidth);
-});
-
test('applies the chosen ratio to a caller-owned root', () => {
const { locator } = render(
{
- const ref = createRef();
- const { container } = render(
-
- Content
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected AutoGrid element.');
-
- expectForwardsDomProps(target, ref);
-});
-
afterEach(async () => {
await page.viewport(1024, 800);
});
@@ -242,108 +224,31 @@ test('does not let long unbreakable content expand auto-fit tracks', () => {
expect(long.getBoundingClientRect().width).toBeCloseTo(short.getBoundingClientRect().width, 1);
});
-test('keeps the inline axis under RTL and vertical writing mode', () => {
+test('keeps the inline axis under RTL', () => {
const { locator } = render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
,
);
const rtl = locator.getByTestId('grid-rtl').element();
- const vertical = locator.getByTestId('grid-vertical').element();
const rtlFirst = locator.getByTestId('rtl-first').element();
const rtlSecond = locator.getByTestId('rtl-second').element();
- const verticalFirst = locator.getByTestId('vertical-first').element();
- const verticalSecond = locator.getByTestId('vertical-second').element();
if (
!(rtl instanceof HTMLElement) ||
- !(vertical instanceof HTMLElement) ||
!(rtlFirst instanceof HTMLElement) ||
- !(rtlSecond instanceof HTMLElement) ||
- !(verticalFirst instanceof HTMLElement) ||
- !(verticalSecond instanceof HTMLElement)
+ !(rtlSecond instanceof HTMLElement)
) {
throw new Error('Expected AutoGrid elements.');
}
expect(getComputedStyle(rtl).display).toBe('grid');
- expect(getComputedStyle(vertical).display).toBe('grid');
expect(rtlFirst.getBoundingClientRect().left).toBeGreaterThan(
rtlSecond.getBoundingClientRect().left,
);
- expect(verticalSecond.getBoundingClientRect().top).toBeGreaterThan(
- verticalFirst.getBoundingClientRect().top,
- );
-});
-
-test('applies root layout props and ignores unsupported Box utilities from an object spread', () => {
- const props = {
- borderStyle: 'solid',
- borderWidth: 'thick',
- gap: 'sp8',
- inlineSize: '10rem',
- minColumnInlineSize: '6rem',
- padding: 'sp16',
- } as const;
- const { locator } = render(
-
-
-
-
- ,
- );
- const container = locator.getByTestId('container').element();
- const element = locator.getByTestId('grid').element();
- if (!(container instanceof HTMLElement) || !(element instanceof HTMLElement)) {
- throw new Error('Expected AutoGrid elements.');
- }
-
- expect(element.getBoundingClientRect().width).toBeLessThan(
- container.getBoundingClientRect().width,
- );
- expect(element.offsetWidth).toBe(element.clientWidth);
-});
-
-test('renders semantic elements and a consumer-owned render prop', () => {
- const ref = createRef();
- const semanticResult = render(
-
- First
- Second
- ,
- );
- const list = semanticResult.locator.getByRole('list', { name: 'Items' });
- expect(list.element().tagName).toBe('UL');
-
- const customResult = render(
- }
- >
- One
- Two
- ,
- );
- const section = customResult.locator.getByTestId('auto-grid-render').element();
- if (!(section instanceof HTMLElement)) throw new Error('Expected render callback element.');
-
- expect(section.tagName).toBe('SECTION');
- expect(ref.current).toBe(section);
- expect(getComputedStyle(section).display).toBe('grid');
});
const itemStyle = {
diff --git a/packages/@luke-ui/react/src/core/blockquote/blockquote.browser.test.tsx b/packages/@luke-ui/react/src/core/blockquote/blockquote.browser.test.tsx
deleted file mode 100644
index 3e634406..00000000
--- a/packages/@luke-ui/react/src/core/blockquote/blockquote.browser.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Blockquote } from '@luke-ui/react/blockquote';
-import { createRef } from 'react';
-import { test } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
-import { render } from '../test-utils/render.js';
-
-test('Blockquote forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- Quoted text
-
,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected a Blockquote element.');
-
- expectForwardsDomProps(target, ref);
-});
diff --git a/packages/@luke-ui/react/src/core/button/button.browser.test.tsx b/packages/@luke-ui/react/src/core/button/button.browser.test.tsx
index d92b9ecb..2e2e0508 100644
--- a/packages/@luke-ui/react/src/core/button/button.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/button/button.browser.test.tsx
@@ -1,12 +1,11 @@
import { Button } from '@luke-ui/react/button';
import { Icon } from '@luke-ui/react/icon';
import { Text } from '@luke-ui/react/text';
-import { act, createRef } from 'react';
+import { act } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { expect, test } from 'vite-plus/test';
-import { page, userEvent } from 'vite-plus/test/context';
+import { page } from 'vite-plus/test/context';
import { expectNoAxeViolations } from '../test-utils/axe.js';
-import { expectForwardsDomProps, forwardedDomProps } from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import {
captureVisual,
@@ -56,18 +55,6 @@ function ButtonScene() {
);
}
-test('Button forwards className, data attributes, id, and ref to the button element', () => {
- const ref = createRef();
- const { locator } = render(
- ,
- );
- const button = locator.getByRole('button').element();
-
- expectForwardsDomProps(button, ref);
-});
-
test('pressing a Button runs its onPress handler', async () => {
let pressed = false;
const { locator, user } = render();
@@ -82,33 +69,6 @@ test('the Button scene has no axe violations', async () => {
await expectNoAxeViolations(container);
});
-test('a text-appearance Button keeps button semantics and runs onPress', async () => {
- let pressed = false;
- const { locator, user } = render(
- ,
- );
- const button = locator.getByRole('button', { name: 'Action' }).element();
-
- expect(button.tagName).toBe('BUTTON');
-
- await user.click(locator.getByRole('button', { name: 'Action' }));
- expect(pressed).toBe(true);
-});
-
-test('a pending text Button shows a spinner', () => {
- const { locator } = render(
- ,
- );
- const button = locator.getByRole('button', { name: 'Save' });
-
- expect(button.element().getAttribute('data-pending')).toBe('true');
- expect(button.element().querySelector('[role="status"]')).not.toBeNull();
-});
-
test('a text Button has the same layout styles as inline Text without control padding or sizing', () => {
const { locator } = render(
@@ -186,149 +146,6 @@ test('a text Button in a flex parent is not collapsed to a single character per
expect(button.getBoundingClientRect().width).toBeGreaterThan(20);
});
-test('hover inverts the text Button underline, and low prominence reverses the rest state', () => {
- const { locator } = render(
-
-
-
-
- ,
- );
- const low = locator.getByRole('button', { name: 'Low' }).element();
- const standard = locator.getByRole('button', { name: 'Standard' }).element();
- const high = locator.getByRole('button', { name: 'High' }).element();
-
- for (const button of [low, standard, high]) {
- button.style.transition = 'none';
- }
-
- expect(getComputedStyle(low).textDecorationLine).toBe('none');
- expect(getComputedStyle(standard).textDecorationLine).toBe('underline');
- expect(getComputedStyle(high).textDecorationLine).toBe('underline');
-
- low.setAttribute('data-hovered', 'true');
- standard.setAttribute('data-hovered', 'true');
- high.setAttribute('data-hovered', 'true');
-
- expect(getComputedStyle(low).textDecorationLine).toBe('underline');
- expect(getComputedStyle(standard).textDecorationLine).toBe('none');
- expect(getComputedStyle(high).textDecorationLine).toBe('none');
-});
-
-test('focus-visible keeps the underline over hover and pressed', () => {
- const { locator } = render(
-
-
-
- ,
- );
- const low = locator.getByRole('button', { name: 'Low' }).element();
- const standard = locator.getByRole('button', { name: 'Standard' }).element();
- const lowLabel = low.querySelector('span');
- const standardLabel = standard.querySelector('span');
- if (!(lowLabel instanceof HTMLElement) || !(standardLabel instanceof HTMLElement)) {
- throw new Error('Expected text labels.');
- }
-
- for (const button of [low, standard]) {
- button.style.transition = 'none';
- }
-
- low.setAttribute('data-focus-visible', 'true');
- low.setAttribute('data-hovered', 'true');
- standard.setAttribute('data-focus-visible', 'true');
- standard.setAttribute('data-hovered', 'true');
-
- expect(getComputedStyle(low).textDecorationLine).toBe('underline');
- expect(getComputedStyle(lowLabel).textDecorationLine).toBe('underline');
- expect(getComputedStyle(standard).textDecorationLine).toBe('underline');
- expect(getComputedStyle(standardLabel).textDecorationLine).toBe('underline');
-
- low.removeAttribute('data-hovered');
- standard.removeAttribute('data-hovered');
- low.setAttribute('data-pressed', 'true');
- standard.setAttribute('data-pressed', 'true');
-
- expect(getComputedStyle(low).textDecorationLine).toBe('underline');
- expect(getComputedStyle(lowLabel).textDecorationLine).toBe('underline');
- expect(getComputedStyle(standard).textDecorationLine).toBe('underline');
- expect(getComputedStyle(standardLabel).textDecorationLine).toBe('underline');
-});
-
-test('the text Button label paints the underline set on the button', () => {
- const { locator } = render(
- ,
- );
- const button = locator.getByRole('button', { name: 'Save' }).element();
- const label = button.querySelector('span');
- if (!(label instanceof HTMLElement)) throw new Error('Expected a text label.');
-
- // `text-decoration` does not inherit, so a label that sets its own value paints over the button's.
- expect(getComputedStyle(label).textDecorationLine).toBe('underline');
-});
-
-test('pressed text Buttons shift to a perceptibly different foreground colour per tone', () => {
- const { locator } = render(
-
-
-
-
- ,
- );
- const neutral = locator.getByRole('button', { name: 'Neutral' }).element();
- const critical = locator.getByRole('button', { name: 'Critical' }).element();
- const accent = locator.getByRole('button', { name: 'Accent' }).element();
-
- for (const button of [neutral, critical, accent]) {
- // A computed colour read mid-transition returns the rest value.
- button.style.transition = 'none';
- }
-
- const neutralRest = getComputedStyle(neutral).color;
- const criticalRest = getComputedStyle(critical).color;
- const accentRest = getComputedStyle(accent).color;
-
- neutral.setAttribute('data-pressed', 'true');
- critical.setAttribute('data-pressed', 'true');
- accent.setAttribute('data-pressed', 'true');
-
- expect(getComputedStyle(neutral).color).not.toBe(neutralRest);
- expect(getComputedStyle(critical).color).not.toBe(criticalRest);
- expect(getComputedStyle(accent).color).not.toBe(accentRest);
-});
-
-test('disabled Buttons expose disabled state in either appearance', () => {
- const { locator } = render(
-
-
-
- ,
- );
-
- const buttonAppearance = locator.getByRole('button', { name: 'Button appearance' }).element();
- const textAppearance = locator.getByRole('button', { name: 'Text appearance' }).element();
- expect(buttonAppearance).toHaveAttribute('data-disabled', 'true');
- expect(textAppearance).toHaveAttribute('data-disabled', 'true');
-});
-
test('runs onPress before pressAction and tracks Action pending', async () => {
const order: Array = [];
let release!: () => void;
@@ -484,21 +301,15 @@ test('kitchen sink', { tags: ['visual'] }, async () => {
}
});
-test('interactive states', { tags: ['visual'] }, async () => {
+test('focus-visible state', { tags: ['visual'] }, async () => {
const { locator } = render();
const button = page.getByRole('button', { name: 'Action' });
- await userEvent.hover(button);
- await captureVisual(locator, 'button/hover');
- await userEvent.unhover(button);
await focusViaKeyboard(button);
await captureVisual(locator, 'button/focus-visible');
- await userEvent.keyboard('{Space>}');
- await captureVisual(locator, 'button/pressed');
- await userEvent.keyboard('{/Space}');
});
-test('forced-colors states', { tags: ['visual'] }, async () => {
+test('forced-colors resting', { tags: ['visual'] }, async () => {
await emulateForcedColors('active');
try {
@@ -509,17 +320,7 @@ test('forced-colors states', { tags: ['visual'] }, async () => {
,
);
- const action = page.getByRole('button', { name: 'Action' });
-
await captureVisual(locator, 'button/forced-colors-resting');
- await userEvent.hover(action);
- await captureVisual(locator, 'button/forced-colors-hover');
- await userEvent.unhover(action);
- await focusViaKeyboard(action);
- await captureVisual(locator, 'button/forced-colors-focus-visible');
- await userEvent.keyboard('{Space>}');
- await captureVisual(locator, 'button/forced-colors-pressed');
- await userEvent.keyboard('{/Space}');
} finally {
await emulateForcedColors('none');
}
diff --git a/packages/@luke-ui/react/src/core/checkbox/checkbox.browser.test.tsx b/packages/@luke-ui/react/src/core/checkbox/checkbox.browser.test.tsx
index 86de8adc..36924d31 100644
--- a/packages/@luke-ui/react/src/core/checkbox/checkbox.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/checkbox/checkbox.browser.test.tsx
@@ -1,10 +1,8 @@
import { Checkbox } from '@luke-ui/react/checkbox';
import { Text } from '@luke-ui/react/text';
-import { typeStyles } from '@luke-ui/react/theme';
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 { expectNoAxeViolations } from '../test-utils/axe.js';
import { render, visualAppearances } from '../test-utils/render.js';
import {
@@ -54,13 +52,11 @@ function CheckboxScene() {
Email notifications
- {typeStyles.map((typography) => (
-
-
- {typography}: This label wraps to show that the control aligns with its first line.
-
-
- ))}
+
+
+ heading3: This label wraps to show that the control aligns with its first line.
+
+
Standalone control
This label wraps onto a second line so the control should sit on the first line, not float
@@ -88,38 +84,37 @@ function CheckboxScene() {
);
}
-// RAC puts `id` on the control, not the root.
-test('Checkbox forwards className and data attributes to its root, and id to the DOM', () => {
- const { container } = render(
-
- Terms
- ,
- );
- const root = container.firstElementChild;
- if (!(root instanceof HTMLElement)) throw new Error('Expected a Checkbox root.');
-
- expect(root).toHaveClass('forwarded-class');
- expect(root).toHaveAttribute('data-forwarded', 'true');
- expect(container.querySelector('#forwarded-id')).not.toBeNull();
-});
-
-test('Checkbox resolves inputRef to the control, participates in a form, and fires onBlur', () => {
+// Luke UI widens RAC's `inputRef` to accept React Hook Form's callback ref.
+test('Checkbox resolves object and callback inputRef to the control, participates in a form, and fires onBlur', () => {
const inputRef = createRef();
+ const callbackResolved: Array = [];
let blurred = false;
const { container, locator } = render(
- {
- blurred = true;
- }}
- >
- Terms
- ,
+ <>
+ {
+ blurred = true;
+ }}
+ >
+ Terms
+
+ {
+ callbackResolved.push(node);
+ }}
+ name="terms-callback"
+ >
+ Callback
+
+ >,
);
const control = locator.getByRole('checkbox', { name: 'Terms' }).element();
+ const callbackControl = locator.getByRole('checkbox', { name: 'Callback' }).element();
expect(inputRef.current).toBe(control);
+ expect(callbackResolved.at(-1)).toBe(callbackControl);
const form = document.createElement('form');
container.replaceWith(form);
@@ -140,34 +135,6 @@ test('Checkbox resolves inputRef to the control, participates in a form, and fir
form.remove();
});
-// Luke UI widens RAC's `inputRef` to accept React Hook Form's callback ref.
-test('Checkbox resolves a callback inputRef to the control', () => {
- const resolved: Array = [];
- const { locator } = render(
- {
- resolved.push(node);
- }}
- name="terms"
- >
- Terms
- ,
- );
- const control = locator.getByRole('checkbox', { name: 'Terms' }).element();
-
- expect(resolved.at(-1)).toBe(control);
-});
-
-test('clicking a Checkbox label selects it', async () => {
- let selected = false;
- const { locator, user } = render(
- (selected = isSelected)}>Terms ,
- );
-
- await user.click(locator.getByText('Terms'));
- expect(selected).toBe(true);
-});
-
test('the Checkbox scene has no axe violations', async () => {
const { container } = render( );
@@ -262,17 +229,6 @@ async function getAccessibilityNode(nodeId: DomNode['nodeId']) {
return axNode;
}
-function checkboxLabel(checkbox: Locator): HTMLElement {
- const label = checkbox.element().closest('label');
- if (label == null) throw new Error('Expected the checkbox content label.');
- return label;
-}
-
-async function pressCheckbox(checkbox: Locator): Promise {
- checkbox.element().focus();
- await userEvent.keyboard('{Space>}');
-}
-
test('kitchen sink', { tags: ['visual'] }, async () => {
for (const appearance of visualAppearances) {
const { locator } = render( , { appearance });
@@ -286,43 +242,7 @@ test('keyboard focus ring', { tags: ['visual'] }, async () => {
await captureVisual(locator, 'checkbox/focus-visible');
});
-test('interactive states', { tags: ['visual'] }, async () => {
- const { locator } = render(
-
-
- Invalid
-
-
- Invalid selected
-
-
- Invalid indeterminate
-
- ,
- );
- const unchecked = page.getByRole('checkbox', { exact: true, name: 'Invalid' });
- const selected = page.getByRole('checkbox', { exact: true, name: 'Invalid selected' });
- const indeterminate = page.getByRole('checkbox', {
- exact: true,
- name: 'Invalid indeterminate',
- });
-
- for (const [name, checkbox] of [
- ['unchecked', unchecked],
- ['selected', selected],
- ['indeterminate', indeterminate],
- ] as const) {
- const label = checkboxLabel(checkbox);
- await userEvent.hover(label);
- await captureVisual(locator, `checkbox/invalid-hover-${name}`);
- await userEvent.unhover(label);
- await pressCheckbox(checkbox);
- await captureVisual(locator, `checkbox/invalid-pressed-${name}`);
- await userEvent.keyboard('{/Space}');
- }
-});
-
-test('forced-colors states', { tags: ['visual'] }, async () => {
+test('forced-colors resting', { tags: ['visual'] }, async () => {
await emulateForcedColors('active');
try {
@@ -332,9 +252,6 @@ test('forced-colors states', { tags: ['visual'] }, async () => {
Selected
-
- Indeterminate
-
Disabled
@@ -343,7 +260,7 @@ test('forced-colors states', { tags: ['visual'] }, async () => {
,
);
- await captureVisual(locator, 'checkbox/forced-colors-states');
+ await captureVisual(locator, 'checkbox/forced-colors-resting');
} finally {
await emulateForcedColors('none');
}
diff --git a/packages/@luke-ui/react/src/core/cluster/cluster.browser.test.tsx b/packages/@luke-ui/react/src/core/cluster/cluster.browser.test.tsx
index dc8d0d90..f9b1da8d 100644
--- a/packages/@luke-ui/react/src/core/cluster/cluster.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/cluster/cluster.browser.test.tsx
@@ -1,29 +1,11 @@
import { Cluster } from '@luke-ui/react/cluster';
import { vars } from '@luke-ui/react/theme';
-import { createRef } from 'react';
import { afterEach, expect, test } from 'vite-plus/test';
import { page } from 'vite-plus/test/context';
import { breakpoints } from '../../theme/breakpoints.js';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisualAppearance } from '../test-utils/visual.js';
-test('Cluster forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- Content
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected Cluster element.');
-
- expectForwardsDomProps(target, ref);
-});
-
afterEach(async () => {
await page.viewport(1024, 800);
});
@@ -46,29 +28,6 @@ test('flows children on the inline axis and wraps by default', () => {
expect(getComputedStyle(element).alignItems).toBe('center');
});
-test('accepts flexWrap overrides including wrap-reverse', () => {
- const nowrapResult = render(
-
- First
- Second
- ,
- );
- const wrapReverseResult = render(
-
- First
- Second
- ,
- );
- const nowrap = nowrapResult.locator.getByTestId('cluster-nowrap').element();
- const wrapReverse = wrapReverseResult.locator.getByTestId('cluster-wrap-reverse').element();
- if (!(nowrap instanceof HTMLElement) || !(wrapReverse instanceof HTMLElement)) {
- throw new Error('Expected Cluster elements.');
- }
-
- expect(getComputedStyle(nowrap).flexWrap).toBe('nowrap');
- expect(getComputedStyle(wrapReverse).flexWrap).toBe('wrap-reverse');
-});
-
test('nowrap disables wrapping without changing child shrink behaviour', () => {
const { locator } = render(
@@ -115,20 +74,6 @@ test('uses no gap by default and accepts alignment overrides', () => {
expect(second.getBoundingClientRect().left - first.getBoundingClientRect().right).toBe(0);
});
-test('emits no gap utility when gap is omitted', () => {
- const { locator } = render(
-
-
-
- ,
- );
- const element = locator.getByTestId('cluster').element();
- if (!(element instanceof HTMLElement)) throw new Error('Expected Cluster element.');
-
- expect(element.className).not.toMatch(/gap/);
- expect(getComputedStyle(element).gap).toBe('normal');
-});
-
test('wraps when children exceed the inline size', () => {
const { locator } = render(
@@ -146,51 +91,29 @@ test('wraps when children exceed the inline size', () => {
expect(second.offsetTop).toBeGreaterThan(first.offsetTop);
});
-test('keeps the inline axis under RTL and vertical writing mode', () => {
+test('keeps the inline axis under RTL', () => {
const { locator } = render(
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
,
);
const rtl = locator.getByTestId('cluster-rtl').element();
- const vertical = locator.getByTestId('cluster-vertical').element();
- if (!(rtl instanceof HTMLElement) || !(vertical instanceof HTMLElement)) {
- throw new Error('Expected Cluster elements.');
- }
+ if (!(rtl instanceof HTMLElement)) throw new Error('Expected Cluster element.');
expect(getComputedStyle(rtl).flexDirection).toBe('row');
expect(getComputedStyle(rtl).flexWrap).toBe('wrap');
- expect(getComputedStyle(vertical).flexDirection).toBe('row');
- expect(getComputedStyle(vertical).flexWrap).toBe('wrap');
const [rtlFirst, rtlSecond] = rtl.children;
- const [verticalFirst, verticalSecond] = vertical.children;
- if (
- !(rtlFirst instanceof HTMLElement) ||
- !(rtlSecond instanceof HTMLElement) ||
- !(verticalFirst instanceof HTMLElement) ||
- !(verticalSecond instanceof HTMLElement)
- ) {
+ if (!(rtlFirst instanceof HTMLElement) || !(rtlSecond instanceof HTMLElement)) {
throw new Error('Expected Cluster children.');
}
expect(rtlFirst.getBoundingClientRect().left).toBeGreaterThan(
rtlSecond.getBoundingClientRect().left,
);
- expect(verticalSecond.getBoundingClientRect().top).toBeGreaterThan(
- verticalFirst.getBoundingClientRect().top,
- );
});
test('keeps Cluster defaults below sparse responsive alignment overrides', async () => {
@@ -226,65 +149,6 @@ test('keeps Cluster defaults below sparse responsive alignment overrides', async
expect(second.getBoundingClientRect().left).toBeGreaterThan(first.getBoundingClientRect().right);
});
-test('applies root layout props and ignores unsupported Box utilities from an object spread', () => {
- const props = {
- borderStyle: 'solid',
- borderWidth: 'thick',
- gap: 'sp8',
- inlineSize: '10rem',
- padding: 'sp16',
- } as const;
- const { locator } = render(
-
-
-
-
- ,
- );
- const container = locator.getByTestId('container').element();
- const element = locator.getByTestId('cluster').element();
- if (!(container instanceof HTMLElement) || !(element instanceof HTMLElement)) {
- throw new Error('Expected Cluster elements.');
- }
- const child = element.firstElementChild;
- if (!(child instanceof HTMLElement)) throw new Error('Expected Cluster child.');
-
- expect(element.getBoundingClientRect().width).toBeLessThan(
- container.getBoundingClientRect().width,
- );
- expect(child.getBoundingClientRect().left).toBeGreaterThan(element.getBoundingClientRect().left);
- expect(element.offsetWidth).toBe(element.clientWidth);
-});
-
-test('renders semantic elements and a consumer-owned render prop', () => {
- const ref = createRef();
- const semanticResult = render(
-
- Open
- Closed
- ,
- );
- const list = semanticResult.locator.getByRole('list', { name: 'Filters' });
- expect(list.element().tagName).toBe('UL');
-
- const customResult = render(
- }
- >
- Home
- About
- ,
- );
- const nav = customResult.locator.getByTestId('cluster-render').element();
- if (!(nav instanceof HTMLElement)) throw new Error('Expected render callback element.');
-
- expect(nav.tagName).toBe('NAV');
- expect(ref.current).toBe(nav);
- expect(getComputedStyle(nav).flexWrap).toBe('wrap');
-});
-
const itemStyle = {
backgroundColor: vars.color.surface.floating,
borderRadius: vars.radius.detail,
diff --git a/packages/@luke-ui/react/src/core/code/code.browser.test.tsx b/packages/@luke-ui/react/src/core/code/code.browser.test.tsx
index 6044c1b1..7371e91c 100644
--- a/packages/@luke-ui/react/src/core/code/code.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/code/code.browser.test.tsx
@@ -1,27 +1,9 @@
import { Code } from '@luke-ui/react/code';
import { Text } from '@luke-ui/react/text';
-import { createRef } from 'react';
import { test, expect } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisual, captureVisualAppearance, Stack } from '../test-utils/visual.js';
-test('Code forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- npm install
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected a Code element.');
-
- expectForwardsDomProps(target, ref);
-});
-
const LONG_CONTENT =
'one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen';
diff --git a/packages/@luke-ui/react/src/core/combobox-field/combobox-field.browser.test.tsx b/packages/@luke-ui/react/src/core/combobox-field/combobox-field.browser.test.tsx
index b1ed8fd4..d2bd6ec3 100644
--- a/packages/@luke-ui/react/src/core/combobox-field/combobox-field.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/combobox-field/combobox-field.browser.test.tsx
@@ -1,19 +1,16 @@
import { ComboboxField } from '@luke-ui/react/combobox-field';
import { Icon } from '@luke-ui/react/icon';
-import { LoadingSpinner } from '@luke-ui/react/loading-spinner';
import {
ComboboxInput,
ComboboxInputGroup,
ComboboxItem,
ComboboxListBox,
- ComboboxLoadMoreItem,
ComboboxPopover,
ComboboxRoot,
- ComboboxSection,
ComboboxTrigger,
} from '@luke-ui/react/primitives/combobox';
import { Field } from '@luke-ui/react/primitives/field';
-import { createRef, useState } from 'react';
+import { createRef } from 'react';
import type { Key } from 'react-aria-components/ComboBox';
import { expect, test } from 'vite-plus/test';
import { page, userEvent } from 'vite-plus/test/context';
@@ -24,13 +21,7 @@ import {
mockScreenWidth,
} from '../test-utils/mock-screen-width.js';
import { render, visualAppearances } from '../test-utils/render.js';
-import {
- captureVisual,
- captureVisualAppearance,
- emulateForcedColors,
- focusViaKeyboard,
- Stack,
-} from '../test-utils/visual.js';
+import { captureVisual, captureVisualAppearance, Stack } from '../test-utils/visual.js';
import { waitForOverlayEnter } from '../test-utils/wait-for-overlay-enter.js';
type CountryItem = {
@@ -53,55 +44,43 @@ const sceneCountryItems: Array = [
const renderCountryItem = (item: CountryItem) => {item.label} ;
-const renderIconItem = (item: CountryItem) => (
-
-
- {item.label}
-
-);
-
-// RAC inserts a collection template before the root and puts `id` on the control.
-test('ComboboxField forwards className and data attributes to its root, and id to the DOM', () => {
- const { container } = render(
-
- className="forwarded-class"
- data-forwarded="true"
- defaultItems={countryItems}
- description="Helpful context"
- id="forwarded-id"
- label="Country"
- >
- {renderCountryItem}
- ,
- );
- const root = container.querySelector('[data-forwarded="true"]');
- if (!(root instanceof HTMLElement)) throw new Error('Expected a ComboboxField root.');
-
- expect(root).toHaveClass('forwarded-class');
- expect(container.querySelector('#forwarded-id')).not.toBeNull();
-});
-
-test('ComboboxField resolves inputRef to the input, submits its value, and fires onBlur', () => {
+// Luke UI widens RAC's `inputRef` to accept React Hook Form's callback ref.
+test('ComboboxField resolves object and callback inputRefs, submits its value, and fires onBlur', () => {
const inputRef = createRef();
+ const callbackResolved: Array = [];
let blurred = false;
const { container, locator } = render(
-
- defaultItems={countryItems}
- description="Helpful context"
- inputRef={inputRef}
- label="Country"
- name="country"
- onBlur={() => {
- blurred = true;
- }}
- >
- {renderCountryItem}
- ,
+ <>
+
+ defaultItems={countryItems}
+ description="Helpful context"
+ inputRef={inputRef}
+ label="Country object"
+ name="country"
+ onBlur={() => {
+ blurred = true;
+ }}
+ >
+ {renderCountryItem}
+
+
+ defaultItems={countryItems}
+ inputRef={(node: HTMLElement | null) => {
+ callbackResolved.push(node);
+ }}
+ label="Country callback"
+ name="country-callback"
+ >
+ {renderCountryItem}
+
+ >,
);
- const control = locator.getByRole('combobox', { name: 'Country' }).element();
+ const objectControl = locator.getByRole('combobox', { name: 'Country object' }).element();
+ const callbackControl = locator.getByRole('combobox', { name: 'Country callback' }).element();
- expect(inputRef.current).toBe(control);
- expect(control).toHaveAttribute('aria-describedby');
+ expect(inputRef.current).toBe(objectControl);
+ expect(callbackResolved.at(-1)).toBe(callbackControl);
+ expect(objectControl).toHaveAttribute('aria-describedby');
// React Aria submits through a hidden input.
expect(container.querySelector('input[type="hidden"][name="country"]')).not.toBeNull();
@@ -115,56 +94,14 @@ test('ComboboxField resolves inputRef to the input, submits its value, and fires
namedControl.value = 'au';
expect(new FormData(form).get('country')).toBe('au');
- if (!(control instanceof HTMLElement)) throw new Error('Expected an HTML control.');
- control.focus();
- control.blur();
+ if (!(objectControl instanceof HTMLElement)) throw new Error('Expected an HTML control.');
+ objectControl.focus();
+ objectControl.blur();
expect(blurred).toBe(true);
form.remove();
});
-// Luke UI widens RAC's `inputRef` to accept React Hook Form's callback ref.
-test('ComboboxField resolves a callback inputRef to the input', () => {
- const resolved: Array = [];
- const { locator } = render(
-
- defaultItems={countryItems}
- inputRef={(node: HTMLElement | null) => {
- resolved.push(node);
- }}
- label="Country"
- name="country"
- >
- {renderCountryItem}
- ,
- );
- const control = locator.getByRole('combobox', { name: 'Country' }).element();
-
- expect(resolved.at(-1)).toBe(control);
-});
-
-test('picking an option from the ComboboxField popover fills the input', async () => {
- const { locator, user } = render(
-
- {renderCountryItem}
- ,
- );
- const input = locator.getByRole('combobox', { name: 'Country' });
-
- await user.click(input);
-
- const option = page.getByRole('option', { name: 'Australia' });
- // Clicking an option scrolls it into view first, and React Aria closes the popover on a
- // document scroll. While the popover is still entering, that close lands before the click and
- // detaches the option.
- const popover = page.getByRole('listbox').element().parentElement;
- if (popover == null) throw new Error('Expected the popover element.');
- await waitForOverlayEnter(popover);
-
- await user.click(option);
- expect(page.getByRole('combobox', { name: 'Country' })).toHaveValue('Australia');
-});
-
test('the ComboboxField scene has no axe violations', async () => {
const { container } = render(
@@ -278,36 +215,6 @@ test('ComboboxField uses a mobile modal to search and select an option', async (
await expect.poll(() => new FormData(form).get('country')).toBe('ca');
});
-// The primitive renders the control itself, so it takes a plain `ref`.
-test('ComboboxInput resolves object and callback refs to the input element', () => {
- const objectRef = createRef();
- const callbackResolved: Array = [];
- render(
- <>
- aria-label="Country object" defaultItems={countryItems}>
-
-
-
-
- aria-label="Country callback" defaultItems={countryItems}>
-
- {
- callbackResolved.push(node);
- }}
- />
-
-
- >,
- );
-
- const objectInput = page.getByRole('combobox', { name: 'Country object' });
- const callbackInput = page.getByRole('combobox', { name: 'Country callback' });
-
- expect(objectRef.current).toBe(objectInput.element());
- expect(callbackResolved.at(-1)).toBe(callbackInput.element());
-});
-
test('ComboboxField reopens the popover when the focused input is clicked again', async () => {
const { locator } = render(
@@ -351,172 +258,6 @@ test('ComboboxField clearing the tray search clears the selection', async () =>
await expect.element(trigger).toHaveTextContent('');
});
-test('ComboboxField tray clear clears a controlled selection and inputValue', async () => {
- mockScreenWidth(MOBILE_SCREEN_WIDTH);
- const changes: Array = [];
- const inputChanges: Array = [];
-
- function ControlledTrayCombobox() {
- const [value, setValue] = useState('au');
- const [inputValue, setInputValue] = useState('Australia');
- return (
-
- );
- }
-
- const { container } = render( );
- const form = container.querySelector('form');
- if (form == null) throw new Error('Expected the form element.');
-
- await userEvent.click(page.getByRole('button', { name: 'Country Australia' }).element());
- await expect.element(page.getByRole('dialog')).toBeVisible();
- await expect.element(page.getByRole('searchbox', { name: 'Country' })).toHaveValue('Australia');
-
- await userEvent.click(page.getByRole('button', { name: 'Clear search' }).element());
- await expect.element(page.getByRole('searchbox', { name: 'Country' })).toHaveValue('');
- await userEvent.keyboard('{Escape}');
- await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
-
- expect(changes).toEqual([null]);
- expect(inputChanges).toContain('');
- expect(new FormData(form).get('country')).toBe('');
- await expect.element(page.getByRole('button', { name: 'Country' })).toHaveTextContent('');
-});
-
-test('ComboboxField clear selection empties the desktop field', async () => {
- const { locator } = render(
-
- {renderCountryItem}
- ,
- );
- const input = locator.getByRole('combobox', { name: 'Country' });
- await expect.element(input).toHaveValue('Australia');
-
- await userEvent.click(page.getByRole('button', { name: 'Clear selection' }).element());
- await expect.element(input).toHaveValue('');
-});
-
-test('ComboboxField reports a selection the parent had already moved away from', async () => {
- mockScreenWidth(MOBILE_SCREEN_WIDTH);
- const changes: Array = [];
- function ControlledCombobox() {
- const [value, setValue] = useState('au');
- return (
- <>
-
- {
- changes.push(next);
- setValue(next);
- }}
- value={value}
- >
- {renderCountryItem}
-
- >
- );
- }
- render( );
-
- await userEvent.click(page.getByRole('button', { name: 'Move selection' }).element());
- await expect.element(page.getByRole('button', { name: 'Country Canada' })).toBeVisible();
- expect(changes).toEqual([]);
-
- await userEvent.click(page.getByRole('button', { name: 'Country Canada' }).element());
- await userEvent.click(page.getByRole('option', { name: 'Australia' }).element());
- await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
-
- expect(changes).toEqual(['au']);
- await expect.element(page.getByRole('button', { name: 'Country Australia' })).toBeVisible();
-});
-
-test('ComboboxField keeps a controlled selection cleared by the parent', async () => {
- mockScreenWidth(MOBILE_SCREEN_WIDTH);
- function ControlledCombobox() {
- const [value, setValue] = useState('au');
- return (
- <>
-
-
- {renderCountryItem}
-
- >
- );
- }
- render( );
-
- await userEvent.click(page.getByRole('button', { name: 'Reset from outside' }).element());
- await expect
- .element(page.getByRole('button', { name: 'Country Select a country' }))
- .toBeVisible();
-
- await userEvent.click(page.getByRole('button', { name: 'Country Select a country' }).element());
- await expect.element(page.getByRole('dialog')).toBeVisible();
- await expect.element(page.getByRole('searchbox', { name: 'Country' })).toHaveValue('');
-
- await userEvent.keyboard('{Escape}');
- await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
- await expect
- .element(page.getByRole('button', { name: 'Country Select a country' }))
- .toBeVisible();
-});
-
-test('ComboboxField clears the selection when a query replaces it without picking an option', async () => {
- mockScreenWidth(MOBILE_SCREEN_WIDTH);
- const changes: Array = [];
- render(
- changes.push(next)}
- >
- {renderCountryItem}
- ,
- );
-
- const trigger = page.getByRole('button', { name: 'Country Australia' }).element();
- await userEvent.click(trigger);
- const search = page.getByRole('searchbox', { name: 'Country' }).element();
- await userEvent.clear(search);
- await userEvent.type(search, 'Can');
- await userEvent.keyboard('{Escape}');
- await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
-
- expect(changes).toEqual([null]);
- await expect.element(trigger).toHaveTextContent('');
-});
-
test('ComboboxField leaves a consumer-controlled inputValue authoritative', async () => {
const { locator } = render(
@@ -559,16 +300,6 @@ test('ComboboxField clears the selection when the desktop input is emptied', asy
await expect.element(input).toHaveValue('');
});
-test('ComboboxField seeds the desktop input from defaultInputValue', async () => {
- const { locator } = render(
-
- {renderCountryItem}
- ,
- );
-
- await expect.element(locator.getByRole('combobox', { name: 'Country' })).toHaveValue('Aus');
-});
-
test('kitchen sink', { tags: ['visual'] }, async () => {
for (const appearance of visualAppearances) {
const { locator } = render(
@@ -681,89 +412,20 @@ test('kitchen sink', { tags: ['visual'] }, async () => {
}
});
-test('interactive states', { tags: ['visual'] }, async () => {
- const { locator } = render(
-
- {renderCountryItem}
- ,
- );
- const input = page.getByRole('combobox', { name: 'Country' });
- const clear = page.getByRole('button', { name: 'Clear selection' });
- const trigger = page.getByRole('button', { name: 'Toggle options Country' });
-
- await userEvent.hover(input);
- await captureVisual(locator, 'combobox-field/hover');
- await userEvent.unhover(input);
- await userEvent.hover(clear);
- await captureVisual(locator, 'combobox-field/clear-hover');
- await userEvent.unhover(clear);
- await userEvent.hover(trigger);
- await captureVisual(locator, 'combobox-field/trigger-hover');
- await userEvent.unhover(trigger);
- await focusViaKeyboard(input);
- await captureVisual(locator, 'combobox-field/focus-visible');
- await userEvent.tab();
- await captureVisual(locator, 'combobox-field/clear-focus-visible');
- await userEvent.keyboard('{Space>}');
- await captureVisual(locator, 'combobox-field/clear-pressed');
- await userEvent.keyboard('{/Space}');
-});
-
-test('open option and selection states', { tags: ['visual'] }, async () => {
+test('open popover', { tags: ['visual'] }, async () => {
render(
-
-
- }
name="country"
>
{renderCountryItem}
,
);
- const input = page.getByRole('combobox', { name: 'Country' });
-
- await userEvent.click(input);
- await captureVisual(
- page.elementLocator(document.body),
- 'combobox-field/open-selected-disabled-loading',
- );
- await userEvent.keyboard('{Home}');
- await captureVisual(page.elementLocator(document.body), 'combobox-field/option-keyboard-focus');
-});
-test('option with leading icon at both sizes', { tags: ['visual'] }, async () => {
- render(
-
-
- {renderIconItem}
-
-
- {renderIconItem}
-
- ,
- );
- await userEvent.click(page.getByRole('combobox', { name: 'Medium' }));
- await captureVisual(
- page.elementLocator(document.body),
- 'combobox-field/option-leading-icon-medium',
- );
- await userEvent.keyboard('{Escape}');
- await userEvent.click(page.getByRole('combobox', { name: 'Small' }));
- await captureVisual(
- page.elementLocator(document.body),
- 'combobox-field/option-leading-icon-small',
- );
+ await userEvent.click(page.getByRole('combobox', { name: 'Country' }));
+ await captureVisual(page.elementLocator(document.body), 'combobox-field/open');
});
test('mobile tray', { tags: ['visual'] }, async () => {
@@ -792,121 +454,6 @@ test('mobile tray', { tags: ['visual'] }, async () => {
}
});
-test('mobile tray short list', { tags: ['visual'] }, async () => {
- await page.viewport(MOBILE_SCREEN_WIDTH, 700);
- mockScreenWidth(MOBILE_SCREEN_WIDTH);
- try {
- render(
-
-
- {renderCountryItem}
-
- ,
- );
- await userEvent.click(page.getByRole('button', { name: 'Country Select a country...' }));
- await waitForMobileTrayToSettle();
- await captureVisual(page.elementLocator(document.body), 'combobox-field/tray-short');
- } finally {
- // Screen width resets in render-setup `beforeEach`.
- await page.viewport(DESKTOP_SCREEN_WIDTH, 800);
- }
-});
-
-test('forced-colors states', { tags: ['visual'] }, async () => {
- await emulateForcedColors('active');
-
- try {
- const { locator } = render(
-
-
- {renderCountryItem}
-
-
- {renderCountryItem}
-
-
- {renderCountryItem}
-
-
- {renderCountryItem}
-
- ,
- );
- const input = page.getByRole('combobox', { name: 'Interactive' });
- const trigger = page.getByRole('button', { name: 'Toggle options Interactive' }).first();
-
- await captureVisual(locator, 'combobox-field/forced-colors-resting-states');
- await userEvent.hover(trigger);
- await captureVisual(locator, 'combobox-field/forced-colors-trigger-hover');
- await userEvent.unhover(trigger);
- await focusViaKeyboard(input);
- await captureVisual(locator, 'combobox-field/forced-colors-focus-visible');
- await userEvent.keyboard('{ArrowDown}{Home}');
- await captureVisual(
- page.elementLocator(document.body),
- 'combobox-field/forced-colors-open-options',
- );
- await userEvent.keyboard('{Escape}');
- } finally {
- await emulateForcedColors('none');
- }
-});
-
-test('rich section title', { tags: ['visual'] }, async () => {
- render(
-
-
-
- Northern hemisphere countries and territories
- >
- }
- >
- Canada
- United States
-
-
- ,
- );
-
- await userEvent.click(page.getByRole('combobox', { name: 'Country' }));
- await captureVisual(
- page.elementLocator(document.body),
- 'combobox-field/section-title-rich-content',
- );
-});
-
async function waitForMobileTrayToSettle() {
const dialog = page.getByRole('dialog');
await expect.element(dialog).toBeInTheDocument();
diff --git a/packages/@luke-ui/react/src/core/container/container.browser.test.tsx b/packages/@luke-ui/react/src/core/container/container.browser.test.tsx
index 17d26dd3..702673b8 100644
--- a/packages/@luke-ui/react/src/core/container/container.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/container/container.browser.test.tsx
@@ -2,29 +2,11 @@ import { Box } from '@luke-ui/react/box';
import { Container } from '@luke-ui/react/container';
import { Text } from '@luke-ui/react/text';
import { vars } from '@luke-ui/react/theme';
-import { createRef } from 'react';
import { test, afterEach, expect } from 'vite-plus/test';
import { page } from 'vite-plus/test/context';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisualAppearance } from '../test-utils/visual.js';
-test('Container forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- Content
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected Container element.');
-
- expectForwardsDomProps(target, ref);
-});
-
const fixedSizes = [
['ct448', 448],
['ct672', 672],
diff --git a/packages/@luke-ui/react/src/core/em/em.browser.test.tsx b/packages/@luke-ui/react/src/core/em/em.browser.test.tsx
deleted file mode 100644
index 54d9f06b..00000000
--- a/packages/@luke-ui/react/src/core/em/em.browser.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Em } from '@luke-ui/react/em';
-import { createRef } from 'react';
-import { test } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
-import { render } from '../test-utils/render.js';
-
-test('Em forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- stressed
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected an Em element.');
-
- expectForwardsDomProps(target, ref);
-});
diff --git a/packages/@luke-ui/react/src/core/emoji/emoji.browser.test.tsx b/packages/@luke-ui/react/src/core/emoji/emoji.browser.test.tsx
index f103eb8c..9b71e5d0 100644
--- a/packages/@luke-ui/react/src/core/emoji/emoji.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/emoji/emoji.browser.test.tsx
@@ -1,10 +1,8 @@
import { Emoji } from '@luke-ui/react/emoji';
import { Text } from '@luke-ui/react/text';
-import { createRef } from 'react';
import type { CSSProperties } from 'react';
import { test } from 'vite-plus/test';
import { expectNoAxeViolations } from '../test-utils/axe.js';
-import { expectForwardsDomProps, forwardedDomProps } from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisualAppearance, Stack } from '../test-utils/visual.js';
@@ -35,16 +33,6 @@ function EmojiScene() {
);
}
-test('Emoji forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { locator } = render(
- ,
- );
- const target = locator.getByRole('img', { name: 'Celebration' }).element();
-
- expectForwardsDomProps(target, ref);
-});
-
test('the Emoji scene has no axe violations', async () => {
const { container } = render( );
diff --git a/packages/@luke-ui/react/src/core/grid/grid.browser.test.tsx b/packages/@luke-ui/react/src/core/grid/grid.browser.test.tsx
index 89dfa1d5..e868151e 100644
--- a/packages/@luke-ui/react/src/core/grid/grid.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/grid/grid.browser.test.tsx
@@ -2,30 +2,12 @@ import { Box } from '@luke-ui/react/box';
import { Container } from '@luke-ui/react/container';
import { Grid } from '@luke-ui/react/grid';
import { vars } from '@luke-ui/react/theme';
-import { createRef } from 'react';
import { afterEach, expect, test } from 'vite-plus/test';
import { page } from 'vite-plus/test/context';
import { breakpoints } from '../../theme/breakpoints.js';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisualAppearance } from '../test-utils/visual.js';
-test('Grid forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- Content
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected Grid element.');
-
- expectForwardsDomProps(target, ref);
-});
-
afterEach(async () => {
await page.viewport(1024, 800);
});
@@ -148,44 +130,25 @@ test('keeps responsive child spans inside the active column count', async () =>
expect(span.getBoundingClientRect().width).toBeGreaterThan(item.getBoundingClientRect().width);
});
-test('keeps the inline axis under RTL and vertical writing mode', () => {
+test('keeps the inline axis under RTL', () => {
const { locator } = render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
,
);
const rtlFirst = locator.getByTestId('rtl-first').element();
const rtlSecond = locator.getByTestId('rtl-second').element();
- const verticalFirst = locator.getByTestId('vertical-first').element();
- const verticalSecond = locator.getByTestId('vertical-second').element();
- if (
- !(rtlFirst instanceof HTMLElement) ||
- !(rtlSecond instanceof HTMLElement) ||
- !(verticalFirst instanceof HTMLElement) ||
- !(verticalSecond instanceof HTMLElement)
- ) {
+ if (!(rtlFirst instanceof HTMLElement) || !(rtlSecond instanceof HTMLElement)) {
throw new Error('Expected Grid children.');
}
expect(rtlFirst.getBoundingClientRect().left).toBeGreaterThan(
rtlSecond.getBoundingClientRect().left,
);
- expect(verticalSecond.getBoundingClientRect().top).toBeGreaterThan(
- verticalFirst.getBoundingClientRect().top,
- );
});
test('does not let long unbreakable content expand equal grid tracks', () => {
@@ -206,65 +169,6 @@ test('does not let long unbreakable content expand equal grid tracks', () => {
expect(long.getBoundingClientRect().width).toBeCloseTo(short.getBoundingClientRect().width, 1);
});
-test('applies root layout props and ignores unsupported Box utilities from an object spread', () => {
- const props = {
- borderStyle: 'solid',
- borderWidth: 'thick',
- columns: 2,
- gap: 'sp8',
- inlineSize: '10rem',
- padding: 'sp16',
- } as const;
- const { locator } = render(
-
-
-
-
-
- ,
- );
- const container = locator.getByTestId('container').element();
- const element = locator.getByTestId('grid').element();
- if (!(container instanceof HTMLElement) || !(element instanceof HTMLElement)) {
- throw new Error('Expected Grid elements.');
- }
-
- expect(element.getBoundingClientRect().width).toBeLessThan(
- container.getBoundingClientRect().width,
- );
- expect(element.offsetWidth).toBe(element.clientWidth);
-});
-
-test('renders semantic elements and a consumer-owned render prop', () => {
- const ref = createRef();
- const semanticResult = render(
-
- First
- Second
- ,
- );
- const list = semanticResult.locator.getByRole('list', { name: 'Sections' });
- expect(list.element().tagName).toBe('UL');
-
- const customResult = render(
- }
- >
- One
- Two
- ,
- );
- const section = customResult.locator.getByTestId('grid-render').element();
- if (!(section instanceof HTMLElement)) throw new Error('Expected render callback element.');
-
- expect(section.tagName).toBe('SECTION');
- expect(ref.current).toBe(section);
- expect(getComputedStyle(section).display).toBe('grid');
-});
-
const itemStyle = {
backgroundColor: vars.color.surface.floating,
borderRadius: vars.radius.detail,
diff --git a/packages/@luke-ui/react/src/core/heading/heading.browser.test.tsx b/packages/@luke-ui/react/src/core/heading/heading.browser.test.tsx
index 926f7e43..9c001f3c 100644
--- a/packages/@luke-ui/react/src/core/heading/heading.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/heading/heading.browser.test.tsx
@@ -1,8 +1,6 @@
import { Heading, HeadingLevels, useHeadingLevel } from '@luke-ui/react/heading';
-import { createRef } from 'react';
import { expect, test } from 'vite-plus/test';
import { expectNoAxeViolations } from '../test-utils/axe.js';
-import { expectForwardsDomProps, forwardedDomProps } from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import {
captureVisual,
@@ -28,18 +26,6 @@ function HeadingScene() {
);
}
-test('Heading forwards className, data attributes, id, and ref to the heading element', () => {
- const ref = createRef();
- const { locator } = render(
-
- Section title
- ,
- );
- const target = locator.getByRole('heading').element();
-
- expectForwardsDomProps(target, ref);
-});
-
test('the Heading scene has no axe violations', async () => {
const { container } = render( );
diff --git a/packages/@luke-ui/react/src/core/icon-button/icon-button.browser.test.tsx b/packages/@luke-ui/react/src/core/icon-button/icon-button.browser.test.tsx
index e84395c6..f0456bcb 100644
--- a/packages/@luke-ui/react/src/core/icon-button/icon-button.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/icon-button/icon-button.browser.test.tsx
@@ -1,10 +1,8 @@
import { IconButton } from '@luke-ui/react/icon-button';
-import { createRef } from 'react';
import type { JSX } from 'react';
import { expect, test } from 'vite-plus/test';
-import { page, userEvent } from 'vite-plus/test/context';
+import { page } from 'vite-plus/test/context';
import { expectNoAxeViolations } from '../test-utils/axe.js';
-import { expectForwardsDomProps, forwardedDomProps } from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import {
captureVisual,
@@ -41,16 +39,6 @@ function IconButtonScene() {
);
}
-test('IconButton forwards className, data attributes, id, and ref to the button element', () => {
- const ref = createRef();
- const { locator } = render(
- ,
- );
- const target = locator.getByRole('button', { name: 'Add' }).element();
-
- expectForwardsDomProps(target, ref);
-});
-
test('pressing an IconButton runs its onPress handler', async () => {
let pressed = false;
const { locator, user } = render(
@@ -121,21 +109,14 @@ test('kitchen sink', { tags: ['visual'] }, async () => {
}
});
-test('interactive states', { tags: ['visual'] }, async () => {
+test('focus-visible state', { tags: ['visual'] }, async () => {
const { locator } = render( );
- const button = page.getByRole('button', { name: 'Action' });
- await userEvent.hover(button);
- await captureVisual(locator, 'icon-button/hover');
- await userEvent.unhover(button);
- await focusViaKeyboard(button);
+ await focusViaKeyboard(page.getByRole('button', { name: 'Action' }));
await captureVisual(locator, 'icon-button/focus-visible');
- await userEvent.keyboard('{Space>}');
- await captureVisual(locator, 'icon-button/pressed');
- await userEvent.keyboard('{/Space}');
});
-test('forced-colors states', { tags: ['visual'] }, async () => {
+test('forced-colors resting', { tags: ['visual'] }, async () => {
await emulateForcedColors('active');
try {
@@ -146,17 +127,7 @@ test('forced-colors states', { tags: ['visual'] }, async () => {
,
);
- const action = page.getByRole('button', { name: 'Action' });
-
await captureVisual(locator, 'icon-button/forced-colors-resting');
- await userEvent.hover(action);
- await captureVisual(locator, 'icon-button/forced-colors-hover');
- await userEvent.unhover(action);
- await focusViaKeyboard(action);
- await captureVisual(locator, 'icon-button/forced-colors-focus-visible');
- await userEvent.keyboard('{Space>}');
- await captureVisual(locator, 'icon-button/forced-colors-pressed');
- await userEvent.keyboard('{/Space}');
} finally {
await emulateForcedColors('none');
}
diff --git a/packages/@luke-ui/react/src/core/icon-button/icon-button.test.ts b/packages/@luke-ui/react/src/core/icon-button/icon-button.test.ts
index f6888989..9c78a0a5 100644
--- a/packages/@luke-ui/react/src/core/icon-button/icon-button.test.ts
+++ b/packages/@luke-ui/react/src/core/icon-button/icon-button.test.ts
@@ -15,12 +15,6 @@ test('IconButton supports ref and rejects unsupported props', () => {
'aria-label': 'Add',
icon: 'add',
});
- assertType({
- // @ts-expect-error — IconButton has no appearance prop
- appearance: 'button',
- 'aria-label': 'Add',
- icon: 'add',
- });
// @ts-expect-error — accent is not a consumer-selectable tone
assertType({ 'aria-label': 'Add', icon: 'add', tone: 'accent' });
// @ts-expect-error — an icon-only button requires an accessible name
diff --git a/packages/@luke-ui/react/src/core/icon-link/icon-link.browser.test.tsx b/packages/@luke-ui/react/src/core/icon-link/icon-link.browser.test.tsx
index 0f76aab8..80bffee0 100644
--- a/packages/@luke-ui/react/src/core/icon-link/icon-link.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/icon-link/icon-link.browser.test.tsx
@@ -1,10 +1,8 @@
import { IconLink } from '@luke-ui/react/icon-link';
-import { createRef } from 'react';
import type { JSX } from 'react';
import { expect, test } from 'vite-plus/test';
-import { page, userEvent } from 'vite-plus/test/context';
+import { page } from 'vite-plus/test/context';
import { expectNoAxeViolations } from '../test-utils/axe.js';
-import { expectForwardsDomProps, forwardedDomProps } from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import {
captureVisual,
@@ -48,16 +46,6 @@ function IconLinkScene() {
);
}
-test('IconLink forwards className, data attributes, id, and ref to the anchor element', () => {
- const ref = createRef();
- const { locator } = render(
- ,
- );
- const target = locator.getByRole('link', { name: 'Search' }).element();
-
- expectForwardsDomProps(target, ref);
-});
-
test('pressing an IconLink runs its onPress handler', async () => {
let pressed = false;
const { locator, user } = render(
@@ -166,21 +154,14 @@ test('kitchen sink', { tags: ['visual'] }, async () => {
}
});
-test('interactive states', { tags: ['visual'] }, async () => {
+test('focus-visible state', { tags: ['visual'] }, async () => {
const { locator } = render( );
- const link = page.getByRole('link', { name: 'Search' });
- await userEvent.hover(link);
- await captureVisual(locator, 'icon-link/hover');
- await userEvent.unhover(link);
- await focusViaKeyboard(link);
+ await focusViaKeyboard(page.getByRole('link', { name: 'Search' }));
await captureVisual(locator, 'icon-link/focus-visible');
- await userEvent.keyboard('{Enter>}');
- await captureVisual(locator, 'icon-link/pressed');
- await userEvent.keyboard('{/Enter}');
});
-test('forced-colors states', { tags: ['visual'] }, async () => {
+test('forced-colors resting', { tags: ['visual'] }, async () => {
await emulateForcedColors('active');
try {
@@ -190,17 +171,7 @@ test('forced-colors states', { tags: ['visual'] }, async () => {
,
);
- const search = page.getByRole('link', { name: 'Search' });
-
await captureVisual(locator, 'icon-link/forced-colors-resting');
- await userEvent.hover(search);
- await captureVisual(locator, 'icon-link/forced-colors-hover');
- await userEvent.unhover(search);
- await focusViaKeyboard(search);
- await captureVisual(locator, 'icon-link/forced-colors-focus-visible');
- await userEvent.keyboard('{Enter>}');
- await captureVisual(locator, 'icon-link/forced-colors-pressed');
- await userEvent.keyboard('{/Enter}');
} finally {
await emulateForcedColors('none');
}
diff --git a/packages/@luke-ui/react/src/core/icon/icon.browser.test.tsx b/packages/@luke-ui/react/src/core/icon/icon.browser.test.tsx
index 83a5de37..33780d77 100644
--- a/packages/@luke-ui/react/src/core/icon/icon.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/icon/icon.browser.test.tsx
@@ -1,7 +1,7 @@
import { Icon } from '@luke-ui/react/icon';
import { vars } from '@luke-ui/react/theme';
import type { CSSProperties } from 'react';
-import { expect, test } from 'vite-plus/test';
+import { test } from 'vite-plus/test';
import { expectNoAxeViolations } from '../test-utils/axe.js';
import { render, visualAppearances } from '../test-utils/render.js';
import {
@@ -43,16 +43,6 @@ function IconScene() {
);
}
-// Icon does not accept `ref` or data attributes.
-test('forwards className and id to the svg', () => {
- const { container } = render( );
- const svg = container.querySelector('svg');
- if (!(svg instanceof SVGSVGElement)) throw new Error('Expected an svg.');
-
- expect(svg).toHaveClass('forwarded-class');
- expect(svg).toHaveAttribute('id', 'forwarded-id');
-});
-
test('the Icon scene has no axe violations', async () => {
const { container } = render( );
diff --git a/packages/@luke-ui/react/src/core/kbd/kbd.browser.test.tsx b/packages/@luke-ui/react/src/core/kbd/kbd.browser.test.tsx
index fa63856e..6c928856 100644
--- a/packages/@luke-ui/react/src/core/kbd/kbd.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/kbd/kbd.browser.test.tsx
@@ -1,26 +1,8 @@
import { Kbd } from '@luke-ui/react/kbd';
import { Text } from '@luke-ui/react/text';
-import { createRef } from 'react';
import { expect, test } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render } from '../test-utils/render.js';
-test('Kbd forwards className, data attributes, id, and ref to the kbd element', () => {
- const ref = createRef();
- const { container } = render(
-
- ⌘
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected a Kbd element.');
-
- expectForwardsDomProps(target, ref);
-});
-
function getKbdElement(container: HTMLElement): HTMLElement {
const target = container.querySelector('kbd');
if (!(target instanceof HTMLElement)) throw new Error('Expected a Kbd element.');
diff --git a/packages/@luke-ui/react/src/core/link/link.browser.test.tsx b/packages/@luke-ui/react/src/core/link/link.browser.test.tsx
index cc34b988..262c649f 100644
--- a/packages/@luke-ui/react/src/core/link/link.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/link/link.browser.test.tsx
@@ -1,9 +1,7 @@
import { Link } from '@luke-ui/react/link';
-import { createRef } from 'react';
import { expect, test } from 'vite-plus/test';
-import { page, userEvent } from 'vite-plus/test/context';
+import { page } from 'vite-plus/test/context';
import { expectNoAxeViolations } from '../test-utils/axe.js';
-import { expectForwardsDomProps, forwardedDomProps } from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import {
captureVisual,
@@ -45,18 +43,6 @@ function LinkScene() {
);
}
-test('Link forwards className, data attributes, id, and ref to the anchor element', () => {
- const ref = createRef();
- const { locator } = render(
-
- Settings
- ,
- );
- const target = locator.getByRole('link', { name: 'Settings' }).element();
-
- expectForwardsDomProps(target, ref);
-});
-
test('pressing a Link runs its onPress handler', async () => {
let pressed = false;
const { locator, user } = render(
@@ -200,7 +186,7 @@ test('kitchen sink', { tags: ['visual'] }, async () => {
}
});
-test('interactive states', { tags: ['visual'] }, async () => {
+test('focus-visible state', { tags: ['visual'] }, async () => {
const { locator } = render(
@@ -208,47 +194,26 @@ test('interactive states', { tags: ['visual'] }, async () => {
,
);
- const link = page.getByRole('link', { name: 'Destination' });
- await userEvent.hover(link);
- await captureVisual(locator, 'link/hover');
- await userEvent.unhover(link);
- await focusViaKeyboard(link);
+ await focusViaKeyboard(page.getByRole('link', { name: 'Destination' }));
await captureVisual(locator, 'link/focus-visible');
- await userEvent.keyboard('{Enter>}');
- await captureVisual(locator, 'link/pressed');
- await userEvent.keyboard('{/Enter}');
});
-test('forced-colors states', { tags: ['visual'] }, async () => {
+test('forced-colors resting', { tags: ['visual'] }, async () => {
await emulateForcedColors('active');
try {
const { locator } = render(
-
+
Resting
-
- Hovered
-
-
- Pressed and focused
-
Disabled
,
);
- const hovered = page.getByRole('link', { name: 'Hovered' });
-
- await userEvent.hover(hovered);
- await userEvent.tab();
- await userEvent.tab();
- await userEvent.tab();
- await userEvent.keyboard('{Enter>}');
- await captureVisual(locator, 'link/forced-colors-states');
- await userEvent.keyboard('{/Enter}');
+ await captureVisual(locator, 'link/forced-colors-resting');
} finally {
await emulateForcedColors('none');
}
diff --git a/packages/@luke-ui/react/src/core/loading-skeleton/loading-skeleton.browser.test.tsx b/packages/@luke-ui/react/src/core/loading-skeleton/loading-skeleton.browser.test.tsx
index 4d1f0394..a9a18d1d 100644
--- a/packages/@luke-ui/react/src/core/loading-skeleton/loading-skeleton.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/loading-skeleton/loading-skeleton.browser.test.tsx
@@ -1,16 +1,10 @@
import { Button } from '@luke-ui/react/button';
import { LoadingSkeleton, LoadingSkeletonProvider } from '@luke-ui/react/loading-skeleton';
import { TextField } from '@luke-ui/react/text-field';
-import { createRef } from 'react';
import type { CSSProperties } from 'react';
import { afterEach, expect, test } from 'vite-plus/test';
import { expectNoAxeViolations } from '../test-utils/axe.js';
import { emulateReducedMotion } from '../test-utils/emulate-media.js';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisual, captureVisualAppearance, Stack } from '../test-utils/visual.js';
@@ -36,21 +30,6 @@ function LoadingSkeletonScene() {
);
}
-test('LoadingSkeleton forwards className, data attributes, id, and ref to its root', () => {
- const ref = createRef();
- const { container } = render(
-
- Loading copy
- ,
- );
- const target = expectHtmlElement(
- container.firstElementChild,
- 'Expected a LoadingSkeleton element.',
- );
-
- expectForwardsDomProps(target, ref);
-});
-
test('the LoadingSkeleton scene has no axe violations', async () => {
const { container } = render( );
diff --git a/packages/@luke-ui/react/src/core/loading-spinner/loading-spinner.browser.test.tsx b/packages/@luke-ui/react/src/core/loading-spinner/loading-spinner.browser.test.tsx
index 2d50919f..52f94d23 100644
--- a/packages/@luke-ui/react/src/core/loading-spinner/loading-spinner.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/loading-spinner/loading-spinner.browser.test.tsx
@@ -1,10 +1,8 @@
import { LoadingSpinner } from '@luke-ui/react/loading-spinner';
import { vars } from '@luke-ui/react/theme';
-import { createRef } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { test } from 'vite-plus/test';
import { expectNoAxeViolations } from '../test-utils/axe.js';
-import { expectForwardsDomProps, forwardedDomProps } from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import {
captureVisual,
@@ -56,14 +54,6 @@ function LoadingSpinnerScene() {
);
}
-test('LoadingSpinner forwards className, data attributes, id, and ref to its status element', () => {
- const ref = createRef();
- const { locator } = render( );
- const target = locator.getByRole('status').element();
-
- expectForwardsDomProps(target, ref);
-});
-
test('the LoadingSpinner scene has no axe violations', async () => {
const { container } = render( );
diff --git a/packages/@luke-ui/react/src/core/numeral/numeral.browser.test.tsx b/packages/@luke-ui/react/src/core/numeral/numeral.browser.test.tsx
index a3bae436..eb71deaf 100644
--- a/packages/@luke-ui/react/src/core/numeral/numeral.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/numeral/numeral.browser.test.tsx
@@ -1,23 +1,9 @@
import { Numeral } from '@luke-ui/react/numeral';
-import { createRef } from 'react';
import type { CSSProperties } from 'react';
import { test } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisualAppearance, Stack } from '../test-utils/visual.js';
-test('Numeral forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render( );
- const target = expectHtmlElement(container.firstElementChild, 'Expected a Numeral element.');
-
- expectForwardsDomProps(target, ref);
-});
-
const rowStyle = {
display: 'flex',
gap: '1.5rem',
diff --git a/packages/@luke-ui/react/src/core/primitives/button/button.browser.test.tsx b/packages/@luke-ui/react/src/core/primitives/button/button.browser.test.tsx
deleted file mode 100644
index 972a6947..00000000
--- a/packages/@luke-ui/react/src/core/primitives/button/button.browser.test.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Button } from '@luke-ui/react/primitives/button';
-import { createRef } from 'react';
-import { test } from 'vite-plus/test';
-import { expectForwardsDomProps, forwardedDomProps } from '../../test-utils/forwarding.js';
-import { render } from '../../test-utils/render.js';
-
-test('the Button primitive forwards className, data attributes, id, and ref to the button', () => {
- const ref = createRef();
- const { locator } = render(
- ,
- );
- const target = locator.getByRole('button').element();
-
- expectForwardsDomProps(target, ref);
-});
diff --git a/packages/@luke-ui/react/src/core/primitives/combobox/tray.browser.test.tsx b/packages/@luke-ui/react/src/core/primitives/combobox/tray.browser.test.tsx
index 528c50e0..55b48076 100644
--- a/packages/@luke-ui/react/src/core/primitives/combobox/tray.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/primitives/combobox/tray.browser.test.tsx
@@ -40,13 +40,7 @@ function TrayCombobox(
triggerLabel?: string;
} & Pick<
ComboboxRootProps,
- | 'allowsCustomValue'
- | 'form'
- | 'formValue'
- | 'isRequired'
- | 'name'
- | 'validate'
- | 'validationBehavior'
+ 'allowsCustomValue' | 'form' | 'isRequired' | 'name' | 'validate' | 'validationBehavior'
>,
) {
const label = props.label ?? 'Country';
@@ -58,7 +52,6 @@ function TrayCombobox(
defaultItems={countryItems}
defaultValue={props.defaultValue}
form={props.form}
- formValue={props.formValue}
isDisabled={props.isDisabled}
isReadOnly={props.isReadOnly}
isRequired={props.isRequired}
@@ -153,21 +146,6 @@ test('ComboboxTray positions the overlay at the scroll offset each time it opens
window.scrollTo(0, 0);
});
-test('ComboboxTray opens from the trigger, focuses the search field, and dismisses', async () => {
- render( );
-
- expect(page.getByRole('searchbox').elements()).toHaveLength(0);
-
- await openTray();
-
- const searchbox = page.getByRole('searchbox', { name: 'Country' });
- await expect.element(searchbox).toHaveFocus();
- expect(searchbox.element()).toHaveAttribute('aria-haspopup', 'listbox');
-
- await userEvent.keyboard('{Escape}');
- await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
-});
-
test("ComboboxTray search field does not inherit the combobox trigger role's ARIA", async () => {
render( );
await openTray();
@@ -196,17 +174,6 @@ test('ComboboxTray search field does not toggle or close the tray on click or to
await expect.element(searchbox).toHaveFocus();
});
-test('ComboboxTray keeps focus on the search field while arrow keys move the active option', async () => {
- render( );
- await openTray();
-
- const searchbox = page.getByRole('searchbox', { name: 'Country' });
- await userEvent.keyboard('{ArrowDown}');
-
- await expect.element(searchbox).toHaveFocus();
- await expect.poll(() => searchbox.element().getAttribute('aria-activedescendant')).not.toBeNull();
-});
-
test('ComboboxClearButton clears the search text inside a tray', async () => {
render( );
await openTray();
@@ -222,39 +189,38 @@ test('ComboboxClearButton clears the search text inside a tray', async () => {
await expect.element(page.getByRole('dialog')).toBeVisible();
});
-test('ComboboxTrayTrigger names itself from the field label and its selected value', async () => {
- render( );
+test('ComboboxTrayTrigger names itself from the field label, selected value, or an explicit label', async () => {
+ render(
+ <>
+
+
+ >,
+ );
const trigger = page.getByRole('button', { name: 'Country Australia' });
await expect.element(trigger).toBeVisible();
expect(trigger.element()).toHaveAttribute('aria-expanded', 'false');
expect(trigger.element()).toHaveAttribute('aria-haspopup', 'dialog');
+ await expect.element(page.getByRole('button', { name: 'Destination' })).toBeVisible();
});
-test('ComboboxTrayTrigger cannot open a read-only combobox', async () => {
- render( );
-
- const trigger = page.getByRole('button', { name: 'Country Australia' });
- await expect.element(trigger).toBeDisabled();
-
- await expect.element(trigger).toMatchTextContent('Australia');
- expect(page.getByRole('dialog').elements()).toHaveLength(0);
-});
+test('ComboboxTrayTrigger cannot open a read-only or disabled combobox', async () => {
+ render(
+ <>
+
+
+ >,
+ );
-test('ComboboxTrayTrigger cannot open a disabled combobox', async () => {
- render( );
+ const readOnlyTrigger = page.getByRole('button', { name: 'ReadOnly Australia' });
+ const disabledTrigger = page.getByRole('button', { name: 'Disabled Australia' });
- const trigger = page.getByRole('button', { name: 'Country Australia' });
- await expect.element(trigger).toBeDisabled();
+ await expect.element(readOnlyTrigger).toBeDisabled();
+ await expect.element(readOnlyTrigger).toMatchTextContent('Australia');
+ await expect.element(disabledTrigger).toBeDisabled();
expect(page.getByRole('dialog').elements()).toHaveLength(0);
});
-test('ComboboxTrayTrigger uses an explicit accessible name', async () => {
- render( );
-
- await expect.element(page.getByRole('button', { name: 'Destination' })).toBeVisible();
-});
-
test('ComboboxTray search field does not reopen the popover while the tray is exiting', async () => {
render( );
await openTray();
@@ -274,27 +240,6 @@ test('ComboboxTray search field does not reopen the popover while the tray is ex
await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
});
-test('ComboboxTray collection-building pass does not leak duplicate structure', async () => {
- render( );
-
- expect(page.getByRole('searchbox').elements()).toHaveLength(0);
- expect(page.getByRole('button', { name: 'Clear search' }).elements()).toHaveLength(0);
-
- await openTray();
-
- expect(page.getByRole('option').elements()).toHaveLength(countryItems.length);
-});
-
-test('ComboboxTray stays closed after an option is selected', async () => {
- render( );
- await openTray();
-
- await userEvent.click(page.getByRole('option', { name: 'Australia' }));
-
- await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
- await expect.element(page.getByRole('button', { name: 'Country Australia' })).toBeVisible();
-});
-
test('ComboboxTrayTrigger blocks submission of a required, unselected combobox while the tray is closed', async () => {
let submitCount = 0;
const { container } = render(
@@ -492,6 +437,9 @@ test('ComboboxTrayTrigger submits custom text in text mode while the tray is clo
if (form == null) throw new Error('Expected the form element.');
await openTray();
+ // The search field must not keep `name`, or it double-submits with the trigger.
+ expect(page.getByRole('searchbox', { name: 'Country' }).element()).not.toHaveAttribute('name');
+
await enterTraySearch('Freedonia');
expect(new FormData(form).getAll('country')).toEqual(['Freedonia']);
@@ -506,107 +454,14 @@ test('ComboboxTrayTrigger submits custom text in text mode while the tray is clo
expect(new FormData(form).getAll('country')).toEqual(['']);
});
-test('ComboboxTrayTrigger submits the option text, not its key, in text mode', async () => {
- const { container } = render(
- ,
- );
- const form = container.querySelector('form');
- if (form == null) throw new Error('Expected the form element.');
-
- await openTray();
- await userEvent.click(page.getByRole('option', { name: 'Australia' }).element());
- await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
-
- expect(new FormData(form).getAll('country')).toEqual(['Australia']);
-});
-
-test('ComboboxTrayTrigger submits one text-mode value through an external form association', async () => {
- const { container } = render(
- <>
-
-
- >,
- );
- const form = container.querySelector('form');
- if (form == null) throw new Error('Expected the form element.');
-
- // The search field must not keep `name`, or it double-submits with the trigger.
- await openTray();
- const searchbox = page.getByRole('searchbox', { name: 'Country' });
- expect(searchbox.element()).not.toHaveAttribute('name');
-
- await enterTraySearch('Freedonia');
- expect(new FormData(form).getAll('country')).toEqual(['Freedonia']);
-
- await userEvent.keyboard('{Escape}');
- await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
- expect(new FormData(form).getAll('country')).toEqual(['Freedonia']);
-});
-
-test('ComboboxTrayTrigger submits text-mode values for read-only and aria-validated combobox but not a disabled one', async () => {
+test('ComboboxTrayTrigger does not submit text-mode values when disabled', async () => {
const { container } = render(
- <>
-
-
-
- >,
- );
- const forms = [...container.querySelectorAll('form')];
- const [readOnlyForm, ariaForm, disabledForm] = forms;
- if (readOnlyForm == null || ariaForm == null || disabledForm == null) {
- throw new Error('Expected three form elements.');
- }
-
- expect(new FormData(readOnlyForm).getAll('country')).toEqual(['Australia']);
- expect(new FormData(ariaForm).getAll('country')).toEqual(['Australia']);
- expect(new FormData(disabledForm).getAll('country')).toEqual([]);
-});
-
-test('ComboboxTrayTrigger leaves key-mode submission to React Aria', async () => {
- const { container } = render(
- ,
- );
- const form = container.querySelector('form');
- if (form == null) throw new Error('Expected the form element.');
-
- expect(new FormData(form).getAll('country')).toEqual(['au']);
-
- await openTray('Country Australia');
- expect(new FormData(form).getAll('country')).toEqual(['au']);
-
- await userEvent.click(page.getByRole('option', { name: 'Canada' }).element());
- await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
- expect(new FormData(form).getAll('country')).toEqual(['ca']);
-
- // A disabled key-mode field still submits through React Aria's hidden input. The docs rely on that.
- const { container: disabledContainer } = render(
- ,
- );
- const disabledForm = disabledContainer.querySelector('form');
- if (disabledForm == null) throw new Error('Expected the form element.');
- expect(new FormData(disabledForm).getAll('country')).toEqual(['au']);
-});
-
-test('ComboboxTrayTrigger submits text for an explicit formValue of text', async () => {
- const { container } = render(
- ,
);
const form = container.querySelector('form');
if (form == null) throw new Error('Expected the form element.');
- expect(new FormData(form).getAll('country')).toEqual(['Australia']);
+ expect(new FormData(form).getAll('country')).toEqual([]);
});
diff --git a/packages/@luke-ui/react/src/core/prose/prose.browser.test.tsx b/packages/@luke-ui/react/src/core/prose/prose.browser.test.tsx
index 7f09ec0d..3aeb060f 100644
--- a/packages/@luke-ui/react/src/core/prose/prose.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/prose/prose.browser.test.tsx
@@ -4,28 +4,10 @@ import { Heading } from '@luke-ui/react/heading';
import { Prose, proseRecipe } from '@luke-ui/react/prose';
import { Text } from '@luke-ui/react/text';
import type { CSSProperties } from 'react';
-import { createRef } from 'react';
import { test, expect } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisualAppearance, Stack } from '../test-utils/visual.js';
-test('Prose forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- Content
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected a Prose element.');
-
- expectForwardsDomProps(target, ref);
-});
-
function query(root: Element, selector: string) {
const element = root.querySelector(selector);
if (element == null) throw new Error(`Expected a ${selector} in the document.`);
@@ -95,31 +77,6 @@ test('normalises a nested pre margin', () => {
).toBeCloseTo(0, 0);
});
-// Typed ols outside Prose stay on the ordinary markerless reset.
-test('keeps typed ordered lists markerless outside Prose', () => {
- const { container } = render(
-
-
- - a
-
-
- - A
-
-
- - i
-
-
- - I
-
-
- - one
-
- ,
- );
-
- expect(listStyleTypes(container)).toEqual(['none', 'none', 'none', 'none', 'none']);
-});
-
// Chromium and Safari match `type` case-insensitively, so CSS must not restate A/a or I/i.
// `proseRecipe` is public, so the scope must ride the recipe class, not the component.
test('preserves native ordered-list type markers under proseRecipe alone', () => {
diff --git a/packages/@luke-ui/react/src/core/quote/quote.browser.test.tsx b/packages/@luke-ui/react/src/core/quote/quote.browser.test.tsx
deleted file mode 100644
index 73bfe5c9..00000000
--- a/packages/@luke-ui/react/src/core/quote/quote.browser.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Quote } from '@luke-ui/react/quote';
-import { createRef } from 'react';
-import { test } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
-import { render } from '../test-utils/render.js';
-
-test('Quote forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- short quote
-
,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected a Quote element.');
-
- expectForwardsDomProps(target, ref);
-});
diff --git a/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.browser.test.tsx b/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.browser.test.tsx
index 3729ea40..54cab8f8 100644
--- a/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.browser.test.tsx
@@ -1,40 +1,18 @@
import { ScrollFade } from '@luke-ui/react/scroll-fade';
import { Text } from '@luke-ui/react/text';
import { vars } from '@luke-ui/react/theme';
-import { createRef, useState } from 'react';
+import { useState } from 'react';
import { renderToString } from 'react-dom/server';
import { expect, test } from 'vite-plus/test';
import { userEvent } from 'vite-plus/test/context';
import { expectNoAxeViolations } from '../test-utils/axe.js';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
+import { expectHtmlElement } from '../test-utils/forwarding.js';
import { hydrate, render, visualAppearances } from '../test-utils/render.js';
import { captureVisualAppearance, Grid } from '../test-utils/visual.js';
-import { ScrollFade as ScrollFadeSource, logicalEndSide, overflowsOnAxis } from './scroll-fade.js';
-
-/** Standards-based physical gradient angles for logical-end sides. Hard-coded so tests do not share a wrong production mapping. */
-const maskGradientAngle = {
- bottom: 180,
- left: 270,
- right: 90,
- top: 0,
-} as const;
-
-test('ScrollFade forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- Content
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected ScrollFade element.');
+import { ScrollFade as ScrollFadeSource } from './scroll-fade.js';
- expectForwardsDomProps(target, ref);
- expect(target.tagName).toBe('DIV');
-});
+/** Physical gradient angles for logical-end sides — hard-coded so tests do not share a wrong production mapping. */
+const maskGradientAngle = { bottom: 180, left: 270, right: 90, top: 0 } as const;
test('fitting div has no mask, tab stop, region role, or accessible name', async () => {
const { locator } = render(
@@ -97,8 +75,6 @@ test('overflowing block content is keyboard-focusable with a region role and mas
expect(element.getAttribute('aria-label')).toBe('Tall list');
expect(getComputedStyle(element).overflowBlock).toBe('auto');
expect(getComputedStyle(element).overflowInline).toBe('hidden');
- expect(getComputedStyle(element).scrollbarWidth).toBe('none');
- expect(getComputedStyle(element, '::-webkit-scrollbar').display).toBe('none');
element.focus();
await userEvent.keyboard('{ArrowDown}');
await expect.poll(() => element.scrollTop).toBeGreaterThan(0);
@@ -145,101 +121,27 @@ test('region role and accessible name appear together when a fitting div overflo
expect(element.hasAttribute('aria-label')).toBe(false);
});
-test('replacing overflowing content still measures overflow after removal', async () => {
- function Fixture() {
- const [generation, setGeneration] = useState(0);
- return (
- <>
-
-
-
- Generation {generation}
-
-
- >
- );
- }
-
- const { locator } = render( );
- const element = await waitForScrollport(locator.getByTestId('scroll-fade').element(), true);
- expect(element.getAttribute('role')).toBe('region');
- expectMaskToward(element, 'bottom');
-
- const replaceButton = expectHtmlElement(
- locator.getByTestId('replace').element(),
- 'Expected replace button.',
- );
- replaceButton.click();
- await waitForScrollport(element, true);
- expect(element.textContent).toContain('Generation 1');
- expect(element.getAttribute('role')).toBe('region');
- expectMaskToward(element, 'bottom');
-
- replaceButton.click();
- await waitForScrollport(element, true);
- expect(element.textContent).toContain('Generation 2');
- expect(element.tabIndex).toBe(0);
- expectMaskToward(element, 'bottom');
-});
-
-const writingModeCases = [
+for (const writingCase of [
{
direction: 'ltr',
- expectedBlockEnd: 'bottom',
expectedInlineEnd: 'right',
label: 'horizontal-tb LTR',
writingMode: 'horizontal-tb',
},
{
direction: 'rtl',
- expectedBlockEnd: 'bottom',
expectedInlineEnd: 'left',
label: 'horizontal-tb RTL',
writingMode: 'horizontal-tb',
},
+ // One vertical case proves logical inline-end maps to a physical side other than left/right.
{
direction: 'ltr',
- expectedBlockEnd: 'left',
expectedInlineEnd: 'bottom',
label: 'vertical-rl LTR',
writingMode: 'vertical-rl',
},
- {
- direction: 'rtl',
- expectedBlockEnd: 'left',
- expectedInlineEnd: 'top',
- label: 'vertical-rl RTL',
- writingMode: 'vertical-rl',
- },
- {
- direction: 'ltr',
- expectedBlockEnd: 'right',
- expectedInlineEnd: 'bottom',
- label: 'vertical-lr LTR',
- writingMode: 'vertical-lr',
- },
- {
- direction: 'rtl',
- expectedBlockEnd: 'right',
- expectedInlineEnd: 'top',
- label: 'vertical-lr RTL',
- writingMode: 'vertical-lr',
- },
-] as const;
-
-for (const writingCase of writingModeCases) {
+] as const) {
test(`inline overflow maps correctly for ${writingCase.label}`, async () => {
const { locator } = render(
@@ -257,75 +159,16 @@ for (const writingCase of writingModeCases) {
,
);
const element = await waitForScrollport(locator.getByTestId('scroll-fade').element(), true);
- const styles = getComputedStyle(element);
-
- expect(styles.writingMode).toBe(writingCase.writingMode);
- expect(styles.direction).toBe(writingCase.direction);
- expect(overflowsOnAxis(element, 'inline')).toBe(true);
- expect(overflowsOnAxis(element, 'block')).toBe(false);
- expect(styles.overflowInline).toBe('auto');
- expect(styles.overflowBlock).toBe('hidden');
+
+ expect(getComputedStyle(element).writingMode).toBe(writingCase.writingMode);
+ expect(getComputedStyle(element).direction).toBe(writingCase.direction);
expect(element.tabIndex).toBe(0);
expect(element.getAttribute('role')).toBe('region');
expect(element.getAttribute('data-scroll-fade-end')).toBe(writingCase.expectedInlineEnd);
- expect(logicalEndSide(element, 'inline')).toBe(writingCase.expectedInlineEnd);
expectMaskToward(element, writingCase.expectedInlineEnd);
});
-
- test(`block overflow maps correctly for ${writingCase.label}`, async () => {
- const { locator } = render(
-
-
- 長いブロック内容 for logical overflow
-
- ,
- );
- const element = await waitForScrollport(locator.getByTestId('scroll-fade').element(), true);
- const styles = getComputedStyle(element);
-
- expect(styles.writingMode).toBe(writingCase.writingMode);
- expect(styles.direction).toBe(writingCase.direction);
- expect(overflowsOnAxis(element, 'block')).toBe(true);
- expect(overflowsOnAxis(element, 'inline')).toBe(false);
- expect(styles.overflowBlock).toBe('auto');
- expect(styles.overflowInline).toBe('hidden');
- expect(element.tabIndex).toBe(0);
- expect(element.getAttribute('role')).toBe('region');
- expect(element.getAttribute('data-scroll-fade-end')).toBe(writingCase.expectedBlockEnd);
- expect(logicalEndSide(element, 'block')).toBe(writingCase.expectedBlockEnd);
- expectMaskToward(element, writingCase.expectedBlockEnd);
- });
}
-test('CSS direction rtl on the scrollport flips the inline mask', async () => {
- const { locator } = render(
-
-
- Overflowing inline content for CSS direction
-
- ,
- );
- const element = await waitForScrollport(locator.getByTestId('scroll-fade').element(), true);
-
- expect(getComputedStyle(element).direction).toBe('rtl');
- expect(element.getAttribute('data-scroll-fade-end')).toBe('left');
- expect(logicalEndSide(element, 'inline')).toBe('left');
- expectMaskToward(element, 'left');
-});
-
test('inherited CSS direction updates the inline mask without remounting', async () => {
function Fixture() {
const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr');
@@ -351,44 +194,14 @@ test('inherited CSS direction updates the inline mask without remounting', async
const { locator } = render( );
const element = await waitForScrollport(locator.getByTestId('scroll-fade').element(), true);
- expect(getComputedStyle(element).direction).toBe('ltr');
+ expect(element.getAttribute('data-scroll-fade-end')).toBe('right');
expectMaskToward(element, 'right');
await userEvent.click(locator.getByRole('button', { name: 'Switch to RTL' }));
- await expect.poll(() => getComputedStyle(element).direction).toBe('rtl');
await expect.poll(() => element.getAttribute('data-scroll-fade-end')).toBe('left');
-
- expect(logicalEndSide(element, 'inline')).toBe('left');
expectMaskToward(element, 'left');
});
-test('vertical writing mode with RTL and text-orientation upright keeps logical inline end', async () => {
- const { locator } = render(
-
-
- 長い upright contents
-
- ,
- );
- const element = await waitForScrollport(locator.getByTestId('scroll-fade').element(), true);
- const styles = getComputedStyle(element);
-
- expect(styles.writingMode).toBe('vertical-rl');
- expect(styles.direction).toBe('rtl');
- expect(styles.textOrientation).toBe('upright');
- expect(overflowsOnAxis(element, 'inline')).toBe(true);
- expect(element.tabIndex).toBe(0);
- expect(element.getAttribute('data-scroll-fade-end')).toBe('top');
- expect(logicalEndSide(element, 'inline')).toBe('top');
- expectMaskToward(element, 'top');
-});
-
test('inline scroll progression fades start then end across the scroll range', async () => {
const { locator } = render(
{
- const { locator } = render(
-
-
-
-
-
- ,
- );
- const element = await waitForScrollport(locator.getByTestId('scroll-fade').element(), true);
-
- expect(element.tabIndex).toBe(0);
- expect(element.getAttribute('role')).toBe('region');
- expect(element.getAttribute('aria-label')).toBe('Example items');
- expect(locator.getByRole('button', { name: 'First item' }).element().tabIndex).toBe(0);
-});
-
test('nested intrinsic image load updates overflow from fitting to overflowing', async () => {
const tinySvg = svgDataUri(16, 16);
const tallSvg = svgDataUri(16, 320);
@@ -541,7 +330,6 @@ test('nested intrinsic image load updates overflow from fitting to overflowing',
);
const element = await waitForScrollport(locator.getByTestId('scroll-fade').element());
expect(element.tabIndex).toBe(-1);
- expect(element.hasAttribute('role')).toBe(false);
expect(getComputedStyle(element).maskImage).toBe('none');
const image = expectHtmlElement(
@@ -552,23 +340,17 @@ test('nested intrinsic image load updates overflow from fitting to overflowing',
await loadImageSource(image, tallSvg);
await waitForAttribute(element, 'role', 'region');
-
expect(element.tabIndex).toBe(0);
- expect(element.getAttribute('role')).toBe('region');
expect(element.getAttribute('aria-label')).toBe('Image list');
expectMaskToward(element, 'bottom');
await loadImageSource(image, tinySvg);
await waitForAttribute(element, 'role', null);
-
expect(element.tabIndex).toBe(-1);
- expect(element.hasAttribute('role')).toBe(false);
expect(getComputedStyle(element).maskImage).toBe('none');
});
-// WAAPI changes used size without childList/characterData/class/style mutations.
-// Keeps fit ↔ overflow a11y covered for descendant geometry that MutationObserver
-// attribute/childList signals do not see.
+// WAAPI changes used size without MutationObserver-visible childList/attribute mutations.
test('descendant Web Animations size change updates overflow accessibility without DOM mutation', async () => {
const { locator } = render(
,
);
const element = await waitForScrollport(locator.getByTestId('scroll-fade').element());
-
expect(element.tabIndex).toBe(-1);
expect(element.hasAttribute('role')).toBe(false);
- expect(element.hasAttribute('aria-label')).toBe(false);
const child = expectHtmlElement(
locator.getByTestId('animated-child').element(),
'Expected animated child.',
);
- const grow = child.animate([{ blockSize: '2rem' }, { blockSize: '18rem' }], {
+ await child.animate([{ blockSize: '2rem' }, { blockSize: '18rem' }], {
duration: 1,
fill: 'forwards',
- });
- await grow.finished;
+ }).finished;
await waitForAttribute(element, 'role', 'region');
-
expect(element.tabIndex).toBe(0);
- expect(element.getAttribute('role')).toBe('region');
expect(element.getAttribute('aria-label')).toBe('Animated list');
- const shrink = child.animate([{ blockSize: '18rem' }, { blockSize: '2rem' }], {
+ await child.animate([{ blockSize: '18rem' }, { blockSize: '2rem' }], {
duration: 1,
fill: 'forwards',
- });
- await shrink.finished;
+ }).finished;
await waitForAttribute(element, 'role', null);
-
expect(element.tabIndex).toBe(-1);
expect(element.hasAttribute('role')).toBe(false);
- expect(element.hasAttribute('aria-label')).toBe(false);
});
test('SSR markup hydrates without mismatch and then measures overflow', async () => {
@@ -641,7 +415,6 @@ test('SSR markup hydrates without mismatch and then measures overflow', async ()
const { locator, recoverableErrors, unmount } = hydrate(markup, tree);
try {
const element = await waitForScrollport(locator.getByTestId('scroll-fade').element(), true);
-
expect(element.tabIndex).toBe(0);
expect(element.getAttribute('role')).toBe('region');
expect(element.getAttribute('aria-label')).toBe('Hydrated list');
@@ -652,41 +425,28 @@ test('SSR markup hydrates without mismatch and then measures overflow', async ()
}
});
-test('fitting and overflowing default divs have no axe violations', async () => {
- const { container: fitting, locator: fittingLocator } = render(
-
- Short
- ,
- );
- const fittingElement = await waitForScrollport(
- fittingLocator.getByTestId('fitting-axe').element(),
- );
- expect(fittingElement.hasAttribute('role')).toBe(false);
- await expectNoAxeViolations(fitting);
-
- const { container: overflowing, locator } = render(
-
-
- Overflowing content for axe
-
- ,
- );
- const overflowingElement = await waitForScrollport(
- locator.getByTestId('overflowing-axe').element(),
- true,
+test('fitting and overflowing ScrollFades have no axe violations', async () => {
+ const { container, locator } = render(
+ <>
+
+ Short
+
+
+
+ Overflowing content for axe
+
+
+
+ >,
);
- expect(overflowingElement.getAttribute('role')).toBe('region');
- await expectNoAxeViolations(overflowing);
-});
-
-test('the ScrollFade scene has no axe violations', async () => {
- const { container } = render( );
+ await waitForScrollport(locator.getByTestId('fitting-axe').element());
+ await waitForScrollport(locator.getByTestId('overflowing-axe').element(), true);
await expectNoAxeViolations(container);
});
@@ -697,29 +457,6 @@ test('kitchen sink', { tags: ['visual'] }, async () => {
}
});
-test('overflowing focus-visible state', { tags: ['visual'] }, async () => {
- for (const appearance of visualAppearances) {
- const { locator } = render(
-
-
- Design tokens · Layout · Forms · Feedback · Typography
-
- ,
- { appearance },
- );
- const element = await waitForScrollport(locator.getByTestId('scroll-fade').element(), true);
- element.focus({ focusVisible: true });
- expect(element.matches(':focus-visible')).toBe(true);
- expectMaskToward(element, 'right');
- await captureVisualAppearance(locator, 'scroll-mask/focus-visible', appearance);
- }
-});
-
function ScrollFadeScene() {
return (
@@ -761,16 +498,13 @@ function loadImageSource(image: HTMLImageElement, src: string): Promise {
});
}
-/** Waits for ScrollFade's observer-driven React update, not for size polling. */
function waitForAttribute(element: HTMLElement, name: string, value: string | null): Promise {
if (element.getAttribute(name) === value) return Promise.resolve();
-
return new Promise((resolve, reject) => {
const timeoutId = window.setTimeout(() => {
observer.disconnect();
reject(new Error(`Timed out waiting for ${name}=${String(value)}.`));
}, 2000);
-
const observer = new MutationObserver(() => {
if (element.getAttribute(name) === value) {
window.clearTimeout(timeoutId);
@@ -801,7 +535,6 @@ function fadeLayer(maskImage: string): string {
function hasStartFade(maskImage: string): boolean {
const layer = fadeLayer(maskImage);
- // No fade: `0px` or `calc(0% + (0 * …))`. Partial/full fade uses a non-zero multiplier or bare min().
if (/rgb\(0,\s*0,\s*0\)\s+0px/.test(layer)) return false;
if (/rgb\(0,\s*0,\s*0\)\s+calc\(0%\s*\+\s*\(0\s*\*/.test(layer)) return false;
return /rgb\(0,\s*0,\s*0\)\s+calc\(0%\s*\+/.test(layer);
@@ -873,17 +606,12 @@ async function waitForScrollport(node: Element, shouldOverflow?: boolean): Promi
const deadline = Date.now() + 2000;
while (Date.now() < deadline) {
- const axis: 'inline' | 'block' =
- getComputedStyle(node).overflowInline === 'auto' ? 'inline' : 'block';
- const overflows = overflowsOnAxis(node, axis);
- const focusable = node.tabIndex === 0;
- if (shouldOverflow === undefined || overflows === shouldOverflow) {
- if (shouldOverflow === undefined || focusable === shouldOverflow) return node;
- }
+ const overflows = node.tabIndex === 0;
+ if (shouldOverflow === undefined || overflows === shouldOverflow) return node;
await new Promise((resolve) => requestAnimationFrame(resolve));
}
throw new Error(
- `Timed out waiting for ScrollFade overflow=${String(shouldOverflow)} (tabIndex=${node.tabIndex}).`,
+ `Timed out waiting for ScrollFade overflow=${String(shouldOverflow)} (tabIndex=${String(node.tabIndex)}).`,
);
}
diff --git a/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.test.ts b/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.test.ts
index fddf76ea..7559547a 100644
--- a/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.test.ts
+++ b/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.test.ts
@@ -1,19 +1,8 @@
-import { ScrollFade } from '@luke-ui/react/scroll-fade';
-import { createElement } from 'react';
-import { renderToString } from 'react-dom/server';
-import { assertType, expect, expectTypeOf, test } from 'vite-plus/test';
+import { assertType, expectTypeOf, test } from 'vite-plus/test';
import type { ScrollFadeRecipeVariants } from './recipe.css.js';
import type { ScrollFadeProps } from './scroll-fade.js';
test('ScrollFade requires an accessible name for the div root', () => {
- assertType({
- 'aria-label': 'Topics',
- children: 'Content',
- });
- assertType({
- 'aria-labelledby': 'topics-heading',
- children: 'Content',
- });
// @ts-expect-error — div requires aria-label or aria-labelledby
assertType({ children: 'Content' });
// @ts-expect-error — aria-label and aria-labelledby are mutually exclusive
@@ -69,32 +58,6 @@ test('ScrollFade axis is a closed scalar union', () => {
test('ScrollFadeRecipeVariants exposes axis only, not internal overflows state', () => {
expectTypeOf().toEqualTypeOf<{ axis?: 'block' | 'inline' }>();
- assertType({ axis: 'inline' });
- assertType({ axis: 'block' });
- assertType({});
// @ts-expect-error — overflows is internal runtime state, not a public recipe variant
assertType({ overflows: true });
});
-
-test('SSR markup omits overflowing accessibility state', () => {
- const markup = renderToString(
- createElement(
- ScrollFade,
- {
- 'aria-label': 'Hydrated list',
- inlineSize: '8rem',
- padding: 'sp8',
- },
- createElement(
- 'span',
- { style: { display: 'inline-block', inlineSize: '24rem', whiteSpace: 'nowrap' } },
- 'Overflowing inline content for hydration',
- ),
- ),
- );
-
- expect(markup).not.toContain('role="region"');
- expect(markup).not.toMatch(/tabindex=/i);
- expect(markup).not.toContain('aria-label="Hydrated list"');
- expect(markup).toContain('Overflowing inline content for hydration');
-});
diff --git a/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.tsx b/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.tsx
index d8da0869..9544dba8 100644
--- a/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.tsx
+++ b/packages/@luke-ui/react/src/core/scroll-fade/scroll-fade.tsx
@@ -66,7 +66,7 @@ export function ScrollFade(props: ScrollFadeProps): JSX.Element {
export type ScrollFadeAxis = 'inline' | 'block';
/** Physical side that corresponds to the active axis's logical end. */
-export type ScrollFadePhysicalSide = 'bottom' | 'left' | 'right' | 'top';
+type ScrollFadePhysicalSide = 'bottom' | 'left' | 'right' | 'top';
type _ScrollFadeOwnedLayoutProperty = 'overflow' | 'overflowX' | 'overflowY';
@@ -219,10 +219,6 @@ function useScrollOverflow(
}
/** Whether `element` overflows on the logical `axis` for its writing mode. */
-export function overflowsOnAxis(element: HTMLElement, axis: ScrollFadeAxis): boolean {
- return overflowsOnAxisForWritingMode(element, axis, getComputedStyle(element).writingMode);
-}
-
function overflowsOnAxisForWritingMode(
element: HTMLElement,
axis: ScrollFadeAxis,
@@ -245,7 +241,7 @@ function overflowsOnAxisForWritingMode(
* Measured from used layout (CSS `direction`, writing mode, `text-orientation`, etc.) via a logical
* inset probe — not from `:dir()`, which ignores `style={{ direction }}`.
*/
-export function logicalEndSide(element: HTMLElement, axis: ScrollFadeAxis): ScrollFadePhysicalSide {
+function logicalEndSide(element: HTMLElement, axis: ScrollFadeAxis): ScrollFadePhysicalSide {
return physicalSideOfLogicalEnd(element, axis === 'inline' ? 'inline-end' : 'block-end');
}
diff --git a/packages/@luke-ui/react/src/core/stack/stack.browser.test.tsx b/packages/@luke-ui/react/src/core/stack/stack.browser.test.tsx
index 9be65a24..cc85247b 100644
--- a/packages/@luke-ui/react/src/core/stack/stack.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/stack/stack.browser.test.tsx
@@ -1,29 +1,11 @@
import { Stack } from '@luke-ui/react/stack';
import { vars } from '@luke-ui/react/theme';
-import { createRef } from 'react';
import { afterEach, expect, test } from 'vite-plus/test';
import { page } from 'vite-plus/test/context';
import { breakpoints } from '../../theme/breakpoints.js';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisualAppearance } from '../test-utils/visual.js';
-test('Stack forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- Content
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected Stack element.');
-
- expectForwardsDomProps(target, ref);
-});
-
afterEach(async () => {
await page.viewport(1024, 800);
});
@@ -49,76 +31,28 @@ test('flows children on the block axis with no gap by default', () => {
expect(second.getBoundingClientRect().top - first.getBoundingClientRect().bottom).toBe(0);
});
-test('emits no gap utility when gap is omitted', () => {
+test('keeps the block axis under RTL', () => {
const { locator } = render(
-
-
-
- ,
- );
- const element = locator.getByTestId('stack').element();
- if (!(element instanceof HTMLElement)) throw new Error('Expected Stack element.');
-
- expect(element.className).not.toMatch(/gap/);
- expect(getComputedStyle(element).gap).toBe('normal');
-});
-
-test('accepts gap and alignItems overrides', () => {
- const { locator } = render(
-
- First
- Second
- ,
- );
- const element = locator.getByTestId('stack').element();
- if (!(element instanceof HTMLElement)) throw new Error('Expected Stack element.');
-
- expect(getComputedStyle(element).alignItems).toBe('center');
-});
-
-test('keeps the block axis under RTL and vertical writing mode', () => {
- const { locator } = render(
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
,
);
const rtl = locator.getByTestId('stack-rtl').element();
- const vertical = locator.getByTestId('stack-vertical').element();
- if (!(rtl instanceof HTMLElement) || !(vertical instanceof HTMLElement)) {
- throw new Error('Expected Stack elements.');
- }
+ if (!(rtl instanceof HTMLElement)) throw new Error('Expected Stack element.');
expect(getComputedStyle(rtl).flexDirection).toBe('column');
- expect(getComputedStyle(vertical).flexDirection).toBe('column');
const [rtlFirst, rtlSecond] = rtl.children;
- const [verticalFirst, verticalSecond] = vertical.children;
- if (
- !(rtlFirst instanceof HTMLElement) ||
- !(rtlSecond instanceof HTMLElement) ||
- !(verticalFirst instanceof HTMLElement) ||
- !(verticalSecond instanceof HTMLElement)
- ) {
+ if (!(rtlFirst instanceof HTMLElement) || !(rtlSecond instanceof HTMLElement)) {
throw new Error('Expected Stack children.');
}
expect(rtlSecond.getBoundingClientRect().top).toBeGreaterThan(
rtlFirst.getBoundingClientRect().top,
);
- expect(verticalSecond.getBoundingClientRect().left).toBeLessThan(
- verticalFirst.getBoundingClientRect().left,
- );
});
test('applies a responsive gap from its required initial value', async () => {
@@ -142,82 +76,6 @@ test('applies a responsive gap from its required initial value', async () => {
expect(second.getBoundingClientRect().top).toBeGreaterThan(first.getBoundingClientRect().bottom);
});
-test('keeps Stack defaults below sparse responsive alignment overrides', async () => {
- await page.viewport(breakpoints.bp640, 800);
- const { locator } = render(
-
-
-
- ,
- );
- const element = locator.getByTestId('stack').element();
- if (!(element instanceof HTMLElement)) throw new Error('Expected Stack element.');
-
- expect(getComputedStyle(element).alignItems).toBe('stretch');
-
- await page.viewport(breakpoints.bp768, 800);
- expect(getComputedStyle(element).alignItems).toBe('center');
-});
-
-test('applies root layout props and ignores unsupported Box utilities from an object spread', () => {
- const props = {
- borderStyle: 'solid',
- borderWidth: 'thick',
- gap: 'sp8',
- inlineSize: '10rem',
- padding: 'sp16',
- } as const;
- const { locator } = render(
-
-
-
-
- ,
- );
- const container = locator.getByTestId('container').element();
- const element = locator.getByTestId('stack').element();
- if (!(container instanceof HTMLElement) || !(element instanceof HTMLElement)) {
- throw new Error('Expected Stack elements.');
- }
- const child = element.firstElementChild;
- if (!(child instanceof HTMLElement)) throw new Error('Expected Stack child.');
-
- expect(element.getBoundingClientRect().width).toBeLessThan(
- container.getBoundingClientRect().width,
- );
- expect(element.getBoundingClientRect().width).toBeGreaterThan(
- child.getBoundingClientRect().width,
- );
- expect(element.offsetWidth).toBe(element.clientWidth);
-});
-
-test('renders semantic elements and a consumer-owned render prop', () => {
- const ref = createRef();
- const semanticResult = render(
-
- Account summary content
- ,
- );
- const section = semanticResult.locator.getByRole('region', { name: 'Account summary' });
- expect(section.element().tagName).toBe('SECTION');
-
- const customResult = render(
- }
- >
- Custom article
- ,
- );
- const article = customResult.locator.getByTestId('stack-render').element();
- if (!(article instanceof HTMLElement)) throw new Error('Expected render callback element.');
-
- expect(article.tagName).toBe('ARTICLE');
- expect(ref.current).toBe(article);
- expect(getComputedStyle(article).flexDirection).toBe('column');
-});
-
const itemStyle = {
backgroundColor: vars.color.surface.floating,
borderRadius: vars.radius.detail,
diff --git a/packages/@luke-ui/react/src/core/strong/strong.browser.test.tsx b/packages/@luke-ui/react/src/core/strong/strong.browser.test.tsx
deleted file mode 100644
index d42ac4d7..00000000
--- a/packages/@luke-ui/react/src/core/strong/strong.browser.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Strong } from '@luke-ui/react/strong';
-import { createRef } from 'react';
-import { test } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
-import { render } from '../test-utils/render.js';
-
-test('Strong forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- important
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected a Strong element.');
-
- expectForwardsDomProps(target, ref);
-});
diff --git a/packages/@luke-ui/react/src/core/styles/stylesheet-contract.test.ts b/packages/@luke-ui/react/src/core/styles/stylesheet-contract.test.ts
index a8731529..95d5f0a6 100644
--- a/packages/@luke-ui/react/src/core/styles/stylesheet-contract.test.ts
+++ b/packages/@luke-ui/react/src/core/styles/stylesheet-contract.test.ts
@@ -1,4 +1,3 @@
-import { existsSync } from 'node:fs';
import { transform } from 'lightningcss';
import { readFile } from 'node:fs/promises';
import { expect, test } from 'vite-plus/test';
@@ -8,7 +7,6 @@ import { typeStyles } from '../../theme/contract.js';
const lukeOwnedLayerNames = ['reset', 'base', 'recipes', 'utilities'] as const;
const lukeOwnedLayerNameSet = new Set(lukeOwnedLayerNames);
const AUTHORITATIVE_LAYER_ORDER_PATTERN = /^@layer reset, base, recipes, utilities;/m;
-const AUTHORITATIVE_LAYER_ORDER_LINE_PATTERN = /^@layer reset, base, recipes, utilities;\n/m;
type TextClassesByTypography = Record>;
const numericLineClampVariants = [2, 3, 4, 5] as const;
type NumericLineClampVariant = (typeof numericLineClampVariants)[number];
@@ -97,7 +95,6 @@ test('builds the public stylesheet with the retained layer contract', async () =
const stylesheetMutations: Array<[string, (css: string) => string]> = [
['missing stable selector', (css: string) => css.replace('.luke-ui-theme', '.theme-root')],
- ['extra stable selector', (css: string) => `${css}\n@layer reset { .luke-ui-extra {} }`],
[
'reordered authoritative layer declarations',
(css: string) => {
@@ -107,31 +104,7 @@ const stylesheetMutations: Array<[string, (css: string) => string]> = [
);
},
],
- [
- 'early individual layer declarations before authoritative order',
- (css: string) => {
- return css.replace(
- AUTHORITATIVE_LAYER_ORDER_LINE_PATTERN,
- '@layer reset;\n@layer base;\n@layer recipes;\n@layer utilities;\n@layer reset, base, recipes, utilities;\n',
- );
- },
- ],
- [
- 'early layer block before authoritative order',
- (css: string) => {
- return css.replace(
- AUTHORITATIVE_LAYER_ORDER_LINE_PATTERN,
- '@layer recipes { .early {} }\n@layer reset, base, recipes, utilities;\n',
- );
- },
- ],
- ['anonymous layer statement', (css: string) => `${css}\n@layer;`],
- ['anonymous layer block', (css: string) => `${css}\n@layer { .anonymous {} }`],
['unknown layer', (css: string) => `${css}\n@layer components;`],
- ['nested layer', (css: string) => `${css}\n@layer recipes { @layer utilities {} }`],
- ['root qualified rule', (css: string) => `${css}\n.root-rule { color: red; }`],
- ['base layer rule', (css: string) => `${css}\n@layer base { .consumer-default { color: red; } }`],
- ['lookalike layer at-rule', (css: string) => `${css}\n@layered {}`],
[
'representative recipe class moved to the wrong layer',
(css: string) => {
@@ -141,44 +114,6 @@ const stylesheetMutations: Array<[string, (css: string) => string]> = [
);
},
],
- [
- 'representative utility class moved to the wrong layer',
- (css: string) => {
- return css.replace(
- '@layer utilities {\n .utility-class { display: grid; }\n}',
- '@layer recipes {\n .utility-class { display: grid; }\n}',
- );
- },
- ],
- [
- 'representative recipe content removed',
- (css: string) =>
- css.replace(
- ' .recipe-class { display: inline-flex; }\n .recipe-class > * { margin-block-start: 1px; }\n',
- '',
- ),
- ],
- [
- 'class-like text in an attribute value',
- (css: string) => {
- return css.replace(
- '@layer recipes {\n .recipe-class { display: inline-flex; }\n .recipe-class > * { margin-block-start: 1px; }\n}',
- '@layer recipes {\n [data-class=".recipe-class"] { display: inline-flex; }\n}',
- );
- },
- ],
- [
- 'redundant empty layer statements after authoritative order',
- (css: string) => css.replace(AUTHORITATIVE_LAYER_ORDER_LINE_PATTERN, '$&@layer recipes;\n'),
- ],
- [
- 'empty transitional recipes layer',
- (css: string) =>
- css.replace(
- '@layer recipes {\n .recipe-class { display: inline-flex; }\n .recipe-class > * { margin-block-start: 1px; }\n}',
- '@layer recipes {}',
- ),
- ],
];
for (const [name, mutate] of stylesheetMutations) {
@@ -192,13 +127,6 @@ for (const [name, mutate] of stylesheetMutations) {
});
}
-test('ships every style rule in one stylesheet', () => {
- const dist = (file: string) => new URL(`../../../dist/${file}`, import.meta.url);
-
- expect(existsSync(dist('stylesheet.css'))).toBe(true);
- expect(existsSync(dist('stylesheet2.css'))).toBe(false);
-});
-
test('queries responsive conditions on the logical inline axis', async () => {
const stylesheet = await readPublicStylesheet();
diff --git a/packages/@luke-ui/react/src/core/text-field/text-field.browser.test.tsx b/packages/@luke-ui/react/src/core/text-field/text-field.browser.test.tsx
index 33d0c13a..85585af7 100644
--- a/packages/@luke-ui/react/src/core/text-field/text-field.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/text-field/text-field.browser.test.tsx
@@ -116,42 +116,36 @@ function TextFieldScene() {
);
}
-// RAC puts `id` on the control, not the root.
-test('TextField forwards className and data attributes to its root, and id to the DOM', () => {
- const { container } = render(
- ,
- );
- const root = container.firstElementChild;
- if (!(root instanceof HTMLElement)) throw new Error('Expected a TextField root.');
-
- expect(root).toHaveClass('forwarded-class');
- expect(root).toHaveAttribute('data-forwarded', 'true');
- expect(container.querySelector('#forwarded-id')).not.toBeNull();
-});
-
-test('TextField resolves inputRef to the input, participates in a form, and fires onBlur', () => {
+// Luke UI widens RAC's `inputRef` to accept React Hook Form's callback ref.
+test('TextField resolves object and callback inputRef to the input, participates in a form, and fires onBlur', () => {
const inputRef = createRef();
+ const callbackResolved: Array = [];
let blurred = false;
const { container, locator } = render(
- {
- blurred = true;
- }}
- />,
+ <>
+ {
+ blurred = true;
+ }}
+ />
+ {
+ callbackResolved.push(node);
+ }}
+ label="Callback"
+ name="callback-name"
+ />
+ >,
);
const control = locator.getByRole('textbox', { name: 'Name' }).element();
+ const callbackControl = locator.getByRole('textbox', { name: 'Callback' }).element();
expect(inputRef.current).toBe(control);
+ expect(callbackResolved.at(-1)).toBe(callbackControl);
expect(control).toHaveAttribute('aria-describedby');
const form = document.createElement('form');
@@ -172,32 +166,6 @@ test('TextField resolves inputRef to the input, participates in a form, and fire
form.remove();
});
-// Luke UI widens RAC's `inputRef` to accept React Hook Form's callback ref.
-test('TextField resolves a callback inputRef to the input', () => {
- const resolved: Array = [];
- const { locator } = render(
- {
- resolved.push(node);
- }}
- label="Name"
- name="full-name"
- />,
- );
- const control = locator.getByRole('textbox', { name: 'Name' }).element();
-
- expect(resolved.at(-1)).toBe(control);
-});
-
-test('typing in a TextField reports each value through onChange', async () => {
- let value = '';
- const { locator, user } = render( (value = next)} />);
- const input = locator.getByRole('textbox', { name: 'Name' });
-
- await user.type(input, 'Luke');
- expect(value).toBe('Luke');
-});
-
test('the TextField scene has no axe violations', async () => {
const { container } = render( );
@@ -227,35 +195,6 @@ function indicatorFor(name: string): SVGSVGElement | null {
return glyph?.closest('svg') ?? null;
}
-// The primitive renders the control itself, so it takes a plain `ref`. Both ref
-// shapes are covered: React Hook Form hands out a callback ref, so the callback
-// arm is the one that decides whether the component is usable with it at all.
-test('InputGroupInput resolves object and callback refs to the input element', () => {
- const objectRef = createRef();
- const callbackResolved: Array = [];
- render(
- <>
-
-
-
-
- {
- callbackResolved.push(node);
- }}
- />
-
- >,
- );
-
- const objectInput = page.getByRole('textbox', { name: 'Amount object' });
- const callbackInput = page.getByRole('textbox', { name: 'Amount callback' });
-
- expect(objectRef.current).toBe(objectInput.element());
- expect(callbackResolved.at(-1)).toBe(callbackInput.element());
-});
-
// The shared invalid selector must not match `:has(:invalid)`: that matches a required,
// empty input from first render — before any interaction or submit — while
// `aria-invalid` stays null, painting an untouched required field invalid even
@@ -312,14 +251,9 @@ test('kitchen sink', { tags: ['visual'] }, async () => {
}
});
-test('interactive states', { tags: ['visual'] }, async () => {
+test('keyboard focus ring', { tags: ['visual'] }, async () => {
const { locator } = render( );
- const input = page.getByRole('textbox', { name: 'Focus me' });
-
- await userEvent.hover(input);
- await captureVisual(locator, 'text-field/hover');
- await userEvent.unhover(input);
- await focusViaKeyboard(input);
+ await focusViaKeyboard(page.getByRole('textbox', { name: 'Focus me' }));
await captureVisual(locator, 'text-field/focus-visible');
});
@@ -329,9 +263,8 @@ test('forced-colors states', { tags: ['visual'] }, async () => {
try {
const { locator } = render(
-
+
-
{
/>
,
);
- const input = page.getByRole('textbox', { name: 'Interactive' });
-
- await captureVisual(locator, 'text-field/forced-colors-resting-states');
- await userEvent.hover(input);
- await captureVisual(locator, 'text-field/forced-colors-hover');
- await userEvent.unhover(input);
- await focusViaKeyboard(input);
- await captureVisual(locator, 'text-field/forced-colors-focus-visible');
+ await captureVisual(locator, 'text-field/forced-colors-states');
} finally {
await emulateForcedColors('none');
}
diff --git a/packages/@luke-ui/react/src/core/text/text.browser.test.tsx b/packages/@luke-ui/react/src/core/text/text.browser.test.tsx
index 8f957dbc..650a657f 100644
--- a/packages/@luke-ui/react/src/core/text/text.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/text/text.browser.test.tsx
@@ -4,29 +4,11 @@ import { Kbd } from '@luke-ui/react/kbd';
import { Strong } from '@luke-ui/react/strong';
import { Text } from '@luke-ui/react/text';
import { typeStyles } from '@luke-ui/react/theme';
-import { createRef } from 'react';
import type { CSSProperties } from 'react';
import { test, expect } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
import { captureVisual, captureVisualAppearance, Stack } from '../test-utils/visual.js';
-test('Text forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
-
- Body copy
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected a Text element.');
-
- expectForwardsDomProps(target, ref);
-});
-
// `shouldInheritFont` inherits `textTransform` and `fontVariantNumeric` along with the other font
// properties, and their variants default to emitting nothing. Without both halves, a composing
// component would reset the surrounding uppercase and numeric styling to `none`/`normal`.
diff --git a/packages/@luke-ui/react/src/core/track/track.browser.test.tsx b/packages/@luke-ui/react/src/core/track/track.browser.test.tsx
index 18a1e972..efad4481 100644
--- a/packages/@luke-ui/react/src/core/track/track.browser.test.tsx
+++ b/packages/@luke-ui/react/src/core/track/track.browser.test.tsx
@@ -1,27 +1,9 @@
import { Heading } from '@luke-ui/react/heading';
import { vars } from '@luke-ui/react/theme';
import { Track } from '@luke-ui/react/track';
-import { createRef } from 'react';
import { expect, test } from 'vite-plus/test';
-import {
- expectForwardsDomProps,
- expectHtmlElement,
- forwardedDomProps,
-} from '../test-utils/forwarding.js';
import { render, visualAppearances } from '../test-utils/render.js';
-import { captureVisualAppearance, variantValuesFor } from '../test-utils/visual.js';
-
-test('Track forwards className, data attributes, id, and ref to its element', () => {
- const ref = createRef();
- const { container } = render(
- ,
- );
- const target = expectHtmlElement(container.firstElementChild, 'Expected Track element.');
-
- expectForwardsDomProps(target, ref);
-});
+import { captureVisualAppearance } from '../test-utils/visual.js';
test('omits a rail wrapper and its gap when a rail prop is absent', () => {
const neither = render(
@@ -110,25 +92,7 @@ test('rails keep their natural inline size while the centre can shrink', () => {
expect(centre.getBoundingClientRect().width).toBeLessThan(64);
});
-test('maps every railAlignment to the expected cross-axis alignment, defaulting to start', () => {
- const alignments = [
- ['start', 'flex-start'],
- ['firstLine', 'flex-start'],
- ['center', 'center'],
- ['end', 'flex-end'],
- ] as const;
-
- for (const [railAlignment, expected] of alignments) {
- const { locator } = render(
- ,
- );
- const element = locator.getByTestId('track').element();
- if (!(element instanceof HTMLElement)) throw new Error('Expected Track element.');
- expect(getComputedStyle(element).alignItems).toBe(expected);
- }
-
+test('defaults railAlignment to start and pins firstLine to the centre’s first line', () => {
const { locator: defaultLocator } = render(