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 collect_coverage = !args.values.bench
46 const rt =
47 runtime === 'node'
48 ? await create_node_runtime({ collect_coverage })
49 : await create_browser_runtime({ collect_coverage })
50 await using _ = rt // workaround for https://issues.chromium.org/issues/409478039
51
52 const client = createBirpc<ClientFunctions, ServerFunctions>(
53 {
54 report_result(run) {
55 if (run.result === 'pass') {
56 console.log(styleText('green', 'PASS'), run.name, styleText('dim', `(${run.duration.toFixed(1)}ms)`))
57 } else {
58 console.log(styleText('red', 'FAIL'), run.name)
59 console.log(run.reason)
60 console.log()
61 }
62
63 results.push(run)
64 },
65 },
66 {
67 post: data => rt.port.postMessage(data),
68 on: fn => {
69 rt.port.onmessage = e => fn(e.data)
70 },
71 serialize: devalue.stringify,
72 deserialize: devalue.parse,
73 },
74 )
75
76 await client.define('__DEV__', !args.values.prod)
77
78 const here = path.join(fileURLToPath(import.meta.url), '..')
79 await Promise.all(files.map(file => client.import('./' + path.relative(here, file))))
80
81 if (args.values.bench) {
82 await client.run_benchmarks({ filter })
83 } else {
84 await client.run_tests({ filter })
85 await client.stop_coverage()
86 coverage.push(...(await rt.coverage()))
87 }
88}
89
90if (args.values.bench) {
91} else {
92 await handle_coverage(coverage)
93
94 if (results.length === 0) {
95 console.log('no tests found')
96 process.exitCode = 1
97 } else {
98 const passed = results.reduce((count, { result }) => count + (result === 'pass' ? 1 : 0), 0)
99 const failed = results.reduce((count, { result }) => count + (result === 'fail' ? 1 : 0), 0)
100
101 console.log()
102 console.log(`${passed} passed`)
103 if (failed) {
104 console.log(`${failed} failed`)
105 process.exitCode = 1
106 }
107 }
108}