@@ -2990,3 +2990,311 @@ describe('Synchronous open()', () => {
29902990 term . dispose ( ) ;
29912991 } ) ;
29922992} ) ;
2993+
2994+ // ============================================================================
2995+ // Dynamic Theme Changes
2996+ // ============================================================================
2997+
2998+ describe ( 'Dynamic Theme Changes' , ( ) => {
2999+ let container : HTMLElement | null = null ;
3000+
3001+ beforeEach ( async ( ) => {
3002+ if ( typeof document !== 'undefined' ) {
3003+ container = document . createElement ( 'div' ) ;
3004+ document . body . appendChild ( container ) ;
3005+ }
3006+ } ) ;
3007+
3008+ afterEach ( ( ) => {
3009+ if ( container && container . parentNode ) {
3010+ container . parentNode . removeChild ( container ) ;
3011+ container = null ;
3012+ }
3013+ } ) ;
3014+
3015+ test ( 'full theme change updates renderer' , async ( ) => {
3016+ if ( ! container ) return ;
3017+
3018+ const term = await createIsolatedTerminal ( {
3019+ theme : { background : '#000000' , foreground : '#ffffff' } ,
3020+ } ) ;
3021+ term . open ( container ) ;
3022+
3023+ // Change to a completely different theme
3024+ term . options . theme = {
3025+ background : '#ff0000' ,
3026+ foreground : '#00ff00' ,
3027+ cursor : '#0000ff' ,
3028+ red : '#aa0000' ,
3029+ } ;
3030+
3031+ // @ts -ignore - accessing private for test
3032+ const renderer = term . renderer ;
3033+ // @ts -ignore - accessing private for test
3034+ expect ( renderer . theme . background ) . toBe ( '#ff0000' ) ;
3035+ // @ts -ignore - accessing private for test
3036+ expect ( renderer . theme . foreground ) . toBe ( '#00ff00' ) ;
3037+ // @ts -ignore - accessing private for test
3038+ expect ( renderer . theme . cursor ) . toBe ( '#0000ff' ) ;
3039+
3040+ term . dispose ( ) ;
3041+ } ) ;
3042+
3043+ test ( 'full theme change updates WASM terminal colors' , async ( ) => {
3044+ if ( ! container ) return ;
3045+
3046+ const term = await createIsolatedTerminal ( ) ;
3047+ term . open ( container ) ;
3048+
3049+ term . options . theme = {
3050+ background : '#112233' ,
3051+ foreground : '#aabbcc' ,
3052+ } ;
3053+
3054+ // Force render state update to pick up new colors
3055+ term . wasmTerm ! . update ( ) ;
3056+ const colors = term . wasmTerm ! . getColors ( ) ;
3057+
3058+ // Verify WASM terminal has the new colors
3059+ expect ( colors . background . r ) . toBe ( 0x11 ) ;
3060+ expect ( colors . background . g ) . toBe ( 0x22 ) ;
3061+ expect ( colors . background . b ) . toBe ( 0x33 ) ;
3062+ expect ( colors . foreground . r ) . toBe ( 0xaa ) ;
3063+ expect ( colors . foreground . g ) . toBe ( 0xbb ) ;
3064+ expect ( colors . foreground . b ) . toBe ( 0xcc ) ;
3065+
3066+ term . dispose ( ) ;
3067+ } ) ;
3068+
3069+ test ( 'partial theme update preserves previous customizations' , async ( ) => {
3070+ if ( ! container ) return ;
3071+
3072+ const term = await createIsolatedTerminal ( ) ;
3073+ term . open ( container ) ;
3074+
3075+ // First: change background only
3076+ term . options . theme = { background : '#111111' } ;
3077+
3078+ // @ts -ignore - accessing private for test
3079+ expect ( term . renderer . theme . background ) . toBe ( '#111111' ) ;
3080+
3081+ // Second: change foreground only — background should be preserved
3082+ term . options . theme = { foreground : '#222222' } ;
3083+
3084+ // @ts -ignore - accessing private for test
3085+ expect ( term . renderer . theme . background ) . toBe ( '#111111' ) ;
3086+ // @ts -ignore - accessing private for test
3087+ expect ( term . renderer . theme . foreground ) . toBe ( '#222222' ) ;
3088+
3089+ term . dispose ( ) ;
3090+ } ) ;
3091+
3092+ test ( 'successive partial updates accumulate correctly' , async ( ) => {
3093+ if ( ! container ) return ;
3094+
3095+ const term = await createIsolatedTerminal ( ) ;
3096+ term . open ( container ) ;
3097+
3098+ term . options . theme = { background : '#aaaaaa' } ;
3099+ term . options . theme = { foreground : '#bbbbbb' } ;
3100+ term . options . theme = { cursor : '#cccccc' } ;
3101+
3102+ // @ts -ignore - accessing private for test
3103+ const theme = term . renderer . theme ;
3104+ expect ( theme . background ) . toBe ( '#aaaaaa' ) ;
3105+ expect ( theme . foreground ) . toBe ( '#bbbbbb' ) ;
3106+ expect ( theme . cursor ) . toBe ( '#cccccc' ) ;
3107+
3108+ term . dispose ( ) ;
3109+ } ) ;
3110+
3111+ test ( 'theme reset to empty object restores defaults' , async ( ) => {
3112+ if ( ! container ) return ;
3113+
3114+ const term = await createIsolatedTerminal ( {
3115+ theme : { background : '#ff0000' , foreground : '#00ff00' } ,
3116+ } ) ;
3117+ term . open ( container ) ;
3118+
3119+ // @ts -ignore - accessing private for test
3120+ expect ( term . renderer . theme . background ) . toBe ( '#ff0000' ) ;
3121+
3122+ // Reset to empty — should restore defaults
3123+ term . options . theme = { } ;
3124+
3125+ // @ts -ignore - accessing private for test
3126+ expect ( term . renderer . theme . background ) . toBe ( '#1e1e1e' ) ;
3127+ // @ts -ignore - accessing private for test
3128+ expect ( term . renderer . theme . foreground ) . toBe ( '#d4d4d4' ) ;
3129+
3130+ term . dispose ( ) ;
3131+ } ) ;
3132+
3133+ test ( 'theme reset to null restores defaults' , async ( ) => {
3134+ if ( ! container ) return ;
3135+
3136+ const term = await createIsolatedTerminal ( {
3137+ theme : { background : '#ff0000' } ,
3138+ } ) ;
3139+ term . open ( container ) ;
3140+
3141+ // @ts -ignore - accessing private for test
3142+ expect ( term . renderer . theme . background ) . toBe ( '#ff0000' ) ;
3143+
3144+ // Reset to null
3145+ term . options . theme = null as any ;
3146+
3147+ // @ts -ignore - accessing private for test
3148+ expect ( term . renderer . theme . background ) . toBe ( '#1e1e1e' ) ;
3149+
3150+ term . dispose ( ) ;
3151+ } ) ;
3152+
3153+ test ( 'theme change before open() is applied correctly' , async ( ) => {
3154+ if ( ! container ) return ;
3155+
3156+ const term = await createIsolatedTerminal ( {
3157+ theme : { background : '#111111' } ,
3158+ } ) ;
3159+
3160+ // Change theme before open
3161+ term . options . theme = { background : '#222222' } ;
3162+
3163+ // Open — should use the latest theme
3164+ term . open ( container ) ;
3165+
3166+ // The buildWasmConfig reads from options.theme which is now #222222
3167+ // @ts -ignore - accessing private for test
3168+ expect ( term . renderer . theme . background ) . toBe ( '#222222' ) ;
3169+
3170+ term . dispose ( ) ;
3171+ } ) ;
3172+
3173+ test ( 'ANSI palette color cells re-resolve after theme change' , async ( ) => {
3174+ if ( ! container ) return ;
3175+
3176+ const term = await createIsolatedTerminal ( {
3177+ theme : { red : '#cd3131' } ,
3178+ } ) ;
3179+ term . open ( container ) ;
3180+
3181+ // Write text with ANSI red (color index 1)
3182+ term . write ( '\x1b[31mRed text\x1b[0m' ) ;
3183+
3184+ // Change theme — new red
3185+ term . options . theme = { red : '#ff0000' } ;
3186+
3187+ // Force render state update and read cells
3188+ term . wasmTerm ! . update ( ) ;
3189+ const line = term . wasmTerm ! . getLine ( 0 ) ;
3190+ expect ( line ) . not . toBeNull ( ) ;
3191+
3192+ // First cell ('R') should now have the new red color
3193+ const cell = line ! [ 0 ] ;
3194+ expect ( cell . fg_r ) . toBe ( 0xff ) ;
3195+ expect ( cell . fg_g ) . toBe ( 0x00 ) ;
3196+ expect ( cell . fg_b ) . toBe ( 0x00 ) ;
3197+
3198+ term . dispose ( ) ;
3199+ } ) ;
3200+
3201+ test ( 'explicit RGB color cells remain unchanged after theme change' , async ( ) => {
3202+ if ( ! container ) return ;
3203+
3204+ const term = await createIsolatedTerminal ( ) ;
3205+ term . open ( container ) ;
3206+
3207+ // Write text with explicit RGB color
3208+ term . write ( '\x1b[38;2;100;200;50mRGB text\x1b[0m' ) ;
3209+
3210+ // Change theme
3211+ term . options . theme = {
3212+ foreground : '#ffffff' ,
3213+ background : '#000000' ,
3214+ red : '#ff0000' ,
3215+ } ;
3216+
3217+ // Force render state update and read cells
3218+ term . wasmTerm ! . update ( ) ;
3219+ const line = term . wasmTerm ! . getLine ( 0 ) ;
3220+ expect ( line ) . not . toBeNull ( ) ;
3221+
3222+ // First cell ('R') should still have the explicit RGB color
3223+ const cell = line ! [ 0 ] ;
3224+ expect ( cell . fg_r ) . toBe ( 100 ) ;
3225+ expect ( cell . fg_g ) . toBe ( 200 ) ;
3226+ expect ( cell . fg_b ) . toBe ( 50 ) ;
3227+
3228+ term . dispose ( ) ;
3229+ } ) ;
3230+
3231+ test ( 'theme change triggers full redraw' , async ( ) => {
3232+ if ( ! container ) return ;
3233+
3234+ const term = await createIsolatedTerminal ( ) ;
3235+ term . open ( container ) ;
3236+
3237+ // Clear any existing dirty state
3238+ term . wasmTerm ! . clearDirty ( ) ;
3239+ expect ( term . wasmTerm ! . needsFullRedraw ( ) ) . toBe ( false ) ;
3240+
3241+ // Change theme
3242+ term . options . theme = { background : '#ff0000' } ;
3243+
3244+ // Should need a full redraw
3245+ expect ( term . wasmTerm ! . needsFullRedraw ( ) ) . toBe ( true ) ;
3246+
3247+ // After clearing, no longer dirty
3248+ term . wasmTerm ! . clearDirty ( ) ;
3249+ expect ( term . wasmTerm ! . needsFullRedraw ( ) ) . toBe ( false ) ;
3250+
3251+ term . dispose ( ) ;
3252+ } ) ;
3253+
3254+ test ( 'invalid color values do not crash' , async ( ) => {
3255+ if ( ! container ) return ;
3256+
3257+ const term = await createIsolatedTerminal ( ) ;
3258+ term . open ( container ) ;
3259+
3260+ // Should not throw
3261+ term . options . theme = {
3262+ background : 'not-a-color' ,
3263+ foreground : 'rgb(999,0,0)' ,
3264+ red : '' ,
3265+ } ;
3266+
3267+ // @ts -ignore - accessing private for test
3268+ expect ( term . renderer . theme . background ) . toBe ( 'not-a-color' ) ;
3269+
3270+ term . dispose ( ) ;
3271+ } ) ;
3272+
3273+ test ( 'default fg/bg cells update after theme change' , async ( ) => {
3274+ if ( ! container ) return ;
3275+
3276+ const term = await createIsolatedTerminal ( {
3277+ theme : { foreground : '#aaaaaa' , background : '#111111' } ,
3278+ } ) ;
3279+ term . open ( container ) ;
3280+
3281+ // Write text with default colors (no SGR)
3282+ term . write ( 'Hello' ) ;
3283+
3284+ // Change theme
3285+ term . options . theme = { foreground : '#ffffff' , background : '#000000' } ;
3286+
3287+ // Force render state update and read cells
3288+ term . wasmTerm ! . update ( ) ;
3289+ const line = term . wasmTerm ! . getLine ( 0 ) ;
3290+ expect ( line ) . not . toBeNull ( ) ;
3291+
3292+ // First cell ('H') should have new default foreground
3293+ const cell = line ! [ 0 ] ;
3294+ expect ( cell . fg_r ) . toBe ( 0xff ) ;
3295+ expect ( cell . fg_g ) . toBe ( 0xff ) ;
3296+ expect ( cell . fg_b ) . toBe ( 0xff ) ;
3297+
3298+ term . dispose ( ) ;
3299+ } ) ;
3300+ } ) ;
0 commit comments