mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-06 12:57:46 +00:00
[cli] Add full stdio mockability for unit tests (#8052)
This PR is a follow-up to #8039, which provides an intuitive syntax for writing unit tests for interactive CLI commands. The heart of this is the new `await expect(stream).toOutput(test)` custom Jest matcher, which is intended for use with the mock Client `stdout` and `stderr` stream properties. The `test` is a string that will wait for the stream to output via "data" events until a match is found, or it will timeout (after 3 seconds by default). The timeout error has nice Jest-style formatting so that you can easily identify what was output: <img width="553" alt="Screen Shot 2022-06-29 at 10 33 06 PM" src="https://user-images.githubusercontent.com/71256/176600324-cb1ebecb-e891-42d9-bdc9-4864d3594a8c.png"> Below is an example of a unit test that was added for an interactive `vc login` session: ```typescript it('should allow login via email', async () => { const user = useUser(); const exitCodePromise = login(client); // Wait for login interactive prompt await expect(client.stderr).toOutput(`> Log in to Vercel`); // Move down to "Email" option client.stdin.write('\x1B[B'); // Down arrow client.stdin.write('\x1B[B'); // Down arrow client.stdin.write('\x1B[B'); // Down arrow client.stdin.write('\r'); // Return key // Wait for email input prompt await expect(client.stderr).toOutput('> Enter your email address:'); // Write user email address into prompt client.stdin.write(`${user.email}\n`); // Wait for login success message await expect(client.stderr).toOutput( `Success! Email authentication complete for ${user.email}` ); // Assert that the `login()` command returned 0 exit code await expect(exitCodePromise).resolves.toEqual(0); }); ``` **Note:** as a consequence of this PR, prompts are now written to stderr instead of stdout, so this change _may_ be considered a breaking change, in which case we should tag it for major release.
This commit is contained in:
@@ -1,41 +1,39 @@
|
||||
import chalk from 'chalk';
|
||||
import renderLink from './link';
|
||||
import wait, { StopSpinner } from './wait';
|
||||
import { Writable } from 'stream';
|
||||
import type { WritableTTY } from '../../types';
|
||||
|
||||
export interface OutputOptions {
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export interface PrintOptions {
|
||||
w?: Writable;
|
||||
}
|
||||
|
||||
export interface LogOptions extends PrintOptions {
|
||||
export interface LogOptions {
|
||||
color?: typeof chalk;
|
||||
}
|
||||
|
||||
export class Output {
|
||||
stream: WritableTTY;
|
||||
debugEnabled: boolean;
|
||||
private spinnerMessage: string;
|
||||
private _spinner: StopSpinner | null;
|
||||
isTTY: boolean;
|
||||
|
||||
constructor({ debug: debugEnabled = false }: OutputOptions = {}) {
|
||||
constructor(
|
||||
stream: WritableTTY,
|
||||
{ debug: debugEnabled = false }: OutputOptions = {}
|
||||
) {
|
||||
this.stream = stream;
|
||||
this.debugEnabled = debugEnabled;
|
||||
this.spinnerMessage = '';
|
||||
this._spinner = null;
|
||||
this.isTTY = process.stdout.isTTY || false;
|
||||
}
|
||||
|
||||
isDebugEnabled = () => {
|
||||
return this.debugEnabled;
|
||||
};
|
||||
|
||||
print = (str: string, { w }: PrintOptions = { w: process.stderr }) => {
|
||||
print = (str: string) => {
|
||||
this.stopSpinner();
|
||||
const stream: Writable = w || process.stderr;
|
||||
stream.write(str);
|
||||
this.stream.write(str);
|
||||
};
|
||||
|
||||
log = (str: string, color = chalk.grey) => {
|
||||
@@ -111,11 +109,17 @@ export class Output {
|
||||
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
|
||||
return;
|
||||
}
|
||||
if (this.isTTY) {
|
||||
if (this.stream.isTTY) {
|
||||
if (this._spinner) {
|
||||
this._spinner.text = message;
|
||||
} else {
|
||||
this._spinner = wait(message, delay);
|
||||
this._spinner = wait(
|
||||
{
|
||||
text: message,
|
||||
stream: this.stream,
|
||||
},
|
||||
delay
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.print(`${message}\n`);
|
||||
@@ -157,7 +161,3 @@ export class Output {
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
export default function createOutput(opts?: OutputOptions) {
|
||||
return new Output(opts);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user