@@ -433,7 +433,7 @@ test.describe('Phase 4 — Breadcrumb chip geometry (real browser)', () => {
433433 // rightmost crumb right no longer meets textarea position → RED.
434434 // -------------------------------------------------------------------------
435435
436- test ( '4c — gutter-fill: crumb row spans from column margin to surface left for indented block (guards 3b )' , async ( { page } ) => {
436+ test ( '4c — shallow indent: ◀ clamps at the page edge and the crumb band spills left into the margin (G1 left-spill )' , async ( { page } ) => {
437437 await page . addInitScript ( ( ) => {
438438 localStorage . setItem ( 'quarto-hub:preferences' , JSON . stringify ( {
439439 version : 1 ,
@@ -503,6 +503,7 @@ test.describe('Phase 4 — Breadcrumb chip geometry (real browser)', () => {
503503 const outRect = outArrow . getBoundingClientRect ( ) ;
504504 const crumbRects = crumbs . map ( ( el ) => el . getBoundingClientRect ( ) ) ;
505505 return {
506+ outArrowLeft : outRect . left ,
506507 outArrowRight : outRect . right ,
507508 crumbLeft : Math . min ( ...crumbRects . map ( ( r ) => r . left ) ) ,
508509 crumbRight : Math . max ( ...crumbRects . map ( ( r ) => r . right ) ) ,
@@ -514,81 +515,79 @@ test.describe('Phase 4 — Breadcrumb chip geometry (real browser)', () => {
514515
515516 console . log (
516517 `4c: colLeft=${ colLeft . toFixed ( 2 ) } , surfaceLeft=${ surfaceLeft . toFixed ( 2 ) } , ` +
517- `outArrowRight=${ chipGeom . outArrowRight . toFixed ( 2 ) } , ` +
518+ `outArrowLeft= ${ chipGeom . outArrowLeft . toFixed ( 2 ) } , outArrowRight=${ chipGeom . outArrowRight . toFixed ( 2 ) } , ` +
518519 `crumbLeft=${ chipGeom . crumbLeft . toFixed ( 2 ) } , crumbRight=${ chipGeom . crumbRight . toFixed ( 2 ) } ` ,
519520 ) ;
520521
521- // (a) ◀ right edge ≈ colLeft (±3px): ◀ sits in the outer margin, flush
522- // at the text-column left. Guards the margin-anchor positioning logic.
523- const TOL_COL = 3 ;
524- expect (
525- chipGeom . outArrowRight ,
526- `◀ right (${ chipGeom . outArrowRight . toFixed ( 2 ) } ) must be ≈ colLeft (${ colLeft . toFixed ( 2 ) } ) ±${ TOL_COL } px. ` +
527- `Guards ◀ anchored flush at the text-column margin.` ,
528- ) . toBeGreaterThanOrEqual ( colLeft - TOL_COL ) ;
522+ // G1 left-spill model (NOT the pre-G1 "3b" ◀-at-colLeft model — that was
523+ // superseded in the same commit and never browser-validated). A loose
524+ // list item's indent gutter (here ~34px) is narrower than the comfortable
525+ // 2-crumb band (2·CRUMB_W = 44px), so computeChipGeometry pushes the chip
526+ // LEFT until ◀ clamps at the page edge (chipLeft = 0). See the unit suite
527+ // BreadcrumbChip.geometry.test.ts cases (b)/(c) for the blessed contract.
528+ const TOL = 3 ;
529+
530+ // (a) ◀ is clamped at the page edge (chipLeft ≈ 0), NOT anchored at colLeft.
531+ // This is THE binding assertion for the left-spill clamp: reverting to
532+ // the ◀-at-colLeft anchor moves outArrowLeft to ≈ colLeft − MIN_GLYPH_W
533+ // (~9.5px), failing this.
529534 expect (
530- chipGeom . outArrowRight ,
531- `◀ right (${ chipGeom . outArrowRight . toFixed ( 2 ) } ) must be ≈ colLeft (${ colLeft . toFixed ( 2 ) } ) ±${ TOL_COL } px. ` +
532- `Guards ◀ anchored flush at the text-column margin.` ,
533- ) . toBeLessThanOrEqual ( colLeft + TOL_COL ) ;
535+ chipGeom . outArrowLeft ,
536+ `◀ left (${ chipGeom . outArrowLeft . toFixed ( 2 ) } ) must be ≈ 0 (clamped at the page edge). ` +
537+ `Guards the G1 left-spill clamp for a shallow indent gutter.` ,
538+ ) . toBeLessThanOrEqual ( TOL ) ;
539+ expect ( chipGeom . outArrowLeft ) . toBeGreaterThanOrEqual ( - TOL ) ;
534540
535- // (b) leftmost .q2-crumb left ≈ colLeft (±3px): crumbs never enter the
536- // outer margin. Guards the crumb band starting at colLeft .
541+ // (b) the crumbs spill LEFT into the outer margin (crumbLeft < colLeft) and
542+ // start right at ◀'s right edge — the band is pushed left of the column .
537543 expect (
538544 chipGeom . crumbLeft ,
539- `leftmost crumb left (${ chipGeom . crumbLeft . toFixed ( 2 ) } ) must be ≈ colLeft (${ colLeft . toFixed ( 2 ) } ) ± ${ TOL_COL } px. ` +
540- `Guards crumbs staying out of the outer margin.` ,
541- ) . toBeGreaterThanOrEqual ( colLeft - TOL_COL ) ;
545+ `leftmost crumb left (${ chipGeom . crumbLeft . toFixed ( 2 ) } ) must be < colLeft (${ colLeft . toFixed ( 2 ) } ): ` +
546+ `the shallow-gutter band spills into the outer margin (G1) .` ,
547+ ) . toBeLessThan ( colLeft - TOL ) ;
542548 expect (
543549 chipGeom . crumbLeft ,
544- `leftmost crumb left (${ chipGeom . crumbLeft . toFixed ( 2 ) } ) must be ≈ colLeft (${ colLeft . toFixed ( 2 ) } ) ±${ TOL_COL } px. ` +
545- `Guards crumbs staying out of the outer margin.` ,
546- ) . toBeLessThanOrEqual ( colLeft + TOL_COL ) ;
547-
548- // (c) rightmost .q2-crumb right ≈ surfaceLeft (±10px): the crumb band meets
549- // the textarea anchor for indented blocks. Generous tolerance —
550- // "close in common cases", not pixel-perfect.
551- // Guards the textarea-anchor: reverting to the region-wrapper anchor →
552- // surfaceLeft = colLeft (no indent) → crumb right no longer meets textarea.
550+ `leftmost crumb left (${ chipGeom . crumbLeft . toFixed ( 2 ) } ) must meet ◀ right (${ chipGeom . outArrowRight . toFixed ( 2 ) } ).` ,
551+ ) . toBeGreaterThanOrEqual ( chipGeom . outArrowRight - TOL ) ;
552+
553+ // (c) the band's right edge still reaches the pivot (surfaceLeft) for this
554+ // shallow-but-nonzero gutter — the band width equals the comfortable
555+ // natural width, which here ≈ the gutter, so it lands at the surface.
553556 const TOL_SURF = 10 ;
554557 expect (
555558 chipGeom . crumbRight ,
556559 `rightmost crumb right (${ chipGeom . crumbRight . toFixed ( 2 ) } ) must be ≈ surfaceLeft (${ surfaceLeft . toFixed ( 2 ) } ) ±${ TOL_SURF } px. ` +
557- `Guards textarea-anchor + gutter band meeting the indented surface.` ,
560+ `Guards the band meeting the indented surface (textarea anchor) .` ,
558561 ) . toBeGreaterThanOrEqual ( surfaceLeft - TOL_SURF ) ;
559- expect (
560- chipGeom . crumbRight ,
561- `rightmost crumb right (${ chipGeom . crumbRight . toFixed ( 2 ) } ) must be ≈ surfaceLeft (${ surfaceLeft . toFixed ( 2 ) } ) ±${ TOL_SURF } px. ` +
562- `Guards textarea-anchor + gutter band meeting the indented surface.` ,
563- ) . toBeLessThanOrEqual ( surfaceLeft + TOL_SURF ) ;
562+ expect ( chipGeom . crumbRight ) . toBeLessThanOrEqual ( surfaceLeft + TOL_SURF ) ;
564563
565564 await iframe . locator ( 'textarea' ) . first ( ) . press ( 'Escape' ) ;
566565 } ) ;
567566
568567 // -------------------------------------------------------------------------
569- // 4d — Zero-indent Div: ◀ anchored at colLeft, crumbs stay out of margin.
570- //
571- // Fixture: a fenced-Div-wrapped paragraph (zero indent).
572- // Non-indenting containers (Div) get 0-width indent gutter → the crumb
573- // band overshoots right into the content (never into the margin).
568+ // 4d — Zero-indent Div under the G1 left-spill model.
574569 //
575- // New design contract (all coords iframe-relative via .evaluate()):
576- // - ◀ right edge ≈ colLeft (±3px): ◀ sits in the outer margin, flush at
577- // the text-column left.
578- // - leftmost .q2-crumb left ≥ colLeft − 3: crumbs never enter the outer
579- // margin even for zero-indent blocks.
570+ // Fixture: a fenced-Div-wrapped paragraph (zero indent gutter).
580571 //
581- // For zero-indent blocks the crumb band's right edge OVERSHOOTS into the
582- // content (rightmost crumb right > surfaceLeft) — that is correct and
583- // intentional; do NOT assert meets-surface here.
572+ // G1 design contract (all coords iframe-relative via .evaluate()):
573+ // - ◀ clamps at the page edge: outArrowLeft ≈ 0 (chipLeft = 0). The chip
574+ // cannot fit its comfortable band in a zero gutter, so it is pushed all
575+ // the way left until ◀ hits x=0.
576+ // - crumbs spill LEFT into the outer margin: crumbLeft < colLeft. (A zero-
577+ // indent block's crumbs DO enter the margin — the "where you came from"
578+ // direction; see BreadcrumbChip's layout-model doc.)
579+ // - the band overshoots RIGHT past the pivot into content: crumbRight >
580+ // surfaceLeft (geometry unit-test case (c)).
584581 //
585- // Confirmed sample numbers (run 2026-06-16 ):
586- // colLeft=25.5, ◀=[9.5,25.5 ], crumbs=[25.5,41.5 ], surfaceLeft≈25.5 (no indent).
582+ // Real browser sample (run 2026-06-19 ):
583+ // colLeft=25.5, ◀=[0,16 ], crumbs=[16,60 ], surfaceLeft≈25.5 (no indent).
587584 //
588- // Guards: the ◀-at-colLeft anchor and the crumbs-stay-out-of-margin invariant.
585+ // This REPLACES the pre-G1 "3b" expectations (◀ at colLeft, crumbs out of the
586+ // margin) that the old assertions encoded — superseded in the G1 commit and
587+ // never browser-validated before now.
589588 // -------------------------------------------------------------------------
590589
591- test ( '4d — zero-indent Div: ◀ anchored at colLeft , crumbs stay out of margin (guards ◀-anchor + no-margin-spill for crumbs )' , async ( { page } ) => {
590+ test ( '4d — zero-indent Div: ◀ clamps at the page edge , crumbs spill left into margin and band overshoots right (G1 left-spill )' , async ( { page } ) => {
592591 await page . addInitScript ( ( ) => {
593592 localStorage . setItem ( 'quarto-hub:preferences' , JSON . stringify ( {
594593 version : 1 ,
@@ -643,6 +642,7 @@ test.describe('Phase 4 — Breadcrumb chip geometry (real browser)', () => {
643642 const outRect = outArrow . getBoundingClientRect ( ) ;
644643 const crumbRects = crumbs . map ( ( el ) => el . getBoundingClientRect ( ) ) ;
645644 return {
645+ outArrowLeft : outRect . left ,
646646 outArrowRight : outRect . right ,
647647 crumbLeft : Math . min ( ...crumbRects . map ( ( r ) => r . left ) ) ,
648648 crumbRight : Math . max ( ...crumbRects . map ( ( r ) => r . right ) ) ,
@@ -652,41 +652,52 @@ test.describe('Phase 4 — Breadcrumb chip geometry (real browser)', () => {
652652 expect ( chipGeom , 'chip geometry must be non-null' ) . not . toBeNull ( ) ;
653653 if ( ! chipGeom ) throw new Error ( 'impossible — asserted above' ) ;
654654
655- // surfaceLeft for logging only (zero-indent → ≈ colLeft).
655+ // surfaceLeft (zero-indent Div → ≈ colLeft; the band overshoots right of it ).
656656 const surfaceLeft = await iframe . locator ( 'textarea' ) . first ( ) . evaluate ( ( el ) => {
657657 return el . getBoundingClientRect ( ) . left ;
658658 } ) ;
659659
660660 console . log (
661661 `4d: colLeft=${ colLeft . toFixed ( 2 ) } , surfaceLeft=${ surfaceLeft . toFixed ( 2 ) } , ` +
662- `outArrowRight=${ chipGeom . outArrowRight . toFixed ( 2 ) } , ` +
662+ `outArrowLeft= ${ chipGeom . outArrowLeft . toFixed ( 2 ) } , outArrowRight=${ chipGeom . outArrowRight . toFixed ( 2 ) } , ` +
663663 `crumbLeft=${ chipGeom . crumbLeft . toFixed ( 2 ) } , crumbRight=${ chipGeom . crumbRight . toFixed ( 2 ) } ` ,
664664 ) ;
665665
666- // (i) ◀ right edge ≈ colLeft (±3px): ◀ sits in the outer margin, flush
667- // at the text-column left. Guards the ◀-at-colLeft anchor logic.
668- const TOL_COL = 3 ;
666+ // G1 left-spill model for a ZERO-indent Div (gutter = 0). The comfortable
667+ // band (2·CRUMB_W) cannot fit a zero gutter, so the chip is pushed left
668+ // until ◀ clamps at the page edge (chipLeft = 0) and the band spills RIGHT
669+ // past the pivot into the content (BreadcrumbChip.geometry.test.ts case (c)).
670+ // NB: this REPLACES the pre-G1 "3b" expectations (◀ at colLeft, crumbs
671+ // out of the margin) — those were superseded in the same commit and never
672+ // browser-validated. Under G1, a zero-indent block's crumbs DO enter the
673+ // margin (the "where you came from" direction).
674+ const TOL = 3 ;
675+
676+ // (i) ◀ is clamped at the page edge (chipLeft ≈ 0), NOT anchored at colLeft.
677+ // Binding assertion for the clamp: the ◀-at-colLeft revert moves
678+ // outArrowLeft to ≈ colLeft − MIN_GLYPH_W (~9.5px), failing this.
669679 expect (
670- chipGeom . outArrowRight ,
671- `◀ right (${ chipGeom . outArrowRight . toFixed ( 2 ) } ) must be ≈ colLeft (${ colLeft . toFixed ( 2 ) } ) ±${ TOL_COL } px. ` +
672- `Guards ◀ anchored flush at the text-column margin.` ,
673- ) . toBeGreaterThanOrEqual ( colLeft - TOL_COL ) ;
674- expect (
675- chipGeom . outArrowRight ,
676- `◀ right (${ chipGeom . outArrowRight . toFixed ( 2 ) } ) must be ≈ colLeft (${ colLeft . toFixed ( 2 ) } ) ±${ TOL_COL } px. ` +
677- `Guards ◀ anchored flush at the text-column margin.` ,
678- ) . toBeLessThanOrEqual ( colLeft + TOL_COL ) ;
679-
680- // (ii) leftmost .q2-crumb left ≥ colLeft − 3: crumbs do NOT enter the
681- // outer margin even for zero-indent blocks. The gutter is zero so the
682- // band overshoots right (into content), never left.
683- // Guards: crumbs-stay-out-of-margin invariant.
680+ chipGeom . outArrowLeft ,
681+ `◀ left (${ chipGeom . outArrowLeft . toFixed ( 2 ) } ) must be ≈ 0 (clamped at the page edge) for a zero-indent block.` ,
682+ ) . toBeLessThanOrEqual ( TOL ) ;
683+ expect ( chipGeom . outArrowLeft ) . toBeGreaterThanOrEqual ( - TOL ) ;
684+
685+ // (ii) the crumbs spill LEFT into the outer margin (crumbLeft < colLeft) —
686+ // the zero gutter forces the band left of the column.
684687 expect (
685688 chipGeom . crumbLeft ,
686- `leftmost crumb left (${ chipGeom . crumbLeft . toFixed ( 2 ) } ) must be ≥ colLeft (${ colLeft . toFixed ( 2 ) } ) − 3. ` +
687- `Crumbs must never enter the outer page margin. ` +
688- `For zero-indent blocks the gutter overshoots right (into content), not left.` ,
689- ) . toBeGreaterThanOrEqual ( colLeft - 3 ) ;
689+ `leftmost crumb left (${ chipGeom . crumbLeft . toFixed ( 2 ) } ) must be < colLeft (${ colLeft . toFixed ( 2 ) } ): ` +
690+ `a zero-indent block's band spills into the outer margin (G1).` ,
691+ ) . toBeLessThan ( colLeft - TOL ) ;
692+
693+ // (iii) the band overshoots RIGHT past the pivot into the content
694+ // (crumbRight > surfaceLeft) — the comfortable width has nowhere to go
695+ // but right once ◀ is pinned at x=0.
696+ expect (
697+ chipGeom . crumbRight ,
698+ `rightmost crumb right (${ chipGeom . crumbRight . toFixed ( 2 ) } ) must overshoot surfaceLeft (${ surfaceLeft . toFixed ( 2 ) } ): ` +
699+ `the comfortable band spills right past the pivot for a zero gutter (G1 case c).` ,
700+ ) . toBeGreaterThan ( surfaceLeft + TOL ) ;
690701
691702 await iframe . locator ( 'textarea' ) . first ( ) . press ( 'Escape' ) ;
692703 } ) ;
0 commit comments