@astryxdesign/core 0.6.0 → 0.6.1
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/CHANGELOG.md +41 -3
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +12 -1
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +44 -15
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +3 -7
- package/dist/Center/Center.d.ts +23 -16
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +7 -5
- package/dist/CodeBlock/CodeBlock.js +2 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +12 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +12 -2
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +1 -0
- package/dist/Field/InputClearButton.d.ts +2 -2
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +5 -1
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +16 -4
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +12 -1
- package/dist/HoverCard/useHoverCard.js +2 -2
- package/dist/Indicator/CheckboxIndicator.js +2 -2
- package/dist/Indicator/RadioIndicator.js +2 -2
- package/dist/Layer/layerStack.d.ts +10 -0
- package/dist/Layer/layerStack.d.ts.map +1 -1
- package/dist/Layer/layerStack.js +21 -9
- package/dist/Layer/useLayerDismissal.d.ts +2 -3
- package/dist/Layer/useLayerDismissal.d.ts.map +1 -1
- package/dist/Layer/useLayerDismissal.js +2 -3
- package/dist/NavIcon/NavIcon.js +2 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +12 -2
- package/dist/Popover/usePopover.d.ts +3 -2
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +4 -2
- package/dist/ProgressBar/ProgressBar.js +2 -2
- package/dist/ScrollableArea/ScrollableArea.d.ts +79 -0
- package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -0
- package/dist/ScrollableArea/ScrollableArea.js +144 -0
- package/dist/ScrollableArea/index.d.ts +11 -0
- package/dist/ScrollableArea/index.d.ts.map +1 -0
- package/dist/ScrollableArea/index.js +11 -0
- package/dist/StatusDot/StatusDot.js +2 -2
- package/dist/TextArea/TextArea.js +2 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +19 -4
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +12 -2
- package/dist/Typeahead/BaseTypeahead.d.ts +21 -14
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +56 -20
- package/dist/astryx.css +15 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/scrollGeometry.d.ts +24 -0
- package/dist/hooks/scrollGeometry.d.ts.map +1 -0
- package/dist/hooks/scrollGeometry.js +86 -0
- package/dist/hooks/scrollOwnerRegistry.d.ts +15 -0
- package/dist/hooks/scrollOwnerRegistry.d.ts.map +1 -0
- package/dist/hooks/scrollOwnerRegistry.js +24 -0
- package/dist/hooks/useFocusTrap.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +22 -11
- package/dist/hooks/useScrollableArea.d.ts +51 -0
- package/dist/hooks/useScrollableArea.d.ts.map +1 -0
- package/dist/hooks/useScrollableArea.js +287 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +2 -6
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/derivedVarRegistry.js +1 -1
- package/dist/theme/localTokens.d.ts +8 -11
- package/dist/theme/localTokens.d.ts.map +1 -1
- package/dist/theme/localTokens.js +17 -71
- package/dist/theme/themeAdaptations.d.ts.map +1 -1
- package/dist/theme/themeAdaptations.js +4 -4
- package/dist/utils/themeProps.d.ts +10 -10
- package/dist/utils/themeProps.d.ts.map +1 -1
- package/dist/utils/themeProps.js +27 -10
- package/locales/en.json +16 -0
- package/locales/pseudo.json +12 -0
- package/package.json +7 -2
- package/src/AppShell/AppShell.test.tsx +36 -0
- package/src/AppShell/AppShell.tsx +4 -1
- package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
- package/src/Banner/Banner.test.tsx +3 -1
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +56 -1
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +211 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +134 -2
- package/src/BottomSheet/BottomSheetSwitcher.tsx +43 -20
- package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +10 -5
- package/src/Breadcrumbs/BreadcrumbItem.spec.md +225 -0
- package/src/Breadcrumbs/BreadcrumbItem.tsx +8 -13
- package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -2
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +49 -2
- package/src/Center/Center.doc.mjs +32 -28
- package/src/Center/Center.spec.md +225 -0
- package/src/Center/Center.test.tsx +42 -4
- package/src/Center/Center.tsx +24 -17
- package/src/Chat/ChatSystemMessage.test.tsx +2 -9
- package/src/CodeBlock/CodeBlock.doc.mjs +2 -2
- package/src/CodeBlock/CodeBlock.tsx +2 -2
- package/src/DateInput/DateInput.test.tsx +4 -4
- package/src/DateInput/DateInput.tsx +15 -4
- package/src/DateRangeInput/DateRangeInput.test.tsx +2 -2
- package/src/DateTimeInput/DateTimeInput.test.tsx +6 -4
- package/src/DateTimeInput/DateTimeInput.tsx +18 -7
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +4 -77
- package/src/Field/Field.test.tsx +42 -0
- package/src/Field/Field.tsx +6 -0
- package/src/Field/InputClearButton.test.tsx +35 -1
- package/src/Field/InputClearButton.tsx +7 -3
- package/src/Field/PanelSearchInput.tsx +21 -8
- package/src/FieldStatus/FieldStatus.spec.md +27 -17
- package/src/FieldStatus/FieldStatus.test.tsx +7 -5
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +198 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +13 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +305 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +317 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +155 -0
- package/src/FileInput/FileInput.tsx +10 -1
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -3
- package/src/HoverCard/HoverCard.doc.mjs +4 -4
- package/src/HoverCard/useHoverCard.tsx +2 -2
- package/src/Indicator/CheckboxIndicator.tsx +2 -2
- package/src/Indicator/Indicator.doc.mjs +2 -2
- package/src/Indicator/Indicator.test.tsx +1 -1
- package/src/Indicator/RadioIndicator.tsx +2 -2
- package/src/Layer/layerStack.ts +20 -9
- package/src/Layer/useLayerDismissal.ts +2 -3
- package/src/MultiSelector/MultiSelector.test.tsx +4 -4
- package/src/NavIcon/NavIcon.doc.mjs +4 -4
- package/src/NavIcon/NavIcon.tsx +2 -2
- package/src/NumberInput/NumberInput.tsx +18 -7
- package/src/Popover/Popover.doc.mjs +10 -10
- package/src/Popover/Popover.spec.md +55 -65
- package/src/Popover/Popover.test.tsx +29 -0
- package/src/Popover/usePopover.doc.mjs +4 -4
- package/src/Popover/usePopover.tsx +7 -4
- package/src/ProgressBar/ProgressBar.doc.mjs +4 -4
- package/src/ProgressBar/ProgressBar.test.tsx +1 -31
- package/src/ProgressBar/ProgressBar.tsx +2 -2
- package/src/RadioList/RadioList.test.tsx +5 -144
- package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +255 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +12 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +232 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +503 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +217 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +100 -0
- package/src/ScrollableArea/ScrollableArea.spec.md +189 -0
- package/src/ScrollableArea/ScrollableArea.test.tsx +299 -0
- package/src/ScrollableArea/ScrollableArea.tsx +259 -0
- package/src/ScrollableArea/index.ts +26 -0
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +121 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +5 -172
- package/src/Selector/Selector.test.tsx +4 -4
- package/src/Spinner/Spinner.test.tsx +0 -18
- package/src/StatusDot/StatusDot.doc.mjs +4 -4
- package/src/StatusDot/StatusDot.tsx +2 -2
- package/src/TabList/TabList.test.tsx +5 -9
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +191 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +45 -0
- package/src/TabList/__tests__/Tabs.a11y.renders.tsx +92 -0
- package/src/TabList/__tests__/Tabs.a11y.states.ts +247 -0
- package/src/TabList/__tests__/Tabs.a11y.test.tsx +153 -0
- package/src/Table/Table.doc.mjs +2 -2
- package/src/TextArea/TextArea.doc.mjs +4 -4
- package/src/TextArea/TextArea.tsx +2 -2
- package/src/TextInput/TextInput.doc.mjs +2 -1
- package/src/TextInput/TextInput.test.tsx +94 -0
- package/src/TextInput/TextInput.tsx +22 -6
- package/src/TimeInput/TimeInput.tsx +18 -7
- package/src/Toast/ToastViewport.test.tsx +1 -39
- package/src/Typeahead/BaseTypeahead.doc.mjs +229 -33
- package/src/Typeahead/BaseTypeahead.spec.md +269 -0
- package/src/Typeahead/BaseTypeahead.test.tsx +200 -0
- package/src/Typeahead/BaseTypeahead.tsx +99 -30
- package/src/hooks/index.ts +13 -0
- package/src/hooks/scrollGeometry.ts +155 -0
- package/src/hooks/scrollOwnerRegistry.ts +47 -0
- package/src/hooks/useFocusTrap.ts +22 -11
- package/src/hooks/useFocusTrapEscapeShim.test.tsx +4 -3
- package/src/hooks/useScrollableArea.doc.mjs +108 -0
- package/src/hooks/useScrollableArea.test.tsx +437 -0
- package/src/hooks/useScrollableArea.ts +469 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.test.ts +65 -105
- package/src/theme/defineTheme.ts +3 -9
- package/src/theme/derivedVarRegistry.ts +1 -1
- package/src/theme/localTokens.ts +25 -96
- package/src/theme/publicThemeHelperContract.test.ts +2 -2
- package/src/theme/themeAdaptations.test.ts +16 -42
- package/src/theme/themeAdaptations.ts +6 -9
- package/src/utils/themeProps.test.ts +29 -10
- package/src/utils/themeProps.ts +36 -17
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 3
|
|
3
|
+
template_version: 1
|
|
4
|
+
kind: module
|
|
5
|
+
id: module:ScrollableArea/useScrollableArea
|
|
6
|
+
authority: current
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: cixzhang
|
|
10
|
+
approved_at: 2026-09-11
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers: [public-api, behavior, accessibility]
|
|
13
|
+
verified_by: [packages/core/src/hooks/useScrollableArea.test.tsx]
|
|
14
|
+
parent_component: component:ScrollableArea
|
|
15
|
+
references: [architecture:public-component-api, spec:AST-025/DEC-1]
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# useScrollableArea module contract
|
|
19
|
+
|
|
20
|
+
## Intent
|
|
21
|
+
|
|
22
|
+
useScrollableArea is the canonical reusable behavior core for components that
|
|
23
|
+
already own a viewport and content root. It provides safe prop/ref composition,
|
|
24
|
+
logical-axis measurement, live owner registration, edge state, keyboard access,
|
|
25
|
+
and scroll chaining without inserting structure.
|
|
26
|
+
|
|
27
|
+
## Compatibility and migration
|
|
28
|
+
|
|
29
|
+
- Released default preserved: not yet released
|
|
30
|
+
- Compatibility class: additive public hook and types
|
|
31
|
+
- Migration decision: `spec:AST-025/DEC-1`
|
|
32
|
+
|
|
33
|
+
## Ownership boundary
|
|
34
|
+
|
|
35
|
+
**Owns**
|
|
36
|
+
|
|
37
|
+
- Effective-axis measurement and stable logical-edge state.
|
|
38
|
+
- Observation, invalidation, DOM owner registration, conditional viewport access,
|
|
39
|
+
and effective-axis chaining.
|
|
40
|
+
- Safe composition of caller viewport/content props and refs.
|
|
41
|
+
|
|
42
|
+
**Does not own / non-goals**
|
|
43
|
+
|
|
44
|
+
- Viewport layout, paint, overflow declarations, scrollbar presentation, or
|
|
45
|
+
insertion of a content wrapper.
|
|
46
|
+
- Sticky positioning and public owner lookup.
|
|
47
|
+
|
|
48
|
+
## Public API and concepts
|
|
49
|
+
|
|
50
|
+
| Concept | Closed values or states | Meaning | Default | Owner | Stability |
|
|
51
|
+
| -------------- | ---------------------------------- | ------------------------------------- | ------------------------- | ----------------------------------------- | --------- |
|
|
52
|
+
| axis | `inline`, `block`, `both` | requested logical scroll intent | required | `spec:AST-025` | stable |
|
|
53
|
+
| keyboard owner | `content`, named `viewport` | where keyboard scrolling is reached | required | `spec:AST-025` | stable |
|
|
54
|
+
| overscroll | `allow`, `contain` | edge propagation on effective axes | `allow` | `spec:AST-025` | stable |
|
|
55
|
+
| axis state | `isScrollable`, `atStart`, `atEnd` | effective ownership and logical edges | inactive, both edges true | `module:ScrollableArea/useScrollableArea` | stable |
|
|
56
|
+
|
|
57
|
+
## Behavioral contract
|
|
58
|
+
|
|
59
|
+
| ID | Invariant | Basis | Acceptance and implementation state |
|
|
60
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------- |
|
|
61
|
+
| FR1 | An axis MUST be effective only when requested, computed overflow is scroll-capable, and geometry exceeds 1px. | `spec:AST-025` FR1–FR4 | implemented |
|
|
62
|
+
| FR2 | Viewport and content geometry MUST be observed; invalidations MUST coalesce and preserve the last valid state while unmeasurable. | `spec:AST-025` FR5–FR9 | implemented |
|
|
63
|
+
| FR3 | Logical edges and behavior-owned physical overflow MUST map through direction and horizontal, vertical, or sideways writing modes without requiring adopters to recreate the mapping. | `spec:AST-025` FR1, FR3–FR4 | implemented |
|
|
64
|
+
| FR4 | The nearest registered effective owner MUST win independently by axis. | `spec:AST-025` FR10, FR18 | implemented privately for future Sticky |
|
|
65
|
+
| FR5 | Viewport keyboard props and overscroll containment MUST appear only while applicable requested axes are effective. | `spec:AST-025` FR12–FR14 | implemented |
|
|
66
|
+
| FR6 | Prop getters MUST compose refs and preserve caller handlers/classes/styles/ARIA while behavior-owned accessibility and non-cancellable behavior win conflicts. | `spec:AST-025` IR4 | implemented |
|
|
67
|
+
| FR7 | The viewport prop getter MUST consume caller `xstyle` and internally compose geometry-driven `clip`/`auto`/`hidden` overflow plus explicit fitting Sticky containment. | `spec:AST-025` FR21 | implemented |
|
|
68
|
+
|
|
69
|
+
### Transformation and precedence order
|
|
70
|
+
|
|
71
|
+
- **ORD1 — Effective state.** requested logical axis → computed physical axis →
|
|
72
|
+
scroll-capable overflow → geometry tolerance → normalized logical edge.
|
|
73
|
+
- **ORD2 — Prop composition.** caller props and `xstyle` → behavior-owned
|
|
74
|
+
fitting/active overflow style → stable composed ref → behavior-owned keyboard,
|
|
75
|
+
state, and chaining outputs.
|
|
76
|
+
|
|
77
|
+
### Performance and resources
|
|
78
|
+
|
|
79
|
+
- **PR1 — Stable publication.** Equal effective/edge state reuses the prior object.
|
|
80
|
+
- **PR2 — Shared observation.** Both element boxes use the shared ResizeObserver;
|
|
81
|
+
all secondary invalidation signals coalesce through one animation frame.
|
|
82
|
+
|
|
83
|
+
## Accessibility contract
|
|
84
|
+
|
|
85
|
+
- **AR1 — Named viewport owner.** Viewport-owned access requires a label and adds
|
|
86
|
+
the viewport to the tab order only while an applicable axis is effective.
|
|
87
|
+
- **AR2 — Focus continuity.** State changes never move or blur focus.
|
|
88
|
+
|
|
89
|
+
## Design relationships
|
|
90
|
+
|
|
91
|
+
No visual representation is owned by this hook.
|
|
92
|
+
|
|
93
|
+
## Parent and system relationships
|
|
94
|
+
|
|
95
|
+
- `component:ScrollableArea` owns the reference viewport/content composition.
|
|
96
|
+
- `spec:AST-025` owns the shared behavior and adoption boundary.
|
|
97
|
+
|
|
98
|
+
## Verification map
|
|
99
|
+
|
|
100
|
+
| Contract | Verification | Representative states | Mutation or failure expectation |
|
|
101
|
+
| ---------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| FR1–FR6, AR1–AR2 | `useScrollableArea.test.tsx` plus real-browser Storybook probe | horizontal/vertical/sideways, LTR/RTL, fit/overflow/edge, nested owners, focused loss | style-only or geometry-only ownership, physical-edge leakage, stale state, ref loss, or dead containment fails |
|
|
103
|
+
|
|
104
|
+
## Decision log
|
|
105
|
+
|
|
106
|
+
### DEC-1 — Prop getters are the public composition seam
|
|
107
|
+
|
|
108
|
+
**Reference:** `module:ScrollableArea/useScrollableArea/DEC-1`
|
|
109
|
+
**Decider:** cixzhang, 2026-09-11
|
|
110
|
+
|
|
111
|
+
One returned prop object per owned element keeps refs, accessibility, handlers,
|
|
112
|
+
and behavior together without making consumer spread order part of correctness.
|
|
113
|
+
|
|
114
|
+
## Open questions
|
|
115
|
+
|
|
116
|
+
None.
|
|
117
|
+
|
|
118
|
+
## Content boundary
|
|
119
|
+
|
|
120
|
+
This file does not duplicate consumer signatures/examples, ScrollableArea's
|
|
121
|
+
structure/presentation, or Sticky's future API and positioning mechanism.
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file SegmentedControl.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, SegmentedControl components
|
|
6
|
-
* @output
|
|
7
|
-
*
|
|
6
|
+
* @output Component-specific callback, composition, layout, optional-key, and
|
|
7
|
+
* styling tests. Shared radio-group role, name, state, focus, and adopted
|
|
8
|
+
* interactions live in RadioList/__tests__/RadioGroup.a11y.*.
|
|
9
|
+
* @position Component-owned regression tests that do not duplicate the reusable
|
|
10
|
+
* radio-group contract.
|
|
8
11
|
*
|
|
9
12
|
* SYNC: When SegmentedControl components change, update tests to match new behavior
|
|
10
13
|
*/
|
|
@@ -47,41 +50,6 @@ beforeEach(() => {
|
|
|
47
50
|
});
|
|
48
51
|
|
|
49
52
|
describe('SegmentedControl', () => {
|
|
50
|
-
it('renders a radiogroup with radio buttons', () => {
|
|
51
|
-
render(
|
|
52
|
-
<SegmentedControl value="grid" onChange={() => {}} label="View mode">
|
|
53
|
-
<SegmentedControlItem value="grid" label="Grid" />
|
|
54
|
-
<SegmentedControlItem value="list" label="List" />
|
|
55
|
-
</SegmentedControl>,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
expect(screen.getByRole('radiogroup')).toBeInTheDocument();
|
|
59
|
-
expect(screen.getByRole('radiogroup')).toHaveAttribute(
|
|
60
|
-
'aria-label',
|
|
61
|
-
'View mode',
|
|
62
|
-
);
|
|
63
|
-
expect(screen.getByRole('radio', {name: 'Grid'})).toBeInTheDocument();
|
|
64
|
-
expect(screen.getByRole('radio', {name: 'List'})).toBeInTheDocument();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('marks selected item with aria-checked', () => {
|
|
68
|
-
render(
|
|
69
|
-
<SegmentedControl value="grid" onChange={() => {}} label="View mode">
|
|
70
|
-
<SegmentedControlItem value="grid" label="Grid" />
|
|
71
|
-
<SegmentedControlItem value="list" label="List" />
|
|
72
|
-
</SegmentedControl>,
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
expect(screen.getByRole('radio', {name: 'Grid'})).toHaveAttribute(
|
|
76
|
-
'aria-checked',
|
|
77
|
-
'true',
|
|
78
|
-
);
|
|
79
|
-
expect(screen.getByRole('radio', {name: 'List'})).toHaveAttribute(
|
|
80
|
-
'aria-checked',
|
|
81
|
-
'false',
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
53
|
it('calls onChange when an item is clicked', async () => {
|
|
86
54
|
const user = userEvent.setup();
|
|
87
55
|
const handleChange = vi.fn();
|
|
@@ -230,49 +198,6 @@ describe('SegmentedControl', () => {
|
|
|
230
198
|
expect(labelStyle.textOverflow).toBe('ellipsis');
|
|
231
199
|
});
|
|
232
200
|
|
|
233
|
-
it('uses roving tabindex — selected item has tabIndex 0, others -1', () => {
|
|
234
|
-
render(
|
|
235
|
-
<SegmentedControl value="list" onChange={() => {}} label="View mode">
|
|
236
|
-
<SegmentedControlItem value="grid" label="Grid" />
|
|
237
|
-
<SegmentedControlItem value="list" label="List" />
|
|
238
|
-
<SegmentedControlItem value="table" label="Table" />
|
|
239
|
-
</SegmentedControl>,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
expect(screen.getByRole('radio', {name: 'Grid'})).toHaveAttribute(
|
|
243
|
-
'tabIndex',
|
|
244
|
-
'-1',
|
|
245
|
-
);
|
|
246
|
-
expect(screen.getByRole('radio', {name: 'List'})).toHaveAttribute(
|
|
247
|
-
'tabIndex',
|
|
248
|
-
'0',
|
|
249
|
-
);
|
|
250
|
-
expect(screen.getByRole('radio', {name: 'Table'})).toHaveAttribute(
|
|
251
|
-
'tabIndex',
|
|
252
|
-
'-1',
|
|
253
|
-
);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('keeps a tab stop when the value matches no item (tab-stop repair)', () => {
|
|
257
|
-
render(
|
|
258
|
-
<SegmentedControl
|
|
259
|
-
value="nonexistent"
|
|
260
|
-
onChange={() => {}}
|
|
261
|
-
label="View mode">
|
|
262
|
-
<SegmentedControlItem value="grid" label="Grid" />
|
|
263
|
-
<SegmentedControlItem value="list" label="List" />
|
|
264
|
-
<SegmentedControlItem value="table" label="Table" />
|
|
265
|
-
</SegmentedControl>,
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
// No item is selected, but the group must remain Tab-reachable: the first
|
|
269
|
-
// enabled radio is promoted to tabIndex=0.
|
|
270
|
-
expect(screen.getByRole('radio', {name: 'Grid'})).toHaveAttribute(
|
|
271
|
-
'tabIndex',
|
|
272
|
-
'0',
|
|
273
|
-
);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
201
|
it('promotes the first enabled item when the value matches no item and the first is disabled', () => {
|
|
277
202
|
render(
|
|
278
203
|
<SegmentedControl
|
|
@@ -360,7 +285,6 @@ describe('SegmentedControl keyboard navigation', () => {
|
|
|
360
285
|
await user.keyboard('{ArrowRight}');
|
|
361
286
|
|
|
362
287
|
expect(handleChange).toHaveBeenCalledWith('list');
|
|
363
|
-
expect(screen.getByRole('radio', {name: 'List'})).toHaveFocus();
|
|
364
288
|
});
|
|
365
289
|
|
|
366
290
|
it('navigates with ArrowLeft and selects', async () => {
|
|
@@ -379,7 +303,6 @@ describe('SegmentedControl keyboard navigation', () => {
|
|
|
379
303
|
await user.keyboard('{ArrowLeft}');
|
|
380
304
|
|
|
381
305
|
expect(handleChange).toHaveBeenCalledWith('grid');
|
|
382
|
-
expect(screen.getByRole('radio', {name: 'Grid'})).toHaveFocus();
|
|
383
306
|
});
|
|
384
307
|
|
|
385
308
|
it('wraps around from last to first with ArrowRight', async () => {
|
|
@@ -398,7 +321,6 @@ describe('SegmentedControl keyboard navigation', () => {
|
|
|
398
321
|
await user.keyboard('{ArrowRight}');
|
|
399
322
|
|
|
400
323
|
expect(handleChange).toHaveBeenCalledWith('grid');
|
|
401
|
-
expect(screen.getByRole('radio', {name: 'Grid'})).toHaveFocus();
|
|
402
324
|
});
|
|
403
325
|
|
|
404
326
|
it('wraps around from first to last with ArrowLeft', async () => {
|
|
@@ -417,7 +339,6 @@ describe('SegmentedControl keyboard navigation', () => {
|
|
|
417
339
|
await user.keyboard('{ArrowLeft}');
|
|
418
340
|
|
|
419
341
|
expect(handleChange).toHaveBeenCalledWith('table');
|
|
420
|
-
expect(screen.getByRole('radio', {name: 'Table'})).toHaveFocus();
|
|
421
342
|
});
|
|
422
343
|
|
|
423
344
|
it('Home key focuses first item', async () => {
|
|
@@ -460,59 +381,6 @@ describe('SegmentedControl keyboard navigation', () => {
|
|
|
460
381
|
});
|
|
461
382
|
|
|
462
383
|
describe('SegmentedControl disabled state', () => {
|
|
463
|
-
it('marks entire group as disabled', () => {
|
|
464
|
-
render(
|
|
465
|
-
<SegmentedControl
|
|
466
|
-
value="grid"
|
|
467
|
-
onChange={() => {}}
|
|
468
|
-
label="View mode"
|
|
469
|
-
isDisabled>
|
|
470
|
-
<SegmentedControlItem value="grid" label="Grid" />
|
|
471
|
-
<SegmentedControlItem value="list" label="List" />
|
|
472
|
-
</SegmentedControl>,
|
|
473
|
-
);
|
|
474
|
-
|
|
475
|
-
expect(screen.getByRole('radiogroup')).toHaveAttribute(
|
|
476
|
-
'aria-disabled',
|
|
477
|
-
'true',
|
|
478
|
-
);
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it('removes the tab stop from the selected item when the group is disabled (navigation-13)', () => {
|
|
482
|
-
render(
|
|
483
|
-
<SegmentedControl
|
|
484
|
-
value="grid"
|
|
485
|
-
onChange={() => {}}
|
|
486
|
-
label="View mode"
|
|
487
|
-
isDisabled>
|
|
488
|
-
<SegmentedControlItem value="grid" label="Grid" />
|
|
489
|
-
<SegmentedControlItem value="list" label="List" />
|
|
490
|
-
</SegmentedControl>,
|
|
491
|
-
);
|
|
492
|
-
// Selected segment must not be a focusable-but-dead tab stop when disabled.
|
|
493
|
-
const selected = screen.getByRole('radio', {name: 'Grid'});
|
|
494
|
-
expect(selected).toHaveAttribute('tabIndex', '-1');
|
|
495
|
-
expect(selected).toHaveAttribute('aria-disabled', 'true');
|
|
496
|
-
// No enabled segment is tabbable either.
|
|
497
|
-
expect(screen.getByRole('radio', {name: 'List'})).toHaveAttribute(
|
|
498
|
-
'tabIndex',
|
|
499
|
-
'-1',
|
|
500
|
-
);
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it('removes the tab stop from an individually disabled selected item', () => {
|
|
504
|
-
render(
|
|
505
|
-
<SegmentedControl value="grid" onChange={() => {}} label="View mode">
|
|
506
|
-
<SegmentedControlItem value="grid" label="Grid" isDisabled />
|
|
507
|
-
<SegmentedControlItem value="list" label="List" />
|
|
508
|
-
</SegmentedControl>,
|
|
509
|
-
);
|
|
510
|
-
expect(screen.getByRole('radio', {name: 'Grid'})).toHaveAttribute(
|
|
511
|
-
'tabIndex',
|
|
512
|
-
'-1',
|
|
513
|
-
);
|
|
514
|
-
});
|
|
515
|
-
|
|
516
384
|
it('does not call onChange when group is disabled', async () => {
|
|
517
385
|
const user = userEvent.setup({pointerEventsCheck: 0});
|
|
518
386
|
const handleChange = vi.fn();
|
|
@@ -543,11 +411,6 @@ describe('SegmentedControl disabled state', () => {
|
|
|
543
411
|
</SegmentedControl>,
|
|
544
412
|
);
|
|
545
413
|
|
|
546
|
-
expect(screen.getByRole('radio', {name: 'List'})).toHaveAttribute(
|
|
547
|
-
'aria-disabled',
|
|
548
|
-
'true',
|
|
549
|
-
);
|
|
550
|
-
|
|
551
414
|
await user.click(screen.getByRole('radio', {name: 'List'}));
|
|
552
415
|
expect(handleChange).not.toHaveBeenCalled();
|
|
553
416
|
});
|
|
@@ -634,20 +497,6 @@ describe('SegmentedControl disabled state', () => {
|
|
|
634
497
|
expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
|
|
635
498
|
});
|
|
636
499
|
|
|
637
|
-
it('keeps the selected segment focusable when a reason is provided', () => {
|
|
638
|
-
renderControl();
|
|
639
|
-
const selected = screen.getByRole('radio', {name: 'Grid', hidden: true});
|
|
640
|
-
expect(selected).toHaveAttribute('aria-disabled', 'true');
|
|
641
|
-
expect(selected).toHaveAttribute('tabindex', '0');
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
it('links the reason tooltip from the group via aria-describedby', () => {
|
|
645
|
-
renderControl();
|
|
646
|
-
const group = screen.getByRole('radiogroup');
|
|
647
|
-
const tooltip = screen.getByRole('tooltip', h);
|
|
648
|
-
expect(group.getAttribute('aria-describedby')).toContain(tooltip.id);
|
|
649
|
-
});
|
|
650
|
-
|
|
651
500
|
it('blocks selection while focusable-disabled', () => {
|
|
652
501
|
const onChange = vi.fn();
|
|
653
502
|
renderControl({onChange});
|
|
@@ -655,22 +504,6 @@ describe('SegmentedControl disabled state', () => {
|
|
|
655
504
|
fireEvent.click(list);
|
|
656
505
|
expect(onChange).not.toHaveBeenCalled();
|
|
657
506
|
});
|
|
658
|
-
|
|
659
|
-
it('drops all segments from the tab order when disabled without a reason', () => {
|
|
660
|
-
render(
|
|
661
|
-
<SegmentedControl
|
|
662
|
-
value="grid"
|
|
663
|
-
onChange={() => {}}
|
|
664
|
-
label="View mode"
|
|
665
|
-
isDisabled>
|
|
666
|
-
<SegmentedControlItem value="grid" label="Grid" />
|
|
667
|
-
<SegmentedControlItem value="list" label="List" />
|
|
668
|
-
</SegmentedControl>,
|
|
669
|
-
);
|
|
670
|
-
for (const radio of screen.getAllByRole('radio', h)) {
|
|
671
|
-
expect(radio).toHaveAttribute('tabindex', '-1');
|
|
672
|
-
}
|
|
673
|
-
});
|
|
674
507
|
});
|
|
675
508
|
});
|
|
676
509
|
|
|
@@ -3032,8 +3032,8 @@ describe('Selector clear icon theme target', () => {
|
|
|
3032
3032
|
// The canonical target lands on the icon element itself (not the button),
|
|
3033
3033
|
// so a theme can restyle just this glyph (color, size, hover) via
|
|
3034
3034
|
// `defineTheme` — a button-level target could not reach the icon's own
|
|
3035
|
-
// color/size. The original per-component name
|
|
3036
|
-
//
|
|
3035
|
+
// color/size. The original per-component name remains as a compatibility
|
|
3036
|
+
// alias.
|
|
3037
3037
|
const icon = getClearIcon();
|
|
3038
3038
|
expect(icon).toHaveClass('astryx-input-clear-icon');
|
|
3039
3039
|
expect(icon).toHaveClass('astryx-selector-clear-icon');
|
|
@@ -3060,8 +3060,8 @@ describe('Selector clear icon theme target', () => {
|
|
|
3060
3060
|
it('routes the clear glyph through the shared clear button, keeping the legacy target', () => {
|
|
3061
3061
|
// The clear affordance now composes the shared InputClearButton (a ghost
|
|
3062
3062
|
// Button with a secondary/sm glyph), so the icon carries the canonical
|
|
3063
|
-
// `astryx-input-clear-icon` target
|
|
3064
|
-
//
|
|
3063
|
+
// `astryx-input-clear-icon` target plus the supported compatibility alias
|
|
3064
|
+
// `astryx-selector-clear-icon`. Aside from those target classes
|
|
3065
3065
|
// it matches the shared button's own `close`/`sm`/`secondary` glyph
|
|
3066
3066
|
// exactly, so the default look is defined in one place.
|
|
3067
3067
|
render(
|
|
@@ -72,24 +72,6 @@ describe('Spinner', () => {
|
|
|
72
72
|
expect(spinner).toHaveAttribute('data-shade', 'inherit');
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
it('has role="status"', () => {
|
|
76
|
-
render(<Spinner data-testid="spinner" />);
|
|
77
|
-
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('has aria-label="Loading" by default', () => {
|
|
81
|
-
render(<Spinner data-testid="spinner" />);
|
|
82
|
-
expect(screen.getByTestId('spinner')).toHaveAttribute(
|
|
83
|
-
'aria-label',
|
|
84
|
-
'Loading',
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('names the status element from the visible string label', () => {
|
|
89
|
-
render(<Spinner label="Fetching data" data-testid="spinner" />);
|
|
90
|
-
expect(screen.getByRole('status')).toHaveAccessibleName('Fetching data');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
75
|
it('does not duplicate a visible string label as aria-label', () => {
|
|
94
76
|
render(<Spinner label="Fetching data" data-testid="spinner" />);
|
|
95
77
|
const status = screen.getByRole('status');
|
|
@@ -63,8 +63,8 @@ export const docs = {
|
|
|
63
63
|
theming: {
|
|
64
64
|
targets: [
|
|
65
65
|
{className: 'astryx-status-dot', visualProps: ['variant']},
|
|
66
|
-
//
|
|
67
|
-
//
|
|
66
|
+
// Retained beside the canonical names for backwards compatibility.
|
|
67
|
+
// New themes use the canonical targets above.
|
|
68
68
|
{className: 'astryx-statusdot', visualProps: ['variant'], deprecatedFor: 'status-dot'},
|
|
69
69
|
],
|
|
70
70
|
},
|
|
@@ -124,8 +124,8 @@ export const docsZh = {
|
|
|
124
124
|
theming: {
|
|
125
125
|
targets: [
|
|
126
126
|
{className: 'astryx-status-dot', visualProps: ['variant']},
|
|
127
|
-
//
|
|
128
|
-
//
|
|
127
|
+
// Retained beside the canonical names for backwards compatibility.
|
|
128
|
+
// New themes use the canonical targets above.
|
|
129
129
|
{className: 'astryx-statusdot', visualProps: ['variant'], deprecatedFor: 'status-dot'},
|
|
130
130
|
],
|
|
131
131
|
},
|
|
@@ -203,8 +203,8 @@ export function StatusDot({
|
|
|
203
203
|
themeProps(
|
|
204
204
|
'status-dot',
|
|
205
205
|
{variant},
|
|
206
|
-
// `statusdot` ran the compound name together;
|
|
207
|
-
//
|
|
206
|
+
// `statusdot` ran the compound name together; keep it emitted so
|
|
207
|
+
// existing themes continue to work.
|
|
208
208
|
{legacyNames: ['statusdot']},
|
|
209
209
|
),
|
|
210
210
|
stylex.props(
|
|
@@ -1316,7 +1316,7 @@ describe('TabList ARIA pattern — role="tablist"', () => {
|
|
|
1316
1316
|
return vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1317
1317
|
}
|
|
1318
1318
|
|
|
1319
|
-
it('
|
|
1319
|
+
it('does not mix navigation semantics into explicit Tabs', () => {
|
|
1320
1320
|
render(
|
|
1321
1321
|
<TabList value="b" onChange={() => {}} role="tablist" aria-label="Views">
|
|
1322
1322
|
<Tab value="a" label="Alpha" panelId="panel-a" />
|
|
@@ -1324,14 +1324,10 @@ describe('TabList ARIA pattern — role="tablist"', () => {
|
|
|
1324
1324
|
</TabList>,
|
|
1325
1325
|
);
|
|
1326
1326
|
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
expect(tabs[0]).toHaveAttribute('aria-selected', 'false');
|
|
1332
|
-
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
|
|
1333
|
-
expect(tabs[1]).toHaveAttribute('aria-controls', 'panel-b');
|
|
1334
|
-
expect(tabs[1]).not.toHaveAttribute('aria-current');
|
|
1327
|
+
expect(screen.queryByRole('navigation')).toBeNull();
|
|
1328
|
+
for (const tab of screen.getAllByRole('tab')) {
|
|
1329
|
+
expect(tab).not.toHaveAttribute('aria-current');
|
|
1330
|
+
}
|
|
1335
1331
|
});
|
|
1336
1332
|
|
|
1337
1333
|
it('lets the consumer name the tablist from another element', () => {
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Tabs.a11y.chromium.spec.ts
|
|
5
|
+
* @input Uses the shared Tabs contract, Chromium harness, checked-in stories, inventory, and exact known failures
|
|
6
|
+
* @output Real-browser and accessibility-tree evidence for explicit TabList Tabs parts
|
|
7
|
+
* @position Browser lane required by AST-013; it makes no real-AT claim.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
expect,
|
|
12
|
+
test,
|
|
13
|
+
type CDPSession,
|
|
14
|
+
type Locator,
|
|
15
|
+
type Page,
|
|
16
|
+
} from '@playwright/test';
|
|
17
|
+
import {
|
|
18
|
+
TABS_PATTERN,
|
|
19
|
+
blockingResults,
|
|
20
|
+
checkAccessibilitySpec,
|
|
21
|
+
formatFailures,
|
|
22
|
+
formatReport,
|
|
23
|
+
neverExercised,
|
|
24
|
+
summarize,
|
|
25
|
+
unmatchedKnownFailures,
|
|
26
|
+
type BindingResult,
|
|
27
|
+
} from '@astryxdesign/a11y-spec';
|
|
28
|
+
import {
|
|
29
|
+
createChromiumHarness,
|
|
30
|
+
holdMotionStill,
|
|
31
|
+
} from '@astryxdesign/a11y-spec/chromium';
|
|
32
|
+
import {
|
|
33
|
+
DEFAULT_STORYBOOK_DIR,
|
|
34
|
+
serveStorybook,
|
|
35
|
+
type StaticServer,
|
|
36
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
37
|
+
import {TABS_KNOWN_FAILURES} from './Tabs.a11y.known-failures';
|
|
38
|
+
import {
|
|
39
|
+
TABS_BINDING_STATES,
|
|
40
|
+
type TabsBindingRow,
|
|
41
|
+
type TabsBindingState,
|
|
42
|
+
} from './Tabs.a11y.states';
|
|
43
|
+
|
|
44
|
+
let storybook: StaticServer;
|
|
45
|
+
|
|
46
|
+
test.beforeAll(async () => {
|
|
47
|
+
storybook = await serveStorybook(
|
|
48
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test.afterAll(async () => {
|
|
53
|
+
await storybook?.close();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function storyUrl(storyId: string): string {
|
|
57
|
+
return `${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function subjectFor(page: Page, state: TabsBindingRow): Locator {
|
|
61
|
+
const root = page.locator('#storybook-root');
|
|
62
|
+
if ('selector' in state.subject) {
|
|
63
|
+
return root.locator(state.subject.selector);
|
|
64
|
+
}
|
|
65
|
+
return root.getByRole(state.subject.role, {
|
|
66
|
+
name: state.subject.name,
|
|
67
|
+
includeHidden: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function relatedFor(
|
|
72
|
+
page: Page,
|
|
73
|
+
state: TabsBindingRow,
|
|
74
|
+
): Readonly<Record<string, Locator>> {
|
|
75
|
+
const metadata = state as TabsBindingState;
|
|
76
|
+
const root = page.locator('#storybook-root');
|
|
77
|
+
return Object.fromEntries(
|
|
78
|
+
Object.entries(metadata.relations ?? {}).map(([name, relation]) => [
|
|
79
|
+
name,
|
|
80
|
+
root.locator(relation.selector),
|
|
81
|
+
]),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function mountState(page: Page, state: TabsBindingRow): Promise<void> {
|
|
86
|
+
await page.goto(storyUrl(state.storyId), {waitUntil: 'load'});
|
|
87
|
+
await holdMotionStill(page);
|
|
88
|
+
await subjectFor(page, state).waitFor({state: 'attached'});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function runState(
|
|
92
|
+
page: Page,
|
|
93
|
+
cdp: CDPSession,
|
|
94
|
+
state: TabsBindingRow,
|
|
95
|
+
): Promise<BindingResult> {
|
|
96
|
+
return checkAccessibilitySpec({
|
|
97
|
+
spec: TABS_PATTERN,
|
|
98
|
+
binding: state.binding,
|
|
99
|
+
state: state.id,
|
|
100
|
+
facts: state.facts,
|
|
101
|
+
knownFailures: TABS_KNOWN_FAILURES,
|
|
102
|
+
mount: async () => {
|
|
103
|
+
await mountState(page, state);
|
|
104
|
+
return createChromiumHarness({
|
|
105
|
+
page,
|
|
106
|
+
subject: subjectFor(page, state),
|
|
107
|
+
cdp,
|
|
108
|
+
visibleLabel: state.facts.visibleLabel ? undefined : null,
|
|
109
|
+
related: relatedFor(page, state),
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
test('every binding fact matches the rendered semantic state', async ({
|
|
116
|
+
page,
|
|
117
|
+
}) => {
|
|
118
|
+
test.setTimeout(3 * 60 * 1000);
|
|
119
|
+
const cdp = await page.context().newCDPSession(page);
|
|
120
|
+
const wrong: string[] = [];
|
|
121
|
+
|
|
122
|
+
for (const state of TABS_BINDING_STATES) {
|
|
123
|
+
await mountState(page, state);
|
|
124
|
+
const subject = await createChromiumHarness({
|
|
125
|
+
page,
|
|
126
|
+
subject: subjectFor(page, state),
|
|
127
|
+
cdp,
|
|
128
|
+
related: relatedFor(page, state),
|
|
129
|
+
}).subject();
|
|
130
|
+
const computed = await subject.computed();
|
|
131
|
+
|
|
132
|
+
const expectedRole =
|
|
133
|
+
state.facts.part === 'tablist'
|
|
134
|
+
? 'tablist'
|
|
135
|
+
: state.facts.part === 'tab'
|
|
136
|
+
? 'tab'
|
|
137
|
+
: state.facts.active === false
|
|
138
|
+
? null
|
|
139
|
+
: 'tabpanel';
|
|
140
|
+
if (computed.role !== expectedRole) {
|
|
141
|
+
wrong.push(
|
|
142
|
+
`${state.id}: declares role=${expectedRole}, browser exposes ${computed.role}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
state.facts.selected != null &&
|
|
147
|
+
computed.selected !== state.facts.selected
|
|
148
|
+
) {
|
|
149
|
+
wrong.push(
|
|
150
|
+
`${state.id}: declares selected=${state.facts.selected}, browser exposes ${computed.selected}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
state.facts.disabled != null &&
|
|
155
|
+
computed.disabled !== state.facts.disabled
|
|
156
|
+
) {
|
|
157
|
+
wrong.push(
|
|
158
|
+
`${state.id}: declares disabled=${state.facts.disabled}, browser exposes ${computed.disabled}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
expect(wrong).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('every expectation is exercised by at least one bound 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 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
|
+
});
|
|
178
|
+
|
|
179
|
+
for (const state of TABS_BINDING_STATES) {
|
|
180
|
+
test(`${state.binding} [${state.id}] — ${state.summary}`, async ({page}) => {
|
|
181
|
+
test.setTimeout(3 * 60 * 1000);
|
|
182
|
+
const cdp = await page.context().newCDPSession(page);
|
|
183
|
+
const result = await runState(page, cdp, state);
|
|
184
|
+
const report = summarize(TABS_PATTERN, [result]);
|
|
185
|
+
// eslint-disable-next-line no-console -- the report is this run's artifact
|
|
186
|
+
console.log(formatReport(report));
|
|
187
|
+
expect(formatFailures(blockingResults([result]))).toBe('');
|
|
188
|
+
expect(report.unrunLayers).toEqual([]);
|
|
189
|
+
expect(report.counts.unexpectedPass).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
}
|