11/** Landing anatomy: mark, centred prompt box, telemetry disclosure,
22 * starters — gone once the transcript has content. */
3- import { afterEach , describe , expect , test } from "bun:test" ;
3+ import { describe , expect , test } from "bun:test" ;
44import type { CapturedSpan } from "@opentui/core" ;
55import { rgbToHex } from "@opentui/core" ;
66import { defined } from "../../testkit/defined.js" ;
77import { makePermissionItems , type Harness } from "./harness" ;
88import {
99 appendStreamRow ,
1010 applyLandingSuggestion ,
11- LANDING_IDLE_REPAINT_INTERVAL_MS ,
1211 noticeText ,
1312 paintChrome ,
1413 setChromeZones ,
@@ -17,7 +16,7 @@ import {
1716 toggleTasksPanel ,
1817} from "./shell/chrome" ;
1918import { withAppShell } from "./test-helpers" ;
20- import { isLanding } from "./shell/internals" ;
19+ import { isLanding , shellInternals } from "./shell/internals" ;
2120import {
2221 setPromptModelLabel ,
2322 setPromptWorkspace ,
@@ -46,58 +45,8 @@ import { SNOW_CHAR } from "./mark-raster";
4645const SIZE = { width : 80 , height : 24 } as const ;
4746const NOTICE = "Anonymous usage telemetry is enabled. Disable in /settings." ;
4847
49- const nativeSetInterval = globalThis . setInterval ;
50- const nativeClearInterval = globalThis . clearInterval ;
51-
5248const stripSnow = ( text : string ) => text . replaceAll ( SNOW_CHAR , " " ) ;
5349
54- interface IdleTimerHandle {
55- unref ?: ( ) => void ;
56- }
57-
58- /** Intercepts the idle interval so tests can see a still-armed timer
59- * regardless of cadence. */
60- function wrapLandingIdleTimer ( ) : {
61- armed : IdleTimerHandle [ ] ;
62- cleared : IdleTimerHandle [ ] ;
63- } {
64- const armed : IdleTimerHandle [ ] = [ ] ;
65- const cleared : IdleTimerHandle [ ] = [ ] ;
66- // Do not arm a real interval. Callers inject clocks or only inspect handles.
67- globalThis . setInterval = ( (
68- handler : Parameters < typeof nativeSetInterval > [ 0 ] ,
69- delay ?: number ,
70- ...args : unknown [ ]
71- ) => {
72- if ( delay === LANDING_IDLE_REPAINT_INTERVAL_MS ) {
73- const handle : IdleTimerHandle = { } ;
74- armed . push ( handle ) ;
75- return handle ;
76- }
77- return nativeSetInterval . call ( globalThis , handler , delay , ...args ) ;
78- } ) as typeof nativeSetInterval ;
79- globalThis . clearInterval = ( (
80- handle : Parameters < typeof nativeClearInterval > [ 0 ] ,
81- ) => {
82- cleared . push ( handle as IdleTimerHandle ) ;
83- if ( armed . includes ( handle as IdleTimerHandle ) ) return ;
84- return nativeClearInterval . call ( globalThis , handle ) ;
85- } ) as typeof nativeClearInterval ;
86- return { armed, cleared } ;
87- }
88-
89- function soleLandingIdleHandle (
90- armed : readonly IdleTimerHandle [ ] ,
91- ) : IdleTimerHandle {
92- const handle = armed [ 0 ] ;
93- if ( armed . length !== 1 || handle === undefined ) {
94- throw new Error (
95- `expected exactly one ${ LANDING_IDLE_REPAINT_INTERVAL_MS } ms interval, got ${ armed . length } ` ,
96- ) ;
97- }
98- return handle ;
99- }
100-
10150/** Newly added scroll-box children need a layout pass before they paint. */
10251async function settle ( h : Harness ) : Promise < void > {
10352 await h . renderOnce ( ) ;
@@ -314,17 +263,12 @@ describe("landing screen", () => {
314263 ) ;
315264 } , 15_000 ) ;
316265
317- describe ( "landing idle timer" , ( ) => {
318- afterEach ( ( ) => {
319- globalThis . setInterval = nativeSetInterval ;
320- globalThis . clearInterval = nativeClearInterval ;
321- } ) ;
322-
323- test ( "reduced-motion mount never arms the idle timer and never draws snow" , async ( ) => {
324- const { armed } = wrapLandingIdleTimer ( ) ;
266+ describe ( "landing idle motion" , ( ) => {
267+ test ( "reduced-motion mount never arms snow and never draws it" , async ( ) => {
325268 await withAppShell (
326269 async ( shell , h ) => {
327- expect ( armed ) . toHaveLength ( 0 ) ;
270+ expect ( shellInternals ( shell ) ?. landingIdleTimerCancel ) . toBeNull ( ) ;
271+ expect ( shellInternals ( shell ) ?. markSnow ) . toBeNull ( ) ;
328272 await settle ( h ) ;
329273 const first = markRows ( h ) . join ( "\n" ) ;
330274 expect ( first . includes ( SNOW_CHAR ) ) . toBe ( false ) ;
@@ -349,17 +293,20 @@ describe("landing screen", () => {
349293 ) ;
350294 } ) ;
351295
352- test ( "a deferred system notice does not clear the landing idle timer" , async ( ) => {
353- const { armed, cleared } = wrapLandingIdleTimer ( ) ;
296+ test ( "a deferred system notice does not stop landing snow" , async ( ) => {
354297 await withAppShell (
355298 async ( shell ) => {
356- const handle = soleLandingIdleHandle ( armed ) ;
299+ expect ( shellInternals ( shell ) ?. landingIdleTimerCancel ) . toBeTypeOf (
300+ "function" ,
301+ ) ;
357302 surfaceSystemNotice (
358303 shell ,
359304 "mcp github did not connect (ECONNREFUSED) — its tools are unavailable; /mcp for detail" ,
360305 ) ;
361306 expect ( isLanding ( shell ) ) . toBe ( true ) ;
362- expect ( cleared ) . not . toContain ( handle ) ;
307+ expect ( shellInternals ( shell ) ?. landingIdleTimerCancel ) . toBeTypeOf (
308+ "function" ,
309+ ) ;
363310 } ,
364311 {
365312 shell : {
@@ -369,14 +316,15 @@ describe("landing screen", () => {
369316 ) ;
370317 } ) ;
371318
372- test ( "appending a transcript row clears the landing idle timer" , async ( ) => {
373- const { armed, cleared } = wrapLandingIdleTimer ( ) ;
319+ test ( "appending a transcript row stops landing snow" , async ( ) => {
374320 await withAppShell (
375321 async ( shell ) => {
376- const handle = soleLandingIdleHandle ( armed ) ;
322+ expect ( shellInternals ( shell ) ?. landingIdleTimerCancel ) . toBeTypeOf (
323+ "function" ,
324+ ) ;
377325 appendStreamRow ( shell , { role : "user" , text : "first prompt" } ) ;
378326 expect ( isLanding ( shell ) ) . toBe ( false ) ;
379- expect ( cleared ) . toContain ( handle ) ;
327+ expect ( shellInternals ( shell ) ?. landingIdleTimerCancel ) . toBeNull ( ) ;
380328 } ,
381329 {
382330 shell : {
@@ -386,13 +334,14 @@ describe("landing screen", () => {
386334 ) ;
387335 } ) ;
388336
389- test ( "disposing the shell with no transcript clears the landing idle timer" , async ( ) => {
390- const { armed, cleared } = wrapLandingIdleTimer ( ) ;
337+ test ( "disposing the shell with no transcript stops landing snow" , async ( ) => {
391338 await withAppShell (
392339 async ( shell ) => {
393- const handle = soleLandingIdleHandle ( armed ) ;
340+ expect ( shellInternals ( shell ) ?. landingIdleTimerCancel ) . toBeTypeOf (
341+ "function" ,
342+ ) ;
394343 shell . dispose ( ) ;
395- expect ( cleared ) . toContain ( handle ) ;
344+ expect ( shellInternals ( shell ) ?. landingIdleTimerCancel ) . toBeNull ( ) ;
396345 } ,
397346 {
398347 shell : {
0 commit comments