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
1 change: 1 addition & 0 deletions frontend/e2e/QUARANTINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ trace (now auto-captured on retry).
| atlas-containment.spec.ts:105 | interaction-race | 2026-08-17 | 2026-09-17 | the Area tool's own marquee draw (a real pointer-capture drag, not React Flow's own node drag) occasionally no-ops on the LAST of several marquees drawn in one long test -- self-heals on retry every time observed; waitForViewportStable already applied at every zoom-changing step, root cause not yet isolated |
| workflow-runs-panel.spec.ts:116 | unclear | 2026-08-16 | 2026-09-16 | await first trace |
| layout.spec.ts:163 | unclear (contention-only) | 2026-08-16 | 2026-09-16 | fails only under parallel local load |
| atlas-folder-import.spec.ts:113 | unclear (contention-only) | 2026-08-18 | 2026-09-18 | "Add 1 cards" click times out only under 4-worker parallel local load (own dedicated server, so not a cross-spec data race); passes clean in isolation and on retry every time observed |
| composition-canvas-interactions.spec.ts:25 | interaction-race | 2026-08-17 | 2026-09-17 | clickCanvasNode's own toPass retry loop times out ("element is outside of the viewport" across every retry) selecting the upstream node before the Inspector-composed drag; reproduces in isolation and pre-existing on unmodified main (unrelated to goal 0081 slice A3), not yet traced |
| composition-canvas-interactions.spec.ts:92 | CI-only skip | 2026-08-15 | goal 0069's revisit clause | the one honest skip; four fix layers recorded. Reproduced LOCALLY too as of 2026-08-16 (2/2 attempts, on both the pre- and post-0080-burn-down code -- not a burn-down regression), contradicting the in-file comment's "every local mode... passes 10/10"; that comment needs a re-check, not yet done here |

Expand Down
12 changes: 4 additions & 8 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, zoomAllTheWayOut } from './fixtures/atlasBoard'
import { clickCorner, openCard, zoomAllTheWayOut } from './fixtures/atlasBoard'
import { deleteViaPageMenu } from './fixtures/atlasPage'

// Atlas capture doors (goal 0081 slice A3, LOCKED design §2b/§3b):
Expand Down Expand Up @@ -65,9 +65,7 @@ test('paste text opens the placement popover prefilled with title and note', asy
await expect(card).toContainText('Finish the vendor review before rollout.')

// Cleanup (testing.md's within-file/within-worker discipline).
await card.click()
await expect(card).toHaveAttribute('data-flipped', 'true')
await card.getByTestId('atlas-note-open').click()
await openCard(page, card)
const overlay = page.locator('[data-component="atlas-card-overlay"]')
await expect(overlay).toBeVisible()
await deleteViaPageMenu(page, overlay)
Expand Down Expand Up @@ -95,9 +93,7 @@ test('paste HTML converts to Markdown before prefilling the popover', async ({ p
await expect(card).toBeVisible()
await expect(card).toContainText('Prod credentials never stay with the requester.')

await card.click()
await expect(card).toHaveAttribute('data-flipped', 'true')
await card.getByTestId('atlas-note-open').click()
await openCard(page, card)
const overlay = page.locator('[data-component="atlas-card-overlay"]')
await expect(overlay).toBeVisible()
await deleteViaPageMenu(page, overlay)
Expand Down Expand Up @@ -132,7 +128,7 @@ test('Scratchpad seed is a container card with the inbox guidance note', async (
await page.getByRole('link', { name: 'Atlas' }).click()
await expect(page.getByTestId('atlas-board')).toBeVisible()

const scratchpad = page.locator('[data-testid="atlas-note-card"][aria-label="Flip Scratchpad"]')
const scratchpad = page.locator('[data-testid="atlas-note-card"][aria-label="Open Scratchpad"]')
await expect(scratchpad).toBeVisible()
await expect(scratchpad).toContainText('Quick captures land here. Drag notes out to file them, or promote them into cards.')
// Still an ordinary Topic card structurally (containment is a role
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/atlas-containment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ test('atlas containment: area drawing, marker-box grouping, drag filing, dissolv
// children. Placements stay >= 0.3 board-width fractions apart --
// a card's own rendered footprint at this zoom level is wide
// enough that a tighter gap lands a later click ON the earlier
// card (toggling its flip) instead of on empty canvas. ---
// card (selecting it) instead of on empty canvas. ---
await armAndPlaceTopicCard(page, board, popover, 0.25, 0.05, 'ZzC2eMemberA')
await armAndPlaceTopicCard(page, board, popover, 0.55, 0.05, 'ZzC2eMemberB')
await page.keyboard.press('a')
Expand Down
12 changes: 2 additions & 10 deletions frontend/e2e/atlas-delete-relationships.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import { test, expect } from './fixtures/server'
import { deleteViaPageMenu } from './fixtures/atlasPage'
import { openViaFlip } from './fixtures/atlasBoard'
import { noteCard, openCard } from './fixtures/atlasBoard'

// Split out of atlas.spec.ts (architecture.md's 500-line convention,
// the same split atlas-share.spec.ts/atlas-projections.spec.ts already
// established) once the delete-blast-radius toast case (goal 0103)
// pushed that file over the limit.

// Precise per-card matching, same reasoning as atlas.spec.ts's own
// local copy: a card's own BACK face can legitimately contain another
// card's title in its "<kind> -> <other title>" link row, so aria-label
// carries the exact title instead of a substring match.
function noteCard(page: import('@playwright/test').Page, title: string) {
return page.locator(`[data-testid="atlas-note-card"][aria-label="Flip ${title}"]`)
}

test('deleting a linked card names the blast radius in the undo toast, and undo restores the link edge', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
Expand All @@ -24,7 +16,7 @@ test('deleting a linked card names the blast radius in the undo toast, and undo
// "Getting started" carries exactly one seeded link (to "Ada
// Lovelace") and no children -- the leaf-with-links case.
const gettingStarted = noteCard(page, 'Getting started')
await openViaFlip(gettingStarted)
await openCard(page, gettingStarted)
const overlay = page.locator('[data-component="atlas-card-overlay"]')
await deleteViaPageMenu(page, overlay)
await expect(gettingStarted).toHaveCount(0)
Expand Down
21 changes: 9 additions & 12 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 { openViaFlip } from './fixtures/atlasBoard'
import { openCard } from './fixtures/atlasBoard'

// Synced-folder onboarding (docs/goals/0067) over real Go bindings
// (Wails3 server mode): AtlasService.PickFolder's own MILL_TEST_
Expand All @@ -19,12 +19,9 @@ import { openViaFlip } from './fixtures/atlasBoard'
// container category below assigns the ordinary seeded "Topic" Kind,
// not a dedicated container concept.

// Precise per-card matching: a plain hasText substring filter is
// unreliable here since a card's own BACK face can legitimately
// contain another card's title (its own "<kind> -> <other title>"
// link row) -- aria-label carries the exact title instead.
// Precise per-card matching: aria-label carries the exact title.
function noteCard(page: import('@playwright/test').Page, title: string) {
return page.locator(`[data-testid="atlas-note-card"][aria-label="Flip ${title}"]`)
return page.locator(`[data-testid="atlas-note-card"][aria-label="Open ${title}"]`)
}

function groupCard(page: import('@playwright/test').Page, title: string) {
Expand Down Expand Up @@ -69,7 +66,7 @@ test('add from folder: scan, partial accept, containment, and mirror rendering a
// The rejected entry never becomes a card; every accepted root-level
// entry does, under "My space" -- containment for the nested entry
// is checked separately below. "Reports" now holds its own imported
// child, so it renders as a region frame, not a flippable leaf note.
// child, so it renders as a region frame, not a plain leaf note.
await expect(page.getByTestId('atlas-note-card').filter({ hasText: 'Project Plan' })).toHaveCount(0)
const notesCard = noteCard(page, 'Meeting Notes')
const logoCard = noteCard(page, 'Logo')
Expand All @@ -86,7 +83,7 @@ test('add from folder: scan, partial accept, containment, and mirror rendering a
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('Reports')
const summaryCard = noteCard(page, 'Q1 Summary')
await expect(summaryCard).toBeVisible()
await openViaFlip(summaryCard)
await openCard(page, summaryCard)
const overlay = page.locator('[data-component="atlas-card-overlay"]')
await expect(overlay).toBeVisible()
await expect(overlay.getByTestId('atlas-page-mirror-path')).toHaveValue(/Reports\/Q1 Summary\.md$/)
Expand All @@ -95,13 +92,13 @@ test('add from folder: scan, partial accept, containment, and mirror rendering a
// Cleanup (testing.md's within-file/within-worker discipline): the
// child card must go before its own container can be deleted. Once
// "Reports" holds no children, it renders as a plain note card,
// deleted the same flip-then-Open way as every other leaf below.
// deleted the same select-then-commit way as every other leaf below.
await deleteViaPageMenu(page, overlay)
await expect(summaryCard).not.toBeVisible()

await page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }).click()
for (const card of [noteCard(page, 'Reports'), notesCard, logoCard]) {
await openViaFlip(card)
await openCard(page, card)
await expect(overlay).toBeVisible()
await deleteViaPageMenu(page, overlay)
await expect(overlay).not.toBeVisible()
Expand Down Expand Up @@ -166,14 +163,14 @@ test('add from folder: an already-imported file stays flagged and default-unchec
// this scenario created -- the one just made here, then the first
// one back under "My space".
const overlay = page.locator('[data-component="atlas-card-overlay"]')
await openViaFlip(secondNotesCard)
await openCard(page, secondNotesCard)
await expect(overlay).toBeVisible()
await deleteViaPageMenu(page, overlay)
await expect(overlay).not.toBeVisible()

await page.getByTestId('atlas-breadcrumb').getByText('My space', { exact: true }).click()
const firstNotesCard = noteCard(page, 'Meeting Notes')
await openViaFlip(firstNotesCard)
await openCard(page, firstNotesCard)
await expect(overlay).toBeVisible()
await deleteViaPageMenu(page, overlay)
await expect(overlay).not.toBeVisible()
Expand Down
141 changes: 129 additions & 12 deletions frontend/e2e/atlas-gestures.spec.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,101 @@
import { test, expect } from './fixtures/server'
import { groupCard, noteCard } from './fixtures/atlasCards'
import { clickCorner, zoomAllTheWayOut } from './fixtures/atlasBoard'
import { contextMenu } from './fixtures/contextMenu'

// The gesture model (goal 0074) + surface-scoped shortcuts (goal 0071
// slice): click glances, double-click commits, cmd-click opens,
// cmd-ArrowUp climbs the depth ladder -- split from atlas.spec.ts at
// the 500-line convention, same seam the share/projection groups
// already took.
// The click model (goal 0102's gesture table) + surface-scoped
// shortcuts (goal 0071 slice): plain click selects/replaces, a second
// click on the already-selected node commits (leaf -> page, place ->
// zoom), double-click reproduces the same outcome as two plain clicks,
// shift-click toggles, cmd-click commits instantly, click-empty
// deselects, cmd-ArrowUp climbs the depth ladder -- split from
// atlas.spec.ts at the 500-line convention, same seam the share/
// projection groups already took.

test('the gesture model: double-click commits -- a leaf opens its page, a frame body and a region chip zoom in, chips flip on single click', async ({ page }) => {
function selectedWrapper(page: import('@playwright/test').Page, card: import('@playwright/test').Locator) {
return page.locator('.react-flow__node.selected').filter({ has: card })
}

test('plain click selects (replacing any prior selection); a second click on the already-selected node commits', 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 scratchpad = noteCard(page, 'Scratchpad')

await getting.click()
await expect(selectedWrapper(page, getting)).toHaveCount(1)

// A DIFFERENT card's plain click replaces the selection outright --
// never a surface pop, never a co-selection.
await scratchpad.click()
await expect(selectedWrapper(page, scratchpad)).toHaveCount(1)
await expect(selectedWrapper(page, getting)).toHaveCount(0)

// The already-selected card's own second click commits -- a leaf's
// commit is its page.
await scratchpad.click()
const overlay = page.locator('[data-component="atlas-card-overlay"]')
await expect(overlay).toBeVisible()
await expect(overlay.getByTestId('atlas-page-title')).toHaveValue('Scratchpad')
await page.keyboard.press('Escape')
await expect(overlay).not.toBeVisible()
})

test('a plain click leaves the selection ring visibly showing on the clicked card while it still holds DOM focus, for a note card and a sticky', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
await expect(page.getByTestId('atlas-board')).toBeVisible()

// Regression: Primer's own [role="button"]:focus:not(:focus-visible)
// reset zeroes any box-shadow scoped to that inner role="button"
// element -- exactly the state a plain mouse click leaves the
// clicked card in (focused, but never :focus-visible from a pointer
// gesture), which made a single-card selection invisible. The ring
// must show on the wrapper, immune to that reset, while the card is
// still focused -- not just once focus moves elsewhere.
const getting = noteCard(page, 'Getting started')
await getting.click()
expect(await page.evaluate(() => document.activeElement?.getAttribute('data-testid'))).toBe('atlas-note-card')
const cardWrapper = selectedWrapper(page, getting)
await expect(cardWrapper).toHaveCount(1)
await expect.poll(() => cardWrapper.evaluate((el) => getComputedStyle(el).boxShadow)).not.toBe('none')

// A sticky note's own, separately-declared (heavier) ring rule.
const board = page.getByTestId('atlas-board')
await zoomAllTheWayOut(page)
await page.keyboard.press('n')
await clickCorner(board, 'top-right')
const noteTA = page.getByTestId('atlas-sticky-textarea')
await noteTA.fill('ZzE2eStickyRing')
await noteTA.blur()
const sticky = page.locator('[data-testid="atlas-sticky-note"]')
await sticky.click()
expect(await page.evaluate(() => document.activeElement?.getAttribute('data-testid'))).toBe('atlas-sticky-note')
const stickyWrapper = page.locator('.react-flow__node.selected').filter({ has: sticky })
await expect(stickyWrapper).toHaveCount(1)
await expect.poll(() => stickyWrapper.evaluate((el) => getComputedStyle(el).boxShadow)).not.toBe('none')

// Cleanup (testing.md's within-file discipline).
const menu = contextMenu(page)
await sticky.click({ button: 'right' })
await menu.getByText('Delete note', { exact: true }).click()
await expect(sticky).toHaveCount(0)
})

test('a real double-click reproduces the same select-then-commit outcome as two plain clicks, for both a leaf and a frame body', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Atlas' }).click()
await expect(page.getByTestId('atlas-board')).toBeVisible()

// Leaf double-click = open its page; the commit unflips, so the
// card is front-facing again once the page closes.
const getting = noteCard(page, 'Getting started')
await getting.dblclick()
const overlay = page.locator('[data-component="atlas-card-overlay"]')
await expect(overlay).toBeVisible()
await expect(overlay.getByTestId('atlas-page-title')).toHaveValue('Getting started')
await page.keyboard.press('Escape')
await expect(overlay).not.toBeVisible()
await expect(getting).toHaveAttribute('data-flipped', 'false')

// Frame body double-click = zoom into the place (padding strip:
// the frame centre belongs to its preview-child nodes).
Expand All @@ -35,15 +109,58 @@ test('the gesture model: double-click commits -- a leaf opens its page, a frame
await expect(page.getByTestId('atlas-breadcrumb')).not.toContainText('Example area')
await page.keyboard.press('Meta+ArrowUp')
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('My space')
})

test('⌘-click commits instantly with no prior selection needed; a plain click on empty canvas deselects', 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')
await expect(selectedWrapper(page, getting)).toHaveCount(0)

// ⌘-click = the pointer twin of ⌘↵: opens the card's page with no
// flip step, and the card is front-facing behind it.
// ⌘-click = the pointer twin of ⌘↵: opens the card's page directly,
// with no prior select-click needed.
await getting.click({ modifiers: ['Meta'] })
const overlay = page.locator('[data-component="atlas-card-overlay"]')
await expect(overlay).toBeVisible()
await expect(overlay.getByTestId('atlas-page-title')).toHaveValue('Getting started')
await page.keyboard.press('Escape')
await expect(overlay).not.toBeVisible()
await expect(getting).toHaveAttribute('data-flipped', 'false')

// Click-empty deselects (React Flow's own native pane-click
// behavior) -- select the card fresh first.
await getting.click()
await expect(selectedWrapper(page, getting)).toHaveCount(1)
const board = page.getByTestId('atlas-board')
await zoomAllTheWayOut(page)
await clickCorner(board, 'top-left')
await expect(selectedWrapper(page, getting)).toHaveCount(0)
})

test('the Escape ladder: clears a live selection first, then -- with nothing selected -- goes up one level', 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')

const charter = noteCard(page, 'Project charter')
await charter.click()
await expect(selectedWrapper(page, charter)).toHaveCount(1)

// First rung: a live selection exists, so Escape clears it and stays
// at this level.
await page.keyboard.press('Escape')
await expect(selectedWrapper(page, charter)).toHaveCount(0)
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('Example area')

// Second rung: nothing selected, so the SAME key now climbs the
// depth ladder one step -- the same signal ⌘↑ bumps.
await page.keyboard.press('Escape')
await expect(page.getByTestId('atlas-breadcrumb')).not.toContainText('Example area')
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('My space')
})

test('atlas.up is surface-scoped: listed under "On this page" in the palette on Atlas, absent and inert elsewhere', async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/atlas-jump.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function jumpDialog(page: import('@playwright/test').Page) {
}

function noteCard(page: import('@playwright/test').Page, title: string) {
return page.locator(`[data-testid="atlas-note-card"][aria-label="Flip ${title}"]`)
return page.locator(`[data-testid="atlas-note-card"][aria-label="Open ${title}"]`)
}

function groupCard(page: import('@playwright/test').Page, title: string) {
Expand Down
Loading
Loading