UNPKG

@applicvision/js-toolbox

Version:

A collection of tools for modern JavaScript development

247 lines (209 loc) 5.22 kB
import { AssertionError } from 'node:assert' import readline from 'node:readline' import { setTimeout } from 'node:timers/promises' import style from '@applicvision/js-toolbox/style' import { DefaultLogger, TestTimeoutError } from '@applicvision/js-toolbox/test' import { intercept } from '@applicvision/js-toolbox/function-spy' export class NodeLogger extends DefaultLogger { suiteLogger(suite) { const logger = new SuiteLogger(suite) logger.start() return logger } testLogger(test) { const logger = new TestLogger(test) logger.start() return logger } } export class SimpleLogger extends DefaultLogger { suiteLogger(suite) { const logger = new SimpleSuiteLogger(suite) logger.start() return logger } testLogger(test) { const logger = new SimpleTestLogger(test) logger.start() return logger } } class SimpleTestLogger { constructor(test, suite) { this.test = test this.suite = suite } start() { const { todo, skip } = this.test if (skip) { console.log(style.cyan(`- ${this.test.description}`)) } else if (todo) { console.log(style.blue(`TODO: ${this.test.description}`)) } } finished() { const { error, description, duration } = this.test if (error) { TestLogger.logError(description, error) } else { console.log( style.bold.green('✓'), style.gray(description), duration > 50 ? `(${duration} ms)` : '' ) } } static logError(title, error) { console.group(style.bold.red('✗'), title) console.log() if (error instanceof AssertionError) { console.error(error.message) error.file && console.log(style.dim(error.file)) } else if (error instanceof TestTimeoutError) { console.error(error.message) } else { console.error(error) } console.log() console.groupEnd() } } class TestLogger extends SimpleTestLogger { #animationController = { stop() { } } start() { super.start() const { todo, skip } = this.test if (!skip && !todo) { this.#startRunning() } } #startRunning() { this.logInterceptor = intercept( console, ['log', 'warn', 'info', 'error', 'trace'], (mock) => { mock.restore() this.#animationController.stop() console.group(style.dim(`⌄ ${this.test.description}`)) }) animateText(this.test.description, spinnerCharacters, ((this.test.suite?.level ?? 0)) * 2, this.#animationController) } finished() { if (this.logInterceptor.called) { console.groupEnd() } else { this.#animationController.stop() this.logInterceptor.restore() } super.finished() } } class SimpleSuiteLogger { constructor(suite) { this.suite = suite } start() { console.log() if (!this.suite.shouldRun) { console.log(style.bold.cyan(`- ${this.suite.description}`)) console.log() return } if (this.suite.level === 1) { console.log(style.bold.italic(this.suite.description)) } else { console.log(style.bold(this.suite.description)) } console.group() } finished() { console.log('') console.groupEnd() } } class SuiteLogger extends SimpleSuiteLogger { #animationController = { stop() { }, updateText() { } } async startHooks(hookName, count) { this.abortHookLog = new AbortController() try { await setTimeout(500, true, { signal: this.abortHookLog.signal }) } catch (error) { if (error.code == 'ABORT_ERR') { return } throw error } this.logInterceptor = intercept( console, ['log', 'warn', 'info', 'error', 'trace'], () => this.#animationController.stop(), { times: 1 } ) animateText(style.dim(`Running ${hookName} 1/${count}`), spinnerCharacters, this.suite.level * 2, this.#animationController ) } finishedHook(hookName, finishedCount, totalCount) { this.#animationController.updateText(style.dim(`Running ${hookName} ${finishedCount + 1}/${totalCount}`)) } finishedHooks(hookName) { this.abortHookLog?.abort() this.logInterceptor?.restore() this.#animationController.stop() } } const simpleCharacters = ['-', '\\', '|', '/'] const spinnerCharacters = [ '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏' ] const barCharacters = ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "▊", "▋", "▌", "▍", "▎" ] async function animateText(text, characterSet, indentation, controller) { let running = true const startTime = Date.now() controller.stop = (clear = true) => { running = false if (clear) clearLine() } controller.updateText = newText => { text = newText } for (let index = 0; running; index = (index + 1) % characterSet.length) { const character = characterSet[index] const duration = Math.floor((Date.now() - startTime) / 1000) const timeString = duration >= 2 ? ` (${duration}s)` : '' const indentationAndSpinner = `${' '.repeat(indentation)}${characterSet[index]}` const slicedText = text.slice(0, process.stdout.columns - indentationAndSpinner.length - timeString.length - 3) const stringToWrite = `${' '.repeat(indentation)}${character} ${slicedText}${timeString}` clearLine() process.stdout.write(stringToWrite) await setTimeout(100) } } function clearLine() { readline.cursorTo(process.stdout, 0) readline.clearLine(process.stdout, 0) }