@applicvision/js-toolbox
Version:
A collection of tools for modern JavaScript development
247 lines (209 loc) • 5.22 kB
JavaScript
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)
}