Skip to content

Commit

Permalink
add additional tests for last frame rendering scenarios
Browse files Browse the repository at this point in the history
  • Loading branch information
nickwesselman committed Aug 29, 2024
1 parent 7d11de2 commit 6084c86
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 21 deletions.
6 changes: 3 additions & 3 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Props = {
readonly writeToStderr: (data: string) => void;
readonly exitOnCtrlC: boolean;
readonly onExit: (error?: Error) => void;
readonly debug: boolean;
readonly hasCursor: boolean;
};

type State = {
Expand Down Expand Up @@ -128,13 +128,13 @@ export default class App extends PureComponent<Props, State> {
}

override componentDidMount() {
if (!this.props.debug) {
if (this.props.hasCursor) {
cliCursor.hide(this.props.stdout);
}
}

override componentWillUnmount() {
if (!this.props.debug) {
if (this.props.hasCursor) {
cliCursor.show(this.props.stdout);
}

Expand Down
25 changes: 12 additions & 13 deletions src/ink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export default class Ink {
// This variable is used only in debug mode to store full static output
// so that it's rerendered every time, not just new static parts, like in non-debug mode
private fullStaticOutput: string;
// CI environments and piped output don't handle erasing ansi escapes well,
// so it's better to only render last frame of non-static output
private readonly renderLastFrameOnly: boolean;
private exitPromise?: Promise<void>;
private restoreConsole?: () => void;
private readonly unsubscribeResize?: () => void;
Expand All @@ -67,12 +70,8 @@ export default class Ink {
leading: true,
trailing: true,
});

// CI environments and piped output don't handle erasing ansi escapes well,
// so it's better to only render last frame of non-static output
if (options.renderLastFrameOnly === undefined) {
options.renderLastFrameOnly = isInCi || !options.stdout.isTTY;
}
this.renderLastFrameOnly =
options.renderLastFrameOnly ?? (isInCi || !options.stdout.isTTY);

// Ignore last render after unmounting a tree to prevent empty output before exit
this.isUnmounted = false;
Expand Down Expand Up @@ -114,7 +113,7 @@ export default class Ink {
this.patchConsole();
}

if (!options.renderLastFrameOnly) {
if (!this.renderLastFrameOnly) {
options.stdout.on('resize', this.resized);

this.unsubscribeResize = () => {
Expand Down Expand Up @@ -165,7 +164,7 @@ export default class Ink {
return;
}

if (this.options.renderLastFrameOnly) {
if (this.renderLastFrameOnly) {
if (hasStaticOutput) {
this.options.stdout.write(staticOutput);
}
Expand Down Expand Up @@ -209,7 +208,7 @@ export default class Ink {
writeToStdout={this.writeToStdout}
writeToStderr={this.writeToStderr}
exitOnCtrlC={this.options.exitOnCtrlC}
debug={this.options.debug}
hasCursor={!this.options.debug && !this.renderLastFrameOnly}
onExit={this.unmount}
>
{node}
Expand All @@ -229,7 +228,7 @@ export default class Ink {
return;
}

if (this.options.renderLastFrameOnly) {
if (this.renderLastFrameOnly) {
this.options.stdout.write(data);
return;
}
Expand All @@ -250,7 +249,7 @@ export default class Ink {
return;
}

if (this.options.renderLastFrameOnly) {
if (this.renderLastFrameOnly) {
this.options.stderr.write(data);
return;
}
Expand Down Expand Up @@ -278,7 +277,7 @@ export default class Ink {
this.unsubscribeResize();
}

if (this.options.renderLastFrameOnly) {
if (this.renderLastFrameOnly) {
this.options.stdout.write(this.lastOutput + '\n');
} else if (!this.options.debug) {
this.log.done();
Expand Down Expand Up @@ -306,7 +305,7 @@ export default class Ink {
}

clear(): void {
if (!this.options.renderLastFrameOnly && !this.options.debug) {
if (!this.renderLastFrameOnly && !this.options.debug) {
this.log.clear();
}
}
Expand Down
111 changes: 108 additions & 3 deletions test/components.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import EventEmitter from 'node:events';
import test from 'ava';
import chalk from 'chalk';
import React, {Component, useState} from 'react';
import {spy} from 'sinon';
import React, {Component, useEffect, useState} from 'react';
import {spy, type SinonSpy} from 'sinon';
import ansiEscapes from 'ansi-escapes';
import {
Box,
Expand Down Expand Up @@ -645,7 +645,7 @@ test('render only last frame when run in CI', async t => {
t.true(output.includes('Counter: 5'));
});

test('render all frames if CI environment variable equals false', async t => {
test('render all frames if CI environment variable equals false and there is an output TTY', async t => {
const output = await run('ci', {
// eslint-disable-next-line @typescript-eslint/naming-convention
env: {CI: 'false'},
Expand All @@ -657,6 +657,111 @@ test('render all frames if CI environment variable equals false', async t => {
}
});

function TestLastFrame(props: {
readonly continueTest: () => void;
readonly message1: string;
readonly message2: string;
}) {
const [message, setMessage] = useState(props.message1);
useEffect(() => {
setTimeout(() => {
setMessage(props.message2);
props.continueTest();
}, 1);
}, [message]);
return <Text>{message}</Text>;
}

test('render only last frame when run without an output tty', async t => {
// Create without a TTY
const stdout = createStdout(undefined, false);
const message1 = 'Hello';
const message2 = 'World';

let continueTest = () => {};
const synchronizer = new Promise<void>((resolve, _reject) => {
continueTest = resolve;
});

const {unmount} = render(
<TestLastFrame
continueTest={continueTest}
message1={message1}
message2={message2}
/>,
{
stdout,
},
);
await synchronizer;
unmount();

const writeSpy = stdout.write as SinonSpy;
t.true(writeSpy.calledOnce);
t.true((writeSpy.firstCall.args[0] as string).includes(message2));
});

test('render only last frame when enabled in options', async t => {
const stdout = createStdout();
const message1 = 'Hello';
const message2 = 'World';

let continueTest = () => {};
const synchronizer = new Promise<void>((resolve, _reject) => {
continueTest = resolve;
});

const {unmount} = render(
<TestLastFrame
continueTest={continueTest}
message1={message1}
message2={message2}
/>,
{
stdout,
renderLastFrameOnly: true,
},
);
await synchronizer;
unmount();

const writeSpy = stdout.write as SinonSpy;
t.true(writeSpy.calledOnce);
t.true((writeSpy.firstCall.args[0] as string).includes(message2));
});

test('render all frames when enabled in options', async t => {
// Create without a TTY so last frame is the default
const stdout = createStdout(undefined, false);
const message1 = 'Hello';
const message2 = 'World';

let continueTest = () => {};
const synchronizer = new Promise<void>((resolve, _reject) => {
continueTest = resolve;
});

const {unmount} = render(
<TestLastFrame
continueTest={continueTest}
message1={message1}
message2={message2}
/>,
{
stdout,
// Override the default
renderLastFrameOnly: false,
},
);
await synchronizer;
unmount();

const writeSpy = stdout.write as SinonSpy;
t.true(writeSpy.calledTwice);
t.true((writeSpy.firstCall.args[0] as string).includes(message1));
t.true((writeSpy.secondCall.args[0] as string).includes(message2));
});

test('reset prop when it’s removed from the element', t => {
const stdout = createStdout();

Expand Down
4 changes: 2 additions & 2 deletions test/helpers/create-stdout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ type FakeStdout = {
get: () => string;
} & NodeJS.WriteStream;

const createStdout = (columns?: number): FakeStdout => {
const createStdout = (columns?: number, isTTY?: boolean): FakeStdout => {
const stdout = new EventEmitter() as unknown as FakeStdout;
stdout.columns = columns ?? 100;

const write = spy();
stdout.write = write;
stdout.isTTY = true;
stdout.isTTY = isTTY ?? true;

stdout.get = () => write.lastCall.args[0] as string;

Expand Down

0 comments on commit 6084c86

Please sign in to comment.