Skip to content

Commit a3bc88b

Browse files
serpentbladeclaude
andcommitted
test(vr): add switch + popover VR cells (behavioral ×6 green + screenshot)
Authors the missing VR coverage for the @rozie-ui/switch + popover families (shipped 419069e / 9ce102a but never given VR cells). Behavioral cells (all 6 targets pass): - SwitchBehaviorDemo + switch.spec.ts: WAI-ARIA toggle — click + Space/Enter keyboard, aria-checked, @change (Lit CustomEvent e.detail unwrap), disabled stays inert. set-on direct-model write. - PopoverBehaviorDemo + popover.spec.ts: @floating-ui click popover — anchor-click open, Escape + outside-click dismiss, re-open, @change (bare-boolean Lit unwrap), set-closed direct-model write. Screenshot cells (registered, auto-fixme until Linux baselines land): - SwitchScreenshot (inline) → matrix.spec.ts; renders 3 states ×6. - PopoverScreenshot (open panel, position:absolute escapes mount clip) → overlay-screenshot.spec.ts; renders ×6. Wiring: host/main.ts (EXAMPLES/LIT_TAGS/DEFAULT_PROPS), and the Angular cross-tree prebuild lockstep for both family src roots — vite.config.ts (resolveCrossTreeBareImports + prebuildExtraRoots), tsconfig.app.json include, build-cells.mjs {SWITCH,POPOVER}_SRC cleanup sweep. Adds @floating-ui/dom to the VR harness deps so Vite resolves popover's engine import cross-tree (it is not hoisted to the repo root like @tanstack/table-core is). SURFACED BUG (documented, not fixed here): on Svelte, a programmatic popover OPEN from a click on an element OUTSIDE the popover races the click-outside dismissal — the opening click bubbles to document where the just-attached, same-tick outside listener immediately dismisses it (the other 5 targets defer listener attachment). The popover cell's direct-model-write step therefore asserts a programmatic CLOSE (race-free ×6). Tracked in project_vr_direct_model_write_null_react_solid_lit. Full VR behavioral suite: 693 passed, 0 regressions (the 248 toHaveScreenshot failures are the known macOS-vs-Linux baseline artifact, green on CI's Linux). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JE6gykywo57CcqUJZTsB9
1 parent 54720bb commit a3bc88b

14 files changed

Lines changed: 710 additions & 57 deletions
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<!--
2+
PopoverBehaviorDemo — VR behavioral cell for @rozie-ui/popover (wraps
3+
@floating-ui/dom). Drives the click-trigger floating primitive: a two-way
4+
r-model:open (live open + @change readouts), the anchor-click open gesture,
5+
Escape + click-outside dismissal, and a `set-closed` direct-model-write button
6+
(programmatic CLOSE). The floating panel is r-if="open" so the spec asserts
7+
presence/absence via role="dialog". See popover.spec.ts. Behavioral-only.
8+
9+
WHY THE DIRECT-MODEL-WRITE IS A *CLOSE*, NOT AN OPEN: a programmatic OPEN from
10+
an external button races with the click-outside dismissal on Svelte — the
11+
opening click keeps bubbling to `document`, where the just-attached outside
12+
listener (Svelte attaches it synchronously on the same tick; the other 5 targets
13+
defer it) sees the click target is outside the popover and immediately dismisses.
14+
Closing has no such race, so it is the cross-target-robust direct-write
15+
assertion. The surfaced Svelte race is tracked in
16+
project_vr_direct_model_write_null_react_solid_lit.
17+
18+
The @change payload is a BARE boolean (not an object), so its Lit-consumer
19+
unwrap differs from the object-payload families (number-field/switch): the
20+
CustomEvent carries the boolean in e.detail; the typeof guard keeps the boolean
21+
primitive path on the other 5 targets from throwing on `'detail' in e`.
22+
-->
23+
<rozie name="PopoverBehaviorDemo">
24+
25+
<components>
26+
{
27+
Popover: '../../packages/ui/popover/src/Popover.rozie',
28+
}
29+
</components>
30+
31+
<data>
32+
{
33+
panelOpen: false,
34+
lastChange: '',
35+
}
36+
</data>
37+
38+
<script>
39+
const onChange = (e) => {
40+
const v = e && typeof e === 'object' && 'detail' in e ? e.detail : e
41+
$data.lastChange = String(v)
42+
}
43+
const applyClose = () => {
44+
$data.panelOpen = false
45+
}
46+
const valueText = () => ($data.panelOpen ? 'open' : 'closed')
47+
</script>
48+
49+
<template>
50+
<div class="popover-demo">
51+
<h3>Popover — behavioral</h3>
52+
53+
<div class="controls">
54+
<button type="button" data-testid="set-closed" @click="applyClose()">Set closed</button>
55+
<span class="readout" data-testid="readout-value">{{ valueText() }}</span>
56+
<span class="readout" data-testid="readout-change">{{ $data.lastChange }}</span>
57+
</div>
58+
59+
<Popover
60+
r-model:open="$data.panelOpen"
61+
trigger="click"
62+
placement="bottom"
63+
:offset="8"
64+
@change="onChange($event)"
65+
>
66+
<template #anchor>
67+
<button type="button" data-testid="popover-anchor">Menu</button>
68+
</template>
69+
<div class="popover-body" data-testid="popover-content">
70+
<p>Floating content</p>
71+
</div>
72+
</Popover>
73+
</div>
74+
</template>
75+
76+
<style>
77+
.popover-demo {
78+
font-family: system-ui, -apple-system, sans-serif;
79+
color: #1a1a1a;
80+
padding: 1rem;
81+
width: 360px;
82+
}
83+
.popover-demo .controls {
84+
display: flex;
85+
align-items: center;
86+
gap: 0.5rem;
87+
margin-bottom: 1rem;
88+
}
89+
.popover-demo .readout {
90+
font-variant-numeric: tabular-nums;
91+
min-width: 4ch;
92+
}
93+
.popover-body {
94+
font-size: 0.9rem;
95+
}
96+
</style>
97+
98+
</rozie>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<!--
2+
PopoverScreenshotDemo.rozie — content-STABLE screenshot consumer for
3+
@rozie-ui/popover.
4+
5+
Where PopoverBehaviorDemo.rozie is the BEHAVIORAL cell (open gesture / Escape /
6+
outside-click), THIS is the SEPARATE deterministic SCREENSHOT cell — never
7+
extend it for behavior. Made deterministic by a FIXED open=true seed (the
8+
floating panel + arrow are shown), a fixed placement (bottom), :offset, NO
9+
interaction, and a fixed frame. Floating UI computes identical x/y from the same
10+
anchor/viewport across all six targets (the engine-computed-byte-identity
11+
premise), so a single shared baseline holds.
12+
13+
ESCAPES THE MOUNT CLIP: the floating content is `position: absolute` (laid out
14+
by Floating UI relative to the anchor, not the rozie-mount wrapper), so this
15+
cell is captured PAGE-LEVEL by overlay-screenshot.spec.ts (the Dialog/Toaster
16+
precedent), NOT the mount-clipped matrix.spec.ts. The Linux PNG baseline is
17+
generated separately in pinned Linux Docker (NOT here — macOS baselines are
18+
forbidden, feedback_vr_linux_baselines); until it lands the cell auto-fixmes on
19+
baselineExists().
20+
-->
21+
<rozie name="PopoverScreenshotDemo">
22+
23+
<components>
24+
{
25+
Popover: '../../packages/ui/popover/src/Popover.rozie',
26+
}
27+
</components>
28+
29+
<data>
30+
{
31+
open: true,
32+
}
33+
</data>
34+
35+
<template>
36+
<div class="popover-screenshot-demo">
37+
<Popover
38+
r-model:open="$data.open"
39+
trigger="click"
40+
placement="bottom"
41+
:offset="8"
42+
arrow
43+
>
44+
<template #anchor>
45+
<button type="button" class="popover-screenshot-anchor">Menu</button>
46+
</template>
47+
<div class="popover-screenshot-body">
48+
<p>Floating content</p>
49+
</div>
50+
</Popover>
51+
</div>
52+
</template>
53+
54+
<style>
55+
.popover-screenshot-demo {
56+
font-family: system-ui, -apple-system, sans-serif;
57+
color: #1a1a1a;
58+
padding: 2rem;
59+
width: 240px;
60+
height: 160px;
61+
}
62+
.popover-screenshot-anchor {
63+
font: inherit;
64+
padding: 0.4rem 0.8rem;
65+
}
66+
.popover-screenshot-body {
67+
font-size: 0.9rem;
68+
}
69+
</style>
70+
71+
</rozie>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<!--
2+
SwitchBehaviorDemo — VR behavioral cell for @rozie-ui/switch (pure-Rozie, no
3+
engine). Drives the WAI-ARIA toggle: a two-way r-model:modelValue (live value +
4+
@change readouts), click + keyboard (Space/Enter) toggling, a `set-on`
5+
direct-model-write button, and a sibling DISABLED switch that must stay inert.
6+
The spec clicks the control, presses Space, and checks aria-checked + clamp on
7+
the disabled instance — see switch.spec.ts. Behavioral-only.
8+
-->
9+
<rozie name="SwitchBehaviorDemo">
10+
11+
<components>
12+
{
13+
Switch: '../../packages/ui/switch/src/Switch.rozie',
14+
}
15+
</components>
16+
17+
<data>
18+
{
19+
on: false,
20+
lastChange: '',
21+
}
22+
</data>
23+
24+
<script>
25+
const onChange = (e) => {
26+
// Lit delivers a child's @emit as a CustomEvent → payload lives in e.detail;
27+
// the other 5 targets pass the payload object as arg0 directly (documented
28+
// cross-target consumer edge — see authoring playbook §5). `checked` is the
29+
// switch's @emit payload key; the unwrap guard reads `e.checked == null`
30+
// (false != null, so a genuine `{ checked: false }` arg0 is NOT unwrapped).
31+
const p = e && e.checked == null && e.detail ? e.detail : e
32+
$data.lastChange = p && p.checked != null ? String(p.checked) : 'null'
33+
}
34+
const applyOn = () => {
35+
$data.on = true
36+
}
37+
const valueText = () => ($data.on ? 'on' : 'off')
38+
</script>
39+
40+
<template>
41+
<div class="switch-demo">
42+
<h3>Switch — behavioral</h3>
43+
44+
<div class="controls">
45+
<button type="button" data-testid="set-on" @click="applyOn()">Set on</button>
46+
<span class="readout" data-testid="readout-value">{{ valueText() }}</span>
47+
<span class="readout" data-testid="readout-change">{{ $data.lastChange }}</span>
48+
</div>
49+
50+
<Switch
51+
r-model:modelValue="$data.on"
52+
ariaLabel="Wi-Fi"
53+
@change="onChange($event)"
54+
/>
55+
56+
<!-- A disabled switch the spec proves is inert (no toggle on click). -->
57+
<Switch
58+
:modelValue="false"
59+
:disabled="true"
60+
ariaLabel="Airplane mode"
61+
/>
62+
</div>
63+
</template>
64+
65+
<style>
66+
.switch-demo {
67+
font-family: system-ui, -apple-system, sans-serif;
68+
color: #1a1a1a;
69+
padding: 1rem;
70+
width: 360px;
71+
display: flex;
72+
flex-direction: column;
73+
gap: 1rem;
74+
align-items: flex-start;
75+
}
76+
.switch-demo .controls {
77+
display: flex;
78+
align-items: center;
79+
gap: 0.5rem;
80+
}
81+
.switch-demo .readout {
82+
font-variant-numeric: tabular-nums;
83+
min-width: 3ch;
84+
}
85+
</style>
86+
87+
</rozie>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!--
2+
SwitchScreenshotDemo.rozie — content-STABLE screenshot consumer for
3+
@rozie-ui/switch.
4+
5+
Where SwitchBehaviorDemo.rozie is the BEHAVIORAL cell (click/keyboard/disabled),
6+
THIS is the SEPARATE deterministic SCREENSHOT cell — never extend it for
7+
behavior. Made deterministic by FIXED states (one on, one off, one disabled),
8+
NO interaction, NO focus (no focus-visible ring), and a fixed frame. One .rozie
9+
source → six per-framework screenshot consumers; the Linux PNG baseline is
10+
generated separately in pinned Linux Docker (NOT here — macOS baselines are
11+
forbidden, feedback_vr_linux_baselines). Until it lands the matrix cell
12+
auto-fixmes on baselineExists(). INLINE-rendering family → standard
13+
mount-clipped matrix cell (specs/matrix.spec.ts).
14+
-->
15+
<rozie name="SwitchScreenshotDemo">
16+
17+
<components>
18+
{
19+
Switch: '../../packages/ui/switch/src/Switch.rozie',
20+
}
21+
</components>
22+
23+
<template>
24+
<div class="switch-screenshot-demo">
25+
<Switch :modelValue="true" ariaLabel="On" />
26+
<Switch :modelValue="false" ariaLabel="Off" />
27+
<Switch :modelValue="false" :disabled="true" ariaLabel="Disabled" />
28+
</div>
29+
</template>
30+
31+
<style>
32+
.switch-screenshot-demo {
33+
font-family: system-ui, -apple-system, sans-serif;
34+
color: #1a1a1a;
35+
padding: 1rem;
36+
width: 200px;
37+
display: flex;
38+
flex-direction: column;
39+
gap: 1rem;
40+
align-items: flex-start;
41+
}
42+
</style>
43+
44+
</rozie>

0 commit comments

Comments
 (0)