mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-06 04:22:01 +00:00
[cli] Rewrite Unit tests to TypeScript + Jest (#6638)
* Rewrites the CLI unit tests to be TypeScript and use Jest (consistent with the unit tests in the other packages in this repo). * The file structure of the new test files mirrors that of the `src` directory, so that it's easy to find the relevant tests. * Code coverage is also properly set up through Jest - you can already see a big increase in coverage from this PR. * Adds a mock API server framework, with the intention of making it possible to write granular tests of the CLI commands. Using mocks also has the benefit of not requiring `VERCEL_TOKEN` env var to be set, so 3rd party PRs will be able to have their tests run. Ultimately this will also help with test coverage, since we will be writing unit tests that test the commands' code directly. * Converts `Output` into a proper class (which is subclassed for the mocks).
This commit is contained in:
@@ -2,45 +2,54 @@ import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import renderLink from './link';
|
||||
import wait, { StopSpinner } from './wait';
|
||||
|
||||
export type Output = ReturnType<typeof _createOutput>;
|
||||
import { Writable } from 'stream';
|
||||
|
||||
export interface OutputOptions {
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
// Singleton
|
||||
let instance: Output | null = null;
|
||||
|
||||
export default function createOutput(opts?: OutputOptions) {
|
||||
if (!instance) {
|
||||
instance = _createOutput(opts);
|
||||
}
|
||||
return instance;
|
||||
export interface PrintOptions {
|
||||
w?: Writable;
|
||||
}
|
||||
|
||||
function _createOutput({ debug: debugEnabled = false }: OutputOptions = {}) {
|
||||
let spinnerMessage = '';
|
||||
let spinner: StopSpinner | null = null;
|
||||
export interface LogOptions extends PrintOptions {
|
||||
color?: typeof chalk;
|
||||
}
|
||||
|
||||
function isDebugEnabled() {
|
||||
return debugEnabled;
|
||||
export class Output {
|
||||
private debugEnabled: boolean;
|
||||
private spinnerMessage: string;
|
||||
private _spinner: StopSpinner | null;
|
||||
|
||||
constructor({ debug: debugEnabled = false }: OutputOptions = {}) {
|
||||
this.debugEnabled = debugEnabled;
|
||||
this.spinnerMessage = '';
|
||||
this._spinner = null;
|
||||
}
|
||||
|
||||
function print(str: string) {
|
||||
stopSpinner();
|
||||
process.stderr.write(str);
|
||||
get isTTY() {
|
||||
return process.stdout.isTTY;
|
||||
}
|
||||
|
||||
function log(str: string, color = chalk.grey) {
|
||||
print(`${color('>')} ${str}\n`);
|
||||
}
|
||||
isDebugEnabled = () => {
|
||||
return this.debugEnabled;
|
||||
};
|
||||
|
||||
function dim(str: string, color = chalk.grey) {
|
||||
print(`${color(`> ${str}`)}\n`);
|
||||
}
|
||||
print = (str: string, { w }: PrintOptions = { w: process.stderr }) => {
|
||||
this.stopSpinner();
|
||||
const stream: Writable = w || process.stderr;
|
||||
stream.write(str);
|
||||
};
|
||||
|
||||
function warn(
|
||||
log = (str: string, color = chalk.grey) => {
|
||||
this.print(`${color('>')} ${str}\n`);
|
||||
};
|
||||
|
||||
dim = (str: string, color = chalk.grey) => {
|
||||
this.print(`${color(`> ${str}`)}\n`);
|
||||
};
|
||||
|
||||
warn = (
|
||||
str: string,
|
||||
slug: string | null = null,
|
||||
link: string | null = null,
|
||||
@@ -48,10 +57,10 @@ function _createOutput({ debug: debugEnabled = false }: OutputOptions = {}) {
|
||||
options?: {
|
||||
boxen?: boxen.Options;
|
||||
}
|
||||
) {
|
||||
) => {
|
||||
const details = slug ? `https://err.sh/vercel/${slug}` : link;
|
||||
|
||||
print(
|
||||
this.print(
|
||||
boxen(
|
||||
chalk.bold.yellow('WARN! ') +
|
||||
str +
|
||||
@@ -68,112 +77,99 @@ function _createOutput({ debug: debugEnabled = false }: OutputOptions = {}) {
|
||||
}
|
||||
)
|
||||
);
|
||||
print('\n');
|
||||
}
|
||||
this.print('\n');
|
||||
};
|
||||
|
||||
function note(str: string) {
|
||||
log(chalk`{yellow.bold NOTE:} ${str}`);
|
||||
}
|
||||
note = (str: string) => {
|
||||
this.log(chalk`{yellow.bold NOTE:} ${str}`);
|
||||
};
|
||||
|
||||
function error(
|
||||
error = (
|
||||
str: string,
|
||||
slug?: string,
|
||||
link?: string,
|
||||
action = 'Learn More'
|
||||
) {
|
||||
print(`${chalk.red(`Error!`)} ${str}\n`);
|
||||
) => {
|
||||
this.print(`${chalk.red(`Error!`)} ${str}\n`);
|
||||
const details = slug ? `https://err.sh/vercel/${slug}` : link;
|
||||
if (details) {
|
||||
print(`${chalk.bold(action)}: ${renderLink(details)}\n`);
|
||||
this.print(`${chalk.bold(action)}: ${renderLink(details)}\n`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function prettyError(
|
||||
prettyError = (
|
||||
err: Pick<Error, 'message'> & { link?: string; action?: string }
|
||||
) {
|
||||
return error(err.message, undefined, err.link, err.action);
|
||||
}
|
||||
) => {
|
||||
return this.error(err.message, undefined, err.link, err.action);
|
||||
};
|
||||
|
||||
function ready(str: string) {
|
||||
print(`${chalk.cyan('> Ready!')} ${str}\n`);
|
||||
}
|
||||
ready = (str: string) => {
|
||||
this.print(`${chalk.cyan('> Ready!')} ${str}\n`);
|
||||
};
|
||||
|
||||
function success(str: string) {
|
||||
print(`${chalk.cyan('> Success!')} ${str}\n`);
|
||||
}
|
||||
success = (str: string) => {
|
||||
this.print(`${chalk.cyan('> Success!')} ${str}\n`);
|
||||
};
|
||||
|
||||
function debug(str: string) {
|
||||
if (debugEnabled) {
|
||||
log(
|
||||
debug = (str: string) => {
|
||||
if (this.debugEnabled) {
|
||||
this.log(
|
||||
`${chalk.bold('[debug]')} ${chalk.gray(
|
||||
`[${new Date().toISOString()}]`
|
||||
)} ${str}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setSpinner(message: string, delay: number = 300): void {
|
||||
spinnerMessage = message;
|
||||
if (debugEnabled) {
|
||||
debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
|
||||
spinner = (message: string, delay: number = 300): void => {
|
||||
this.spinnerMessage = message;
|
||||
if (this.debugEnabled) {
|
||||
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
|
||||
return;
|
||||
}
|
||||
if (spinner) {
|
||||
spinner.text = message;
|
||||
if (this._spinner) {
|
||||
this._spinner.text = message;
|
||||
} else {
|
||||
spinner = wait(message, delay);
|
||||
this._spinner = wait(message, delay);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function stopSpinner() {
|
||||
if (debugEnabled && spinnerMessage) {
|
||||
const msg = `Spinner stopped (${spinnerMessage})`;
|
||||
spinnerMessage = '';
|
||||
debug(msg);
|
||||
stopSpinner = () => {
|
||||
if (this.debugEnabled && this.spinnerMessage) {
|
||||
const msg = `Spinner stopped (${this.spinnerMessage})`;
|
||||
this.spinnerMessage = '';
|
||||
this.debug(msg);
|
||||
}
|
||||
if (spinner) {
|
||||
spinner();
|
||||
spinner = null;
|
||||
spinnerMessage = '';
|
||||
if (this._spinner) {
|
||||
this._spinner();
|
||||
this._spinner = null;
|
||||
this.spinnerMessage = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function time<T>(
|
||||
time = async <T>(
|
||||
label: string | ((r?: T) => string),
|
||||
fn: Promise<T> | (() => Promise<T>)
|
||||
) {
|
||||
) => {
|
||||
const promise = typeof fn === 'function' ? fn() : fn;
|
||||
|
||||
if (debugEnabled) {
|
||||
if (this.debugEnabled) {
|
||||
const startLabel = typeof label === 'function' ? label() : label;
|
||||
debug(startLabel);
|
||||
this.debug(startLabel);
|
||||
const start = Date.now();
|
||||
const r = await promise;
|
||||
const endLabel = typeof label === 'function' ? label(r) : label;
|
||||
const duration = Date.now() - start;
|
||||
const durationPretty =
|
||||
duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(2)}s`;
|
||||
debug(`${endLabel} ${chalk.gray(`[${durationPretty}]`)}`);
|
||||
this.debug(`${endLabel} ${chalk.gray(`[${durationPretty}]`)}`);
|
||||
return r;
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
return {
|
||||
isDebugEnabled,
|
||||
print,
|
||||
log,
|
||||
warn,
|
||||
error,
|
||||
prettyError,
|
||||
ready,
|
||||
success,
|
||||
debug,
|
||||
dim,
|
||||
time,
|
||||
note,
|
||||
spinner: setSpinner,
|
||||
stopSpinner,
|
||||
};
|
||||
}
|
||||
|
||||
export default function createOutput(opts?: OutputOptions) {
|
||||
return new Output(opts);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user