Skip to content

Commit 7f6aebe

Browse files
cscheidclaude
andcommitted
test(hub-client e2e): expect breadcrumb chip to flip below at the document top (bd-pvcnea83)
The edit-chrome flip (this branch) intentionally moves the breadcrumb chip BELOW the edit surface when a block is flush against the viewport top — there is no room above. The existing breadcrumb-geometry test asserted the chip is always ABOVE the surface using a top-of-document paragraph fixture, so it became a hard CI failure (chip bottom 116.30 > textarea top 66.00) once the flip landed. - Rewrite that test to assert the flipped-below placement at the top: chip top ≥ surface bottom (not overlapping the edited text), anchored near the surface bottom, and chip top ≥ 0 (uncropped — the whole point of the flip). - Add a companion test proving the flip is CONDITIONAL: editing a later paragraph (ample headroom above) keeps the chip in its default ABOVE placement. Both verified locally against the real hub e2e (chromium): top block → chip below (textarea bottom 91.5, chip top 95.5); non-top block → chip above. Full breadcrumb-geometry spec: 8 passed, 1 skipped, 1 pre-existing-flaky (4b scroll-tracking, passed on retry — edits a mid-document block, unaffected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9daa5a9 commit 7f6aebe

1 file changed

Lines changed: 94 additions & 14 deletions

File tree

hub-client/e2e/q2-preview-breadcrumb-geometry.spec.ts

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,14 @@ test.describe('Phase 4 — Breadcrumb chip geometry (real browser)', () => {
6565
});
6666

6767
// -------------------------------------------------------------------------
68-
// Existing vertical test (P3.5 tier i item 1) — kept byte-for-byte.
69-
// Guards: chip sits above the active edit surface, never occluding line 1.
68+
// Vertical placement at the top of the document (P3.5 tier i item 1).
69+
// The first block is flush against the viewport top, so there is no room
70+
// ABOVE it for the chip — it must flip BELOW the edit surface rather than be
71+
// clipped above the scroll area (bd-pvcnea83). Guards: chip flipped below,
72+
// anchored near the surface bottom, and never clipped above the viewport.
7073
// -------------------------------------------------------------------------
7174

72-
test('chip sits above the active edit surface, never occluding line 1', async ({ page }) => {
75+
test('chip flips below the edit surface at the document top (uncropped, never occluding the edited text)', async ({ page }) => {
7376
// Enable the nesting-cursor BEFORE any navigation — addInitScript re-applies
7477
// on every document load, including the iframe.
7578
await page.addInitScript(() => {
@@ -129,30 +132,33 @@ test.describe('Phase 4 — Breadcrumb chip geometry (real browser)', () => {
129132
// TypeScript narrowing (already asserted above).
130133
if (!chipBox || !taBox) throw new Error('impossible — asserted above');
131134

135+
const surfaceBottom = taBox.y + taBox.height;
132136
console.log(
133137
`Chip box: y=${chipBox.y.toFixed(2)}, bottom=${(chipBox.y + chipBox.height).toFixed(2)}, height=${chipBox.height.toFixed(2)}\n` +
134-
`Textarea box: y=${taBox.y.toFixed(2)}`,
138+
`Textarea box: y=${taBox.y.toFixed(2)}, bottom=${surfaceBottom.toFixed(2)}`,
135139
);
136140

137-
// Step 5: assertions.
141+
// Step 5: assertions (bd-pvcnea83 — flipped-below placement at the top).
138142
const TOL = 2;
139143

140-
// (a) Chip bottom is AT OR ABOVE surface top — never occludes line 1.
144+
// (a) Chip top is AT OR BELOW the surface bottom — the chip flipped below the
145+
// edit box (no room above at the document top), so it never overlaps the
146+
// edited text.
141147
expect(
142-
chipBox.y + chipBox.height,
143-
`chip bottom (${(chipBox.y + chipBox.height).toFixed(2)}) must be ≤ textarea top (${taBox.y.toFixed(2)}) + ${TOL}px tolerance`,
144-
).toBeLessThanOrEqual(taBox.y + TOL);
148+
chipBox.y,
149+
`chip top (${chipBox.y.toFixed(2)}) must be ≥ surface bottom (${surfaceBottom.toFixed(2)}) ${TOL}px — flipped below, not overlapping the surface`,
150+
).toBeGreaterThanOrEqual(surfaceBottom - TOL);
145151

146-
// (b) Chip bottom is anchored close to the surface top (not floating far above).
152+
// (b) Chip is anchored close to the surface bottom (not floating far below).
147153
// The gap must be less than ~one chip-gap (12 px) so the chip is still
148154
// visually attached and the useLayoutEffect positioning is doing real work.
149155
expect(
150-
taBox.y - (chipBox.y + chipBox.height),
151-
`chip must be anchored near the surface top — gap (${(taBox.y - (chipBox.y + chipBox.height)).toFixed(2)}px) must be < 12px`,
156+
chipBox.y - surfaceBottom,
157+
`chip must be anchored near the surface bottom — gap (${(chipBox.y - surfaceBottom).toFixed(2)}px) must be < 12px`,
152158
).toBeLessThan(12);
153159

154-
// (c) At the document top, the chip sits in the page margin (not clipped above
155-
// the viewport — `top` is clamped to ≥ 0 in real CSS).
160+
// (c) The chip is not clipped above the viewport top (the whole point of the
161+
// flip — `top` is ≥ 0 in real CSS).
156162
expect(
157163
chipBox.y,
158164
`chip top (${chipBox.y.toFixed(2)}) must be ≥ 0 — not clipped above the viewport`,
@@ -162,6 +168,80 @@ test.describe('Phase 4 — Breadcrumb chip geometry (real browser)', () => {
162168
await iframe.locator('textarea').first().press('Escape');
163169
});
164170

171+
// -------------------------------------------------------------------------
172+
// Companion to the above: the flip is CONDITIONAL. When a block has room
173+
// above it (not at the document top), the chip stays in its default ABOVE
174+
// placement — proving bd-pvcnea83 only flips when there isn't room, rather
175+
// than always rendering below.
176+
// -------------------------------------------------------------------------
177+
178+
test('chip stays above the edit surface when there is room above (non-top block)', async ({ page }) => {
179+
await page.addInitScript(() => {
180+
localStorage.setItem('quarto-hub:preferences', JSON.stringify({
181+
version: 1,
182+
scrollSyncEnabled: true,
183+
errorOverlayCollapsed: true,
184+
colorScheme: 'auto',
185+
unlockNestingCursor: true,
186+
}));
187+
});
188+
189+
const serverUrl = getServerUrl();
190+
// A title plus several paragraphs gives a later paragraph ample headroom
191+
// above it, so the chip is NOT clipped and keeps its default above placement.
192+
const QMD = [
193+
'---',
194+
'title: With a title',
195+
'format: q2-preview',
196+
'---',
197+
'',
198+
'Para one.',
199+
'',
200+
'Para two.',
201+
'',
202+
'Para three.',
203+
'',
204+
'Para four.',
205+
'',
206+
].join('\n');
207+
208+
const docId = await createProjectOnServer(serverUrl, [
209+
{ path: '_quarto.yml', content: 'project:\n type: default\n', contentType: 'text' },
210+
{ path: 'breadcrumb-above.qmd', content: QMD, contentType: 'text' },
211+
]);
212+
213+
const iframe = await openFile(page, serverUrl, docId, 'breadcrumb-above.qmd');
214+
215+
// Edit a LATER paragraph (index 2 → "Para three.") — well below the top.
216+
await iframe.locator('p[data-block-pool-id]').nth(2).click();
217+
await iframe.locator('textarea').first().waitFor({ timeout: 10_000 });
218+
const chip = iframe.locator('[data-testid="q2-breadcrumb-chip"]');
219+
await chip.waitFor({ timeout: 5000 });
220+
221+
const chipBox = await chip.boundingBox();
222+
const taBox = await iframe.locator('textarea').first().boundingBox();
223+
if (!chipBox || !taBox) throw new Error('chip/textarea bounding box must be non-null');
224+
225+
console.log(
226+
`Chip box: y=${chipBox.y.toFixed(2)}, bottom=${(chipBox.y + chipBox.height).toFixed(2)}\n` +
227+
`Textarea box: y=${taBox.y.toFixed(2)}`,
228+
);
229+
230+
const TOL = 2;
231+
// Default ABOVE placement: chip bottom sits at or above the surface top.
232+
expect(
233+
chipBox.y + chipBox.height,
234+
`chip bottom (${(chipBox.y + chipBox.height).toFixed(2)}) must be ≤ surface top (${taBox.y.toFixed(2)}) + ${TOL}px — default above placement`,
235+
).toBeLessThanOrEqual(taBox.y + TOL);
236+
// Anchored near the surface top.
237+
expect(
238+
taBox.y - (chipBox.y + chipBox.height),
239+
`chip must be anchored near the surface top — gap (${(taBox.y - (chipBox.y + chipBox.height)).toFixed(2)}px) must be < 12px`,
240+
).toBeLessThan(12);
241+
242+
await iframe.locator('textarea').first().press('Escape');
243+
});
244+
165245
// -------------------------------------------------------------------------
166246
// 4a — DESIGN GO/NO-GO GATE (not a regression guard).
167247
//

0 commit comments

Comments
 (0)