@@ -248,3 +248,156 @@ describe("Output persistence", () => {
248248 expect ( setItemSpy ) . not . toHaveBeenCalled ( ) ;
249249 } ) ;
250250} ) ;
251+
252+ describe ( "Output accessibility-tree exposure cap" , ( ) => {
253+ const makeLines = ( count : number ) : OutputLine [ ] =>
254+ Array . from ( { length : count } , ( _ , i ) => ( {
255+ content : < div > { `line ${ i } ` } </ div > ,
256+ id : i ,
257+ sourceContent : `line ${ i } ` ,
258+ sourceType : "test" ,
259+ type : OutputType . ServerMessage ,
260+ } ) ) ;
261+
262+ // Instantiate an Output with history and a real frozen container so
263+ // freezeOverflow/trimFrozen operate on actual DOM.
264+ const makeOutput = ( lines : OutputLine [ ] ) : Output => {
265+ const output = new Output ( { client : { } as MudClient } ) ;
266+ Object . defineProperty ( output , "allLines" , { value : lines , writable : true } ) ;
267+ const frozenDiv = document . createElement ( "div" ) ;
268+ Object . defineProperty ( output , "frozenRef" , { value : { current : frozenDiv } } ) ;
269+ return output ;
270+ } ;
271+
272+ const frozen = ( output : Output ) : HTMLDivElement =>
273+ ( output as unknown as { frozenRef : { current : HTMLDivElement } } ) . frozenRef . current ;
274+
275+ const freeze = ( output : Output ) =>
276+ ( output as unknown as { freezeOverflow : ( ) => void } ) . freezeOverflow ( ) ;
277+
278+ const hiddenCount = ( output : Output ) : number =>
279+ frozen ( output ) . querySelectorAll ( '[aria-hidden="true"]' ) . length ;
280+
281+ it ( "hides frozen lines beyond the exposure cap, oldest first" , ( ) => {
282+ const total = Output . LIVE_WINDOW_SIZE + Output . A11Y_EXPOSED_FROZEN_LINES + 50 ;
283+ const output = makeOutput ( makeLines ( total ) ) ;
284+
285+ freeze ( output ) ;
286+
287+ const frozenDiv = frozen ( output ) ;
288+ expect ( frozenDiv . children . length ) . toBe ( total - Output . LIVE_WINDOW_SIZE ) ;
289+ expect ( hiddenCount ( output ) ) . toBe ( 50 ) ;
290+ // The oldest lines are hidden; the most recent frozen lines are exposed.
291+ expect ( frozenDiv . children [ 0 ] . getAttribute ( "aria-hidden" ) ) . toBe ( "true" ) ;
292+ expect ( frozenDiv . children [ 49 ] . getAttribute ( "aria-hidden" ) ) . toBe ( "true" ) ;
293+ expect ( frozenDiv . children [ 50 ] . hasAttribute ( "aria-hidden" ) ) . toBe ( false ) ;
294+ expect (
295+ frozenDiv . children [ frozenDiv . children . length - 1 ] . hasAttribute ( "aria-hidden" )
296+ ) . toBe ( false ) ;
297+ } ) ;
298+
299+ it ( "exposes everything while under the cap" , ( ) => {
300+ const output = makeOutput (
301+ makeLines ( Output . LIVE_WINDOW_SIZE + Output . A11Y_EXPOSED_FROZEN_LINES )
302+ ) ;
303+
304+ freeze ( output ) ;
305+
306+ expect ( frozen ( output ) . children . length ) . toBe ( Output . A11Y_EXPOSED_FROZEN_LINES ) ;
307+ expect ( hiddenCount ( output ) ) . toBe ( 0 ) ;
308+ } ) ;
309+
310+ it ( "hides incrementally as more lines freeze" , ( ) => {
311+ const start = Output . LIVE_WINDOW_SIZE + Output . A11Y_EXPOSED_FROZEN_LINES ;
312+ const output = makeOutput ( makeLines ( start ) ) ;
313+ freeze ( output ) ;
314+ expect ( hiddenCount ( output ) ) . toBe ( 0 ) ;
315+
316+ ( output as unknown as { allLines : OutputLine [ ] } ) . allLines = makeLines ( start + 10 ) ;
317+ freeze ( output ) ;
318+
319+ expect ( hiddenCount ( output ) ) . toBe ( 10 ) ;
320+ } ) ;
321+
322+ it ( "keeps the hidden count consistent across trims from the front" , ( ) => {
323+ const total = Output . LIVE_WINDOW_SIZE + Output . A11Y_EXPOSED_FROZEN_LINES + 30 ;
324+ const output = makeOutput ( makeLines ( total ) ) ;
325+ freeze ( output ) ;
326+ expect ( hiddenCount ( output ) ) . toBe ( 30 ) ;
327+
328+ ( output as unknown as { trimFrozen : ( n : number ) => void } ) . trimFrozen ( 30 ) ;
329+
330+ // The 30 hidden (oldest) lines were removed; nothing exposed got hidden.
331+ expect ( hiddenCount ( output ) ) . toBe ( 0 ) ;
332+ expect ( frozen ( output ) . children . length ) . toBe ( Output . A11Y_EXPOSED_FROZEN_LINES ) ;
333+
334+ // Freezing more lines re-hides from the new front, incrementally.
335+ ( output as unknown as { allLines : OutputLine [ ] } ) . allLines = makeLines ( total - 30 + 5 ) ;
336+ freeze ( output ) ;
337+ expect ( hiddenCount ( output ) ) . toBe ( 5 ) ;
338+ } ) ;
339+ } ) ;
340+
341+ describe ( "Output history exposure toggle" , ( ) => {
342+ const makeLines = ( count : number ) : OutputLine [ ] =>
343+ Array . from ( { length : count } , ( _ , i ) => ( {
344+ content : < div > { `line ${ i } ` } </ div > ,
345+ id : i ,
346+ sourceContent : `line ${ i } ` ,
347+ sourceType : "test" ,
348+ type : OutputType . ServerMessage ,
349+ } ) ) ;
350+
351+ const makeOutput = ( lines : OutputLine [ ] ) : Output => {
352+ const output = new Output ( { client : { } as MudClient } ) ;
353+ Object . defineProperty ( output , "allLines" , { value : lines , writable : true } ) ;
354+ const frozenDiv = document . createElement ( "div" ) ;
355+ Object . defineProperty ( output , "frozenRef" , { value : { current : frozenDiv } } ) ;
356+ return output ;
357+ } ;
358+
359+ const frozen = ( output : Output ) : HTMLDivElement =>
360+ ( output as unknown as { frozenRef : { current : HTMLDivElement } } ) . frozenRef . current ;
361+
362+ const freeze = ( output : Output ) =>
363+ ( output as unknown as { freezeOverflow : ( ) => void } ) . freezeOverflow ( ) ;
364+
365+ const hiddenCount = ( output : Output ) : number =>
366+ frozen ( output ) . querySelectorAll ( '[aria-hidden="true"]' ) . length ;
367+
368+ const setRevealed = ( output : Output , historyRevealed : boolean ) => {
369+ Object . assign ( output . state , { historyRevealed } ) ;
370+ } ;
371+
372+ it ( "exposes all frozen lines while revealed, including new arrivals" , ( ) => {
373+ const total = Output . LIVE_WINDOW_SIZE + Output . A11Y_EXPOSED_FROZEN_LINES + 40 ;
374+ const output = makeOutput ( makeLines ( total ) ) ;
375+ freeze ( output ) ;
376+ expect ( hiddenCount ( output ) ) . toBe ( 40 ) ;
377+
378+ setRevealed ( output , true ) ;
379+ freeze ( output ) ;
380+ expect ( hiddenCount ( output ) ) . toBe ( 0 ) ;
381+
382+ // New lines freezing while revealed stay exposed too.
383+ ( output as unknown as { allLines : OutputLine [ ] } ) . allLines = makeLines ( total + 10 ) ;
384+ freeze ( output ) ;
385+ expect ( hiddenCount ( output ) ) . toBe ( 0 ) ;
386+ } ) ;
387+
388+ it ( "re-hides everything beyond the cap when revealed is switched off" , ( ) => {
389+ const total = Output . LIVE_WINDOW_SIZE + Output . A11Y_EXPOSED_FROZEN_LINES + 40 ;
390+ const output = makeOutput ( makeLines ( total ) ) ;
391+ setRevealed ( output , true ) ;
392+ freeze ( output ) ;
393+ expect ( hiddenCount ( output ) ) . toBe ( 0 ) ;
394+
395+ setRevealed ( output , false ) ;
396+ freeze ( output ) ;
397+
398+ const frozenDiv = frozen ( output ) ;
399+ expect ( hiddenCount ( output ) ) . toBe ( 40 ) ;
400+ expect ( frozenDiv . children [ 0 ] . getAttribute ( "aria-hidden" ) ) . toBe ( "true" ) ;
401+ expect ( frozenDiv . children [ 40 ] . hasAttribute ( "aria-hidden" ) ) . toBe ( false ) ;
402+ } ) ;
403+ } ) ;
0 commit comments