bak
This commit is contained in:
1 parent
94e9846c13
commit
e4aa0dce86
11 files changed
+2227
-32
No files matched your search
@@ -0,0 +1,185 @@
|
||||
export interface TestResult {
|
||||
suite: string
|
||||
name: string
|
||||
ok: boolean
|
||||
error?: string
|
||||
stack?: string
|
||||
durationMs: number
|
||||
}
|
||||
|
||||
interface SuiteDef {
|
||||
name: string
|
||||
tests: { name: string; fn: () => void | Promise<void> }[]
|
||||
beforeEach?: () => void | Promise<void>
|
||||
afterEach?: () => void | Promise<void>
|
||||
}
|
||||
|
||||
const suites: SuiteDef[] = []
|
||||
let current: SuiteDef | null = null
|
||||
|
||||
export function suite(name: string, fn: () => void): void {
|
||||
const prev = current
|
||||
current = { name, tests: [] }
|
||||
try {
|
||||
fn()
|
||||
suites.push(current)
|
||||
} finally {
|
||||
current = prev
|
||||
}
|
||||
}
|
||||
|
||||
export function test(name: string, fn: () => void | Promise<void>): void {
|
||||
if (!current) throw new Error(`test("${name}") called outside of suite()`)
|
||||
current.tests.push({ name, fn })
|
||||
}
|
||||
|
||||
export function beforeEach(fn: () => void | Promise<void>): void {
|
||||
if (!current) throw new Error('beforeEach outside of suite()')
|
||||
current.beforeEach = fn
|
||||
}
|
||||
|
||||
export function afterEach(fn: () => void | Promise<void>): void {
|
||||
if (!current) throw new Error('afterEach outside of suite()')
|
||||
current.afterEach = fn
|
||||
}
|
||||
|
||||
class Expectation {
|
||||
constructor(private value: unknown) {}
|
||||
|
||||
toBe(expected: unknown): void {
|
||||
if (!Object.is(this.value, expected)) {
|
||||
throw new Error(`expected ${this.fmt(expected)}, got ${this.fmt(this.value)}`)
|
||||
}
|
||||
}
|
||||
|
||||
toEqual(expected: unknown): void {
|
||||
const a = JSON.stringify(this.value)
|
||||
const b = JSON.stringify(expected)
|
||||
if (a !== b) throw new Error(`expected ${b}, got ${a}`)
|
||||
}
|
||||
|
||||
toBeCloseTo(expected: number, precision = 4): void {
|
||||
const v = this.value as number
|
||||
const eps = Math.pow(10, -precision) / 2
|
||||
if (Math.abs(v - expected) > eps) {
|
||||
throw new Error(`expected ≈${expected} (p=${precision}), got ${v}`)
|
||||
}
|
||||
}
|
||||
|
||||
toBeTruthy(): void {
|
||||
if (!this.value) throw new Error(`expected truthy, got ${this.fmt(this.value)}`)
|
||||
}
|
||||
|
||||
toBeFalsy(): void {
|
||||
if (this.value) throw new Error(`expected falsy, got ${this.fmt(this.value)}`)
|
||||
}
|
||||
|
||||
toBeNull(): void {
|
||||
if (this.value !== null) throw new Error(`expected null, got ${this.fmt(this.value)}`)
|
||||
}
|
||||
|
||||
toBeDefined(): void {
|
||||
if (this.value === undefined) throw new Error('expected defined value, got undefined')
|
||||
}
|
||||
|
||||
toBeGreaterThan(n: number): void {
|
||||
if (!((this.value as number) > n)) throw new Error(`expected > ${n}, got ${this.value}`)
|
||||
}
|
||||
|
||||
toBeLessThan(n: number): void {
|
||||
if (!((this.value as number) < n)) throw new Error(`expected < ${n}, got ${this.value}`)
|
||||
}
|
||||
|
||||
toContain(sub: string): void {
|
||||
const s = this.value as string
|
||||
if (!s || !s.includes(sub)) {
|
||||
throw new Error(`expected string to contain ${JSON.stringify(sub)}`)
|
||||
}
|
||||
}
|
||||
|
||||
toHaveLength(n: number): void {
|
||||
const l = (this.value as { length: number }).length
|
||||
if (l !== n) throw new Error(`expected length ${n}, got ${l}`)
|
||||
}
|
||||
|
||||
private fmt(v: unknown): string {
|
||||
if (v === undefined) return 'undefined'
|
||||
try {
|
||||
return JSON.stringify(v)
|
||||
} catch {
|
||||
return String(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function expect(value: unknown): Expectation {
|
||||
return new Expectation(value)
|
||||
}
|
||||
|
||||
function cleanStack(stack: string | undefined): string | undefined {
|
||||
if (!stack) return undefined
|
||||
const lines = stack.split('\n')
|
||||
const filtered = lines.filter((l) => {
|
||||
if (l.includes('/testRunner.ts')) return false
|
||||
if (l.includes('/testRunner.js')) return false
|
||||
if (l.includes('node_modules')) return false
|
||||
return true
|
||||
})
|
||||
return filtered.slice(0, 10).join('\n')
|
||||
}
|
||||
|
||||
export interface RunOptions {
|
||||
onProgress?: (done: number, total: number) => void
|
||||
onTestStart?: (suite: string, name: string) => void
|
||||
interTestDelayMs?: number
|
||||
}
|
||||
|
||||
export async function runAll(
|
||||
register: () => void,
|
||||
opts: RunOptions = {},
|
||||
): Promise<TestResult[]> {
|
||||
suites.length = 0
|
||||
register()
|
||||
const total = suites.reduce((a, s) => a + s.tests.length, 0)
|
||||
const results: TestResult[] = []
|
||||
let done = 0
|
||||
const delay = Math.max(0, opts.interTestDelayMs ?? 0)
|
||||
for (const s of suites) {
|
||||
for (const t of s.tests) {
|
||||
opts.onTestStart?.(s.name, t.name)
|
||||
await new Promise((r) => requestAnimationFrame(() => r(null)))
|
||||
const start = performance.now()
|
||||
try {
|
||||
if (s.beforeEach) await s.beforeEach()
|
||||
await t.fn()
|
||||
if (s.afterEach) await s.afterEach()
|
||||
results.push({
|
||||
suite: s.name,
|
||||
name: t.name,
|
||||
ok: true,
|
||||
durationMs: performance.now() - start,
|
||||
})
|
||||
} catch (err) {
|
||||
try {
|
||||
if (s.afterEach) await s.afterEach()
|
||||
} catch {
|
||||
/* ignore cleanup errors if test already failed */
|
||||
}
|
||||
const e = err as Error
|
||||
results.push({
|
||||
suite: s.name,
|
||||
name: t.name,
|
||||
ok: false,
|
||||
error: e.message || String(err),
|
||||
stack: cleanStack(e.stack),
|
||||
durationMs: performance.now() - start,
|
||||
})
|
||||
}
|
||||
done++
|
||||
opts.onProgress?.(done, total)
|
||||
if (delay > 0) await new Promise((r) => setTimeout(r, delay))
|
||||
else await new Promise((r) => setTimeout(r, 0))
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
Reference in new issue
Block a user