@@ -211,3 +211,103 @@ test('atlas.up is surface-scoped: listed under "On this page" in the palette on
211211 await expect ( page . getByTestId ( 'composition-view' ) ) . toBeVisible ( )
212212} )
213213
214+ // The two 0104 residual e2e cases goal 0106 slice B absorbs (useAtlasKeyboardNav.ts's
215+ // key table), plus a verify-only case for arrows-pan-with-no-selection
216+ // (already implemented in slice A -- reported, not built here).
217+
218+ test ( 'Tab cycles focus and selection across top-level cards in reading order' , async ( { page } ) => {
219+ await page . goto ( '/' )
220+ await page . getByRole ( 'link' , { name : 'Atlas' } ) . click ( )
221+ await expect ( page . getByTestId ( 'atlas-board' ) ) . toBeVisible ( )
222+
223+ const exampleArea = groupCard ( page , 'Example area' )
224+ const getting = noteCard ( page , 'Getting started' )
225+
226+ // Focus lands inside the board wrapper (a click), then Escape's
227+ // first rung clears the live selection while DOM focus stays on the
228+ // clicked card -- the same mechanism the Escape-ladder test above
229+ // already proves -- so the FIRST Tab below starts from "nothing
230+ // selected", not from this card as an anchor.
231+ await getting . click ( )
232+ await page . keyboard . press ( 'Escape' )
233+ await expect ( selectedWrapper ( page , getting ) ) . toHaveCount ( 0 )
234+
235+ // Reading order is left-to-right (atlasKeyboardNavGeometry.ts): the
236+ // seed's own X positions put Example area (80) before Getting
237+ // started (532), both on the same row (internal/domain/atlas/
238+ // builtin.go).
239+ await page . keyboard . press ( 'Tab' )
240+ await expect ( selectedWrapper ( page , exampleArea ) ) . toHaveCount ( 1 )
241+
242+ await page . keyboard . press ( 'Tab' )
243+ await expect ( selectedWrapper ( page , exampleArea ) ) . toHaveCount ( 0 )
244+ await expect ( selectedWrapper ( page , getting ) ) . toHaveCount ( 1 )
245+
246+ await page . keyboard . press ( 'Shift+Tab' )
247+ await expect ( selectedWrapper ( page , getting ) ) . toHaveCount ( 0 )
248+ await expect ( selectedWrapper ( page , exampleArea ) ) . toHaveCount ( 1 )
249+ } )
250+
251+ test ( 'arrow-nudge persists the selected card\'s position' , async ( { page } ) => {
252+ await page . goto ( '/' )
253+ await page . getByRole ( 'link' , { name : 'Atlas' } ) . click ( )
254+ await expect ( page . getByTestId ( 'atlas-board' ) ) . toBeVisible ( )
255+
256+ const getting = noteCard ( page , 'Getting started' )
257+ const gettingNode = page . locator ( '.react-flow__node' ) . filter ( { has : getting } )
258+ await getting . click ( )
259+ await expect ( selectedWrapper ( page , getting ) ) . toHaveCount ( 1 )
260+
261+ // React Flow writes translate(x,y) in flow coords directly onto the
262+ // node element's own style -- camera-independent, unlike
263+ // boundingBox() (same technique atlas.spec.ts's own arrange-persists
264+ // test already established).
265+ const before = ( await gettingNode . evaluate ( ( el ) => ( el as HTMLElement ) . style . transform ) ) ?? ''
266+ for ( let i = 0 ; i < 5 ; i ++ ) await page . keyboard . press ( 'ArrowRight' )
267+
268+ let after = before
269+ await expect . poll ( async ( ) => {
270+ after = ( await gettingNode . evaluate ( ( el ) => ( el as HTMLElement ) . style . transform ) ) ?? ''
271+ return after
272+ } ) . not . toBe ( before )
273+
274+ // The Go-side SetPosition write is batched to keyup, not per-pixel --
275+ // a reload renders the SAME persisted position, proving the nudge
276+ // actually round-tripped through the backend, not just the live DOM.
277+ await page . reload ( )
278+ await expect ( page . getByTestId ( 'atlas-view' ) ) . toBeVisible ( )
279+ await expect ( gettingNode ) . toBeVisible ( )
280+ await expect . poll ( async ( ) => gettingNode . evaluate ( ( el ) => ( el as HTMLElement ) . style . transform ) , { timeout : 10_000 } ) . toBe ( after )
281+
282+ // Cleanup: nudge back to the seeded position (testing.md's
283+ // within-file discipline -- no later test in this file depends on
284+ // it, but leaving drift around is still unnecessary).
285+ await getting . click ( )
286+ await expect ( selectedWrapper ( page , getting ) ) . toHaveCount ( 1 )
287+ for ( let i = 0 ; i < 5 ; i ++ ) await page . keyboard . press ( 'ArrowLeft' )
288+ await expect . poll ( async ( ) => gettingNode . evaluate ( ( el ) => ( el as HTMLElement ) . style . transform ) ) . toBe ( before )
289+ } )
290+
291+ // Verify-only (goal 0106 slice B's residual audit): arrows with NO
292+ // selection pan the camera instead of nudging -- already implemented
293+ // in slice A (useAtlasKeyboardNav.ts), not new work here.
294+ test ( 'arrows with no selection pan the camera instead of nudging' , async ( { page } ) => {
295+ await page . goto ( '/' )
296+ await page . getByRole ( 'link' , { name : 'Atlas' } ) . click ( )
297+ const board = page . getByTestId ( 'atlas-board' )
298+ await expect ( board ) . toBeVisible ( )
299+
300+ const getting = noteCard ( page , 'Getting started' )
301+ await getting . click ( )
302+ await page . keyboard . press ( 'Escape' )
303+ await expect ( selectedWrapper ( page , getting ) ) . toHaveCount ( 0 )
304+
305+ const viewport = board . locator ( '.react-flow__viewport' )
306+ const before = await viewport . evaluate ( ( el ) => ( el as HTMLElement ) . style . transform )
307+ await page . keyboard . press ( 'ArrowRight' )
308+ await expect . poll ( async ( ) => viewport . evaluate ( ( el ) => ( el as HTMLElement ) . style . transform ) ) . not . toBe ( before )
309+
310+ // Cleanup: pan back so this test leaves the camera where it found it.
311+ await page . keyboard . press ( 'ArrowLeft' )
312+ } )
313+
0 commit comments