Skip to content

Commit ce4c661

Browse files
authored
Merge branch 'main' into fix/extensions-list-review
2 parents 9034c8e + 1e1a6ee commit ce4c661

13 files changed

Lines changed: 533 additions & 155 deletions

frontend/e2e/atlas-capture.spec.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { test, expect } from './fixtures/server'
2+
import { placeNoteClear } from './fixtures/atlasEmptyRegion'
23
import { blurSticky, stickyEditor } from './fixtures/codeEditor'
34
import type { Page } from '@playwright/test'
4-
import { clickCorner, deleteSticky, zoomAllTheWayOut } from './fixtures/atlasBoard'
5+
import { deleteSticky, zoomAllTheWayOut } from './fixtures/atlasBoard'
56

67
// Atlas capture doors (goal 0081 slice A3, fallback redesigned by goal
78
// 0218) and the Scratchpad seed rework, driven end to end against the
@@ -95,17 +96,25 @@ test('paste is inert while an editable field has focus', async ({ page }) => {
9596
// never create a second note on top of it.
9697
const board = page.getByTestId('atlas-board')
9798
await zoomAllTheWayOut(page)
98-
await page.keyboard.press('n')
99-
await clickCorner(board, 'top-left')
99+
await placeNoteClear(page, board)
100100
const editorContent = page.locator('[data-testid="atlas-sticky-editor"] [contenteditable="true"]')
101101
await editorContent.waitFor()
102102
await editorContent.focus()
103103

104104
await dispatchPaste(page, { text: 'should stay in the editor' })
105105
await expect(page.locator('[data-testid="atlas-sticky-note"]').filter({ hasText: 'should stay in the editor' })).toHaveCount(0)
106106

107+
// A typed marker before the commit makes the committed note findable
108+
// for cleanup regardless of whether the synthetic paste actually
109+
// inserted text (a synthetic ClipboardEvent carries no real
110+
// clipboard payload into the editor). Without this, the commit
111+
// leaked a note -- usually BLANK -- into the worker's shared board,
112+
// stretching every later spec's fitView extents (the measured cause
113+
// of seeded cards landing under the minimap).
114+
await page.keyboard.type('ZzInertPasteNote')
107115
await blurSticky(page)
108116
await expect(stickyEditor(page)).toHaveCount(0)
117+
await deleteSticky(page, page.locator('[data-testid="atlas-sticky-note"]').filter({ hasText: 'ZzInertPasteNote' }))
109118
})
110119

111120
test('Scratchpad seed is a container card with the inbox guidance note', async ({ page }) => {

frontend/e2e/atlas-gestures.spec.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { test, expect } from './fixtures/server'
2+
import { placeNoteClear } from './fixtures/atlasEmptyRegion'
23
import { blurSticky, fillSticky } from './fixtures/codeEditor'
34
import { groupCard, noteCard } from './fixtures/atlasCards'
4-
import { clickCorner, zoomAllTheWayOut, clickFrameGutter } from './fixtures/atlasBoard'
5+
import { clickHittable, clickCorner, zoomAllTheWayOut, clickFrameGutter } from './fixtures/atlasBoard'
56
import { contextMenu } from './fixtures/contextMenu'
67
import { waitForViewportStable } from './fixtures/animation'
78

@@ -21,23 +22,31 @@ function selectedWrapper(page: import('@playwright/test').Page, card: import('@p
2122
test('plain click selects (replacing any prior selection); a second click on the already-selected node commits', async ({ page }) => {
2223
await page.goto('/')
2324
await page.getByRole('link', { name: 'Atlas' }).click()
24-
await expect(page.getByTestId('atlas-board')).toBeVisible()
25+
const board = page.getByTestId('atlas-board')
26+
await expect(board).toBeVisible()
27+
// The restore-driven fitView animates via d3's JS interpolation
28+
// (fixtures/animation.ts's own header) -- a click fired mid-flight
29+
// fails actionability as "element is not stable" against whatever
30+
// extents worker-shared leftovers have stretched the fit to.
31+
await waitForViewportStable(board)
2532

2633
const getting = noteCard(page, 'Discovery workstream')
2734
const scratchpad = noteCard(page, 'Scratchpad')
2835

29-
await getting.click()
36+
await clickHittable(page, getting)
3037
await expect(selectedWrapper(page, getting)).toHaveCount(1)
3138

3239
// A DIFFERENT card's plain click replaces the selection outright --
33-
// never a surface pop, never a co-selection.
34-
await scratchpad.click()
40+
// never a surface pop, never a co-selection. clickHittable: the
41+
// minimap legitimately paints over whichever card the restored
42+
// viewport left beneath it.
43+
await clickHittable(page, scratchpad)
3544
await expect(selectedWrapper(page, scratchpad)).toHaveCount(1)
3645
await expect(selectedWrapper(page, getting)).toHaveCount(0)
3746

3847
// The already-selected card's own second click commits -- a leaf's
3948
// commit is its page.
40-
await scratchpad.click()
49+
await clickHittable(page, scratchpad)
4150
const overlay = page.locator('[data-component="atlas-card-overlay"]')
4251
await expect(overlay).toBeVisible()
4352
await expect(overlay.getByTestId('atlas-page-title')).toHaveValue('Scratchpad')
@@ -67,8 +76,7 @@ test('a plain click leaves the selection ring visibly showing on the clicked car
6776
// A sticky note's own, separately-declared (heavier) ring rule.
6877
const board = page.getByTestId('atlas-board')
6978
await zoomAllTheWayOut(page)
70-
await page.keyboard.press('n')
71-
await clickCorner(board, 'top-right')
79+
await placeNoteClear(page, board)
7280
await fillSticky(page, 'ZzE2eStickyRing')
7381
await blurSticky(page)
7482
const sticky = page.locator('[data-testid="atlas-sticky-note"]')
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { test } from './fixtures/server'
2+
import { expect } from '@playwright/test'
3+
import { deleteSticky, dragResizeHandle } from './fixtures/atlasBoard'
4+
import { findEmptyBoardPoint, placeNoteClear } from './fixtures/atlasEmptyRegion'
5+
import { fillSticky, stickyEditor, blurSticky } from './fixtures/codeEditor'
6+
7+
// The note's box model and resize/edit interaction contract (goal
8+
// 0248): content-driven height with a floor and a cap, corner-square
9+
// resize handles, an edit session no resize drag can end, and a
10+
// usable default footprint. Split from atlas-note-markdown.spec.ts
11+
// along the rendering-vs-box-model seam (500-line convention).
12+
// Shared worker pool: every note created here is deleted before its
13+
// test ends.
14+
15+
// Regression (owner-reported live: a note's box snapped small on
16+
// commit, clipping its own second line): the note's box height is
17+
// content-driven in BOTH the editing and at-rest render, floored by a
18+
// min-height, never a fixed clamp -- entering/leaving edit never snaps
19+
// the box smaller than its own content.
20+
test('a note holding two lines fits both, no invisible clip at rest', async ({ page }) => {
21+
await page.goto('/')
22+
await page.getByRole('link', { name: 'Atlas' }).click()
23+
const board = page.getByTestId('atlas-board')
24+
await expect(board).toBeVisible()
25+
await placeNoteClear(page, board)
26+
await expect(stickyEditor(page)).toBeVisible()
27+
28+
const editable = stickyEditor(page).locator('[contenteditable="true"]')
29+
await editable.click()
30+
await page.keyboard.type('# Plan')
31+
await page.keyboard.press('Enter')
32+
await page.keyboard.type('some text here')
33+
34+
const sticky = page.getByTestId('atlas-sticky-note')
35+
const editingBox = await sticky.boundingBox()
36+
if (!editingBox) throw new Error('sticky has no bounding box while editing')
37+
38+
// A real outside press commits it (the pointer-driven contract) --
39+
// the same "click away" gesture the live repro used, not a
40+
// keyboard shortcut. An EMPTY pane point, never a corner: a corner
41+
// press can land on a seeded card's own drill door, silently
42+
// changing the restored session level for every later test on this
43+
// worker.
44+
const away = await findEmptyBoardPoint(page, board, [sticky])
45+
const awayBase = await board.boundingBox()
46+
if (!awayBase) throw new Error('board has no bounding box')
47+
await board.click({ position: { x: away.x - awayBase.x, y: away.y - awayBase.y } })
48+
await expect(stickyEditor(page)).toHaveCount(0)
49+
50+
await expect(sticky.locator('h1')).toHaveText('Plan')
51+
const secondLine = sticky.locator('p', { hasText: 'some text here' })
52+
await expect(secondLine).toBeVisible()
53+
54+
const restBox = await sticky.boundingBox()
55+
const lineBox = await secondLine.boundingBox()
56+
if (!restBox || !lineBox) throw new Error('missing a bounding box at rest')
57+
58+
// No edit -> rest size snap: the box never shrinks on commit.
59+
expect(restBox.height).toBeGreaterThanOrEqual(editingBox.height - 2)
60+
// The N1 pin: the second line's own box is fully CONTAINED within
61+
// the note's box, not clipped past its bottom edge -- the exact
62+
// defect measured live (the line existed in the DOM but sat below
63+
// the visible, clipped box).
64+
expect(lineBox.y + lineBox.height).toBeLessThanOrEqual(restBox.y + restBox.height + 1)
65+
66+
await deleteSticky(page, sticky)
67+
})
68+
69+
// Regression (owner-reported live: the resize handles rendered as four
70+
// 514x5px full-width blue bars instead of small corner squares, ONLY
71+
// while a note was selected AND focused for editing -- selected-but-
72+
// not-editing measured fine): AtlasStickyNode.module.css's own
73+
// `.sticky.editing > *` rule used to catch the library's own
74+
// unwrapped `.react-flow__resize-control` elements (rendered as DIRECT
75+
// children of the editing wrapper, no wrapping element of their own),
76+
// forcing every handle to the note's own width.
77+
test('resize handles are small corner squares while editing and selected, never full-width bars', async ({ page }) => {
78+
await page.goto('/')
79+
await page.getByRole('link', { name: 'Atlas' }).click()
80+
const board = page.getByTestId('atlas-board')
81+
await expect(board).toBeVisible()
82+
await placeNoteClear(page, board)
83+
await fillSticky(page, 'Handle check')
84+
await blurSticky(page)
85+
86+
const sticky = page.getByTestId('atlas-sticky-note')
87+
await sticky.dblclick()
88+
await expect(stickyEditor(page)).toBeVisible()
89+
90+
const handles = page.locator('.react-flow__resize-control.handle')
91+
await expect(handles.first()).toBeVisible()
92+
const count = await handles.count()
93+
expect(count).toBeGreaterThan(0)
94+
for (let i = 0; i < count; i++) {
95+
const box = await handles.nth(i).boundingBox()
96+
if (!box) throw new Error('resize handle has no bounding box')
97+
expect(box.width).toBeLessThanOrEqual(12)
98+
}
99+
100+
await blurSticky(page)
101+
await deleteSticky(page, sticky)
102+
})
103+
104+
// Regression (owner-reported live: dragging a resize handle dropped
105+
// the contenteditable's own focus, ending the edit session mid-drag):
106+
// the resize handles are excluded from the outside-press "commit"
107+
// detection, a press on a handle keeps its mousedown default
108+
// suppressed so DOM focus never leaves the contenteditable at all,
109+
// and the capture/restore pair covers any refresh-driven editor
110+
// remount landing right after the drag (AtlasStickyNode.tsx's
111+
// captureResizeFocus/restoreResizeFocus).
112+
test('a resize drag never breaks the edit session -- the editor stays focused after mouseup', async ({ page }) => {
113+
await page.goto('/')
114+
await page.getByRole('link', { name: 'Atlas' }).click()
115+
const board = page.getByTestId('atlas-board')
116+
await expect(board).toBeVisible()
117+
await placeNoteClear(page, board)
118+
await fillSticky(page, 'Resize me')
119+
await blurSticky(page)
120+
121+
const sticky = page.getByTestId('atlas-sticky-note')
122+
await sticky.dblclick()
123+
await expect(stickyEditor(page)).toBeVisible()
124+
const editable = stickyEditor(page).locator('[contenteditable="true"]')
125+
await expect(editable).toBeFocused()
126+
127+
const handle = page.locator('.react-flow__resize-control.handle.bottom.right')
128+
await dragResizeHandle(page, handle, 40, 30)
129+
130+
// Still editing, same contenteditable still focused -- the drag
131+
// never committed/unmounted the edit session.
132+
await expect(stickyEditor(page)).toBeVisible()
133+
await expect(editable).toBeFocused()
134+
135+
await blurSticky(page)
136+
await deleteSticky(page, sticky)
137+
})
138+
139+
// Regression (owner-reported live: a freshly click-placed note landed
140+
// at 79x54px, a sliver too small to read): the note tool's own default
141+
// footprint (atlasBoardLayout.ts's STICKY_WIDTH) is a usable width, and
142+
// the editor autofocuses immediately on placement.
143+
test('a fresh note lands at a usable default width, editor focused', async ({ page }) => {
144+
await page.goto('/')
145+
await page.getByRole('link', { name: 'Atlas' }).click()
146+
const board = page.getByTestId('atlas-board')
147+
await expect(board).toBeVisible()
148+
await placeNoteClear(page, board)
149+
150+
const editor = stickyEditor(page)
151+
await expect(editor).toBeVisible()
152+
const editable = editor.locator('[contenteditable="true"], textarea')
153+
await expect(editable).toBeFocused()
154+
155+
// FLOW units via the node wrapper's own inline width, not a screen
156+
// bounding box -- the restored session level varies the zoom, and a
157+
// screen-pixel read of the same footprint shrinks with it.
158+
const wrapper = page.locator('.react-flow__node:has([data-testid="atlas-sticky-note"])')
159+
const flowWidth = await wrapper.evaluate((el) => parseFloat((el as HTMLElement).style.width))
160+
expect(flowWidth).toBeGreaterThanOrEqual(200)
161+
162+
// Escape cancels the still-unpersisted draft -- nothing to clean up.
163+
await page.keyboard.press('Escape')
164+
await expect(page.getByTestId('atlas-sticky-note')).toHaveCount(0)
165+
})

frontend/e2e/atlas-note-markdown.spec.ts

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { test, expect } from './fixtures/server'
2-
import { clickCorner, deleteSticky } from './fixtures/atlasBoard'
2+
import { deleteSticky } from './fixtures/atlasBoard'
3+
import { placeNoteClear } from './fixtures/atlasEmptyRegion'
34
import { contextMenu } from './fixtures/contextMenu'
45
import { fillMilkdown, fillSticky, stickyEditor, blurSticky } from './fixtures/codeEditor'
56

@@ -46,8 +47,7 @@ test('sticky notes render markdown via Milkdown; live formatting, a checkbox tog
4647
await page.getByRole('link', { name: 'Atlas' }).click()
4748
const board = page.getByTestId('atlas-board')
4849
await expect(board).toBeVisible()
49-
await page.keyboard.press('n')
50-
await clickCorner(board, 'top-left')
50+
await placeNoteClear(page, board)
5151
await expect(stickyEditor(page)).toBeVisible()
5252

5353
// Real user primitives (testing.md): type each line, no synthetic
@@ -60,19 +60,42 @@ test('sticky notes render markdown via Milkdown; live formatting, a checkbox tog
6060
await page.keyboard.type('some ')
6161
await page.keyboard.type('**bold**')
6262
await page.keyboard.press('Enter')
63-
// "- [ ] " is its OWN type() call, followed by an explicit wait for
64-
// the task-item transaction to actually land (the icon widget
65-
// appearing) before typing the rest -- typing the trailing text
66-
// WHILE that transaction is still in flight was observed (CI, a
67-
// slower/more-contended runner than a dev machine) to race it: the
68-
// node conversion partially lands, leaving a plain bullet with a
69-
// stray "]" instead of a real task item. Splitting on a WAITED
70-
// condition, rather than gambling on one long continuous call being
71-
// fast enough end to end, is what actually closes the race -- see
72-
// checkboxIcon below for the assertion this wait is priming.
73-
await page.keyboard.type('- [ ] ')
63+
// The task item is built in two WAITED stages at human keystroke
64+
// pacing, each synchronizing on ITS OWN icon (`.bullet` then
65+
// `.unchecked` -- the bare `.milkdown-icon.label` matches both, so
66+
// waiting on it synchronizes on nothing): the bullet and task
67+
// conversions are async editor transactions, and delay-0 typing was
68+
// measured (repeat-each, this spec) to land keystrokes inside their
69+
// windows, partially applying a conversion. The task rule can also
70+
// miss OUTRIGHT under load, leaving literal "[ ]" text -- a real
71+
// user's recovery is erasing and retyping, so the bounded loop below
72+
// performs exactly that recovery before the hard assertion.
73+
await page.keyboard.type('- ', { delay: 40 })
74+
await expect(editable.locator('.milkdown-icon.label.bullet')).toBeVisible()
75+
const unchecked = editable.locator('.milkdown-icon.label.unchecked')
76+
for (let round = 0; round < 3; round++) {
77+
await page.keyboard.type('[ ] ', { delay: 40 })
78+
try {
79+
await unchecked.waitFor({ state: 'visible', timeout: 1_500 })
80+
break
81+
} catch {
82+
for (let i = 0; i < 4; i++) await page.keyboard.press('Backspace')
83+
}
84+
}
85+
await expect(unchecked).toBeVisible()
86+
// The icon proves the task item EXISTS; the caret can still be
87+
// mid-relocation into it (the conversion transaction moves it last)
88+
// -- typing before it lands was observed to insert into the
89+
// preceding h1 instead. Poll the live selection, the only observable
90+
// for caret position.
91+
await expect
92+
.poll(() => page.evaluate(() => {
93+
const anchor = document.getSelection()?.anchorNode
94+
const el = anchor instanceof Element ? anchor : anchor?.parentElement
95+
return el?.closest('li') != null
96+
}))
97+
.toBe(true)
7498
const checkboxIcon = editable.locator('.milkdown-icon.label').first()
75-
await expect(checkboxIcon).toBeVisible()
7699
await page.keyboard.type('first task')
77100

78101
// Live formatting WHILE editing: real elements, never raw syntax --
@@ -110,8 +133,11 @@ test('sticky notes render markdown via Milkdown; live formatting, a checkbox tog
110133
expect(rpcCalls).not.toContain('RenderNoteMarkdown')
111134

112135
// Regression: a REAL double-click enters edit -- the second press
113-
// used to beat the selection snapshot's re-render and no-op.
114-
await page.keyboard.press('Escape')
136+
// used to beat the selection snapshot's re-render and no-op. No
137+
// pane-level Escape first: with nothing selected, Escape NAVIGATES
138+
// (drills up to the root view), and at root fit this note lands
139+
// under the minimap, where every later pointer action hit-tests the
140+
// minimap instead and times out.
115141
await sticky.dblclick()
116142
await expect(stickyEditor(page)).toBeVisible()
117143
await page.keyboard.press('Escape')
@@ -164,8 +190,7 @@ test("the note overlay's editor box stays bounded as content grows, never the pa
164190
await page.getByRole('link', { name: 'Atlas' }).click()
165191
const board = page.getByTestId('atlas-board')
166192
await expect(board).toBeVisible()
167-
await page.keyboard.press('n')
168-
await clickCorner(board, 'top-left')
193+
await placeNoteClear(page, board)
169194
await expect(stickyEditor(page)).toBeVisible()
170195
await fillSticky(page, 'One line')
171196
await blurSticky(page)
@@ -220,8 +245,7 @@ test('a fenced code block renders as a plain block -- no language picker, no Cop
220245
await page.getByRole('link', { name: 'Atlas' }).click()
221246
const board = page.getByTestId('atlas-board')
222247
await expect(board).toBeVisible()
223-
await page.keyboard.press('n')
224-
await clickCorner(board, 'top-left')
248+
await placeNoteClear(page, board)
225249
await expect(stickyEditor(page)).toBeVisible()
226250

227251
const editable = stickyEditor(page).locator('[contenteditable="true"]')
@@ -264,8 +288,7 @@ test('leading whitespace never swallows a line into a code block across reload',
264288
await page.getByRole('link', { name: 'Atlas' }).click()
265289
const board = page.getByTestId('atlas-board')
266290
await expect(board).toBeVisible()
267-
await page.keyboard.press('n')
268-
await clickCorner(board, 'top-right')
291+
await placeNoteClear(page, board)
269292
await expect(stickyEditor(page)).toBeVisible()
270293

271294
const editable = stickyEditor(page).locator('[contenteditable="true"]')
@@ -312,8 +335,7 @@ test('a note holding markdown renders real elements, keeps the edit surface insi
312335
await page.getByRole('link', { name: 'Atlas' }).click()
313336
const board = page.getByTestId('atlas-board')
314337
await expect(board).toBeVisible()
315-
await page.keyboard.press('n')
316-
await clickCorner(board, 'top-right')
338+
await placeNoteClear(page, board)
317339
await expect(stickyEditor(page)).toBeVisible()
318340
const editable = stickyEditor(page).locator('[contenteditable="true"]')
319341
await editable.click()

0 commit comments

Comments
 (0)