@@ -311,8 +311,11 @@ test('TRANSCRIPT: prose has its own type, and chrome does not follow it', () =>
311311 const scoped = / \. m s g \. b o d y \{ [ ^ } ] * f o n t - s i z e : \s * v a r \( - - p r o s e - s i z e \) / . test ( css ) ;
312312 const leaked = / \. m s g \{ [ ^ } ] * f o n t - s i z e : \s * v a r \( - - p r o s e - s i z e \) / . test ( css ) ;
313313 assert . ok ( scoped && ! leaked , 'the prose size leaked onto .msg — chrome inside a turn would scale with it' ) ;
314- assert . match ( css , / \. m s g \. r o l e \{ [ ^ } ] * f o n t - s i z e : \s * 1 1 p x / ,
315- 'the turn label must keep an absolute size, or it grows with the prose it is labelling' ) ;
314+ // The turn label used to be this test's example of in-turn chrome holding an absolute size. T4 took
315+ // it out of the visual layer entirely (see below), so the checkpoint control carries the guard now:
316+ // it still renders inside a .msg and still must not grow with the prose beside it.
317+ assert . match ( css , / \. c k r e s t o r e \{ [ ^ } ] * f o n t - s i z e : \s * 1 1 p x / ,
318+ 'in-turn chrome must keep an absolute size, or it grows with the prose it sits next to' ) ;
316319} ) ;
317320
318321test ( 'TRANSCRIPT: the prose size TRACKS the workbench rather than pinning against it' , ( ) => {
@@ -384,6 +387,56 @@ test('TRANSCRIPT: settings are clamped at the host boundary, to the schema\'s ow
384387 }
385388} ) ;
386389
390+ test ( 'TRANSCRIPT: the speaker label leaves the screen but NOT the accessibility tree' , ( ) => {
391+ // docs/CHAT-TYPOGRAPHY.md D6/T4. Speakers are told apart by treatment — a tinted bubble for you,
392+ // unadorned prose for the assistant — so a label restating it above every message was chrome.
393+ //
394+ // The trap this guards is the obvious "simplification". `display: none` and `visibility: hidden`
395+ // both look like tidier ways to hide a label, and both remove it from the accessibility tree. The
396+ // bubble is a purely VISUAL cue, so that would leave a screen reader with an unattributed wall of
397+ // text and nothing anywhere in the document naming who is speaking — a worse transcript than the
398+ // one we started with, and invisible to whoever makes the change.
399+ const rule = / \. m s g \. r o l e \{ ( [ ^ } ] * ) \} / . exec ( css ) ;
400+ assert . ok ( rule , '.msg .role no longer has a rule' ) ;
401+ const body = rule [ 1 ] ;
402+
403+ assert . ok ( ! / d i s p l a y \s * : \s * n o n e / . test ( body ) ,
404+ 'display:none removes the label from the a11y tree — clip it instead (see the comment on the rule)' ) ;
405+ assert . ok ( ! / v i s i b i l i t y \s * : \s * h i d d e n / . test ( body ) ,
406+ 'visibility:hidden removes the label from the a11y tree — clip it instead' ) ;
407+ assert . match ( body , / c l i p - p a t h : \s * i n s e t \( 5 0 % \) / , 'the label must be clipped out of the visual layer' ) ;
408+ assert . match ( body , / p o s i t i o n : \s * a b s o l u t e / , 'a clipped label must be taken out of flow, or it still reserves a line' ) ;
409+ assert . match ( body , / h e i g h t : \s * 1 p x / , 'the clipped box must not reserve height' ) ;
410+
411+ // And it must still BE there to hide: both speakers labelled on a turn start, neither on a
412+ // continuation (which is the same voice carrying on, and was never labelled).
413+ const at = html . indexOf ( 'function add(role, html)' ) ;
414+ assert . ok ( at !== - 1 , 'function add(role, html) is gone — cannot verify label emission' ) ;
415+ const add = html . slice ( at , at + 700 ) ;
416+ assert . match ( add , / r o l e = = = ' u s e r ' \? ' Y o u ' : ' L e v e l C o d e A I ' / , 'the label text is no longer emitted at all' ) ;
417+ 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' ) ;
418+ } ) ;
419+
420+ test ( 'TRANSCRIPT: dropping the label does not collapse the gap between speakers' , ( ) => {
421+ // The label was doing spacing work nobody had accounted for: ~19px above every turn. Remove it and
422+ // a new turn is separated from a continuation by 12px versus 7px — not a difference you can see, so
423+ // the transcript reads as one undifferentiated column. That is the failure mode of T4 done naively,
424+ // and it would look like "the spacing feels off" rather than like a missing rule.
425+ assert . match ( css , / # l o g > \. m s g : n o t \( \. c o n t \) \{ [ ^ } ] * m a r g i n - t o p : \s * [ \d . ] + e m / ,
426+ 'a turn start must buy back part of the height the label used to occupy, in em so T2 scales it' ) ;
427+ assert . match ( css , / # l o g > \. m s g : f i r s t - c h i l d \{ [ ^ } ] * m a r g i n - t o p : \s * 0 / ,
428+ 'the first turn must not open the transcript with a stray gap' ) ;
429+ assert . match ( css , / \. m s g \. c o n t \{ [ ^ } ] * m a r g i n - t o p : \s * - \d + p x / ,
430+ 'continuations must stay pulled tight, or there is no hierarchy left to see' ) ;
431+
432+ // The whole design rests on the user bubble now: it is the ONLY remaining visual speaker cue.
433+ // Flatten it and the transcript loses the distinction entirely, with no label left to fall back on.
434+ assert . match ( css , / \. m s g \. u s e r \. b o d y \{ [ ^ } ] * b a c k g r o u n d : \s * v a r \( - - f i e l d - b g \) / ,
435+ 'the user bubble is the last visual speaker cue — D6 keeps it deliberately' ) ;
436+ assert . match ( css , / \. m s g \. u s e r \. b o d y \{ [ ^ } ] * b o r d e r : \s * 1 p x s o l i d / ,
437+ 'the bubble needs its border: --field-bg alone is near-invisible in some themes' ) ;
438+ } ) ;
439+
387440test ( 'TRANSCRIPT: the heading scale has steps you can actually see' , ( ) => {
388441 // The old 1.3/1.18/1.07 put 0.11em between h2 and h3 — 1.4px at 13px, i.e. three levels of
389442 // hierarchy that were indistinguishable without selecting the text.
0 commit comments