@@ -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