Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions frontend/e2e/atlas-authoring.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from './fixtures/server'
import { contextMenu, rightClickEmptyArea } from './fixtures/contextMenu'
import { ATLAS_KIND_CONTACT, ATLAS_KIND_TOPIC, selectKind } from './fixtures/kindPicker'
import { clickCorner, groupCard, noteCard, zoomAllTheWayOut } from './fixtures/atlasBoard'
import { clickCorner, groupCard, noteCard, submitCreatePopover, zoomAllTheWayOut } from './fixtures/atlasBoard'

// Atlas creation core (goal 0081 slice A1): the tray, its placement
// popover, right-click create, sticky notes, and the note promotion
Expand Down Expand Up @@ -137,9 +137,21 @@ test('atlas creation core: tray, placement popover, right-click create, sticky n
await clickCorner(board, 'top-right')
await expect(cardTool).toHaveAttribute('data-armed', 'false')
await expect(popover).toBeVisible()

// The create loop (goal 0106 slice B contract item 4: C -> click ->
// title -> Enter -> board) audited: the popover's own resting chrome
// is the kind chip + title input ONLY -- no "Kind"/"Title" labels,
// no visible Submit/Cancel row (Enter/Escape are the only commit/
// cancel paths). The title input is already focused.
await expect(popover.getByText('Kind', { exact: true })).toHaveCount(0)
await expect(popover.getByText('Title', { exact: true })).toHaveCount(0)
await expect(popover.getByTestId('atlas-placement-submit')).toHaveCount(0)
await expect(popover.getByTestId('atlas-placement-cancel')).toHaveCount(0)
await expect(popover.getByTestId('atlas-placement-title')).toBeFocused()

await selectKind(popover, ATLAS_KIND_TOPIC)
await popover.getByTestId('atlas-placement-title').fill('ZzE2eRootCard')
await popover.getByTestId('atlas-placement-submit').click()
await submitCreatePopover(popover)
await expect(popover).not.toBeVisible()
await expect(noteCard(page, 'ZzE2eRootCard')).toBeVisible()
// Regression: one confirm creates exactly ONE card. StrictMode
Expand All @@ -166,7 +178,7 @@ test('atlas creation core: tray, placement popover, right-click create, sticky n
await expect(popover).toBeVisible()
await selectKind(popover, ATLAS_KIND_CONTACT)
await popover.getByTestId('atlas-placement-title').fill('ZzE2eAreaCard')
await popover.getByTestId('atlas-placement-submit').click()
await submitCreatePopover(popover)
await expect(popover).not.toBeVisible()
await expect(noteCard(page, 'ZzE2eAreaCard')).toBeVisible()

Expand Down
67 changes: 67 additions & 0 deletions frontend/e2e/atlas-breadcrumb-siblings.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { test, expect } from './fixtures/server'
import { groupCard } from './fixtures/atlasCards'

// The breadcrumb's own sibling dropdown (goal 0106 slice B contract
// item 5): each real segment is a dropdown trigger listing its own
// level's sibling places (name + card count), current one selected --
// clicking any entry (including the current one) navigates, the same
// capability the crumb's old direct-navigate-on-click carried.

test('a breadcrumb segment opens a dropdown of its level\'s siblings, current one selected, with card counts', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
await expect(page.getByTestId('atlas-board')).toBeVisible()

await groupCard(page, 'Example area').getByTestId('atlas-group-header').click()
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('Example area')

// "My space" is the seeded space's own root -- its siblings are
// every other root-level card, which at the single-root default is
// only itself (3 top-level children: Example area, Getting started,
// Scratchpad).
const mySpaceCrumb = page.getByTestId('atlas-breadcrumb').getByTestId('atlas-breadcrumb-item').filter({ hasText: 'My space' })
await mySpaceCrumb.click()

const dropdown = page.getByTestId('atlas-breadcrumb-siblings')
await expect(dropdown).toBeVisible()
const mySpaceRow = dropdown.getByTestId('atlas-breadcrumb-sibling').filter({ hasText: 'My space' })
await expect(mySpaceRow).toBeVisible()
await expect(mySpaceRow).toContainText('3 cards')

// Clicking the current place navigates to it (reproducing the old
// direct-navigate behavior) -- the crumb collapses back to "My
// space" alone.
await mySpaceRow.click()
await expect(dropdown).toHaveCount(0)
await expect(page.getByTestId('atlas-breadcrumb')).not.toContainText('Example area')
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('My space')
})

test('clicking a DIFFERENT sibling in the dropdown navigates laterally', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
await expect(page.getByTestId('atlas-board')).toBeVisible()

await groupCard(page, 'Example area').getByTestId('atlas-group-header').click()
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('Example area')

// "Example area"'s own siblings are the OTHER top-level children of
// "My space" -- Getting started, Scratchpad -- neither of which is a
// place with its own children, so clicking one lands on an empty
// board (its own drilled-into space), not "Example area"'s own
// contents.
const exampleAreaCrumb = page.getByTestId('atlas-breadcrumb').getByTestId('atlas-breadcrumb-item').filter({ hasText: 'Example area' })
await exampleAreaCrumb.click()
const dropdown = page.getByTestId('atlas-breadcrumb-siblings')
await expect(dropdown).toBeVisible()

const gettingRow = dropdown.getByTestId('atlas-breadcrumb-sibling').filter({ hasText: 'Getting started' })
await expect(gettingRow).toBeVisible()
await expect(gettingRow).toContainText('0 cards')
await gettingRow.click()

await expect(dropdown).toHaveCount(0)
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('My space')
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('Getting started')
await expect(page.getByTestId('atlas-breadcrumb')).not.toContainText('Example area')
})
6 changes: 3 additions & 3 deletions frontend/e2e/atlas-capture.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect } from './fixtures/server'
import type { Page } from '@playwright/test'
import { clickCorner, openCard, zoomAllTheWayOut } from './fixtures/atlasBoard'
import { clickCorner, openCard, submitCreatePopover, zoomAllTheWayOut } from './fixtures/atlasBoard'
import { deleteViaPageMenu } from './fixtures/atlasPage'

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

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

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

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

const card = page.locator('[data-testid="atlas-note-card"]').filter({ hasText: 'Vendor policy' })
Expand Down
14 changes: 9 additions & 5 deletions frontend/e2e/atlas-containment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from './fixtures/server'
import { contextMenu } from './fixtures/contextMenu'
import { ATLAS_KIND_TOPIC, selectKind } from './fixtures/kindPicker'
import { armAndPlaceTopicCard, deleteCardViaMenu, groupCard, noteCard } from './fixtures/atlasBoard'
import { armAndPlaceTopicCard, cancelCreatePopover, deleteCardViaMenu, groupCard, noteCard, submitCreatePopover } from './fixtures/atlasBoard'
import { waitForViewportStable } from './fixtures/animation'

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

// --- Rider (a): a zoomed-into space with zero cards and zero
Expand Down
6 changes: 3 additions & 3 deletions frontend/e2e/atlas-folder-import.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect } from './fixtures/server'
import { deleteViaPageMenu } from './fixtures/atlasPage'
import { openCard } from './fixtures/atlasBoard'
import { clickBreadcrumbSegment, openCard } from './fixtures/atlasBoard'

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

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

await page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }).click()
await clickBreadcrumbSegment(page, page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }), 'My space')
const firstNotesCard = noteCard(page, 'Meeting Notes')
await openCard(page, firstNotesCard)
await expect(overlay).toBeVisible()
Expand Down
100 changes: 100 additions & 0 deletions frontend/e2e/atlas-gestures.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,103 @@ test('atlas.up is surface-scoped: listed under "On this page" in the palette on
await expect(page.getByTestId('composition-view')).toBeVisible()
})

// The two 0104 residual e2e cases goal 0106 slice B absorbs (useAtlasKeyboardNav.ts's
// key table), plus a verify-only case for arrows-pan-with-no-selection
// (already implemented in slice A -- reported, not built here).

test('Tab cycles focus and selection across top-level cards in reading order', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
await expect(page.getByTestId('atlas-board')).toBeVisible()

const exampleArea = groupCard(page, 'Example area')
const getting = noteCard(page, 'Getting started')

// Focus lands inside the board wrapper (a click), then Escape's
// first rung clears the live selection while DOM focus stays on the
// clicked card -- the same mechanism the Escape-ladder test above
// already proves -- so the FIRST Tab below starts from "nothing
// selected", not from this card as an anchor.
await getting.click()
await page.keyboard.press('Escape')
await expect(selectedWrapper(page, getting)).toHaveCount(0)

// Reading order is left-to-right (atlasKeyboardNavGeometry.ts): the
// seed's own X positions put Example area (80) before Getting
// started (532), both on the same row (internal/domain/atlas/
// builtin.go).
await page.keyboard.press('Tab')
await expect(selectedWrapper(page, exampleArea)).toHaveCount(1)

await page.keyboard.press('Tab')
await expect(selectedWrapper(page, exampleArea)).toHaveCount(0)
await expect(selectedWrapper(page, getting)).toHaveCount(1)

await page.keyboard.press('Shift+Tab')
await expect(selectedWrapper(page, getting)).toHaveCount(0)
await expect(selectedWrapper(page, exampleArea)).toHaveCount(1)
})

test('arrow-nudge persists the selected card\'s position', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
await expect(page.getByTestId('atlas-board')).toBeVisible()

const getting = noteCard(page, 'Getting started')
const gettingNode = page.locator('.react-flow__node').filter({ has: getting })
await getting.click()
await expect(selectedWrapper(page, getting)).toHaveCount(1)

// React Flow writes translate(x,y) in flow coords directly onto the
// node element's own style -- camera-independent, unlike
// boundingBox() (same technique atlas.spec.ts's own arrange-persists
// test already established).
const before = (await gettingNode.evaluate((el) => (el as HTMLElement).style.transform)) ?? ''
for (let i = 0; i < 5; i++) await page.keyboard.press('ArrowRight')

let after = before
await expect.poll(async () => {
after = (await gettingNode.evaluate((el) => (el as HTMLElement).style.transform)) ?? ''
return after
}).not.toBe(before)

// The Go-side SetPosition write is batched to keyup, not per-pixel --
// a reload renders the SAME persisted position, proving the nudge
// actually round-tripped through the backend, not just the live DOM.
await page.reload()
await expect(page.getByTestId('atlas-view')).toBeVisible()
await expect(gettingNode).toBeVisible()
await expect.poll(async () => gettingNode.evaluate((el) => (el as HTMLElement).style.transform), { timeout: 10_000 }).toBe(after)

// Cleanup: nudge back to the seeded position (testing.md's
// within-file discipline -- no later test in this file depends on
// it, but leaving drift around is still unnecessary).
await getting.click()
await expect(selectedWrapper(page, getting)).toHaveCount(1)
for (let i = 0; i < 5; i++) await page.keyboard.press('ArrowLeft')
await expect.poll(async () => gettingNode.evaluate((el) => (el as HTMLElement).style.transform)).toBe(before)
})

// Verify-only (goal 0106 slice B's residual audit): arrows with NO
// selection pan the camera instead of nudging -- already implemented
// in slice A (useAtlasKeyboardNav.ts), not new work here.
test('arrows with no selection pan the camera instead of nudging', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
const board = page.getByTestId('atlas-board')
await expect(board).toBeVisible()

const getting = noteCard(page, 'Getting started')
await getting.click()
await page.keyboard.press('Escape')
await expect(selectedWrapper(page, getting)).toHaveCount(0)

const viewport = board.locator('.react-flow__viewport')
const before = await viewport.evaluate((el) => (el as HTMLElement).style.transform)
await page.keyboard.press('ArrowRight')
await expect.poll(async () => viewport.evaluate((el) => (el as HTMLElement).style.transform)).not.toBe(before)

// Cleanup: pan back so this test leaves the camera where it found it.
await page.keyboard.press('ArrowLeft')
})

62 changes: 62 additions & 0 deletions frontend/e2e/atlas-minimap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { test, expect } from './fixtures/server'

// The board's own minimap (goal 0106 slice B contract item 5): React
// Flow's own MiniMap, styled to tokens (ThemedMiniMap.tsx), a
// control-strip toggle button alongside zoom, a palette command, and
// localStorage persistence (default ON) -- each browser context here
// starts with fresh storage (Playwright's own default per-test
// isolation), so every test below reads the untouched default.

test('the minimap renders by default, bottom-right of the board, in the same control cluster as zoom', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
const board = page.getByTestId('atlas-board')
await expect(board).toBeVisible()

await expect(board.locator('.react-flow__minimap')).toBeVisible()
const toggle = board.getByTestId('atlas-minimap-toggle')
await expect(toggle).toBeVisible()
await expect(toggle).toHaveAttribute('aria-pressed', 'true')
// Same cluster as the zoom/fit-view/lock buttons, not a separate
// panel -- `has` must be built off `page`, not a locator already
// scoped through a separate ancestor (`board`), or the filter never
// matches.
await expect(page.locator('.react-flow__controls').filter({ has: page.getByTestId('atlas-minimap-toggle') })).toHaveCount(1)
await expect(board.locator('.react-flow__controls-zoomin')).toBeVisible()
})

test('the toggle button hides and shows the minimap, and the choice persists across a reload', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
const board = page.getByTestId('atlas-board')
await expect(board).toBeVisible()

const toggle = board.getByTestId('atlas-minimap-toggle')
await toggle.click()
await expect(board.locator('.react-flow__minimap')).toHaveCount(0)
await expect(toggle).toHaveAttribute('aria-pressed', 'false')

await page.reload()
await page.getByRole('link', { name: 'Atlas' }).click()
await expect(board).toBeVisible()
await expect(board.locator('.react-flow__minimap')).toHaveCount(0)
await expect(board.getByTestId('atlas-minimap-toggle')).toHaveAttribute('aria-pressed', 'false')
})

test('the "Toggle minimap" palette command flips the same toggle the button does', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
const board = page.getByTestId('atlas-board')
await expect(board).toBeVisible()
await expect(board.locator('.react-flow__minimap')).toBeVisible()

await page.keyboard.press('Meta+/')
const palette = page.getByRole('dialog', { name: 'Command palette' })
await expect(palette).toBeVisible()
await palette.getByRole('combobox').fill('Toggle minimap')
await palette.getByRole('option', { name: 'Toggle minimap' }).click()
await expect(palette).toHaveCount(0)

await expect(board.locator('.react-flow__minimap')).toHaveCount(0)
await expect(board.getByTestId('atlas-minimap-toggle')).toHaveAttribute('aria-pressed', 'false')
})
Loading
Loading