From 547f547523751f319bb8fede82dd6f4411ea61f7 Mon Sep 17 00:00:00 2001 From: Vadim Demedes Date: Wed, 3 May 2023 23:01:07 +0300 Subject: [PATCH 1/2] Fix rendering when terminal is erased on first render --- src/ink.tsx | 4 +- src/log-update.ts | 14 +++++-- test/fixtures/erase-once-with-static.tsx | 33 ++++++++++++++++ test/fixtures/erase-once.tsx | 27 +++++++++++++ test/render.tsx | 48 ++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 test/fixtures/erase-once-with-static.tsx create mode 100644 test/fixtures/erase-once.tsx diff --git a/src/ink.tsx b/src/ink.tsx index 1162777a9..bce2d5497 100644 --- a/src/ink.tsx +++ b/src/ink.tsx @@ -175,8 +175,10 @@ export default class Ink { if (outputHeight >= this.options.stdout.rows) { this.options.stdout.write( - ansiEscapes.clearTerminal + this.fullStaticOutput + output + ansiEscapes.clearTerminal + this.fullStaticOutput ); + + this.log(output, {force: true, erase: false}); this.lastOutput = output; return; } diff --git a/src/log-update.ts b/src/log-update.ts index a38e565a3..2b46a0322 100644 --- a/src/log-update.ts +++ b/src/log-update.ts @@ -5,7 +5,7 @@ import cliCursor from 'cli-cursor'; export type LogUpdate = { clear: () => void; done: () => void; - (str: string): void; + (str: string, options?: {force?: boolean; erase?: boolean}): void; }; const create = (stream: Writable, {showCursor = false} = {}): LogUpdate => { @@ -13,19 +13,25 @@ const create = (stream: Writable, {showCursor = false} = {}): LogUpdate => { let previousOutput = ''; let hasHiddenCursor = false; - const render = (str: string) => { + const render = ( + str: string, + {force = false, erase = true}: {force?: boolean; erase?: boolean} = {} + ) => { if (!showCursor && !hasHiddenCursor) { cliCursor.hide(); hasHiddenCursor = true; } const output = str + '\n'; - if (output === previousOutput) { + if (output === previousOutput && !force) { return; } previousOutput = output; - stream.write(ansiEscapes.eraseLines(previousLineCount) + output); + + const eraser = erase ? ansiEscapes.eraseLines(previousLineCount) : ''; + stream.write(eraser + output); + previousLineCount = output.split('\n').length; }; diff --git a/test/fixtures/erase-once-with-static.tsx b/test/fixtures/erase-once-with-static.tsx new file mode 100644 index 000000000..9dfac4d72 --- /dev/null +++ b/test/fixtures/erase-once-with-static.tsx @@ -0,0 +1,33 @@ +import process from 'node:process'; +import React, {useState} from 'react'; +import {Box, Static, Text, render, useInput} from '../../src/index.js'; + +function Test() { + const [fullHeight, setFullHeight] = useState(true); + + useInput( + input => { + if (input === 'x') { + setFullHeight(false); + } + }, + {isActive: fullHeight} + ); + + return ( + <> + + {item => {item}} + + + + A + B + {fullHeight && C} + + + ); +} + +process.stdout.rows = Number(process.argv[2]); +render(); diff --git a/test/fixtures/erase-once.tsx b/test/fixtures/erase-once.tsx new file mode 100644 index 000000000..267702c0e --- /dev/null +++ b/test/fixtures/erase-once.tsx @@ -0,0 +1,27 @@ +import process from 'node:process'; +import React, {useState} from 'react'; +import {Box, Text, render, useInput} from '../../src/index.js'; + +function Test() { + const [fullHeight, setFullHeight] = useState(true); + + useInput( + input => { + if (input === 'x') { + setFullHeight(false); + } + }, + {isActive: fullHeight} + ); + + return ( + + A + B + {fullHeight && C} + + ); +} + +process.stdout.rows = Number(process.argv[2]); +render(); diff --git a/test/render.tsx b/test/render.tsx index 65da54046..9debe8109 100644 --- a/test/render.tsx +++ b/test/render.tsx @@ -106,6 +106,54 @@ test.serial('erase screen', async t => { } }); +test.serial('erase screen once then continue rendering as usual', async t => { + const ps = term('erase-once', ['3']); + await delay(1000); + + t.true(ps.output.includes(ansiEscapes.clearTerminal)); + t.true(ps.output.includes('A')); + t.true(ps.output.includes('B')); + t.true(ps.output.includes('C')); + + ps.output = ''; + ps.write('x'); + + await ps.waitForExit(); + + t.false(ps.output.includes(ansiEscapes.clearTerminal)); + t.true(ps.output.includes(ansiEscapes.eraseLines(3))); + t.true(ps.output.includes('A')); + t.true(ps.output.includes('B')); + t.false(ps.output.includes('C')); +}); + +test.serial( + 'erase screen once then continue rendering as usual with present', + async t => { + const ps = term('erase-once-with-static', ['3']); + await delay(1000); + + t.true(ps.output.includes(ansiEscapes.clearTerminal)); + t.true(ps.output.includes('X')); + t.true(ps.output.includes('Y')); + t.true(ps.output.includes('Z')); + t.true(ps.output.includes('A')); + t.true(ps.output.includes('B')); + t.true(ps.output.includes('C')); + + ps.output = ''; + ps.write('x'); + + await ps.waitForExit(); + + t.false(ps.output.includes(ansiEscapes.clearTerminal)); + t.true(ps.output.includes(ansiEscapes.eraseLines(2))); + t.true(ps.output.includes('A')); + t.true(ps.output.includes('B')); + t.false(ps.output.includes('C')); + } +); + test.serial( 'erase screen where exists but interactive part is taller than viewport', async t => { From 10bf2e5289900a56d2b58e90e9d01e67b870c804 Mon Sep 17 00:00:00 2001 From: Vadim Demedes Date: Wed, 3 May 2023 23:56:56 +0300 Subject: [PATCH 2/2] Try to fix tests on CI --- test/fixtures/erase-once-with-static.tsx | 2 -- test/fixtures/erase-once.tsx | 2 -- test/render.tsx | 11 ++++++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/fixtures/erase-once-with-static.tsx b/test/fixtures/erase-once-with-static.tsx index 9dfac4d72..b8669fa34 100644 --- a/test/fixtures/erase-once-with-static.tsx +++ b/test/fixtures/erase-once-with-static.tsx @@ -1,4 +1,3 @@ -import process from 'node:process'; import React, {useState} from 'react'; import {Box, Static, Text, render, useInput} from '../../src/index.js'; @@ -29,5 +28,4 @@ function Test() { ); } -process.stdout.rows = Number(process.argv[2]); render(); diff --git a/test/fixtures/erase-once.tsx b/test/fixtures/erase-once.tsx index 267702c0e..ee37d3d55 100644 --- a/test/fixtures/erase-once.tsx +++ b/test/fixtures/erase-once.tsx @@ -1,4 +1,3 @@ -import process from 'node:process'; import React, {useState} from 'react'; import {Box, Text, render, useInput} from '../../src/index.js'; @@ -23,5 +22,4 @@ function Test() { ); } -process.stdout.rows = Number(process.argv[2]); render(); diff --git a/test/render.tsx b/test/render.tsx index 9debe8109..7b5707560 100644 --- a/test/render.tsx +++ b/test/render.tsx @@ -18,7 +18,11 @@ const {spawn} = require('node-pty') as typeof import('node-pty'); const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); -const term = (fixture: string, args: string[] = []) => { +const term = ( + fixture: string, + args: string[] = [], + {rows}: {rows?: number} = {} +) => { let resolve: (value?: unknown) => void; let reject: (error: Error) => void; @@ -43,6 +47,7 @@ const term = (fixture: string, args: string[] = []) => { { name: 'xterm-color', cols: 100, + rows, cwd: __dirname, env } @@ -107,7 +112,7 @@ test.serial('erase screen', async t => { }); test.serial('erase screen once then continue rendering as usual', async t => { - const ps = term('erase-once', ['3']); + const ps = term('erase-once', [], {rows: 3}); await delay(1000); t.true(ps.output.includes(ansiEscapes.clearTerminal)); @@ -130,7 +135,7 @@ test.serial('erase screen once then continue rendering as usual', async t => { test.serial( 'erase screen once then continue rendering as usual with present', async t => { - const ps = term('erase-once-with-static', ['3']); + const ps = term('erase-once-with-static', [], {rows: 3}); await delay(1000); t.true(ps.output.includes(ansiEscapes.clearTerminal));