a post-component library for building user-interfaces on the web.
1import { createBirpc } from 'birpc'
2import * as fs from 'node:fs/promises'
3import * as path from 'node:path'
4import { fileURLToPath } from 'node:url'
5import { parseArgs, styleText } from 'node:util'
6import { create_browser_runtime } from './browser-runtime.ts'
7import { handle_coverage, type Coverage } from './coverage.ts'
8import * as devalue from './devalue.ts'
9import { create_node_runtime } from './node-runtime.ts'
10import type { ClientFunctions, TestResult } from './runtime.ts'
11
12export interface ServerFunctions {
13 report_result(result: TestResult): void
14}
15
16export interface Runtime {
17 port: MessagePort
18 coverage(): Promise<Coverage[]>
19 [Symbol.asyncDispose](): Promise<void>
20}
21
22const args = parseArgs({
23 options: {
24 bench: { type: 'boolean', short: 'b', default: false },
25 prod: { type: 'boolean', short: 'p', default: false },
26 filter: { type: 'string', short: 'f' },
27 },
28 allowPositionals: true,
29})
30
31const filter = args.values.filter !== undefined ? new RegExp(args.values.filter) : undefined
32
33const all_files: { [runtime: string]: string[] } = {}
34for (const arg of args.positionals) {
35 for await (const file of fs.glob(arg)) {
36 const runtime = file.includes('server') ? 'node' : 'browser'
37 ;(all_files[runtime] ??= []).push(file)
38 }
39}
40
41const results: TestResult[] = []
42const coverage: Coverage[] = []
43
44for (const [runtime, files] of Object.entries(all_files)) {
45 const rt = runtime === 'node' ? await create_node_runtime() : await create_browser_runtime()
46 await using _ = rt // workaround for https://issues.chromium.org/issues/409478039
47
48 const client = createBirpc<ClientFunctions, ServerFunctions>(
49 {
50 report_result(run) {
51 if (run.result === 'pass') {
52 console.log(styleText('green', 'PASS'), run.name, styleText('dim', `(${run.duration.toFixed(1)}ms)`))
53 } else {
54 console.log(styleText('red', 'FAIL'), run.name)
55 console.log(run.reason)
56 console.log()
57 }
58
59 results.push(run)
60 },
61 },
62 {
63 post: data => rt.port.postMessage(data),
64 on: fn => {
65 rt.port.onmessage = e => fn(e.data)
66 },
67 serialize: devalue.stringify,
68 deserialize: devalue.parse,
69 },
70 )
71
72 await client.define('__DEV__', !args.values.prod)
73
74 const here = path.join(fileURLToPath(import.meta.url), '..')
75 await Promise.all(files.map(file => client.import('./' + path.relative(here, file))))
76
77 if (args.values.bench) {
78 await client.run_benchmarks({ filter })
79 } else {
80 await client.run_tests({ filter })
81 }
82
83 await client.stop_coverage()
84 coverage.push(...(await rt.coverage()))
85}
86
87if (args.values.bench) {
88} else {
89 await handle_coverage(coverage)
90
91 if (results.length === 0) {
92 console.log('no tests found')
93 process.exitCode = 1
94 } else {
95 const passed = results.reduce((count, { result }) => count + (result === 'pass' ? 1 : 0), 0)
96 const failed = results.reduce((count, { result }) => count + (result === 'fail' ? 1 : 0), 0)
97
98 console.log()
99 console.log(`${passed} passed`)
100 if (failed) {
101 console.log(`${failed} failed`)
102 process.exitCode = 1
103 }
104 }
105}