functionalscript 0.46.1 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fjs/basen/cbase32/module.f.mjs +18 -10
- package/fjs/basen/cbase32/proof.f.d.mts +3 -0
- package/fjs/basen/cbase32/proof.f.mjs +24 -2
- package/fjs/basen/proof.f.mjs +8 -16
- package/fjs/bnf/ll1/proof.f.d.mts +2 -0
- package/fjs/bnf/ll1/proof.f.mjs +33 -18
- package/fjs/bnf/module.f.d.mts +2 -2
- package/fjs/bnf/module.f.mjs +2 -2
- package/fjs/cas/proof.f.mjs +4 -26
- package/fjs/ci/common/module.f.mjs +6 -0
- package/fjs/ci/config/module.f.d.mts +7 -7
- package/fjs/ci/config/module.f.mjs +7 -7
- package/fjs/cli/module.f.mjs +14 -10
- package/fjs/crypto/vdf/module.f.d.mts +0 -1
- package/fjs/crypto/vdf/module.f.mjs +3 -14
- package/fjs/dev/module.f.mjs +10 -20
- package/fjs/djs/parser/module.f.d.mts +101 -53
- package/fjs/djs/parser/module.f.mjs +732 -525
- package/fjs/djs/parser/proof.f.d.mts +6 -0
- package/fjs/djs/parser/proof.f.mjs +315 -11
- package/fjs/djs/parser/types.d.ts +29 -2
- package/fjs/djs/tokenizer/module.f.mjs +122 -105
- package/fjs/djs/tokenizer/proof.f.mjs +17 -0
- package/fjs/djs/transpiler/module.f.mjs +4 -5
- package/fjs/djs/transpiler/proof.f.d.mts +2 -0
- package/fjs/djs/transpiler/proof.f.mjs +25 -0
- package/fjs/edag/amnesia/module.f.d.mts +20 -0
- package/fjs/edag/amnesia/module.f.mjs +303 -0
- package/fjs/edag/amnesia/proof.f.d.mts +79 -0
- package/fjs/edag/amnesia/proof.f.mjs +573 -0
- package/fjs/edag/amnesia/types.d.ts +15 -0
- package/fjs/edag/module.f.d.mts +410 -0
- package/fjs/edag/module.f.mjs +559 -0
- package/fjs/edag/proof.f.d.mts +116 -0
- package/fjs/edag/proof.f.mjs +686 -0
- package/fjs/edag/types.d.ts +55 -0
- package/fjs/effects/node/memory/module.d.mts +37 -3
- package/fjs/effects/node/memory/module.mjs +38 -4
- package/fjs/effects/node/memory/proof.d.mts +5 -2
- package/fjs/effects/node/memory/proof.mjs +36 -6
- package/fjs/effects/node/module.d.mts +9 -1
- package/fjs/effects/node/module.f.d.mts +39 -0
- package/fjs/effects/node/module.f.mjs +46 -0
- package/fjs/effects/node/module.mjs +234 -32
- package/fjs/effects/node/types.d.ts +28 -2
- package/fjs/effects/node/virtual/module.f.d.mts +19 -10
- package/fjs/effects/node/virtual/module.f.mjs +246 -32
- package/fjs/effects/node/virtual/proof.f.d.mts +17 -2
- package/fjs/effects/node/virtual/proof.f.mjs +232 -8
- package/fjs/effects/node/virtual/types.d.ts +57 -1
- package/fjs/emergent_testing/browser/proof.d.mts +55 -0
- package/fjs/emergent_testing/browser/proof.mjs +352 -0
- package/fjs/emergent_testing/browser/species.proof.d.mts +4 -0
- package/fjs/emergent_testing/browser/species.proof.mjs +45 -0
- package/fjs/emergent_testing/browser.d.mts +79 -0
- package/fjs/emergent_testing/browser.mjs +432 -0
- package/fjs/emergent_testing/proof.f.d.mts +1 -1
- package/fjs/emergent_testing/proof.f.mjs +2 -2
- package/fjs/js/string_escape/module.f.d.mts +47 -0
- package/fjs/js/string_escape/module.f.mjs +95 -0
- package/fjs/js/string_escape/proof.f.d.mts +8 -0
- package/fjs/js/string_escape/proof.f.mjs +68 -0
- package/fjs/js/tokenizer/module.f.mjs +14 -12
- package/fjs/mcp/cas/module.f.d.mts +1 -7
- package/fjs/mcp/cas/module.f.mjs +24 -15
- package/fjs/mcp/evo/module.f.mjs +9 -24
- package/fjs/mcp/module.f.d.mts +3 -1
- package/fjs/mcp/module.f.mjs +4 -2
- package/fjs/media/json/rtti/module.f.d.mts +2 -2
- package/fjs/media/json/rtti/module.f.mjs +1 -1
- package/fjs/media/json/schema/module.f.d.mts +15 -5
- package/fjs/media/json/schema/module.f.mjs +42 -15
- package/fjs/media/json/schema/proof.f.d.mts +10 -1
- package/fjs/media/json/schema/proof.f.mjs +88 -16
- package/fjs/media/json/serializer/module.f.mjs +14 -26
- package/fjs/media/json/types.d.ts +1 -1
- package/fjs/media/lock/module.f.d.mts +8 -3
- package/fjs/media/lock/module.f.mjs +9 -3
- package/fjs/media/lock/proof.f.d.mts +1 -1
- package/fjs/media/lock/proof.f.mjs +2 -2
- package/fjs/media/lock/types.d.ts +1 -3
- package/fjs/media/module.f.d.mts +20 -13
- package/fjs/media/module.f.mjs +29 -16
- package/fjs/media/note/module.f.d.mts +14 -7
- package/fjs/media/note/module.f.mjs +14 -7
- package/fjs/media/note/proof.f.mjs +1 -1
- package/fjs/media/proof.f.mjs +5 -5
- package/fjs/media/revision/module.f.d.mts +8 -2
- package/fjs/media/revision/module.f.mjs +9 -3
- package/fjs/media/revision/proof.f.mjs +1 -1
- package/fjs/media/revision/types.d.ts +1 -1
- package/fjs/media/type/module.f.d.mts +20 -0
- package/fjs/media/type/module.f.mjs +74 -0
- package/fjs/media/type/proof.f.d.mts +5 -0
- package/fjs/media/type/proof.f.mjs +24 -1
- package/fjs/module.f.mjs +6 -0
- package/fjs/path/module.f.d.mts +60 -3
- package/fjs/path/module.f.mjs +138 -14
- package/fjs/path/proof.f.d.mts +3 -0
- package/fjs/path/proof.f.mjs +298 -2
- package/fjs/protocol/json_rpc/module.f.d.mts +65 -28
- package/fjs/protocol/json_rpc/module.f.mjs +49 -13
- package/fjs/protocol/json_rpc/proof.f.d.mts +4 -0
- package/fjs/protocol/json_rpc/proof.f.mjs +29 -0
- package/fjs/protocol/json_rpc/types.d.ts +12 -2
- package/fjs/protocol/mcp/module.f.d.mts +89 -54
- package/fjs/protocol/mcp/module.f.mjs +110 -56
- package/fjs/protocol/mcp/proof.f.d.mts +13 -0
- package/fjs/protocol/mcp/proof.f.mjs +107 -7
- package/fjs/protocol/mcp/stdio/module.f.mjs +3 -4
- package/fjs/protocol/mcp/stdio/proof.f.mjs +2 -2
- package/fjs/protocol/mcp/types.d.ts +14 -2
- package/fjs/text/utf8/module.f.mjs +27 -1
- package/fjs/text/utf8/proof.f.mjs +37 -0
- package/fjs/types/array/module.f.mjs +18 -11
- package/fjs/types/array/proof.f.mjs +27 -1
- package/fjs/types/bigfloat/module.f.d.mts +48 -9
- package/fjs/types/bigfloat/module.f.mjs +147 -59
- package/fjs/types/bigfloat/proof.f.d.mts +2 -0
- package/fjs/types/bigfloat/proof.f.mjs +136 -4
- package/fjs/types/bigfloat/types.d.ts +17 -0
- package/fjs/types/function/module.f.d.mts +11 -2
- package/fjs/types/function/module.f.mjs +20 -2
- package/fjs/types/function/proof.f.mjs +5 -1
- package/fjs/types/phantom/types.d.ts +21 -0
- package/fjs/types/result/module.f.d.mts +4 -4
- package/fjs/types/result/module.f.mjs +2 -2
- package/fjs/types/result/proof.f.d.mts +6 -2
- package/fjs/types/result/proof.f.mjs +17 -1
- package/fjs/types/rtti/common/module.f.d.mts +78 -5
- package/fjs/types/rtti/common/module.f.mjs +168 -3
- package/fjs/types/rtti/common/proof.f.d.mts +14 -0
- package/fjs/types/rtti/common/proof.f.mjs +61 -2
- package/fjs/types/rtti/common/types.d.ts +23 -1
- package/fjs/types/rtti/data/module.f.d.mts +57 -1
- package/fjs/types/rtti/data/module.f.mjs +241 -47
- package/fjs/types/rtti/data/proof.f.d.mts +10 -0
- package/fjs/types/rtti/data/proof.f.mjs +272 -39
- package/fjs/types/rtti/data/types.d.ts +14 -7
- package/fjs/types/rtti/host.proof.d.mts +33 -0
- package/fjs/types/rtti/host.proof.mjs +170 -0
- package/fjs/types/rtti/module.f.d.mts +51 -6
- package/fjs/types/rtti/module.f.mjs +50 -3
- package/fjs/types/rtti/parse/module.f.d.mts +31 -24
- package/fjs/types/rtti/parse/module.f.mjs +197 -59
- package/fjs/types/rtti/parse/proof.f.d.mts +37 -2
- package/fjs/types/rtti/parse/proof.f.mjs +171 -47
- package/fjs/types/rtti/proof.f.d.mts +6 -0
- package/fjs/types/rtti/proof.f.mjs +42 -1
- package/fjs/types/rtti/ts/module.f.d.mts +9 -3
- package/fjs/types/rtti/ts/module.f.mjs +68 -15
- package/fjs/types/rtti/ts/proof.f.d.mts +42 -2
- package/fjs/types/rtti/ts/proof.f.mjs +139 -6
- package/fjs/types/rtti/ts/types.d.ts +164 -16
- package/fjs/types/rtti/types.d.ts +31 -3
- package/fjs/types/rtti/validate/module.f.d.mts +42 -25
- package/fjs/types/rtti/validate/module.f.mjs +187 -53
- package/fjs/types/rtti/validate/proof.f.d.mts +28 -2
- package/fjs/types/rtti/validate/proof.f.mjs +430 -75
- package/fjs/types/ts/types.d.ts +4 -0
- package/fjs/web/module.f.d.mts +115 -0
- package/fjs/web/module.f.mjs +581 -0
- package/fjs/web/proof.f.d.mts +39 -0
- package/fjs/web/proof.f.mjs +413 -0
- package/fjs/web/types.d.ts +39 -0
- package/fjs/website/browser-prepare.d.mts +5 -0
- package/fjs/website/browser-prepare.mjs +115 -0
- package/fjs/website/browser-source.d.mts +37 -0
- package/fjs/website/browser-source.mjs +202 -0
- package/fjs/website/browser-source.proof.d.mts +31 -0
- package/fjs/website/browser-source.proof.mjs +162 -0
- package/fjs/website/browser.d.mts +5 -0
- package/fjs/website/browser.mjs +8 -0
- package/fjs/website/module.f.d.mts +3 -3
- package/fjs/website/module.f.mjs +62 -9
- package/fjs/website/proof.f.d.mts +4 -0
- package/fjs/website/proof.f.mjs +18 -3
- package/package.json +2 -3
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proofs for the browser runner.
|
|
3
|
+
*
|
|
4
|
+
* The runner reaches the page only through the root element it is handed, so
|
|
5
|
+
* the DOM stand-in below is enough to drive every rendering branch from Node —
|
|
6
|
+
* no headless browser, and no global `window`/`document` for these proofs to
|
|
7
|
+
* install and unset.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { runInNewContext } from 'node:vm'
|
|
11
|
+
|
|
12
|
+
import { assert, assertEq, assertNotNullish, assertStructurallySame } from '../../asserts/module.f.mjs'
|
|
13
|
+
import { renderBrowserReport, runBrowserProofs, startBrowserTests, startBrowserTestSources } from '../browser.mjs'
|
|
14
|
+
|
|
15
|
+
/** @typedef {{ readonly tag: string, readonly attributes: Map<string, string>, readonly ownerDocument: _Document, textContent: string, children: readonly _Element[], readonly setAttribute: (name: string, value: string) => void, readonly querySelector: (selector: string) => _Element | null, readonly replaceChildren: (...nodes: readonly _Element[]) => void, readonly append: (node: _Element) => void }} _Element */
|
|
16
|
+
/** @typedef {{ defaultView: _View | null, readonly createElement: (tag: string) => _Element }} _Document */
|
|
17
|
+
/** @typedef {{ events: readonly CustomEvent[], readonly dispatchEvent: (event: Event) => boolean, fjsBrowserTestReport?: Promise<unknown> }} _View */
|
|
18
|
+
|
|
19
|
+
/** @type {(node: _Element, name: string) => _Element | null} */
|
|
20
|
+
const find = (node, name) =>
|
|
21
|
+
node.attributes.has(name)
|
|
22
|
+
? node
|
|
23
|
+
: node.children.reduce(
|
|
24
|
+
(/** @type {_Element | null} */ acc, child) => acc ?? find(child, name),
|
|
25
|
+
null)
|
|
26
|
+
|
|
27
|
+
/** @type {(document: _Document, tag: string, attributes: readonly string[], states: string[]) => _Element} */
|
|
28
|
+
const element = (document, tag, attributes, states) => {
|
|
29
|
+
/** @type {_Element} */
|
|
30
|
+
const self = {
|
|
31
|
+
tag,
|
|
32
|
+
attributes: new Map(attributes.map(name => [name, ''])),
|
|
33
|
+
ownerDocument: document,
|
|
34
|
+
textContent: '',
|
|
35
|
+
children: [],
|
|
36
|
+
setAttribute: (name, value) => {
|
|
37
|
+
if (name === 'data-state') { states.push(value) }
|
|
38
|
+
self.attributes.set(name, value)
|
|
39
|
+
},
|
|
40
|
+
// The runner only ever queries an attribute selector of `[name]` form.
|
|
41
|
+
querySelector: selector => self.children.reduce(
|
|
42
|
+
(/** @type {_Element | null} */ acc, child) =>
|
|
43
|
+
acc ?? find(child, selector.slice(1, -1)),
|
|
44
|
+
null),
|
|
45
|
+
replaceChildren: (...nodes) => { self.children = nodes },
|
|
46
|
+
append: node => { self.children = [...self.children, node] },
|
|
47
|
+
}
|
|
48
|
+
return self
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds what the generated page gives the runner: a root carrying the summary
|
|
53
|
+
* paragraph and the result list. `states` records every `data-state` written,
|
|
54
|
+
* so a proof can check the whole progression and not just its last step.
|
|
55
|
+
*
|
|
56
|
+
* @type {(withView?: boolean) => { readonly root: Element, readonly summary: _Element, readonly results: _Element, readonly view: _View, readonly states: readonly string[] }}
|
|
57
|
+
*/
|
|
58
|
+
const page = (withView = true) => {
|
|
59
|
+
/** @type {string[]} */
|
|
60
|
+
const states = []
|
|
61
|
+
/** @type {_Document} */
|
|
62
|
+
const document = {
|
|
63
|
+
defaultView: null,
|
|
64
|
+
createElement: tag => element(document, tag, [], states),
|
|
65
|
+
}
|
|
66
|
+
/** @type {_View} */
|
|
67
|
+
const view = {
|
|
68
|
+
events: [],
|
|
69
|
+
dispatchEvent: event => {
|
|
70
|
+
view.events = [...view.events, /** @type {CustomEvent} */ (event)]
|
|
71
|
+
return true
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
if (withView) { document.defaultView = view }
|
|
75
|
+
const root = element(document, 'main', ['data-browser-tests'], states)
|
|
76
|
+
root.replaceChildren(
|
|
77
|
+
element(document, 'p', ['data-test-summary'], states),
|
|
78
|
+
element(document, 'ol', ['data-test-results'], states))
|
|
79
|
+
return {
|
|
80
|
+
root: /** @type {Element} */ (/** @type {unknown} */ (root)),
|
|
81
|
+
summary: assertNotNullish(root.querySelector('[data-test-summary]')),
|
|
82
|
+
results: assertNotNullish(root.querySelector('[data-test-results]')),
|
|
83
|
+
view,
|
|
84
|
+
states,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @type {(proof: unknown) => ReturnType<typeof runBrowserProofs>} */
|
|
89
|
+
const run = proof => runBrowserProofs([['proof', proof]])
|
|
90
|
+
|
|
91
|
+
/** @type {(element: _Element) => readonly (string | undefined)[]} */
|
|
92
|
+
const statuses = element => element.children.map(child => child.attributes.get('data-status'))
|
|
93
|
+
|
|
94
|
+
export const proof = {
|
|
95
|
+
namedThrow: async () => {
|
|
96
|
+
const named = { throw: () => { throw 'expected' } }.throw
|
|
97
|
+
const report = await run({ extracted: named })
|
|
98
|
+
assertEq(report.status, 'passed')
|
|
99
|
+
},
|
|
100
|
+
path: async () => {
|
|
101
|
+
const report = await run({ 'a.b': () => undefined })
|
|
102
|
+
assertEq(report.results[0]?.path, '["a.b"]')
|
|
103
|
+
},
|
|
104
|
+
arbitraryThrow: async () => {
|
|
105
|
+
const report = await run({ fail: () => { throw Object.create(null) } })
|
|
106
|
+
assertEq(report.status, 'failed')
|
|
107
|
+
assertEq(report.results[0]?.message, 'Unknown thrown value')
|
|
108
|
+
},
|
|
109
|
+
errorFields: async () => {
|
|
110
|
+
const error = new Proxy(new Error(), {
|
|
111
|
+
get: (target, property) => property === 'message' || property === 'stack'
|
|
112
|
+
? Symbol(property)
|
|
113
|
+
: Reflect.get(target, property),
|
|
114
|
+
})
|
|
115
|
+
const report = await run({ fail: () => { throw error } })
|
|
116
|
+
assertEq(report.results[0]?.message, 'Symbol(message)')
|
|
117
|
+
assertEq(report.results[0]?.stack, 'Symbol(stack)')
|
|
118
|
+
},
|
|
119
|
+
errorAccessorThrows: async () => {
|
|
120
|
+
const error = new Error('hidden')
|
|
121
|
+
Object.defineProperty(error, 'message', {
|
|
122
|
+
get: () => { throw new Error('message getter failed') },
|
|
123
|
+
})
|
|
124
|
+
const report = await run({ fail: () => { throw error } })
|
|
125
|
+
assertEq(report.status, 'failed')
|
|
126
|
+
assertEq(report.results[0]?.message, 'Unknown thrown value')
|
|
127
|
+
assertEq(report.results[0]?.stack, 'Unknown thrown value')
|
|
128
|
+
},
|
|
129
|
+
revokedErrorProxy: async () => {
|
|
130
|
+
const { proxy, revoke } = Proxy.revocable(new Error('revoked'), {})
|
|
131
|
+
revoke()
|
|
132
|
+
const report = await run({ fail: () => { throw proxy } })
|
|
133
|
+
assertEq(report.status, 'failed')
|
|
134
|
+
assertEq(report.results[0]?.message, 'Unknown thrown value')
|
|
135
|
+
},
|
|
136
|
+
crossRealmError: async () => {
|
|
137
|
+
// An Error from another realm is not `instanceof Error` here, and its
|
|
138
|
+
// stack is what the report exists to carry.
|
|
139
|
+
const other = runInNewContext(
|
|
140
|
+
'({ fail: () => { throw new Error(\'cross boom\') } })')
|
|
141
|
+
const report = await run({ fail: other.fail })
|
|
142
|
+
assertEq(report.results[0]?.message, 'cross boom')
|
|
143
|
+
const stack = report.results[0]?.stack ?? ''
|
|
144
|
+
assert(stack !== 'cross boom', stack)
|
|
145
|
+
assert(stack.includes('cross boom'), stack)
|
|
146
|
+
},
|
|
147
|
+
errorWithoutStack: async () => {
|
|
148
|
+
const error = new Error('no stack')
|
|
149
|
+
const report = await run({ fail: () => { throw Object.assign(error, { stack: undefined }) } })
|
|
150
|
+
assertEq(report.results[0]?.message, 'no stack')
|
|
151
|
+
assertEq(report.results[0]?.stack, 'no stack')
|
|
152
|
+
},
|
|
153
|
+
expectedThrow: async () => {
|
|
154
|
+
const report = await run({ throw: { silent: () => undefined } })
|
|
155
|
+
assertEq(report.status, 'failed')
|
|
156
|
+
assertEq(report.results[0]?.message, 'Expected the proof to throw')
|
|
157
|
+
},
|
|
158
|
+
crossRealmPromise: async () => {
|
|
159
|
+
// A promise built in another realm is not `instanceof Promise`. The
|
|
160
|
+
// runner has to await it anyway and walk the tree it resolves to,
|
|
161
|
+
// otherwise a rejected cross-realm promise is reported as a pass.
|
|
162
|
+
const other = runInNewContext('({ resolve: value => Promise.resolve(value) })')
|
|
163
|
+
const report = await run({
|
|
164
|
+
nested: () => other.resolve({ child: () => { throw 'boom' } }),
|
|
165
|
+
})
|
|
166
|
+
assertEq(report.totals.tests, 2)
|
|
167
|
+
assertEq(report.totals.failed, 1)
|
|
168
|
+
assertEq(report.results[1]?.path, '.nested().child')
|
|
169
|
+
},
|
|
170
|
+
spoofedPromiseTag: async () => {
|
|
171
|
+
const report = await run({
|
|
172
|
+
nested: () => ({
|
|
173
|
+
[Symbol.toStringTag]: 'Promise',
|
|
174
|
+
then: /** @type {(...args: (() => void)[]) => void} */ ((...args) => { args[0]?.() }),
|
|
175
|
+
}),
|
|
176
|
+
})
|
|
177
|
+
assertEq(report.totals.tests, 2)
|
|
178
|
+
assertEq(report.results[1]?.path, '.nested().then')
|
|
179
|
+
},
|
|
180
|
+
frozenPromiseTag: async () => {
|
|
181
|
+
// A non-extensible spoof leaves the runner nothing to shadow, the same
|
|
182
|
+
// dead end a pinned promise reaches. It is still an ordinary proof
|
|
183
|
+
// tree, so it is walked rather than reported as a brand-check failure.
|
|
184
|
+
const report = await run({
|
|
185
|
+
nested: () => Object.freeze({
|
|
186
|
+
[Symbol.toStringTag]: 'Promise',
|
|
187
|
+
then: () => undefined,
|
|
188
|
+
}),
|
|
189
|
+
})
|
|
190
|
+
assertEq(report.totals.tests, 2)
|
|
191
|
+
assertEq(report.totals.failed, 0)
|
|
192
|
+
assertEq(report.results[1]?.path, '.nested().then')
|
|
193
|
+
},
|
|
194
|
+
exportedTreeThrows: async () => {
|
|
195
|
+
// The exported tree is read before any test runs, and reading it runs
|
|
196
|
+
// user code as well. The module fails; the page still gets its report.
|
|
197
|
+
const p = page()
|
|
198
|
+
const report = await startBrowserTests(p.root,
|
|
199
|
+
[['m', { get bad() { throw new Error('enumerating') } }]])
|
|
200
|
+
assertEq(report.status, 'failed')
|
|
201
|
+
assertStructurallySame({ ...report.totals }, { tests: 1, passed: 0, failed: 1 })
|
|
202
|
+
assertEq(report.results[0]?.module, 'm')
|
|
203
|
+
assertEq(report.results[0]?.message, 'enumerating')
|
|
204
|
+
assertStructurallySame([...p.states], ['running', 'failed'])
|
|
205
|
+
assertEq(p.view.events.length, 1)
|
|
206
|
+
},
|
|
207
|
+
returnedTreeThrows: async () => {
|
|
208
|
+
// Reading the returned tree runs user code. When it throws, the test
|
|
209
|
+
// that produced the value fails and the page still reaches a terminal
|
|
210
|
+
// state — a rejected run would leave it in `running` forever.
|
|
211
|
+
const p = page()
|
|
212
|
+
const report = await startBrowserTests(p.root,
|
|
213
|
+
[['m', { nested: () => ({ get bad() { throw new Error('getter') } }) }]])
|
|
214
|
+
assertEq(report.status, 'failed')
|
|
215
|
+
assertStructurallySame({ ...report.totals }, { tests: 1, passed: 0, failed: 1 })
|
|
216
|
+
assertEq(report.results[0]?.message, 'getter')
|
|
217
|
+
assertStructurallySame([...p.states], ['running', 'failed'])
|
|
218
|
+
assertEq(p.view.events.length, 1)
|
|
219
|
+
},
|
|
220
|
+
speciesResultIsNotAPromise: async () => {
|
|
221
|
+
// `then` builds its result through `constructor[Symbol.species]`, and a
|
|
222
|
+
// promise can make that an ordinary object. The run has to answer with
|
|
223
|
+
// the promise it subscribed to, not with what `then` handed back, or
|
|
224
|
+
// the test ends before the promise settles and the species object
|
|
225
|
+
// itself lands in the report.
|
|
226
|
+
const species = function (/** @type {(...args: (() => void)[]) => void} */ executor) {
|
|
227
|
+
executor(() => undefined, () => undefined)
|
|
228
|
+
return { notAPromise: true }
|
|
229
|
+
}
|
|
230
|
+
const promised = new Promise(resolve =>
|
|
231
|
+
setTimeout(resolve, 1, { child: () => { throw 'boom' } }))
|
|
232
|
+
Object.defineProperty(promised, 'constructor',
|
|
233
|
+
{ value: { [Symbol.species]: species }, configurable: true })
|
|
234
|
+
const report = await run({ nested: () => promised })
|
|
235
|
+
assertEq(report.totals.tests, 2)
|
|
236
|
+
assertEq(report.totals.failed, 1)
|
|
237
|
+
assertEq(report.results[1]?.path, '.nested().child')
|
|
238
|
+
},
|
|
239
|
+
reportingThrows: async () => {
|
|
240
|
+
// Announcing a result as it lands is the page's own rendering. It must
|
|
241
|
+
// not take the run down with it: the report is what the page waits for.
|
|
242
|
+
const report = await runBrowserProofs([['m', { t: () => undefined }]],
|
|
243
|
+
() => { throw new Error('render') })
|
|
244
|
+
assertEq(report.status, 'passed')
|
|
245
|
+
assertEq(report.totals.passed, 1)
|
|
246
|
+
},
|
|
247
|
+
thenIsATestName: async () => {
|
|
248
|
+
// A `then` proof entry is a test called `then`, never a thenable for
|
|
249
|
+
// the runner to adopt.
|
|
250
|
+
const report = await run({ then: () => undefined })
|
|
251
|
+
assertEq(report.totals.tests, 1)
|
|
252
|
+
assertEq(report.results[0]?.path, '.then')
|
|
253
|
+
},
|
|
254
|
+
batches: async () => {
|
|
255
|
+
// More leaves than one batch holds, so the batch loop recurses.
|
|
256
|
+
const report = await run(Object.fromEntries(
|
|
257
|
+
Array.from({ length: 30 }, (_, index) => [`t${index}`, () => undefined])))
|
|
258
|
+
assertEq(report.totals.tests, 30)
|
|
259
|
+
assertEq(report.totals.passed, 30)
|
|
260
|
+
},
|
|
261
|
+
render: async () => {
|
|
262
|
+
const p = page()
|
|
263
|
+
const report = await startBrowserTests(p.root,
|
|
264
|
+
[['m', { ok: () => undefined, bad: () => { throw 'x' } }]])
|
|
265
|
+
assertEq(report.status, 'failed')
|
|
266
|
+
assertStructurallySame([...p.states], ['running', 'failed'])
|
|
267
|
+
assertEq(p.summary.textContent, `1 passed, 1 failed (${report.duration.toFixed(1)} ms)`)
|
|
268
|
+
assertStructurallySame([...statuses(p.results)], ['passed', 'failed'])
|
|
269
|
+
const event = assertNotNullish(p.view.events[0])
|
|
270
|
+
assertEq(event.type, 'fjs-browser-test-complete')
|
|
271
|
+
assertEq(event.detail, report)
|
|
272
|
+
assertEq(await p.view.fjsBrowserTestReport, report)
|
|
273
|
+
},
|
|
274
|
+
renderWithoutView: async () => {
|
|
275
|
+
// A detached document has no window: the run still renders, and
|
|
276
|
+
// nothing is published or announced.
|
|
277
|
+
const p = page(false)
|
|
278
|
+
const report = await startBrowserTests(p.root, [['m', { ok: () => undefined }]])
|
|
279
|
+
assertEq(report.status, 'passed')
|
|
280
|
+
assertEq(p.summary.textContent, `1 passed, 0 failed (${report.duration.toFixed(1)} ms)`)
|
|
281
|
+
assertEq(p.view.events.length, 0)
|
|
282
|
+
assertEq(p.view.fjsBrowserTestReport, undefined)
|
|
283
|
+
},
|
|
284
|
+
renderReport: () => {
|
|
285
|
+
// The renderer is exported on its own for a controller that already
|
|
286
|
+
// holds a report.
|
|
287
|
+
const p = page()
|
|
288
|
+
renderBrowserReport(p.root, {
|
|
289
|
+
status: 'passed',
|
|
290
|
+
browser: 'test',
|
|
291
|
+
totals: { tests: 1, passed: 1, failed: 0 },
|
|
292
|
+
duration: 1,
|
|
293
|
+
results: [{ module: 'm', path: '.t', status: 'passed', duration: 0.5 }],
|
|
294
|
+
})
|
|
295
|
+
assertEq(p.summary.textContent, '1 passed, 0 failed (1.0 ms)')
|
|
296
|
+
assertEq(p.results.children[0]?.textContent, 'PASS m .t (0.5 ms)')
|
|
297
|
+
},
|
|
298
|
+
sources: async () => {
|
|
299
|
+
const p = page()
|
|
300
|
+
const report = await startBrowserTestSources(p.root, ['a.mjs', 'b.mjs'],
|
|
301
|
+
source => Promise.resolve({ proof: { [source]: () => undefined } }))
|
|
302
|
+
assertEq(report.status, 'passed')
|
|
303
|
+
assertEq(report.totals.tests, 2)
|
|
304
|
+
assertStructurallySame([...p.states], ['loading', 'running', 'passed'])
|
|
305
|
+
assertEq(await p.view.fjsBrowserTestReport, report)
|
|
306
|
+
},
|
|
307
|
+
sourcesProgress: async () => {
|
|
308
|
+
const p = page()
|
|
309
|
+
/** @type {(module: { readonly proof?: unknown }) => void} */
|
|
310
|
+
let release = () => undefined
|
|
311
|
+
/** @type {Promise<{ readonly proof?: unknown }>} */
|
|
312
|
+
const pending = new Promise(resolve => { release = resolve })
|
|
313
|
+
const done = startBrowserTestSources(p.root, ['a.mjs', 'b.mjs'],
|
|
314
|
+
source => source === 'a.mjs' ? Promise.resolve({ proof: {} }) : pending)
|
|
315
|
+
await Promise.resolve()
|
|
316
|
+
await Promise.resolve()
|
|
317
|
+
assertEq(p.summary.textContent, 'Loading 1/2: a.mjs')
|
|
318
|
+
release({ proof: {} })
|
|
319
|
+
assertEq((await done).status, 'passed')
|
|
320
|
+
},
|
|
321
|
+
sourcesImporterThrows: async () => {
|
|
322
|
+
// An importer that throws before it returns a promise is a loader
|
|
323
|
+
// failure like any other: the page must not be left in `loading` with
|
|
324
|
+
// no report and no completion event.
|
|
325
|
+
const p = page()
|
|
326
|
+
const report = await startBrowserTestSources(p.root, ['bad.mjs'],
|
|
327
|
+
source => { throw new Error(`no loader for ${source}`) })
|
|
328
|
+
assertEq(report.status, 'infrastructure-error')
|
|
329
|
+
assertStructurallySame({ ...report.totals }, { tests: 1, passed: 0, failed: 1 })
|
|
330
|
+
assertEq(report.results[0]?.message, 'no loader for bad.mjs')
|
|
331
|
+
assertStructurallySame([...p.states], ['loading', 'infrastructure-error'])
|
|
332
|
+
assertEq(p.view.events.length, 1)
|
|
333
|
+
},
|
|
334
|
+
sourcesLoadFailure: async () => {
|
|
335
|
+
const p = page()
|
|
336
|
+
const report = await startBrowserTestSources(p.root, ['ok.mjs', 'bad.mjs'],
|
|
337
|
+
source => source === 'bad.mjs'
|
|
338
|
+
? Promise.reject(new Error('offline'))
|
|
339
|
+
: Promise.resolve({ proof: { t: () => undefined } }))
|
|
340
|
+
assertEq(report.status, 'infrastructure-error')
|
|
341
|
+
// The totals have to agree with `results`: a consumer reading
|
|
342
|
+
// `0 of 0` would take a broken suite for an empty one.
|
|
343
|
+
assertStructurallySame({ ...report.totals }, { tests: 1, passed: 0, failed: 1 })
|
|
344
|
+
assertEq(report.results[0]?.module, 'bad.mjs')
|
|
345
|
+
assertEq(report.results[0]?.message, 'offline')
|
|
346
|
+
assertStructurallySame([...p.states], ['loading', 'infrastructure-error'])
|
|
347
|
+
assert(p.summary.textContent.startsWith('Infrastructure error: 1 failed to load'),
|
|
348
|
+
p.summary.textContent)
|
|
349
|
+
assertStructurallySame([...statuses(p.results)], ['failed'])
|
|
350
|
+
assertEq(p.view.events.length, 1)
|
|
351
|
+
},
|
|
352
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { assertEq } from '../../asserts/module.f.mjs'
|
|
2
|
+
import { runBrowserProofs } from '../browser.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A genuine promise whose `then` always throws: the result promise is built
|
|
6
|
+
* through `constructor[Symbol.species]`, and this `constructor` has none to
|
|
7
|
+
* give. `configurable` decides whether the runner can shadow the property for
|
|
8
|
+
* the length of one subscription.
|
|
9
|
+
*
|
|
10
|
+
* @type {(configurable: boolean) => Promise<unknown>}
|
|
11
|
+
*/
|
|
12
|
+
const throwingSpeciesPromise = configurable => {
|
|
13
|
+
const promised = Promise.resolve({
|
|
14
|
+
child: () => { throw 'boom' },
|
|
15
|
+
})
|
|
16
|
+
const constructor = {}
|
|
17
|
+
Object.defineProperty(constructor, Symbol.species, {
|
|
18
|
+
get: () => { throw new Error('species') },
|
|
19
|
+
})
|
|
20
|
+
Object.defineProperty(promised, 'constructor', { value: constructor, configurable })
|
|
21
|
+
return promised
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @type {(promised: Promise<unknown>) => ReturnType<typeof runBrowserProofs>} */
|
|
25
|
+
const run = promised => runBrowserProofs([['proof', { nested: () => promised }]])
|
|
26
|
+
|
|
27
|
+
export const proof = {
|
|
28
|
+
throwingSpecies: async () => {
|
|
29
|
+
// The intrinsic Promise shadows the hostile `constructor` while the
|
|
30
|
+
// handlers are attached, so the resolved sub-tree still runs.
|
|
31
|
+
const report = await run(throwingSpeciesPromise(true))
|
|
32
|
+
assertEq(report.totals.tests, 2)
|
|
33
|
+
assertEq(report.totals.failed, 1)
|
|
34
|
+
assertEq(report.results[1]?.path, '.nested().child')
|
|
35
|
+
},
|
|
36
|
+
pinnedThrowingSpecies: async () => {
|
|
37
|
+
// Nothing to shadow, so the promise can never be subscribed to. The
|
|
38
|
+
// test that produced it fails, rather than passing on a result the
|
|
39
|
+
// runner never observed.
|
|
40
|
+
const report = await run(throwingSpeciesPromise(false))
|
|
41
|
+
assertEq(report.totals.tests, 1)
|
|
42
|
+
assertEq(report.totals.failed, 1)
|
|
43
|
+
assertEq(report.results[0]?.message, 'species')
|
|
44
|
+
},
|
|
45
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-native proof execution and report rendering.
|
|
3
|
+
*
|
|
4
|
+
* The module deliberately has no Node dependencies: generated applications
|
|
5
|
+
* import it directly as an ES module in the browser.
|
|
6
|
+
* Proof failures resolve the published report with `status: 'failed'`; an
|
|
7
|
+
* automated outer controller is responsible for consuming that status and
|
|
8
|
+
* choosing a nonzero process exit code.
|
|
9
|
+
*
|
|
10
|
+
* Every DOM entry point reaches the page through the `root` element it is
|
|
11
|
+
* given — `root.ownerDocument` and its `defaultView` — never through the
|
|
12
|
+
* runner realm's own `window`/`document`. A page embedding the suite in an
|
|
13
|
+
* iframe therefore renders into that frame, and a proof can drive the module
|
|
14
|
+
* with a stand-in root.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*
|
|
18
|
+
* @import { _TestAndPath } from './types.ts'
|
|
19
|
+
*/
|
|
20
|
+
export type _BrowserTestResult = {
|
|
21
|
+
readonly module: string;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly status: string;
|
|
24
|
+
readonly duration: number;
|
|
25
|
+
readonly message?: string;
|
|
26
|
+
readonly stack?: string;
|
|
27
|
+
};
|
|
28
|
+
export type BrowserTestReport = {
|
|
29
|
+
readonly status: string;
|
|
30
|
+
readonly browser: string;
|
|
31
|
+
readonly totals: {
|
|
32
|
+
readonly tests: number;
|
|
33
|
+
readonly passed: number;
|
|
34
|
+
readonly failed: number;
|
|
35
|
+
};
|
|
36
|
+
readonly duration: number;
|
|
37
|
+
readonly results: readonly _BrowserTestResult[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Runs named proof exports and returns the serializable browser report.
|
|
41
|
+
*
|
|
42
|
+
* @type {(modules: readonly (readonly [string, unknown])[], result?: (result: _BrowserTestResult) => void) => Promise<BrowserTestReport>}
|
|
43
|
+
*/
|
|
44
|
+
export declare const runBrowserProofs: (modules: readonly (readonly [string, unknown])[], result?: (result: _BrowserTestResult) => void) => Promise<BrowserTestReport>;
|
|
45
|
+
export type _BrowserImporter = (source: string) => Promise<{
|
|
46
|
+
readonly proof?: unknown;
|
|
47
|
+
}>;
|
|
48
|
+
export type _LoadedModule = {
|
|
49
|
+
readonly status: 'loaded';
|
|
50
|
+
readonly source: string;
|
|
51
|
+
readonly proof: unknown;
|
|
52
|
+
} | {
|
|
53
|
+
readonly status: 'error';
|
|
54
|
+
readonly source: string;
|
|
55
|
+
readonly error: unknown;
|
|
56
|
+
};
|
|
57
|
+
export type _TestWindow = Window & {
|
|
58
|
+
fjsBrowserTestReport?: Promise<BrowserTestReport>;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Loads proof modules after the page has rendered, reporting module-loading
|
|
62
|
+
* progress before proof execution begins.
|
|
63
|
+
*
|
|
64
|
+
* @type {(root: Element, sources: readonly string[], importer: _BrowserImporter) => Promise<BrowserTestReport>}
|
|
65
|
+
*/
|
|
66
|
+
export declare const startBrowserTestSources: (root: Element, sources: readonly string[], importer: _BrowserImporter) => Promise<BrowserTestReport>;
|
|
67
|
+
/**
|
|
68
|
+
* Renders a completed report in the browser test page.
|
|
69
|
+
*
|
|
70
|
+
* @type {(root: Element, report: BrowserTestReport) => void}
|
|
71
|
+
*/
|
|
72
|
+
export declare const renderBrowserReport: (root: Element, report: BrowserTestReport) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Runs the application, publishes its promise as `window.fjsBrowserTestReport`,
|
|
75
|
+
* and dispatches `fjs-browser-test-complete` with the report in `detail`.
|
|
76
|
+
*
|
|
77
|
+
* @type {(root: Element, modules: readonly (readonly [string, unknown])[]) => Promise<BrowserTestReport>}
|
|
78
|
+
*/
|
|
79
|
+
export declare const startBrowserTests: (root: Element, modules: readonly (readonly [string, unknown])[]) => Promise<BrowserTestReport>;
|