@@ -516,6 +516,42 @@ test('TRANSCRIPT: the speaker label leaves the screen but NOT the accessibility
516516 assert . match ( add , / c o n t \? ' ' : ' < d i v c l a s s = " r o l e " > / , 'a continuation must still omit the label element entirely' ) ;
517517} ) ;
518518
519+ test ( 'SHELL: the transcript is its own containing block — nothing in it is laid out against the page' , ( ) => {
520+ // The page is a 100vh flex column and #log is the only thing in it that scrolls: the composer and
521+ // the status row sit under it and never move. A box taken out of flow (position: absolute) is laid
522+ // out against its nearest POSITIONED ancestor — and with none, against the page, at its unscrolled
523+ // place in the transcript, where neither #log's scrolling nor its clipping reaches it.
524+ //
525+ // The speaker label is such a box; the test above requires it to be. With nothing positioned above
526+ // it, the label of every message that started more than a viewport down the transcript lay below
527+ // the fold. The page grew to reach that one pixel, and once the log was at its end the wheel
528+ // scrolled the PAGE: transcript, composer and status row riding up together over blank space, on
529+ // every conversation past its first long turn. Measured in Chromium against the shipped file, two
530+ // turns in a 1118px viewport: the page was 1963px tall, the labels' offsetParent was <body>, and
531+ // four wheel ticks moved the composer up by 400px. Positioned: 1118px, #log, 0px.
532+ //
533+ // No DOM test can see any of that — the fake DOM lays nothing out — so the rule is pinned here.
534+ const logRules = [ ...cssBlocks . matchAll ( / (?: ^ | [ } \n ] ) \s * # l o g \s * \{ ( [ ^ } ] * ) \} / g) ] . map ( ( m ) => m [ 1 ] ) ;
535+ assert . ok ( logRules . length , '#log no longer has a rule of its own' ) ;
536+
537+ // The same selector each time, so the last declaration is the one in force.
538+ const declared = logRules . map ( ( b ) => ( / (?: ^ | [ ; \s ] ) p o s i t i o n \s * : \s * ( [ a - z - ] + ) / . exec ( b ) || [ ] ) [ 1 ] ) . filter ( Boolean ) ;
539+ const position = declared [ declared . length - 1 ] ;
540+ assert . ok ( position && position !== 'static' ,
541+ '#log must be positioned (position: relative). It is the containing block for the clipped speaker\n'
542+ + 'label; without it every label is laid out against the page, the page grows past the viewport, and\n'
543+ + 'the whole chat — composer included — scrolls away. Got: ' + ( position || 'no position at all' ) ) ;
544+
545+ // The premise. If the shell is ever rebuilt, this is the test to rethink rather than delete.
546+ const shell = blockAt ( cssBlocks , cssBlocks . search ( / (?: ^ | \n ) \s * b o d y \s * \{ / ) ) ;
547+ assert . match ( shell , / (?: ^ | [ ; \s ] ) h e i g h t : \s * 1 0 0 v h / , 'premise: the page is exactly one viewport tall (height, not min-height)' ) ;
548+ assert . ok ( logRules . some ( ( b ) => / o v e r f l o w - y \s * : \s * a u t o / . test ( b ) ) , 'premise: #log is the element that scrolls' ) ;
549+
550+ // And the label is still what needs it: out of flow, with no offsets of its own to pin it anywhere.
551+ const label = / \. m s g \. r o l e \{ ( [ ^ } ] * ) \} / . exec ( cssBlocks ) ;
552+ assert . ok ( label && / p o s i t i o n : \s * a b s o l u t e / . test ( label [ 1 ] ) , 'the label is no longer out of flow — see the test above' ) ;
553+ } ) ;
554+
519555test ( 'TRANSCRIPT: dropping the label does not collapse the gap between speakers' , ( ) => {
520556 // The label was doing spacing work nobody had accounted for: ~19px above every turn. Remove it and
521557 // a new turn is separated from a continuation by 12px versus 7px — not a difference you can see, so
0 commit comments