Files
ArtPlayer/test/site-console.test.js
T

178 lines
6.4 KiB
JavaScript

import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- Native console subscription and frozen build boundaries.
import test from 'node:test'
import ts from 'typescript'
import { generateConsole, moduleRanges, obsoleteMap, upstreamSha256 } from '../scripts/site-vendor/console/build.ts'
import { errorArgument } from '../scripts/site-vendor/console/runtime/errors.ts'
import { css } from '../scripts/site-vendor/console/runtime/style.ts'
import { createSubscriptions } from '../scripts/site-vendor/console/runtime/subscriptions.ts'
function fixture() {
const jobs = new Map()
let id = 0
const calls = []
const parsed = []
const clock = {
setTimeout(fn) {
jobs.set(++id, fn)
return id
},
clearTimeout(key) { jobs.delete(key) },
}
const original = function (...args) {
calls.push({ receiver: this, args })
return 123
}
const target = { log: original }
const subscribe = createSubscriptions(target, ['log'], (method, data) => {
parsed.push({ method, data })
return { method, data }
}, clock)
const flush = () => {
for (const [key, fn] of [...jobs]) {
jobs.delete(key)
fn()
}
}
return { jobs, calls, parsed, target, original, subscribe, flush, clock }
}
test('Console subscriptions forward once with the original receiver and independently remove either viewer', () => {
for (const first of [true, false]) {
const f = fixture()
const a = []
const b = []
const offA = f.subscribe(log => a.push(log))
const hook = f.target.log
const offB = f.subscribe(log => b.push(log))
assert.equal(f.target.log, hook)
const receiver = {}
const object = { nested: true }
assert.equal(f.target.log.call(receiver, object), undefined)
assert.equal(f.calls.length, 1)
assert.equal(f.calls[0].receiver, receiver)
f.flush()
assert.equal(f.parsed.length, 1)
assert.equal(a[0].data[0], object)
assert.equal(b[0].data[0], object)
;(first ? offA : offB)()
f.target.log('survivor')
f.flush()
assert.equal(first ? b.length : a.length, 2)
;(first ? offB : offA)()
assert.equal(f.target.log, f.original)
assert(!('feed' in f.target))
}
})
test('Console subscriptions cancel abandoned logs and do not replay prior logs into a new viewer', () => {
const f = fixture()
const a = []
const b = []
const offA = f.subscribe(log => a.push(log))
f.target.log('old')
const offB = f.subscribe(log => b.push(log))
offA()
f.flush()
assert.deepEqual([a, b], [[], []])
f.target.log('cancelled')
offB()
assert.equal(f.jobs.size, 0)
const off = f.subscribe(log => a.push(log))
f.target.log('new')
f.flush()
assert.deepEqual(a.map(log => log.data[0]), ['new'])
off()
})
test('Console ownership preserves preexisting metadata and wrappers installed later', () => {
const f = fixture()
const previous = { pointers: { unrelated: true } }
const rows = []
Object.defineProperty(f.target, 'feed', { configurable: true, value: previous, writable: true, enumerable: false })
const before = Object.getOwnPropertyDescriptor(f.target, 'feed')
const off = f.subscribe(log => rows.push(log))
const own = f.target.log
const foreign = function (...args) {
return own.apply(this, args)
}
f.target.log = foreign
off()
assert.equal(f.target.log, foreign)
assert.deepEqual(Object.getOwnPropertyDescriptor(f.target, 'feed'), before)
f.target.log('after-dispose')
assert.equal(f.calls.length, 1)
assert.equal(f.jobs.size, 0)
assert.deepEqual(rows, [])
const again = f.subscribe(log => rows.push(log))
f.target.log('again')
f.flush()
assert.equal(rows.length, 1)
again()
assert.equal(f.target.log, foreign)
})
test('Console installation rolls back on a readonly method and keeps other viewers live after a callback throws', () => {
const f = fixture()
Object.defineProperty(f.target, 'error', { value() {}, configurable: true })
const subscribe = createSubscriptions(f.target, ['log', 'error'], () => false, f.clock)
assert.throws(() => subscribe(() => {}), TypeError)
assert.equal(f.target.log, f.original)
assert(!('feed' in f.target))
const failure = new Error('listener failed')
const rows = []
const offA = f.subscribe(() => {
throw failure
})
const offB = f.subscribe(log => rows.push(log))
f.target.log('delivery')
assert.throws(f.flush, error => error === failure)
assert.equal(rows.length, 1)
offA()
offB()
})
test('Console error adaptation keeps existing stacks and arbitrary objects, and adds missing native messages without mutation', () => {
const error = new Error('message')
const object = { stack: 'custom', message: 'keep-object' }
assert.equal(errorArgument(object), object)
assert.equal(errorArgument(error), error)
error.stack = 'method@file:1:2'
assert.equal(errorArgument(error), 'Error: message\nmethod@file:1:2')
assert.equal(error.stack, 'method@file:1:2')
error.message = 'method'
assert.equal(errorArgument(error), 'Error: method\nmethod@file:1:2')
Object.defineProperty(error, 'message', {
get() { throw new Error('getter') },
})
assert.equal(errorArgument(error), error)
})
test('Console build changes only the two owned module bodies and is deterministic', async () => {
const source = fs.readFileSync('refactor/baselines/site-vendor/console-original.js', 'utf8')
assert.equal(createHash('sha256').update(source).digest('hex'), upstreamSha256)
const mask = (text) => {
for (const [id, range] of [...moduleRanges(text)].sort((a, b) => b[1].start - a[1].start))
text = text.slice(0, range.start) + id + text.slice(range.end)
return text
}
const candidate = await generateConsole(process.cwd())
const range = moduleRanges(source).get('W5CS')
const view = ts.createSourceFile('view.js', source.slice(range.start, range.end), ts.ScriptTarget.Latest, true)
const styles = []
function visit(node) {
if (ts.isStringLiteral(node) && node.text.includes('position: relative;'))
styles.push(node.text)
ts.forEachChild(node, visit)
}
visit(view)
assert.deepEqual(styles, [css])
assert.equal(mask(candidate), mask(source).slice(0, -obsoleteMap.length))
assert.equal(candidate, await generateConsole(process.cwd()))
assert.notEqual(candidate, source)
assert.throws(() => moduleRanges('var unrelated = 1'), /Missing owned/)
})