@@ -577,6 +577,100 @@ test("runtime render resolves nested percent sizing from resolved parent layout"
577577 }
578578} ) ;
579579
580+ test ( "runtime render re-resolves percent sizing when parent layout changes (no frame lag)" , async ( ) => {
581+ const stdin = new PassThrough ( ) as PassThrough & { setRawMode : ( enabled : boolean ) => void } ;
582+ stdin . setRawMode = ( ) => { } ;
583+
584+ const stdout = new PassThrough ( ) as PassThrough & {
585+ columns ?: number ;
586+ rows ?: number ;
587+ } ;
588+ stdout . columns = 80 ;
589+ stdout . rows = 24 ;
590+
591+ const stderr = new PassThrough ( ) ;
592+
593+ let parentNode : InkHostNode | null = null ;
594+ let childNode : InkHostNode | null = null ;
595+
596+ function App ( props : { parentWidth : number } ) : React . ReactElement {
597+ const parentRef = React . useRef < InkHostNode | null > ( null ) ;
598+ const childRef = React . useRef < InkHostNode | null > ( null ) ;
599+
600+ useEffect ( ( ) => {
601+ parentNode = parentRef . current ;
602+ childNode = childRef . current ;
603+ } ) ;
604+
605+ return React . createElement (
606+ Box ,
607+ { ref : parentRef , width : props . parentWidth , flexDirection : "row" } ,
608+ React . createElement (
609+ Box ,
610+ { ref : childRef , width : "50%" } ,
611+ React . createElement ( Text , null , "Child" ) ,
612+ ) ,
613+ ) ;
614+ }
615+
616+ const instance = runtimeRender ( React . createElement ( App , { parentWidth : 20 } ) , {
617+ stdin,
618+ stdout,
619+ stderr,
620+ } ) ;
621+ try {
622+ await new Promise ( ( resolve ) => setTimeout ( resolve , 60 ) ) ;
623+ assert . ok ( parentNode != null , "parent ref should be set" ) ;
624+ assert . ok ( childNode != null , "child ref should be set" ) ;
625+ assert . equal ( measureElement ( parentNode ) . width , 20 ) ;
626+ assert . equal ( measureElement ( childNode ) . width , 10 ) ;
627+
628+ instance . rerender ( React . createElement ( App , { parentWidth : 30 } ) ) ;
629+ await new Promise ( ( resolve ) => setTimeout ( resolve , 60 ) ) ;
630+ assert . equal ( measureElement ( parentNode ) . width , 30 ) ;
631+ assert . equal ( measureElement ( childNode ) . width , 15 ) ;
632+ } finally {
633+ instance . unmount ( ) ;
634+ instance . cleanup ( ) ;
635+ }
636+ } ) ;
637+
638+ test ( "runtime render layout generations hide stale layout for removed nodes" , async ( ) => {
639+ const stdin = new PassThrough ( ) as PassThrough & { setRawMode : ( enabled : boolean ) => void } ;
640+ stdin . setRawMode = ( ) => { } ;
641+ const stdout = new PassThrough ( ) ;
642+ const stderr = new PassThrough ( ) ;
643+
644+ let removedNode : InkHostNode | null = null ;
645+
646+ function Before ( ) : React . ReactElement {
647+ const removedRef = React . useRef < InkHostNode | null > ( null ) ;
648+ useEffect ( ( ) => {
649+ removedNode = removedRef . current ;
650+ } ) ;
651+ return React . createElement (
652+ Box ,
653+ { ref : removedRef , width : 22 } ,
654+ React . createElement ( Text , null , "Before" ) ,
655+ ) ;
656+ }
657+
658+ const instance = runtimeRender ( React . createElement ( Before ) , { stdin, stdout, stderr } ) ;
659+ try {
660+ await new Promise ( ( resolve ) => setTimeout ( resolve , 40 ) ) ;
661+ assert . ok ( removedNode != null , "removed node ref should be set" ) ;
662+ assert . equal ( measureElement ( removedNode ) . width , 22 ) ;
663+
664+ instance . rerender ( React . createElement ( Text , null , "After" ) ) ;
665+ await new Promise ( ( resolve ) => setTimeout ( resolve , 40 ) ) ;
666+
667+ assert . deepEqual ( measureElement ( removedNode ) , { width : 0 , height : 0 } ) ;
668+ } finally {
669+ instance . unmount ( ) ;
670+ instance . cleanup ( ) ;
671+ }
672+ } ) ;
673+
580674test ( "render option isScreenReaderEnabled flows to hook context" , async ( ) => {
581675 const stdin = new PassThrough ( ) as PassThrough & { setRawMode : ( enabled : boolean ) => void } ;
582676 stdin . setRawMode = ( ) => { } ;
@@ -948,6 +1042,40 @@ test("rerender updates output", () => {
9481042 assert . match ( result . lastFrame ( ) , / N e w / ) ;
9491043} ) ;
9501044
1045+ test ( "rendering identical tree keeps ANSI frame bytes stable" , async ( ) => {
1046+ const element = React . createElement (
1047+ Box ,
1048+ { flexDirection : "row" } ,
1049+ React . createElement ( Text , { color : "green" , bold : true } , "Left" ) ,
1050+ React . createElement ( Text , null , " " ) ,
1051+ React . createElement ( Text , null , "\u001b[31mRight\u001b[0m" ) ,
1052+ ) ;
1053+
1054+ const captureFrame = async ( ) : Promise < string > => {
1055+ const stdin = new PassThrough ( ) as PassThrough & { setRawMode : ( enabled : boolean ) => void } ;
1056+ stdin . setRawMode = ( ) => { } ;
1057+ const stdout = new PassThrough ( ) ;
1058+ const stderr = new PassThrough ( ) ;
1059+ let writes = "" ;
1060+ stdout . on ( "data" , ( chunk ) => {
1061+ writes += chunk . toString ( "utf-8" ) ;
1062+ } ) ;
1063+
1064+ const instance = runtimeRender ( element , { stdin, stdout, stderr } ) ;
1065+ try {
1066+ await new Promise ( ( resolve ) => setTimeout ( resolve , 30 ) ) ;
1067+ return latestFrameFromWrites ( writes ) ;
1068+ } finally {
1069+ instance . unmount ( ) ;
1070+ instance . cleanup ( ) ;
1071+ }
1072+ } ;
1073+
1074+ const firstFrame = await captureFrame ( ) ;
1075+ const secondFrame = await captureFrame ( ) ;
1076+ assert . equal ( secondFrame , firstFrame ) ;
1077+ } ) ;
1078+
9511079test ( "runtime Static emits only new items on rerender" , async ( ) => {
9521080 interface Item {
9531081 id : string ;
@@ -1138,6 +1266,45 @@ test("ANSI output resets attributes between differently-styled cells", () => {
11381266
11391267// ─── Regression: text inherits background from underlying fillRect ───
11401268
1269+ test ( "nested non-overlapping clips do not leak text" , async ( ) => {
1270+ const stdin = new PassThrough ( ) as PassThrough & { setRawMode : ( enabled : boolean ) => void } ;
1271+ stdin . setRawMode = ( ) => { } ;
1272+ const stdout = new PassThrough ( ) ;
1273+ const stderr = new PassThrough ( ) ;
1274+ let writes = "" ;
1275+ stdout . on ( "data" , ( chunk ) => {
1276+ writes += chunk . toString ( "utf-8" ) ;
1277+ } ) ;
1278+
1279+ const instance = runtimeRender (
1280+ React . createElement (
1281+ Box ,
1282+ { width : 4 , height : 1 , overflow : "hidden" } ,
1283+ React . createElement (
1284+ Box ,
1285+ { position : "absolute" , left : 10 , top : 0 , width : 4 , height : 1 , overflow : "hidden" } ,
1286+ React . createElement ( Text , null , "LEAK" ) ,
1287+ ) ,
1288+ ) ,
1289+ { stdin, stdout, stderr } ,
1290+ ) ;
1291+
1292+ try {
1293+ await new Promise < void > ( ( resolve ) => {
1294+ if ( writes . length > 0 ) {
1295+ resolve ( ) ;
1296+ return ;
1297+ }
1298+ stdout . once ( "data" , ( ) => resolve ( ) ) ;
1299+ } ) ;
1300+ const latest = stripTerminalEscapes ( latestFrameFromWrites ( writes ) ) ;
1301+ assert . equal ( latest . includes ( "LEAK" ) , false , `unexpected clipped leak in output: ${ latest } ` ) ;
1302+ } finally {
1303+ instance . unmount ( ) ;
1304+ instance . cleanup ( ) ;
1305+ }
1306+ } ) ;
1307+
11411308test ( "text over backgroundColor box preserves box background in ANSI output" , ( ) => {
11421309 const previousNoColor = process . env [ "NO_COLOR" ] ;
11431310 const previousForceColor = process . env [ "FORCE_COLOR" ] ;
0 commit comments