|
| 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 | +}) |
0 commit comments