Skip to content

Commit 93a064e

Browse files
alicodingclaude
andcommitted
Merge origin/main into perspectives-diff -- keep both e2e helpers (clickFrameGutter + clickBreadcrumbSegment), unify imports
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd
2 parents 1be07a1 + b615a97 commit 93a064e

42 files changed

Lines changed: 1228 additions & 261 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/e2e/atlas-authoring.spec.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from './fixtures/server'
1111
import { contextMenu, rightClickEmptyArea } from './fixtures/contextMenu'
1212
import { ATLAS_KIND_CONTACT, ATLAS_KIND_TOPIC, selectKind } from './fixtures/kindPicker'
13-
import { clickCorner, groupCard, noteCard, zoomAllTheWayOut } from './fixtures/atlasBoard'
13+
import { clickCorner, groupCard, noteCard, submitCreatePopover, zoomAllTheWayOut } from './fixtures/atlasBoard'
1414

1515
// Atlas creation core (goal 0081 slice A1): the tray, its placement
1616
// popover, right-click create, sticky notes, and the note promotion
@@ -141,9 +141,21 @@ test('atlas creation core: tray, placement popover, right-click create, sticky n
141141
await clickCorner(board, 'top-right')
142142
await expect(cardTool).toHaveAttribute('data-armed', 'false')
143143
await expect(popover).toBeVisible()
144+
145+
// The create loop (goal 0106 slice B contract item 4: C -> click ->
146+
// title -> Enter -> board) audited: the popover's own resting chrome
147+
// is the kind chip + title input ONLY -- no "Kind"/"Title" labels,
148+
// no visible Submit/Cancel row (Enter/Escape are the only commit/
149+
// cancel paths). The title input is already focused.
150+
await expect(popover.getByText('Kind', { exact: true })).toHaveCount(0)
151+
await expect(popover.getByText('Title', { exact: true })).toHaveCount(0)
152+
await expect(popover.getByTestId('atlas-placement-submit')).toHaveCount(0)
153+
await expect(popover.getByTestId('atlas-placement-cancel')).toHaveCount(0)
154+
await expect(popover.getByTestId('atlas-placement-title')).toBeFocused()
155+
144156
await selectKind(popover, ATLAS_KIND_TOPIC)
145157
await popover.getByTestId('atlas-placement-title').fill('ZzE2eRootCard')
146-
await popover.getByTestId('atlas-placement-submit').click()
158+
await submitCreatePopover(popover)
147159
await expect(popover).not.toBeVisible()
148160
await expect(noteCard(page, 'ZzE2eRootCard')).toBeVisible()
149161
// Regression: one confirm creates exactly ONE card. StrictMode
@@ -170,7 +182,7 @@ test('atlas creation core: tray, placement popover, right-click create, sticky n
170182
await expect(popover).toBeVisible()
171183
await selectKind(popover, ATLAS_KIND_CONTACT)
172184
await popover.getByTestId('atlas-placement-title').fill('ZzE2eAreaCard')
173-
await popover.getByTestId('atlas-placement-submit').click()
185+
await submitCreatePopover(popover)
174186
await expect(popover).not.toBeVisible()
175187
await expect(noteCard(page, 'ZzE2eAreaCard')).toBeVisible()
176188

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { test, expect } from './fixtures/server'
2+
import { groupCard } from './fixtures/atlasCards'
3+
4+
// The breadcrumb's own sibling dropdown (goal 0106 slice B contract
5+
// item 5): each real segment is a dropdown trigger listing its own
6+
// level's sibling places (name + card count), current one selected --
7+
// clicking any entry (including the current one) navigates, the same
8+
// capability the crumb's old direct-navigate-on-click carried.
9+
10+
test('a breadcrumb segment opens a dropdown of its level\'s siblings, current one selected, with card counts', async ({ page }) => {
11+
await page.goto('/')
12+
await page.getByRole('link', { name: 'Atlas' }).click()
13+
await expect(page.getByTestId('atlas-board')).toBeVisible()
14+
15+
await groupCard(page, 'Example area').getByTestId('atlas-group-header').click()
16+
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('Example area')
17+
18+
// "My space" is the seeded space's own root -- its siblings are
19+
// every other root-level card, which at the single-root default is
20+
// only itself (3 top-level children: Example area, Getting started,
21+
// Scratchpad).
22+
const mySpaceCrumb = page.getByTestId('atlas-breadcrumb').getByTestId('atlas-breadcrumb-item').filter({ hasText: 'My space' })
23+
await mySpaceCrumb.click()
24+
25+
const dropdown = page.getByTestId('atlas-breadcrumb-siblings')
26+
await expect(dropdown).toBeVisible()
27+
const mySpaceRow = dropdown.getByTestId('atlas-breadcrumb-sibling').filter({ hasText: 'My space' })
28+
await expect(mySpaceRow).toBeVisible()
29+
await expect(mySpaceRow).toContainText('3 cards')
30+
31+
// Clicking the current place navigates to it (reproducing the old
32+
// direct-navigate behavior) -- the crumb collapses back to "My
33+
// space" alone.
34+
await mySpaceRow.click()
35+
await expect(dropdown).toHaveCount(0)
36+
await expect(page.getByTestId('atlas-breadcrumb')).not.toContainText('Example area')
37+
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('My space')
38+
})
39+
40+
test('clicking a DIFFERENT sibling in the dropdown navigates laterally', async ({ page }) => {
41+
await page.goto('/')
42+
await page.getByRole('link', { name: 'Atlas' }).click()
43+
await expect(page.getByTestId('atlas-board')).toBeVisible()
44+
45+
await groupCard(page, 'Example area').getByTestId('atlas-group-header').click()
46+
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('Example area')
47+
48+
// "Example area"'s own siblings are the OTHER top-level children of
49+
// "My space" -- Getting started, Scratchpad -- neither of which is a
50+
// place with its own children, so clicking one lands on an empty
51+
// board (its own drilled-into space), not "Example area"'s own
52+
// contents.
53+
const exampleAreaCrumb = page.getByTestId('atlas-breadcrumb').getByTestId('atlas-breadcrumb-item').filter({ hasText: 'Example area' })
54+
await exampleAreaCrumb.click()
55+
const dropdown = page.getByTestId('atlas-breadcrumb-siblings')
56+
await expect(dropdown).toBeVisible()
57+
58+
const gettingRow = dropdown.getByTestId('atlas-breadcrumb-sibling').filter({ hasText: 'Getting started' })
59+
await expect(gettingRow).toBeVisible()
60+
await expect(gettingRow).toContainText('0 cards')
61+
await gettingRow.click()
62+
63+
await expect(dropdown).toHaveCount(0)
64+
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('My space')
65+
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('Getting started')
66+
await expect(page.getByTestId('atlas-breadcrumb')).not.toContainText('Example area')
67+
})

frontend/e2e/atlas-capture.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test, expect } from './fixtures/server'
22
import type { Page } from '@playwright/test'
3-
import { clickCorner, openCard, zoomAllTheWayOut } from './fixtures/atlasBoard'
3+
import { clickCorner, openCard, submitCreatePopover, zoomAllTheWayOut } from './fixtures/atlasBoard'
44
import { deleteViaPageMenu } from './fixtures/atlasPage'
55

66
// Atlas capture doors (goal 0081 slice A3, LOCKED design §2b/§3b):
@@ -55,7 +55,7 @@ test('paste text opens the placement popover prefilled with title and note', asy
5555
await expect(popover).toBeVisible()
5656
await expect(popover.getByTestId('atlas-placement-title')).toHaveValue('Q3 migration checklist')
5757

58-
await popover.getByTestId('atlas-placement-submit').click()
58+
await submitCreatePopover(popover)
5959
await expect(popover).not.toBeVisible()
6060

6161
const card = page.locator('[data-testid="atlas-note-card"]').filter({ hasText: 'Q3 migration checklist' })
@@ -86,7 +86,7 @@ test('paste HTML converts to Markdown before prefilling the popover', async ({ p
8686
// by the same first-line rule paste-as-plain-text uses.
8787
await expect(popover.getByTestId('atlas-placement-title')).toHaveValue(/Vendor policy/)
8888

89-
await popover.getByTestId('atlas-placement-submit').click()
89+
await submitCreatePopover(popover)
9090
await expect(popover).not.toBeVisible()
9191

9292
const card = page.locator('[data-testid="atlas-note-card"]').filter({ hasText: 'Vendor policy' })

frontend/e2e/atlas-containment.spec.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from './fixtures/server'
1212
import { contextMenu } from './fixtures/contextMenu'
1313
import { ATLAS_KIND_TOPIC, selectKind } from './fixtures/kindPicker'
14-
import { armAndPlaceTopicCard, deleteCardViaMenu, groupCard, noteCard } from './fixtures/atlasBoard'
14+
import { armAndPlaceTopicCard, cancelCreatePopover, deleteCardViaMenu, groupCard, noteCard, submitCreatePopover } from './fixtures/atlasBoard'
1515
import { waitForViewportStable } from './fixtures/animation'
1616

1717
// A LIGHTER zoom-out than fixtures/atlasBoard.ts's own zoomAllTheWayOut
@@ -174,7 +174,7 @@ test('atlas containment: area drawing, marker-box grouping, drag filing, dissolv
174174
await expect(popover).toBeVisible()
175175
await selectKind(popover, ATLAS_KIND_TOPIC)
176176
await popover.getByTestId('atlas-placement-title').fill('ZzC2eInterior')
177-
await popover.getByTestId('atlas-placement-submit').click()
177+
await submitCreatePopover(popover)
178178
await expect(popover).not.toBeVisible()
179179
await page.mouse.wheel(0, 300)
180180
await waitForViewportStable(board)
@@ -239,16 +239,20 @@ test('atlas containment: area drawing, marker-box grouping, drag filing, dissolv
239239
// board point (not the shared clickCorner helper) -- several zoom
240240
// changes have accumulated by now, so a FRACTION of the board's
241241
// current bounds stays reliably empty regardless of the exact
242-
// zoom level, the way a fixed named corner no longer does. ---
242+
// zoom level, the way a fixed named corner no longer does. y=0.3
243+
// (not a bottom corner): the board's own minimap/Controls/creation
244+
// tray chrome (goal 0106 slice B) all live in the bottom band, so
245+
// the vertical middle is the one fraction guaranteed clear of all
246+
// three regardless of which corner a bottom-anchored point picks. ---
243247
await page.keyboard.press('c')
244248
const kindCheckBox = await board.boundingBox()
245249
if (!kindCheckBox) throw new Error('board has no bounding box')
246-
await board.click({ position: { x: kindCheckBox.width * 0.95, y: kindCheckBox.height * 0.95 } })
250+
await board.click({ position: { x: kindCheckBox.width * 0.95, y: kindCheckBox.height * 0.3 } })
247251
await expect(popover).toBeVisible()
248252
await popover.getByTestId('atlas-placement-kind').click()
249253
await expect(popover.getByTestId(`atlas-placement-kind-option-${ATLAS_KIND_TOPIC}`)).toContainText('Something being tracked or worked through.')
250254
await popover.getByTestId(`atlas-placement-kind-option-${ATLAS_KIND_TOPIC}`).click()
251-
await popover.getByTestId('atlas-placement-cancel').click()
255+
await cancelCreatePopover(popover)
252256
await expect(popover).not.toBeVisible()
253257

254258
// --- Rider (a): a zoomed-into space with zero cards and zero

frontend/e2e/atlas-folder-import.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test, expect } from './fixtures/server'
22
import { deleteViaPageMenu } from './fixtures/atlasPage'
3-
import { openCard } from './fixtures/atlasBoard'
3+
import { clickBreadcrumbSegment, openCard } from './fixtures/atlasBoard'
44

55
// Synced-folder onboarding (docs/goals/0067) over real Go bindings
66
// (Wails3 server mode): AtlasService.PickFolder's own MILL_TEST_
@@ -96,7 +96,7 @@ test('add from folder: scan, partial accept, containment, and mirror rendering a
9696
await deleteViaPageMenu(page, overlay)
9797
await expect(summaryCard).not.toBeVisible()
9898

99-
await page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }).click()
99+
await clickBreadcrumbSegment(page, page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }), 'My space')
100100
for (const card of [noteCard(page, 'Reports'), notesCard, logoCard]) {
101101
await openCard(page, card)
102102
await expect(overlay).toBeVisible()
@@ -168,7 +168,7 @@ test('add from folder: an already-imported file stays flagged and default-unchec
168168
await deleteViaPageMenu(page, overlay)
169169
await expect(overlay).not.toBeVisible()
170170

171-
await page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }).click()
171+
await clickBreadcrumbSegment(page, page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }), 'My space')
172172
const firstNotesCard = noteCard(page, 'Meeting Notes')
173173
await openCard(page, firstNotesCard)
174174
await expect(overlay).toBeVisible()

frontend/e2e/atlas-gestures.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+

frontend/e2e/atlas-minimap.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { test, expect } from './fixtures/server'
2+
3+
// The board's own minimap (goal 0106 slice B contract item 5): React
4+
// Flow's own MiniMap, styled to tokens (ThemedMiniMap.tsx), a
5+
// control-strip toggle button alongside zoom, a palette command, and
6+
// localStorage persistence (default ON) -- each browser context here
7+
// starts with fresh storage (Playwright's own default per-test
8+
// isolation), so every test below reads the untouched default.
9+
10+
test('the minimap renders by default, bottom-right of the board, in the same control cluster as zoom', async ({ page }) => {
11+
await page.goto('/')
12+
await page.getByRole('link', { name: 'Atlas' }).click()
13+
const board = page.getByTestId('atlas-board')
14+
await expect(board).toBeVisible()
15+
16+
await expect(board.locator('.react-flow__minimap')).toBeVisible()
17+
const toggle = board.getByTestId('atlas-minimap-toggle')
18+
await expect(toggle).toBeVisible()
19+
await expect(toggle).toHaveAttribute('aria-pressed', 'true')
20+
// Same cluster as the zoom/fit-view/lock buttons, not a separate
21+
// panel -- `has` must be built off `page`, not a locator already
22+
// scoped through a separate ancestor (`board`), or the filter never
23+
// matches.
24+
await expect(page.locator('.react-flow__controls').filter({ has: page.getByTestId('atlas-minimap-toggle') })).toHaveCount(1)
25+
await expect(board.locator('.react-flow__controls-zoomin')).toBeVisible()
26+
})
27+
28+
test('the toggle button hides and shows the minimap, and the choice persists across a reload', async ({ page }) => {
29+
await page.goto('/')
30+
await page.getByRole('link', { name: 'Atlas' }).click()
31+
const board = page.getByTestId('atlas-board')
32+
await expect(board).toBeVisible()
33+
34+
const toggle = board.getByTestId('atlas-minimap-toggle')
35+
await toggle.click()
36+
await expect(board.locator('.react-flow__minimap')).toHaveCount(0)
37+
await expect(toggle).toHaveAttribute('aria-pressed', 'false')
38+
39+
await page.reload()
40+
await page.getByRole('link', { name: 'Atlas' }).click()
41+
await expect(board).toBeVisible()
42+
await expect(board.locator('.react-flow__minimap')).toHaveCount(0)
43+
await expect(board.getByTestId('atlas-minimap-toggle')).toHaveAttribute('aria-pressed', 'false')
44+
})
45+
46+
test('the "Toggle minimap" palette command flips the same toggle the button does', async ({ page }) => {
47+
await page.goto('/')
48+
await page.getByRole('link', { name: 'Atlas' }).click()
49+
const board = page.getByTestId('atlas-board')
50+
await expect(board).toBeVisible()
51+
await expect(board.locator('.react-flow__minimap')).toBeVisible()
52+
53+
await page.keyboard.press('Meta+/')
54+
const palette = page.getByRole('dialog', { name: 'Command palette' })
55+
await expect(palette).toBeVisible()
56+
await palette.getByRole('combobox').fill('Toggle minimap')
57+
await palette.getByRole('option', { name: 'Toggle minimap' }).click()
58+
await expect(palette).toHaveCount(0)
59+
60+
await expect(board.locator('.react-flow__minimap')).toHaveCount(0)
61+
await expect(board.getByTestId('atlas-minimap-toggle')).toHaveAttribute('aria-pressed', 'false')
62+
})

0 commit comments

Comments
 (0)