@astryxdesign/core 0.7.0 → 0.7.1-canary.2b6d129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +12 -2
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +25 -16
- package/dist/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +22 -8
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +3 -26
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +10 -3
- package/dist/Typeahead/BaseTypeahead.d.ts +1 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +12 -3
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts.map +1 -1
- package/dist/utils/focusReturn.d.ts +12 -8
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +44 -3
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.test.tsx +114 -0
- package/src/BottomSheet/BottomSheet.tsx +12 -2
- package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +96 -0
- package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +5 -6
- package/src/CodeBlock/CodeBlock.spec.md +55 -45
- package/src/CodeBlock/CodeBlock.test.tsx +11 -9
- package/src/CodeBlock/CodeBlock.tsx +29 -18
- package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
- package/src/ContextMenu/ContextMenu.spec.md +1 -1
- package/src/ContextMenu/ContextMenu.test.tsx +36 -3
- package/src/ContextMenu/ContextMenu.tsx +27 -11
- package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +153 -0
- package/src/Dialog/Dialog.tsx +2 -30
- package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
- package/src/DropdownMenu/DropdownMenu.test.tsx +97 -1
- package/src/DropdownMenu/DropdownMenu.tsx +10 -3
- package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +73 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
- package/src/Markdown/Markdown.spec.md +37 -23
- package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
- package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
- package/src/PowerSearch/PowerSearch.test.tsx +45 -0
- package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
- package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
- package/src/Typeahead/BaseTypeahead.test.tsx +99 -1
- package/src/Typeahead/BaseTypeahead.tsx +17 -3
- package/src/utils/focusReturn.test.ts +79 -2
- package/src/utils/focusReturn.ts +48 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DropdownMenuFocusReturn.a11y.browser.spec.ts
|
|
5
|
+
* @input The `Core/DropdownMenu` CustomTrigger story (an icon button the
|
|
6
|
+
* caller renders) in a built Storybook, real Chromium/WebKit pointer and
|
|
7
|
+
* keyboard input.
|
|
8
|
+
* @output Browser evidence that the focus the menu returns to a trigger the
|
|
9
|
+
* caller renders follows the last input
|
|
10
|
+
* (docs/architecture/interaction-modality.md INV1,
|
|
11
|
+
* `component:DropdownMenu/DEC-1`): no ring after a pointer pick, the ring
|
|
12
|
+
* after a keyboard pick.
|
|
13
|
+
* @position Cross-browser proof; jsdom only proves which focus call the menu
|
|
14
|
+
* makes. Unless the menu asks for no ring, both engines ring the focus it
|
|
15
|
+
* returns after a pointer pick.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {expect, test, type Locator, type Page} from '@playwright/test';
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_STORYBOOK_DIR,
|
|
21
|
+
serveStorybook,
|
|
22
|
+
type StaticServer,
|
|
23
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
24
|
+
|
|
25
|
+
let storybook: StaticServer;
|
|
26
|
+
test.beforeAll(async () => {
|
|
27
|
+
storybook = await serveStorybook(
|
|
28
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
test.afterAll(async () => {
|
|
32
|
+
await storybook?.close();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function mount(page: Page): Promise<Locator> {
|
|
36
|
+
await page.goto(
|
|
37
|
+
`${storybook.origin}/iframe.html?id=core-dropdownmenu--custom-trigger&viewMode=story`,
|
|
38
|
+
);
|
|
39
|
+
const trigger = page.getByRole('button', {name: 'More actions'});
|
|
40
|
+
await expect(trigger).toBeVisible();
|
|
41
|
+
return trigger;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function matchesFocusVisible(locator: Locator): Promise<boolean> {
|
|
45
|
+
return locator.evaluate(element => element.matches(':focus-visible'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test('a pointer pick returns focus to the trigger without a ring', async ({
|
|
49
|
+
page,
|
|
50
|
+
}) => {
|
|
51
|
+
const trigger = await mount(page);
|
|
52
|
+
await trigger.click();
|
|
53
|
+
const menu = page.getByRole('menu');
|
|
54
|
+
await expect(menu).toBeVisible();
|
|
55
|
+
await page.getByRole('menuitem', {name: 'Rename'}).click();
|
|
56
|
+
await expect(menu).toBeHidden();
|
|
57
|
+
await expect(trigger).toBeFocused();
|
|
58
|
+
expect(await matchesFocusVisible(trigger)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a keyboard pick returns focus to the trigger with its ring', async ({
|
|
62
|
+
page,
|
|
63
|
+
}) => {
|
|
64
|
+
const trigger = await mount(page);
|
|
65
|
+
await trigger.focus();
|
|
66
|
+
await page.keyboard.press('Enter');
|
|
67
|
+
const menu = page.getByRole('menu');
|
|
68
|
+
await expect(page.getByRole('menuitem', {name: 'Rename'})).toBeFocused();
|
|
69
|
+
await page.keyboard.press('Enter');
|
|
70
|
+
await expect(menu).toBeHidden();
|
|
71
|
+
await expect(trigger).toBeFocused();
|
|
72
|
+
expect(await matchesFocusVisible(trigger)).toBe(true);
|
|
73
|
+
});
|
|
@@ -168,26 +168,18 @@ async function runState(
|
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const results: BindingResult[] = [];
|
|
177
|
-
for (const state of CORE_STATUS_MESSAGE_BINDING_STATES) {
|
|
178
|
-
results.push(await runState(page, cdp, state));
|
|
179
|
-
}
|
|
180
|
-
expect(neverExercised(results)).toEqual([]);
|
|
181
|
-
expect(
|
|
182
|
-
unmatchedKnownFailures(CORE_STATUS_MESSAGE_KNOWN_FAILURES, results),
|
|
183
|
-
).toEqual([]);
|
|
184
|
-
});
|
|
171
|
+
// Each state's result, recorded by its own test below so the whole-suite check
|
|
172
|
+
// at the end reads it instead of measuring every state a second time. A state
|
|
173
|
+
// whose test did not run in this worker (a filtered run, or a worker restarted
|
|
174
|
+
// after a failure) is measured by the whole-suite check itself.
|
|
175
|
+
const recorded = new Map<string, BindingResult>();
|
|
185
176
|
|
|
186
177
|
for (const state of CORE_STATUS_MESSAGE_BINDING_STATES) {
|
|
187
178
|
test(`${state.binding} [${state.id}] — ${state.summary}`, async ({page}) => {
|
|
188
179
|
test.setTimeout(2 * 60 * 1000);
|
|
189
180
|
const cdp = await page.context().newCDPSession(page);
|
|
190
181
|
const result = await runState(page, cdp, state);
|
|
182
|
+
recorded.set(state.id, result);
|
|
191
183
|
const report = summarize(STATUS_MESSAGE_PATTERN, [result]);
|
|
192
184
|
// eslint-disable-next-line no-console -- the report is this run's artifact
|
|
193
185
|
console.log(formatReport(report));
|
|
@@ -196,3 +188,18 @@ for (const state of CORE_STATUS_MESSAGE_BINDING_STATES) {
|
|
|
196
188
|
expect(report.counts.unexpectedPass).toBe(0);
|
|
197
189
|
});
|
|
198
190
|
}
|
|
191
|
+
|
|
192
|
+
test('every expectation is exercised by at least one Core binding state', async ({
|
|
193
|
+
page,
|
|
194
|
+
}) => {
|
|
195
|
+
test.setTimeout(4 * 60 * 1000);
|
|
196
|
+
const cdp = await page.context().newCDPSession(page);
|
|
197
|
+
const results: BindingResult[] = [];
|
|
198
|
+
for (const state of CORE_STATUS_MESSAGE_BINDING_STATES) {
|
|
199
|
+
results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
|
|
200
|
+
}
|
|
201
|
+
expect(neverExercised(results)).toEqual([]);
|
|
202
|
+
expect(
|
|
203
|
+
unmatchedKnownFailures(CORE_STATUS_MESSAGE_KNOWN_FAILURES, results),
|
|
204
|
+
).toEqual([]);
|
|
205
|
+
});
|
|
@@ -64,18 +64,30 @@ constrained renderer seams. Callers may opt into the canonical plugin protocol f
|
|
|
64
64
|
bounded source syntax, immutable document transformation, typed extension
|
|
65
65
|
rendering, and native typed document-start frontmatter. They may separately opt into dollar-delimited math by supplying one typed
|
|
66
66
|
renderer for both inline and display expressions. The parser accepts matching
|
|
67
|
-
explicit options.
|
|
68
|
-
|
|
67
|
+
explicit options. Plugins and math change nothing when absent; the default Code
|
|
68
|
+
block path renders `diff` and `diff-<language>` fences as CodeBlock's diff layer
|
|
69
|
+
(FR29), and every other parsing, rendering, styling, and streaming behavior is
|
|
70
|
+
the released one.
|
|
69
71
|
|
|
70
72
|
## Compatibility and migration
|
|
71
73
|
|
|
72
|
-
- Released default preserved: `yes`
|
|
73
|
-
- Compatibility class: additive, opt-in public API
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
- Released default preserved: `yes`, except the default Code block path for `diff` fences (FR29, below)
|
|
75
|
+
- Compatibility class: additive, opt-in public API, with one visible default-path
|
|
76
|
+
rendering change (FR29, below); existing parser nodes, DOM, styling, targets,
|
|
77
|
+
dollar-delimited text, `components`, and `inlinePlugins` remain unchanged unless
|
|
78
|
+
the caller supplies `plugins`, supplies `components.math`, or passes the matching
|
|
79
|
+
explicit parser option.
|
|
77
80
|
- Controlled/uncontrolled behavior: not applicable
|
|
78
81
|
- Migration decision: none
|
|
82
|
+
- Default-path rendering change for diff fences (FR29), a visible default change
|
|
83
|
+
and not a breaking one: a `diff` fence that rendered as plain text renders as CodeBlock's diff
|
|
84
|
+
layer over plaintext with a `diff` label, and a `diff-<language>` fence —
|
|
85
|
+
which rendered as plain text with a `diff` label and viewport name, its
|
|
86
|
+
leading word being all Markdown read — as the layer over that language with
|
|
87
|
+
a `<language> diff` label; the header, copy, disclosure, and viewport name
|
|
88
|
+
stay, and a supplied `code` renderer's contract is unchanged. The way back to
|
|
89
|
+
plain text is a `text` fence or a custom `code` renderer. Every other fence
|
|
90
|
+
is unchanged.
|
|
79
91
|
- Default-path layout change (FR26–FR28): a default table's columns may become
|
|
80
92
|
wider than the released fixed buckets, never narrower; header labels wrap
|
|
81
93
|
instead of truncating; a table that squashed its columns may now scroll.
|
|
@@ -228,6 +240,7 @@ unions. Enabled calls return the explicit `InlineNodeWithMath` and
|
|
|
228
240
|
| FR26 | On the default table path each column has a content-derived width floor: the larger of its own longest unbreakable token and a readable minimum derived from that column's content, bounded so a table of short columns still fits its container. The floor applies to the cell's text, not to its padded box, so it means the same amount of content at every density. A header label contributes its own readable minimum to that comparison. A table whose floors exceed the container grows and scrolls rather than dividing the container equally. |
|
|
229
241
|
| FR27 | Default table header cells never truncate: no ellipsis and no width clamp. A header reads in full, on one line up to a bounded width and wrapping past it. Inline code inside a default table cell keeps its token whole rather than breaking mid-token; a supplied `inlineCode` renderer owns its own wrapping. |
|
|
230
242
|
| FR28 | Markdown's Table block owns spacing, sizing, and alignment. The nested Table's own Scroll region remains the sole owner of the table's horizontal overflow, its accessible name, and its conditional keyboard focusability; Markdown adds no second scroll container, name, or tab stop of its own. |
|
|
243
|
+
| FR29 | On the default Code block path the fence's full first word selects CodeBlock's diff layer (`component:CodeBlock` FR9): `diff` renders the layer over plaintext, `diff-<language>` the layer over `<language>` (the language read by the same leading-word rule as any other fence); every other fence is unchanged. A supplied `code` renderer's contract is unchanged: it keeps receiving the leading word characters as `language` — `diff` for both `diff` and `diff-<language>` fences — and nothing new. |
|
|
231
244
|
|
|
232
245
|
FR23 includes CommonMark-compatible lazy paragraph continuation inside blockquotes
|
|
233
246
|
and ordered, unordered, or task-list items. Omitting a repeated quote marker or
|
|
@@ -421,22 +434,23 @@ and this change preserves the existing spelling exactly.
|
|
|
421
434
|
|
|
422
435
|
## Verification map
|
|
423
436
|
|
|
424
|
-
| Contract | Verification
|
|
425
|
-
| ---------------------- |
|
|
426
|
-
| FR1–FR5 | `Markdown.test.tsx`, theme-target tests, and `scripts/check-knowledge.mjs`
|
|
427
|
-
| FR6–FR10 | `parser.test.ts` and `Markdown.test.tsx`
|
|
428
|
-
| FR11 | `incremental.test.ts` and `Markdown.test.tsx`
|
|
429
|
-
| FR12–FR16 | plugin, transform, adapter, performance, and Outline parser tests
|
|
430
|
-
| FR17–FR18 | container parse/validation, ownership, and dependency tests
|
|
431
|
-
| FR19 | diagnostic-channel tests in development and production
|
|
432
|
-
| FR20–FR22 | canonical/server imports, inference, streaming, preparation, theming tests
|
|
433
|
-
| FR23 | parser, incremental, renderer, nesting, and public option tests
|
|
434
|
-
| FR24 | `plugins/frontmatter.test.tsx`, Storybook, and server rendering
|
|
435
|
-
| FR25 | `parser.public.test.ts`, plugin SSR tests, and package export checks
|
|
436
|
-
| FR26–FR28 | `Markdown.test.tsx` and a real-browser layer over checked-in narrow-width stories
|
|
437
|
-
|
|
|
438
|
-
|
|
|
439
|
-
|
|
|
437
|
+
| Contract | Verification | Representative states | Failure signal |
|
|
438
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
439
|
+
| FR1–FR5 | `Markdown.test.tsx`, theme-target tests, and `scripts/check-knowledge.mjs` | Default block/inline output and all current targets | Existing DOM, target, spacing, or renderer behavior changes. |
|
|
440
|
+
| FR6–FR10 | `parser.test.ts` and `Markdown.test.tsx` | Opt-out, inline/display math, escapes, currency, code, links, plugins | A delimiter is claimed without opt-in, TeX is formatted as Markdown, or opaque contexts leak. |
|
|
441
|
+
| FR11 | `incremental.test.ts` and `Markdown.test.tsx` | Every-character top-level/list/task-list/blockquote splits, CRLF, source ranges | Streaming diverges from a full parse, shows partial syntax, mistakes a nested closer for an opener, or crosses a closed container. |
|
|
442
|
+
| FR12–FR16 | plugin, transform, adapter, performance, and Outline parser tests | omitted/empty lists, immutable transforms, invalid outputs, live updates, one compatible Remark plugin, duplicate headings | Empty behavior forks, input mutates, invalid structure escapes, transforms reparse, adapter loses content, budgets fail, or heading targets diverge. |
|
|
443
|
+
| FR17–FR18 | container parse/validation, ownership, and dependency tests | leaf/container declarations, nested containers, foreign read/remove/mint/edit, unmet/misordered dependencies | Plugin-built parsed children, invalid content, lost fallback children, foreign mint/edit, or generic ownership codes. |
|
|
444
|
+
| FR19 | diagnostic-channel tests in development and production | every phase, advisory reports, rate suppression, no handler, malformed list, duplicate Core, version skew | A silent production failure, document content in a diagnostic, or one entrypoint throwing where another recovers. |
|
|
445
|
+
| FR20–FR22 | canonical/server imports, inference, streaming, preparation, theming tests | server imports, no explicit type args, chunk boundaries, recreated lists, themed/unthemed extensions | Client references, explicit-type workarounds, oscillation, per-render re-preparation, or unreachable opted-in output. |
|
|
446
|
+
| FR23 | parser, incremental, renderer, nesting, and public option tests | mixed lists; escaped prose/code table cells, nested inline code, full/streaming parity; autolink omitted/enabled | A mixed list splits or loses state, an escaped table pipe changes cells or exposes its backslash, or autolink changes other syntax. |
|
|
447
|
+
| FR24 | `plugins/frontmatter.test.tsx`, Storybook, and server rendering | complete, malformed, non-leading, LF/CRLF, unfinished streaming, full plugin stack | Metadata syntax renders after completion, unfinished syntax leaks while streaming, typing is lost, or later plugins stop composing. |
|
|
448
|
+
| FR25 | `parser.public.test.ts`, plugin SSR tests, and package export checks | server/RSC parsing with plugins, synchronous SSR, suspending renderer streaming boundary | A server import gains `use client`, plugin execution needs serialization, SSR loses fallback, or direct RSC rendering is misrepresented as supported. |
|
|
449
|
+
| FR26–FR28 | `Markdown.test.tsx` and a real-browser layer over checked-in narrow-width stories | short, prose, and token-heavy tables; header-only rows and empty cells; long headers over short bodies; reading columns from phone to document width | A column collapses below its content floor, cell padding eats the floor, a header truncates, an inline-code token breaks mid-token, or a second scroll viewport, name, or tab stop appears around a table. |
|
|
450
|
+
| FR29 | `Markdown.test.tsx` (a `diff` fence → plaintext + layer, a `diff-tsx` fence → tsx + layer, a plain fence unchanged, a custom `code` renderer receives `language="diff"` for both diff fences and nothing new) | default path and custom renderer, `diff` and `diff-<language>` fences | a fence word other than `diff`/`diff-<language>` reaching the layer, or a custom renderer receiving a new prop or a different `language` |
|
|
451
|
+
| Public syntax/types | `Markdown.public.test.ts`, core typecheck, and `Markdown.doc.mjs` | Legacy exhaustive switches, math opt-ins, inferred extension-node unions | A released union widens, an enabled union loses nodes, or docs drift from declarations. |
|
|
452
|
+
| Navigation contract | `parser.test.ts`, `Markdown.test.tsx`, and `Markdown.renderBoundary.test.tsx` | Parsed and rendered links, including transformed built-in links; accepted ordinary schemes; rejected destinations; links versus images | A blocked destination reaches navigation or a custom link renderer, or resource policy narrows accepted navigation. |
|
|
453
|
+
| Security/accessibility | `parser.test.ts`, `Markdown.test.tsx`, and renderer guidance | Inert expression strings and renderer-owned semantics | Astryx executes math as HTML or silently claims renderer-owned accessibility. |
|
|
440
454
|
|
|
441
455
|
Focused tests continue to pin all nine current target names and default block
|
|
442
456
|
placement. Math intentionally adds no target and no default anatomy.
|
|
@@ -412,9 +412,10 @@ describe('createMarkdownFenceTransform', () => {
|
|
|
412
412
|
expect(createNode).not.toHaveBeenCalled();
|
|
413
413
|
expect(document.querySelector('pre')).toHaveTextContent('plain source');
|
|
414
414
|
expect(screen.getByRole('button', {name: 'Copy code'})).toBeInTheDocument();
|
|
415
|
+
// CodeBlock's named viewport; its tab stop follows overflow (none in jsdom).
|
|
415
416
|
expect(screen.getByRole('group', {name: 'text'})).toHaveAttribute(
|
|
416
|
-
'
|
|
417
|
-
'
|
|
417
|
+
'data-scroll-axis',
|
|
418
|
+
'both',
|
|
418
419
|
);
|
|
419
420
|
|
|
420
421
|
rerender(
|
|
@@ -71,21 +71,6 @@ async function runState(
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
test('every expectation is exercised by at least one NumberInput state', async ({
|
|
75
|
-
page,
|
|
76
|
-
}) => {
|
|
77
|
-
test.setTimeout(4 * 60 * 1000);
|
|
78
|
-
const cdp = await page.context().newCDPSession(page);
|
|
79
|
-
const results: BindingResult[] = [];
|
|
80
|
-
for (const state of NUMBER_INPUT_A11Y_STATES) {
|
|
81
|
-
results.push(await runState(page, cdp, state));
|
|
82
|
-
}
|
|
83
|
-
expect(neverExercised(results)).toEqual([]);
|
|
84
|
-
expect(unmatchedKnownFailures(NUMBER_INPUT_KNOWN_FAILURES, results)).toEqual(
|
|
85
|
-
[],
|
|
86
|
-
);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
74
|
test('the Chromium read-only omission remains exact visible debt', async ({
|
|
90
75
|
page,
|
|
91
76
|
}) => {
|
|
@@ -104,13 +89,35 @@ test('the Chromium read-only omission remains exact visible debt', async ({
|
|
|
104
89
|
expect(readOnly?.knownFailure).toBe(NUMBER_INPUT_KNOWN_FAILURES[0]);
|
|
105
90
|
});
|
|
106
91
|
|
|
92
|
+
// Each state's result, recorded by its own test below so the whole-suite check
|
|
93
|
+
// at the end reads it instead of measuring every state a second time. A state
|
|
94
|
+
// whose test did not run in this worker (a filtered run, or a worker restarted
|
|
95
|
+
// after a failure) is measured by the whole-suite check itself.
|
|
96
|
+
const recorded = new Map<string, BindingResult>();
|
|
97
|
+
|
|
107
98
|
for (const state of NUMBER_INPUT_A11Y_STATES) {
|
|
108
99
|
test(`NumberInput [${state.id}] — ${state.summary}`, async ({page}) => {
|
|
109
100
|
const cdp = await page.context().newCDPSession(page);
|
|
110
101
|
const result = await runState(page, cdp, state);
|
|
102
|
+
recorded.set(state.id, result);
|
|
111
103
|
const report = summarize(SPINBUTTON_PATTERN, [result]);
|
|
112
104
|
expect(formatFailures(blockingResults([result]))).toBe('');
|
|
113
105
|
expect(report.unrunLayers).toEqual([]);
|
|
114
106
|
expect(report.counts.unexpectedPass).toBe(0);
|
|
115
107
|
});
|
|
116
108
|
}
|
|
109
|
+
|
|
110
|
+
test('every expectation is exercised by at least one NumberInput state', async ({
|
|
111
|
+
page,
|
|
112
|
+
}) => {
|
|
113
|
+
test.setTimeout(4 * 60 * 1000);
|
|
114
|
+
const cdp = await page.context().newCDPSession(page);
|
|
115
|
+
const results: BindingResult[] = [];
|
|
116
|
+
for (const state of NUMBER_INPUT_A11Y_STATES) {
|
|
117
|
+
results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
|
|
118
|
+
}
|
|
119
|
+
expect(neverExercised(results)).toEqual([]);
|
|
120
|
+
expect(unmatchedKnownFailures(NUMBER_INPUT_KNOWN_FAILURES, results)).toEqual(
|
|
121
|
+
[],
|
|
122
|
+
);
|
|
123
|
+
});
|
|
@@ -733,3 +733,48 @@ describe('field menu grouping', () => {
|
|
|
733
733
|
});
|
|
734
734
|
});
|
|
735
735
|
});
|
|
736
|
+
|
|
737
|
+
describe('field menu after the search source changes', () => {
|
|
738
|
+
it('opens the new field menu on the first click after a config change', async () => {
|
|
739
|
+
const user = userEvent.setup();
|
|
740
|
+
// A caller that loads field options asynchronously hands PowerSearch a
|
|
741
|
+
// new config, which builds a new search source.
|
|
742
|
+
const loadedConfig: PowerSearchConfig = {
|
|
743
|
+
...config,
|
|
744
|
+
fields: [
|
|
745
|
+
...config.fields,
|
|
746
|
+
{
|
|
747
|
+
key: 'owner',
|
|
748
|
+
label: 'Owner',
|
|
749
|
+
defaultOperator: 'is',
|
|
750
|
+
operators: [{key: 'is', label: 'is', value: {type: 'string'}}],
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
};
|
|
754
|
+
const renderSearch = (searchConfig: PowerSearchConfig) => (
|
|
755
|
+
<>
|
|
756
|
+
<PowerSearch config={searchConfig} filters={[]} onChange={() => {}} />
|
|
757
|
+
<button type="button">Sort</button>
|
|
758
|
+
</>
|
|
759
|
+
);
|
|
760
|
+
const {rerender} = render(renderSearch(config));
|
|
761
|
+
const input = screen.getByRole('combobox');
|
|
762
|
+
|
|
763
|
+
await user.click(input);
|
|
764
|
+
await waitFor(() => {
|
|
765
|
+
expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
766
|
+
});
|
|
767
|
+
await user.click(screen.getByRole('button', {name: 'Sort'}));
|
|
768
|
+
expect(input).toHaveAttribute('aria-expanded', 'false');
|
|
769
|
+
|
|
770
|
+
rerender(renderSearch(loadedConfig));
|
|
771
|
+
await user.click(input);
|
|
772
|
+
|
|
773
|
+
await waitFor(() => {
|
|
774
|
+
expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
775
|
+
expect(
|
|
776
|
+
screen.getByRole('option', {name: /Owner/, hidden: true}),
|
|
777
|
+
).toBeInTheDocument();
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
});
|
|
@@ -166,29 +166,17 @@ test('the state inventory describes the labels the page actually renders', async
|
|
|
166
166
|
expect(wrong).toEqual([]);
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// test takes.
|
|
175
|
-
test.setTimeout(5 * 60 * 1000);
|
|
176
|
-
const cdp = await page.context().newCDPSession(page);
|
|
177
|
-
const results: BindingResult[] = [];
|
|
178
|
-
for (const state of SWITCH_BINDING_STATES) {
|
|
179
|
-
results.push(await runState(page, cdp, state));
|
|
180
|
-
}
|
|
181
|
-
// An expectation that applies to nothing is not coverage, however green it
|
|
182
|
-
// looks. This is the only place that can notice: the contract never sees the
|
|
183
|
-
// states, so whether a condition matches a real one is a binding-side fact.
|
|
184
|
-
expect(neverExercised(results)).toEqual([]);
|
|
185
|
-
expect(unmatchedKnownFailures(SWITCH_KNOWN_FAILURES, results)).toEqual([]);
|
|
186
|
-
});
|
|
169
|
+
// Each state's result, recorded by its own test below so the whole-suite check
|
|
170
|
+
// at the end reads it instead of measuring every state a second time. A state
|
|
171
|
+
// whose test did not run in this worker (a filtered run, or a worker restarted
|
|
172
|
+
// after a failure) is measured by the whole-suite check itself.
|
|
173
|
+
const recorded = new Map<string, BindingResult>();
|
|
187
174
|
|
|
188
175
|
for (const state of SWITCH_BINDING_STATES) {
|
|
189
176
|
test(`Switch [${state.id}] — ${state.summary}`, async ({page}) => {
|
|
190
177
|
const cdp = await page.context().newCDPSession(page);
|
|
191
178
|
const result = await runState(page, cdp, state);
|
|
179
|
+
recorded.set(state.id, result);
|
|
192
180
|
const report = summarize(SWITCH_PATTERN, [result]);
|
|
193
181
|
// eslint-disable-next-line no-console -- the report is this run's artifact
|
|
194
182
|
console.log(formatReport(report));
|
|
@@ -201,3 +189,22 @@ for (const state of SWITCH_BINDING_STATES) {
|
|
|
201
189
|
expect(report.counts.unexpectedPass).toBe(0);
|
|
202
190
|
});
|
|
203
191
|
}
|
|
192
|
+
|
|
193
|
+
test('every expectation is exercised by at least one bound state', async ({
|
|
194
|
+
page,
|
|
195
|
+
}) => {
|
|
196
|
+
// A whole-binding sweep: every state's result, read from its own test or,
|
|
197
|
+
// when that test did not run in this worker, measured here (minutes of real
|
|
198
|
+
// browser work in that case, so the budget stays).
|
|
199
|
+
test.setTimeout(5 * 60 * 1000);
|
|
200
|
+
const cdp = await page.context().newCDPSession(page);
|
|
201
|
+
const results: BindingResult[] = [];
|
|
202
|
+
for (const state of SWITCH_BINDING_STATES) {
|
|
203
|
+
results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
|
|
204
|
+
}
|
|
205
|
+
// An expectation that applies to nothing is not coverage, however green it
|
|
206
|
+
// looks. This is the only place that can notice: the contract never sees the
|
|
207
|
+
// states, so whether a condition matches a real one is a binding-side fact.
|
|
208
|
+
expect(neverExercised(results)).toEqual([]);
|
|
209
|
+
expect(unmatchedKnownFailures(SWITCH_KNOWN_FAILURES, results)).toEqual([]);
|
|
210
|
+
});
|
|
@@ -163,24 +163,18 @@ test('every binding fact matches the rendered semantic state', async ({
|
|
|
163
163
|
expect(wrong).toEqual([]);
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const results: BindingResult[] = [];
|
|
172
|
-
for (const state of TABS_BINDING_STATES) {
|
|
173
|
-
results.push(await runState(page, cdp, state));
|
|
174
|
-
}
|
|
175
|
-
expect(neverExercised(results)).toEqual([]);
|
|
176
|
-
expect(unmatchedKnownFailures(TABS_KNOWN_FAILURES, results)).toEqual([]);
|
|
177
|
-
});
|
|
166
|
+
// Each state's result, recorded by its own test below so the whole-suite check
|
|
167
|
+
// at the end reads it instead of measuring every state a second time. A state
|
|
168
|
+
// whose test did not run in this worker (a filtered run, or a worker restarted
|
|
169
|
+
// after a failure) is measured by the whole-suite check itself.
|
|
170
|
+
const recorded = new Map<string, BindingResult>();
|
|
178
171
|
|
|
179
172
|
for (const state of TABS_BINDING_STATES) {
|
|
180
173
|
test(`${state.binding} [${state.id}] — ${state.summary}`, async ({page}) => {
|
|
181
174
|
test.setTimeout(3 * 60 * 1000);
|
|
182
175
|
const cdp = await page.context().newCDPSession(page);
|
|
183
176
|
const result = await runState(page, cdp, state);
|
|
177
|
+
recorded.set(state.id, result);
|
|
184
178
|
const report = summarize(TABS_PATTERN, [result]);
|
|
185
179
|
// eslint-disable-next-line no-console -- the report is this run's artifact
|
|
186
180
|
console.log(formatReport(report));
|
|
@@ -189,3 +183,16 @@ for (const state of TABS_BINDING_STATES) {
|
|
|
189
183
|
expect(report.counts.unexpectedPass).toBe(0);
|
|
190
184
|
});
|
|
191
185
|
}
|
|
186
|
+
|
|
187
|
+
test('every expectation is exercised by at least one bound state', async ({
|
|
188
|
+
page,
|
|
189
|
+
}) => {
|
|
190
|
+
test.setTimeout(5 * 60 * 1000);
|
|
191
|
+
const cdp = await page.context().newCDPSession(page);
|
|
192
|
+
const results: BindingResult[] = [];
|
|
193
|
+
for (const state of TABS_BINDING_STATES) {
|
|
194
|
+
results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
|
|
195
|
+
}
|
|
196
|
+
expect(neverExercised(results)).toEqual([]);
|
|
197
|
+
expect(unmatchedKnownFailures(TABS_KNOWN_FAILURES, results)).toEqual([]);
|
|
198
|
+
});
|
|
@@ -176,26 +176,18 @@ test('every state declaration matches the browser accessibility node', async ({
|
|
|
176
176
|
expect(wrong).toEqual([]);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const results: BindingResult[] = [];
|
|
185
|
-
for (const state of TEXT_INPUT_BINDING_STATES) {
|
|
186
|
-
results.push(await runState(page, cdp, state));
|
|
187
|
-
}
|
|
188
|
-
expect(neverExercised(results)).toEqual([]);
|
|
189
|
-
expect(unmatchedKnownFailures(TEXT_INPUT_KNOWN_FAILURES, results)).toEqual(
|
|
190
|
-
[],
|
|
191
|
-
);
|
|
192
|
-
});
|
|
179
|
+
// Each state's result, recorded by its own test below so the whole-suite check
|
|
180
|
+
// at the end reads it instead of measuring every state a second time. A state
|
|
181
|
+
// whose test did not run in this worker (a filtered run, or a worker restarted
|
|
182
|
+
// after a failure) is measured by the whole-suite check itself.
|
|
183
|
+
const recorded = new Map<string, BindingResult>();
|
|
193
184
|
|
|
194
185
|
for (const state of TEXT_INPUT_BINDING_STATES) {
|
|
195
186
|
test(`${state.binding} [${state.id}] — ${state.summary}`, async ({page}) => {
|
|
196
187
|
test.setTimeout(3 * 60 * 1000);
|
|
197
188
|
const cdp = await page.context().newCDPSession(page);
|
|
198
189
|
const result = await runState(page, cdp, state);
|
|
190
|
+
recorded.set(state.id, result);
|
|
199
191
|
const report = summarize(TEXT_INPUT_PATTERN, [result]);
|
|
200
192
|
// eslint-disable-next-line no-console -- the report is this run's artifact
|
|
201
193
|
console.log(formatReport(report));
|
|
@@ -204,3 +196,18 @@ for (const state of TEXT_INPUT_BINDING_STATES) {
|
|
|
204
196
|
expect(report.counts.unexpectedPass).toBe(0);
|
|
205
197
|
});
|
|
206
198
|
}
|
|
199
|
+
|
|
200
|
+
test('every expectation is exercised and every known failure matches once', async ({
|
|
201
|
+
page,
|
|
202
|
+
}) => {
|
|
203
|
+
test.setTimeout(8 * 60 * 1000);
|
|
204
|
+
const cdp = await page.context().newCDPSession(page);
|
|
205
|
+
const results: BindingResult[] = [];
|
|
206
|
+
for (const state of TEXT_INPUT_BINDING_STATES) {
|
|
207
|
+
results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
|
|
208
|
+
}
|
|
209
|
+
expect(neverExercised(results)).toEqual([]);
|
|
210
|
+
expect(unmatchedKnownFailures(TEXT_INPUT_KNOWN_FAILURES, results)).toEqual(
|
|
211
|
+
[],
|
|
212
|
+
);
|
|
213
|
+
});
|
|
@@ -143,22 +143,17 @@ test('the state inventory matches rendered labels and applicability facts', asyn
|
|
|
143
143
|
expect(wrong).toEqual([]);
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const results: BindingResult[] = [];
|
|
152
|
-
for (const state of TOGGLE_BUTTON_BINDING_STATES) {
|
|
153
|
-
results.push(await runState(page, cdp, state));
|
|
154
|
-
}
|
|
155
|
-
expect(neverExercised(results)).toEqual([]);
|
|
156
|
-
});
|
|
146
|
+
// Each state's result, recorded by its own test below so the whole-suite check
|
|
147
|
+
// at the end reads it instead of measuring every state a second time. A state
|
|
148
|
+
// whose test did not run in this worker (a filtered run, or a worker restarted
|
|
149
|
+
// after a failure) is measured by the whole-suite check itself.
|
|
150
|
+
const recorded = new Map<string, BindingResult>();
|
|
157
151
|
|
|
158
152
|
for (const state of TOGGLE_BUTTON_BINDING_STATES) {
|
|
159
153
|
test(`ToggleButton [${state.id}] — ${state.summary}`, async ({page}) => {
|
|
160
154
|
const cdp = await page.context().newCDPSession(page);
|
|
161
155
|
const result = await runState(page, cdp, state);
|
|
156
|
+
recorded.set(state.id, result);
|
|
162
157
|
const report = summarize(TOGGLE_BUTTON_PATTERN, [result]);
|
|
163
158
|
// eslint-disable-next-line no-console -- the report is this run's evidence
|
|
164
159
|
console.log(formatReport(report));
|
|
@@ -167,3 +162,15 @@ for (const state of TOGGLE_BUTTON_BINDING_STATES) {
|
|
|
167
162
|
expect(report.counts.unexpectedPass).toBe(0);
|
|
168
163
|
});
|
|
169
164
|
}
|
|
165
|
+
|
|
166
|
+
test('every expectation is exercised by at least one ToggleButton state', async ({
|
|
167
|
+
page,
|
|
168
|
+
}) => {
|
|
169
|
+
test.setTimeout(5 * 60 * 1000);
|
|
170
|
+
const cdp = await page.context().newCDPSession(page);
|
|
171
|
+
const results: BindingResult[] = [];
|
|
172
|
+
for (const state of TOGGLE_BUTTON_BINDING_STATES) {
|
|
173
|
+
results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
|
|
174
|
+
}
|
|
175
|
+
expect(neverExercised(results)).toEqual([]);
|
|
176
|
+
});
|