diff --git a/package.json b/package.json index f8d10b9b..7bffd8f0 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,8 @@ } }, "dependencies": { + "cli-table3": "^0.6.2", + "colors": "^1.4.0", "yargs": "17.5.1" } } diff --git a/src/CLIUtils.ts b/src/CLIUtils.ts index 34a72011..92cf2c3e 100644 --- a/src/CLIUtils.ts +++ b/src/CLIUtils.ts @@ -16,11 +16,13 @@ import { parse, createRunner } from '../lib/main.js'; import { readFileSync, readdirSync, lstatSync } from 'fs'; -import { join, isAbsolute, extname } from 'path'; +import { join, isAbsolute, extname, relative } from 'path'; import { pathToFileURL } from 'url'; import { cwd } from 'process'; import { PuppeteerRunnerOwningBrowserExtension } from '../lib/main.js'; import { Browser } from 'puppeteer'; +import Table from 'cli-table3'; +import colors from 'colors'; export function getJSONFilesFromFolder(path: string): string[] { return readdirSync(path) @@ -75,6 +77,60 @@ export function getHeadlessEnvVar(headless?: string) { } } +type Result = { + startedAt: Date; + file: string; + finishedAt: Date; + success: boolean; + title: string; +}; + +export function createStatusReport(results: Result[]): Table.Table { + const table = new Table({ + head: ['Title', 'Status', 'File', 'Duration'], + chars: { + top: '═', + 'top-mid': '╤', + 'top-left': '╔', + 'top-right': '╗', + bottom: '═', + 'bottom-mid': '╧', + 'bottom-left': '╚', + 'bottom-right': '╝', + left: '║', + 'left-mid': '╟', + mid: '─', + 'mid-mid': '┼', + right: '║', + 'right-mid': '╢', + middle: '│', + }, + style: { + head: ['bold'], + }, + }); + + const resultTextColor = colors.white; + for (const result of results) { + const row: string[] = []; + + const duration = + result.finishedAt?.getTime()! - result.startedAt.getTime() || 0; + const status = result.success + ? resultTextColor.bgGreen(' Success ') + : resultTextColor.bgRed(' Failure '); + + row.push(result.title); + row.push(status); + row.push(relative(process.cwd(), result.file)); + row.push(`${duration}ms`); + + table.push(row); + } + + return table; +} + export async function runFiles( files: string[], opts: { log: boolean; headless: boolean | 'chrome'; extension?: string } = { @@ -95,12 +151,24 @@ export async function runFiles( ); Extension = module.default; } + + const results: Result[] = []; for (const file of files) { + const result: Result = { + title: '', + startedAt: new Date(), + finishedAt: new Date(), + file, + success: true, + }; + opts.log && console.log(`Running ${file}...`); try { const content = readFileSync(file, 'utf-8'); const object = JSON.parse(content); const recording = parse(object); + result.title = recording.title; + const { default: puppeteer } = await import('puppeteer'); browser = await puppeteer.launch({ headless: opts.headless, @@ -112,9 +180,21 @@ export async function runFiles( opts.log && console.log(`Finished running ${file}`); } catch (err) { opts.log && console.error(`Error running ${file}`, err); - throw err; + result.success = false; } finally { + result.finishedAt = new Date(); + results.push(result); + await browser?.close(); } } + + if (opts.log) { + const statusReport = createStatusReport(results); + console.log(statusReport.toString()); + } + + if (results.every((result) => result.success)) return; + + throw new Error('Some recordings have failed to run.'); } diff --git a/test/cli.test.ts b/test/cli.test.ts index 1f21aa4c..9cf6db3c 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -15,6 +15,7 @@ */ import { + createStatusReport, runFiles, getHeadlessEnvVar, getRecordingPaths, @@ -23,6 +24,8 @@ import { import { assert } from 'chai'; import path from 'path'; import url from 'url'; +import { HorizontalTableRow } from 'cli-table3'; +import colors from 'colors'; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); @@ -114,4 +117,44 @@ describe('cli', () => { assert.isTrue(files.every((file) => file.endsWith('.json'))); }); }); + + describe('createStatusReport', () => { + it('is able to create a successful status report', () => { + const date = new Date(); + const result = { + startedAt: date, + file: path.join(__dirname, 'resources', 'replay-fail.json'), + finishedAt: new Date(date.getTime() + 1000), + success: true, + title: 'Test run', + }; + const [statusReport] = createStatusReport([result]); + const [title, status, file, duration] = + statusReport as HorizontalTableRow; + + assert.strictEqual(status, colors.white.bgGreen(' Success ')); + assert.strictEqual(duration, '1000ms'); + assert.isString(file); + assert.strictEqual(title, result.title); + }); + + it('is able to create a failed status report', () => { + const date = new Date(); + const result = { + startedAt: date, + file: path.join(__dirname, 'resources', 'replay-fail.json'), + finishedAt: date, + success: false, + title: 'Test run', + }; + const [statusReport] = createStatusReport([result]); + const [title, status, file, duration] = + statusReport as HorizontalTableRow; + + assert.strictEqual(status, colors.white.bgRed(' Failure ')); + assert.strictEqual(duration, '0ms'); + assert.isString(file); + assert.strictEqual(title, result.title); + }); + }); });