@exodus/test 1.0.0-rc.69 → 1.0.0-rc.70
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/bin/browsers.js +4 -2
- package/bin/index.js +19 -14
- package/bin/reporter.js +1 -0
- package/bundler/bundle.js +75 -43
- package/bundler/modules/fs.cjs +37 -26
- package/bundler/modules/globals.cjs +52 -26
- package/bundler/modules/text-encoding-utf.cjs +16 -3
- package/bundler/modules/tty.cjs +10 -0
- package/bundler/modules/util-format.cjs +6 -1
- package/package.json +4 -3
- package/src/engine.pure.cjs +4 -0
- package/src/jest.js +3 -1
package/bin/browsers.js
CHANGED
|
@@ -29,7 +29,7 @@ export const close = () => Promise.all(Object.values(launched).map((p) => p.then
|
|
|
29
29
|
|
|
30
30
|
async function newPage(runner, browser, { binary, dropNetwork }) {
|
|
31
31
|
const context = await (browser.newContext ? browser.newContext() : browser.createBrowserContext())
|
|
32
|
-
if (dropNetwork && context.setOffline) await context.setOffline(true)
|
|
32
|
+
if (dropNetwork && context.setOffline && binary !== 'webkit') await context.setOffline(true) // WebKit crashes if this is done prior to navigation to /dev/null
|
|
33
33
|
let page
|
|
34
34
|
try {
|
|
35
35
|
page = await context.newPage()
|
|
@@ -40,6 +40,9 @@ async function newPage(runner, browser, { binary, dropNetwork }) {
|
|
|
40
40
|
return newPage(runner, browser, { binary, dropNetwork })
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
await page.goto('file:///dev/null') // Need to load a secure origin for e.g. crypto.subtle to be available
|
|
44
|
+
|
|
45
|
+
if (dropNetwork && context.setOffline) await context.setOffline(true)
|
|
43
46
|
if (dropNetwork && page.setOfflineMode) await page.setOfflineMode(true)
|
|
44
47
|
assert(!dropNetwork || context.setOffline || page.setOfflineMode)
|
|
45
48
|
return { context, page }
|
|
@@ -55,7 +58,6 @@ export async function run(runner, args, { binary, devtools, dropNetwork, timeout
|
|
|
55
58
|
assert(Object.hasOwn(launchers, runner), 'Unexpected runner')
|
|
56
59
|
if (!launched[runner]) launched[runner] = launchers[runner]({ binary, devtools })
|
|
57
60
|
const { page, context } = await newPage(runner, await launched[runner], { binary, dropNetwork })
|
|
58
|
-
await page.goto('file:///dev/null') // Need to load a secure origin for e.g. crypto.subtle to be available
|
|
59
61
|
|
|
60
62
|
page.on('console', (message) => {
|
|
61
63
|
const type = message.type()
|
package/bin/index.js
CHANGED
|
@@ -21,28 +21,29 @@ import * as browsers from './browsers.js'
|
|
|
21
21
|
const bindir = dirname(fileURLToPath(import.meta.url))
|
|
22
22
|
const DEFAULT_PATTERNS = [`**/?(*.)+(spec|test).?([cm])[jt]s?(x)`] // do not trust magic dirs by default
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const bundleOpts = { pure: true, bundle: true, esbuild: true, ts: 'auto' }
|
|
25
|
+
const bareboneOpts = { ...bundleOpts, barebone: true }
|
|
25
26
|
const hermesAv = ['-Og', '-Xmicrotask-queue']
|
|
26
27
|
const ENGINES = new Map(
|
|
27
28
|
Object.entries({
|
|
28
29
|
'node:test': { binary: 'node', pure: false, hasImportLoader: true, ts: 'flag', haveIsOk: true },
|
|
29
30
|
'node:pure': { binary: 'node', pure: true, hasImportLoader: true, ts: 'flag', haveIsOk: true },
|
|
30
|
-
'node:bundle': { binary: 'node', ...
|
|
31
|
+
'node:bundle': { binary: 'node', ...bundleOpts },
|
|
31
32
|
'bun:pure': { binary: 'bun', pure: true, hasImportLoader: false, ts: 'auto' },
|
|
32
|
-
'bun:bundle': { binary: 'bun', ...
|
|
33
|
+
'bun:bundle': { binary: 'bun', ...bundleOpts },
|
|
33
34
|
'electron-as-node:test': { binary: 'electron', pure: false, hasImportLoader: true, ts: 'flag' },
|
|
34
35
|
'electron-as-node:pure': { binary: 'electron', pure: true, hasImportLoader: true, ts: 'flag' },
|
|
35
|
-
'electron-as-node:bundle': { binary: 'electron', ...
|
|
36
|
-
'deno:bundle': { binary: 'deno', binaryArgs: ['run'], target: 'deno1', ...
|
|
37
|
-
'd8:bundle': { binary: 'd8', ...
|
|
38
|
-
'jsc:bundle': { binary: 'jsc',
|
|
39
|
-
'hermes:bundle': { binary: 'hermes', binaryArgs: hermesAv, target: 'es2018', ...
|
|
36
|
+
'electron-as-node:bundle': { binary: 'electron', ...bundleOpts },
|
|
37
|
+
'deno:bundle': { binary: 'deno', binaryArgs: ['run'], target: 'deno1', ...bundleOpts },
|
|
38
|
+
'd8:bundle': { binary: 'd8', ...bareboneOpts },
|
|
39
|
+
'jsc:bundle': { binary: 'jsc', target: 'safari13', ...bareboneOpts },
|
|
40
|
+
'hermes:bundle': { binary: 'hermes', binaryArgs: hermesAv, target: 'es2018', ...bareboneOpts },
|
|
40
41
|
// Browser engines
|
|
41
|
-
'chrome:puppeteer': { binary: 'chrome', browsers: 'puppeteer', ...
|
|
42
|
-
'chromium:playwright': { binary: 'chromium', browsers: 'playwright', ...
|
|
43
|
-
'firefox:puppeteer': { binary: 'firefox', browsers: 'puppeteer', ...
|
|
44
|
-
'firefox:playwright': { binary: 'firefox', browsers: 'playwright', ...
|
|
45
|
-
'webkit:playwright': { binary: 'webkit', browsers: 'playwright', ...
|
|
42
|
+
'chrome:puppeteer': { binary: 'chrome', browsers: 'puppeteer', ...bundleOpts },
|
|
43
|
+
'chromium:playwright': { binary: 'chromium', browsers: 'playwright', ...bundleOpts },
|
|
44
|
+
'firefox:puppeteer': { binary: 'firefox', browsers: 'puppeteer', ...bundleOpts },
|
|
45
|
+
'firefox:playwright': { binary: 'firefox', browsers: 'playwright', ...bundleOpts },
|
|
46
|
+
'webkit:playwright': { binary: 'webkit', browsers: 'playwright', ...bundleOpts },
|
|
46
47
|
})
|
|
47
48
|
)
|
|
48
49
|
|
|
@@ -239,6 +240,8 @@ setEnv('EXODUS_TEST_ENGINE', options.engine) // e.g. 'hermes:bundle', 'node:bund
|
|
|
239
240
|
setEnv('EXODUS_TEST_PLATFORM', options.binary) // e.g. 'hermes', 'node'
|
|
240
241
|
setEnv('EXODUS_TEST_TIMEOUT', options.testTimeout)
|
|
241
242
|
setEnv('EXODUS_TEST_IS_BROWSER', options.browsers ? '1' : '')
|
|
243
|
+
setEnv('EXODUS_TEST_IS_BAREBONE', options.barebone ? '1' : '')
|
|
244
|
+
setEnv('EXODUS_TEST_ENVIRONMENT', options.bundle ? 'bundle' : '') // perhaps switch to _IS_BUNDLED?
|
|
242
245
|
|
|
243
246
|
assert(!options.devtools || options.browsers, '--devtools can be only used with browser engines')
|
|
244
247
|
|
|
@@ -312,7 +315,9 @@ if (options.jest) {
|
|
|
312
315
|
const { loadJestConfig } = await import('../src/jest.config.js')
|
|
313
316
|
const config = await loadJestConfig(process.cwd())
|
|
314
317
|
jestConfig = config
|
|
315
|
-
if (
|
|
318
|
+
if (options.bundle) {
|
|
319
|
+
setEnv('EXODUS_TEST_JEST_CONFIG', JSON.stringify(jestConfig))
|
|
320
|
+
} else {
|
|
316
321
|
args.push(options.hasImportLoader ? '--import' : '-r', resolve(bindir, 'jest.js'))
|
|
317
322
|
}
|
|
318
323
|
|
package/bin/reporter.js
CHANGED
|
@@ -27,6 +27,7 @@ export const format = (chunk) => {
|
|
|
27
27
|
return chunk
|
|
28
28
|
.replaceAll(/^✔ PASS /gmu, color('✔ PASS ', 'green'))
|
|
29
29
|
.replaceAll(/^⏭ SKIP /gmu, color('⏭ SKIP ', dim))
|
|
30
|
+
.replaceAll(/^ℹ DIAGNOSTIC /gmu, color('ℹ DIAGNOSTIC ', 'blue'))
|
|
30
31
|
.replaceAll(/^✖ FAIL /gmu, color('✖ FAIL ', 'red'))
|
|
31
32
|
.replaceAll(/^⚠ WARN /gmu, color('⚠ WARN ', 'blue'))
|
|
32
33
|
.replaceAll(/^‼ FATAL /gmu, `${color('‼', 'red')} ${color(' FATAL ', 'bgRed')} `)
|
package/bundler/bundle.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
-
import { basename, dirname, extname, resolve, join } from 'node:path'
|
|
5
|
+
import { basename, dirname, extname, resolve, join, relative } from 'node:path'
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
7
|
import { randomUUID as uuid, randomBytes } from 'node:crypto'
|
|
8
8
|
import * as esbuild from 'esbuild'
|
|
@@ -68,7 +68,6 @@ export const init = async ({ platform, jest, flow, target, jestConfig, outdir, e
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const barebone = new Set(['hermes', 'jsc', 'd8'])
|
|
72
71
|
const hermesSupported = {
|
|
73
72
|
arrow: false,
|
|
74
73
|
class: false, // we get a safeguard check this way that it's not used
|
|
@@ -111,7 +110,11 @@ const loadSourceFileBase = async (filepath) => {
|
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
export const build = async (...files) => {
|
|
114
|
-
const
|
|
113
|
+
const envOverride = { FORCE_COLOR: '0', NO_COLOR: '1' }
|
|
114
|
+
const getEnv = (key) => (Object.hasOwn(envOverride, key) ? envOverride[key] : process.env[key]) // We know key is safe as it comes from regex below
|
|
115
|
+
const specificLoadPipeline = [
|
|
116
|
+
(src) => src.replace(/\b(?:process\.env\.([A-Z0-9_]+))\b/gu, (_, x) => stringify(getEnv(x))),
|
|
117
|
+
]
|
|
115
118
|
const loadSourceFile = async (filepath) => {
|
|
116
119
|
let contents = await loadSourceFileBase(filepath)
|
|
117
120
|
for (const transform of specificLoadPipeline) contents = await transform(contents, filepath)
|
|
@@ -124,7 +127,7 @@ export const build = async (...files) => {
|
|
|
124
127
|
const stringify = (x) => ([undefined, null].includes(x) ? `${x}` : JSON.stringify(x))
|
|
125
128
|
|
|
126
129
|
if (!['node', 'electron'].includes(options.platform)) {
|
|
127
|
-
if (
|
|
130
|
+
if (process.env.EXODUS_TEST_IS_BAREBONE) {
|
|
128
131
|
const entropy = randomBytes(options.entropySize ?? 5 * 1024).toString('base64')
|
|
129
132
|
input.push(`globalThis.EXODUS_TEST_CRYPTO_ENTROPY = ${stringify(entropy)};`)
|
|
130
133
|
}
|
|
@@ -168,7 +171,7 @@ export const build = async (...files) => {
|
|
|
168
171
|
const buildWrap = async (opts) => esbuild.build(opts).catch((err) => err)
|
|
169
172
|
let main = input.join(';\n')
|
|
170
173
|
const exit = `EXODUS_TEST_PROCESS.exitCode = 1; EXODUS_TEST_PROCESS._maybeProcessExitCode();`
|
|
171
|
-
if (
|
|
174
|
+
if (process.env.EXODUS_TEST_IS_BAREBONE) {
|
|
172
175
|
main = `try {\n${main}\n} catch (err) { print(err); ${exit} }`
|
|
173
176
|
} else if (process.env.EXODUS_TEST_IS_BROWSER) {
|
|
174
177
|
main = `try {\n${main}\n} catch (err) { console.error(err); ${exit} }`
|
|
@@ -176,23 +179,67 @@ export const build = async (...files) => {
|
|
|
176
179
|
|
|
177
180
|
const fsfiles = await getPackageFiles(filename ? dirname(resolve(filename)) : process.cwd())
|
|
178
181
|
const fsFilesContents = new Map()
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
182
|
+
const fsFilesDirs = new Map()
|
|
183
|
+
const cwd = process.cwd()
|
|
184
|
+
const fixturesRegex = /(fixtures|samples)/u
|
|
185
|
+
const aggressiveExtensions = /\.(json|txt|hex)$/u // These are bundled when just used in path.join and by wildcard from fixtures/
|
|
186
|
+
const fileAllowed = (f) =>
|
|
187
|
+
f && f.startsWith(`${cwd}/`) && resolve(f) === f && /^[a-z0-9@_./-]+$/iu.test(relative(cwd, f))
|
|
188
|
+
|
|
189
|
+
const fsFilesAdd = async (file) => {
|
|
190
|
+
if (!fileAllowed(file)) return
|
|
191
|
+
try {
|
|
192
|
+
const data = await readFile(file, 'base64')
|
|
193
|
+
if (fsFilesContents.has(file)) {
|
|
194
|
+
assert(fsFilesContents.get(file) === data)
|
|
195
|
+
} else {
|
|
196
|
+
fsFilesContents.set(file, data)
|
|
194
197
|
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
if (e.code !== 'ENOENT') throw e
|
|
195
200
|
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const fixturesSeen = { fs: false, fixtures: false, bundled: false }
|
|
204
|
+
const fsFilesBundleFixtures = async (reason) => {
|
|
205
|
+
if (fixturesSeen.bundled || !filename) return
|
|
206
|
+
if (reason === 'fs' || reason === 'fixtures') fixturesSeen[reason] = true
|
|
207
|
+
if (!fixturesSeen.fs || !fixturesSeen.fixtures) return
|
|
208
|
+
fixturesSeen.bundled = true
|
|
209
|
+
const dir = dirname(resolve(filename))
|
|
210
|
+
for (const name of await readdir(dir, { recursive: true })) {
|
|
211
|
+
const parent = dirname(name)
|
|
212
|
+
if (!fixturesRegex.test(parent)) continue // relative dir path should look like a fixtures dir
|
|
213
|
+
|
|
214
|
+
// Save as directory entry into parent dir
|
|
215
|
+
const subdir = resolve(dir, parent)
|
|
216
|
+
if (fileAllowed(subdir)) {
|
|
217
|
+
if (!fsFilesDirs.has(subdir)) fsFilesDirs.set(subdir, [])
|
|
218
|
+
fsFilesDirs.get(subdir).push(basename(name))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Save to files
|
|
222
|
+
const file = resolve(dir, name)
|
|
223
|
+
if (aggressiveExtensions.test(file)) await fsFilesAdd(file)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
specificLoadPipeline.push(async (source, filepath) => {
|
|
228
|
+
for (const m of source.matchAll(/readFileSync\(\s*(?:"([^"\\]+)"|'([^'\\]+)')[),]/gu)) {
|
|
229
|
+
await fsFilesAdd(resolve(m[1] || m[2])) // resolves from cwd
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// E.g. path.join(import.meta.dirname, './fixtures/data.json'), dirname is inlined by loadPipeline already
|
|
233
|
+
const dir = dirname(filepath)
|
|
234
|
+
for (const m of source.matchAll(/join\(\s*("[^"\\]+"),\s*(?:"([^"\\]+)"|'([^'\\]+)')\s*\)/gu)) {
|
|
235
|
+
if (m[1] !== JSON.stringify(dir)) continue // only allow files relative to dirname, from loadPipeline
|
|
236
|
+
const file = resolve(dir, m[2] || m[3])
|
|
237
|
+
if (aggressiveExtensions.test(file)) await fsFilesAdd(file) // only bundle path.join for specific extensions used as test fixtures
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Both conditions should happen for deep fixtures inclusion
|
|
241
|
+
if (/(readdir|readFile|exists)Sync/u.test(source)) await fsFilesBundleFixtures('fs')
|
|
242
|
+
if (fixturesRegex.test(source)) await fsFilesBundleFixtures('fixtures')
|
|
196
243
|
|
|
197
244
|
return source
|
|
198
245
|
})
|
|
@@ -219,11 +266,12 @@ export const build = async (...files) => {
|
|
|
219
266
|
'fs/promises': api('fs-promises.cjs'),
|
|
220
267
|
http: api('http.cjs'),
|
|
221
268
|
https: api('https.cjs'),
|
|
222
|
-
os: resolveRequire('os-browserify'),
|
|
269
|
+
os: resolveRequire('os-browserify/browser.js'), // 'main' entry point is noop, we want browser entry
|
|
223
270
|
path: resolveRequire('path-browserify'),
|
|
224
271
|
querystring: resolveRequire('querystring-es3'),
|
|
225
272
|
stream: resolveRequire('stream-browserify'),
|
|
226
273
|
timers: resolveRequire('timers-browserify'),
|
|
274
|
+
tty: api('tty.cjs'),
|
|
227
275
|
url: api('url.cjs'),
|
|
228
276
|
util: dirname(resolveRequire('util/')),
|
|
229
277
|
zlib: resolveRequire('browserify-zlib'),
|
|
@@ -241,23 +289,6 @@ export const build = async (...files) => {
|
|
|
241
289
|
platform: 'neutral',
|
|
242
290
|
mainFields: ['browser', 'module', 'main'],
|
|
243
291
|
define: {
|
|
244
|
-
'process.env.FORCE_COLOR': stringify('0'),
|
|
245
|
-
'process.env.NO_COLOR': stringify('1'),
|
|
246
|
-
'process.env.NODE_ENV': stringify(process.env.NODE_ENV),
|
|
247
|
-
'process.env.EXODUS_TEST_CONTEXT': stringify('pure'),
|
|
248
|
-
'process.env.EXODUS_TEST_ENVIRONMENT': stringify('bundle'), // always 'bundle'
|
|
249
|
-
'process.env.EXODUS_TEST_PLATFORM': stringify(process.env.EXODUS_TEST_PLATFORM), // e.g. 'hermes', 'node'
|
|
250
|
-
'process.env.EXODUS_TEST_ENGINE': stringify(process.env.EXODUS_TEST_ENGINE), // e.g. 'hermes:bundle', 'node:bundle'
|
|
251
|
-
'process.env.EXODUS_TEST_IS_BROWSER': stringify(process.env.EXODUS_TEST_IS_BROWSER), // '1' or ''
|
|
252
|
-
'process.env.EXODUS_TEST_JEST_CONFIG': stringify(JSON.stringify(options.jestConfig)),
|
|
253
|
-
'process.env.EXODUS_TEST_EXECARGV': stringify(process.env.EXODUS_TEST_EXECARGV),
|
|
254
|
-
'process.env.EXODUS_TEST_ONLY': stringify(process.env.EXODUS_TEST_ONLY),
|
|
255
|
-
'process.env.EXODUS_TEST_TIMEOUT': stringify(process.env.EXODUS_TEST_TIMEOUT),
|
|
256
|
-
'process.env.NODE_DEBUG': stringify(),
|
|
257
|
-
'process.env.DEBUG': stringify(),
|
|
258
|
-
'process.env.READABLE_STREAM': stringify(),
|
|
259
|
-
'process.env.CI': stringify(process.env.CI),
|
|
260
|
-
'process.env.CI_ENABLE_VERBOSE_LOGS': stringify(process.env.CI_ENABLE_VERBOSE_LOGS),
|
|
261
292
|
'process.browser': stringify(true),
|
|
262
293
|
'process.emitWarning': 'undefined',
|
|
263
294
|
'process.stderr': 'undefined',
|
|
@@ -271,6 +302,7 @@ export const build = async (...files) => {
|
|
|
271
302
|
EXODUS_TEST_RECORDINGS: stringify(EXODUS_TEST_RECORDINGS),
|
|
272
303
|
EXODUS_TEST_FSFILES: stringify(fsfiles), // TODO: can we safely use relative paths?
|
|
273
304
|
EXODUS_TEST_FSFILES_CONTENTS: stringify([...fsFilesContents.entries()]),
|
|
305
|
+
EXODUS_TEST_FSDIRS: stringify([...fsFilesDirs.entries()]),
|
|
274
306
|
},
|
|
275
307
|
alias: {
|
|
276
308
|
// Jest, tape and node:test
|
|
@@ -295,7 +327,7 @@ export const build = async (...files) => {
|
|
|
295
327
|
'node-gyp-build': api('empty/function-throw.cjs'),
|
|
296
328
|
ws: api('ws.cjs'),
|
|
297
329
|
},
|
|
298
|
-
sourcemap:
|
|
330
|
+
sourcemap: process.env.EXODUS_TEST_IS_BAREBONE ? 'inline' : 'linked', // FIXME?
|
|
299
331
|
sourcesContent: false,
|
|
300
332
|
keepNames: true,
|
|
301
333
|
format: 'iife',
|
|
@@ -311,9 +343,8 @@ export const build = async (...files) => {
|
|
|
311
343
|
onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: 'file' }, async (args) => {
|
|
312
344
|
let filepath = args.path
|
|
313
345
|
// Resolve .native versions
|
|
314
|
-
// TODO: move flag to engine options
|
|
315
346
|
// TODO: maybe follow package.json for this
|
|
316
|
-
if (
|
|
347
|
+
if (process.env.EXODUS_TEST_IS_BAREBONE) {
|
|
317
348
|
const maybeNative = filepath.replace(/(\.[cm]?[jt]sx?)$/u, '.native$1')
|
|
318
349
|
if (existsSync(maybeNative)) filepath = maybeNative
|
|
319
350
|
}
|
|
@@ -340,9 +371,10 @@ export const build = async (...files) => {
|
|
|
340
371
|
let res = await buildWrap(config)
|
|
341
372
|
assert.equal(res instanceof Error, res.errors.length > 0)
|
|
342
373
|
|
|
343
|
-
if (fsFilesContents.size > 0) {
|
|
374
|
+
if (fsFilesContents.size > 0 || fsFilesDirs.size > 0) {
|
|
344
375
|
// re-run as we detected that tests depend on fsReadFileSync contents
|
|
345
376
|
config.define.EXODUS_TEST_FSFILES_CONTENTS = stringify([...fsFilesContents.entries()])
|
|
377
|
+
config.define.EXODUS_TEST_FSDIRS = stringify([...fsFilesDirs.entries()])
|
|
346
378
|
res = await buildWrap(config)
|
|
347
379
|
assert.equal(res instanceof Error, res.errors.length > 0)
|
|
348
380
|
}
|
package/bundler/modules/fs.cjs
CHANGED
|
@@ -75,38 +75,49 @@ const stubs = Object.fromEntries(mainKeys.map((key) => [key, () => err(key)]))
|
|
|
75
75
|
const stubsPromises = Object.fromEntries(promisesKeys.map((key) => [key, async () => err(key)]))
|
|
76
76
|
const promises = { ...stubsPromises, constants }
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
const decode = (source, sourceEncoding, encoding) => {
|
|
79
|
+
if (encoding && sourceEncoding === encoding) return source
|
|
80
|
+
const data = Buffer.from(source, sourceEncoding)
|
|
81
|
+
return encoding === undefined ? data : data.toString(encoding)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const getOptions = (arg, options) => {
|
|
85
|
+
if (typeof arg !== 'string') throw new Error('first argument should be string')
|
|
86
|
+
const file = resolve(process.cwd(), arg)
|
|
87
|
+
if (typeof options === 'string') return { file, encoding: options, rest: {} }
|
|
88
|
+
if (options === undefined) return { file, rest: {} }
|
|
89
|
+
if (typeof options !== 'object') throw new Error('Unexpected options')
|
|
90
|
+
const { encoding: enc, ...rest } = options
|
|
91
|
+
if (enc !== undefined && typeof enc !== 'string') throw new Error('encoding should be a string')
|
|
92
|
+
return { file, encoding: enc, rest }
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
const fsFilesContents =
|
|
86
96
|
// eslint-disable-next-line no-undef
|
|
87
97
|
typeof EXODUS_TEST_FSFILES_CONTENTS === 'undefined' ? null : new Map(EXODUS_TEST_FSFILES_CONTENTS)
|
|
88
|
-
const readFileSync = (
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const { encoding: enc, ...rest } = options
|
|
95
|
-
if (enc !== undefined && typeof enc !== 'string') throw new Error('encoding should be a string')
|
|
96
|
-
encoding = enc
|
|
97
|
-
if (Object.keys(rest).length > 0) throw new Error('Unsupported readFileSync options')
|
|
98
|
-
}
|
|
98
|
+
const readFileSync = (arg, options) => {
|
|
99
|
+
const { file, encoding, rest } = getOptions(arg, options)
|
|
100
|
+
if (Object.keys(rest).length > 0) throw new Error('Unsupported readFileSync options')
|
|
101
|
+
if (fsFilesContents?.has(file)) return decode(fsFilesContents.get(file), 'base64', encoding)
|
|
102
|
+
err('readFileSync', file)
|
|
103
|
+
}
|
|
99
104
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// eslint-disable-next-line no-undef
|
|
106
|
+
const fsDir = typeof EXODUS_TEST_FSDIRS === 'undefined' ? null : new Map(EXODUS_TEST_FSDIRS)
|
|
107
|
+
const readdirSync = (arg, options) => {
|
|
108
|
+
const { file: dir, encoding, rest } = getOptions(arg, options)
|
|
109
|
+
if (Object.keys(rest).length > 0) throw new Error('Unsupported readdirSync options')
|
|
110
|
+
const enc = encoding === 'buffer' ? undefined : encoding || 'utf8'
|
|
111
|
+
if (fsDir?.has(dir)) return fsDir.get(dir).map((name) => decode(name, 'utf8', enc))
|
|
112
|
+
err('readdirSync', dir)
|
|
113
|
+
}
|
|
108
114
|
|
|
109
|
-
|
|
115
|
+
// eslint-disable-next-line no-undef
|
|
116
|
+
const fsFiles = typeof EXODUS_TEST_FSFILES === 'undefined' ? null : new Set(EXODUS_TEST_FSFILES)
|
|
117
|
+
const existsSync = (file) => {
|
|
118
|
+
if (fsFiles?.has(file) || fsFilesContents?.has(file) || fsDir?.has(file)) return true
|
|
119
|
+
err('existsSync', file)
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
|
|
122
|
+
const implemented = { existsSync, readFileSync, readdirSync }
|
|
123
|
+
module.exports = { ...stubs, ...implemented, promises, constants, F_OK, R_OK, W_OK, X_OK }
|
|
@@ -3,22 +3,39 @@
|
|
|
3
3
|
|
|
4
4
|
if (!globalThis.global) globalThis.global = globalThis
|
|
5
5
|
if (!globalThis.Buffer) globalThis.Buffer = require('buffer').Buffer
|
|
6
|
-
if (!globalThis.console) {
|
|
7
|
-
// eslint-disable-next-line no-undef
|
|
8
|
-
globalThis.console = { log: print, error: print, warn: print, info: print, debug: print }
|
|
9
|
-
}
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
const consoleKeys = ['log', 'error', 'warn', 'info', 'debug', 'trace']
|
|
8
|
+
if (!globalThis.console) globalThis.console = Object.fromEntries(consoleKeys.map((k) => [k, print])) // eslint-disable-line no-undef
|
|
9
|
+
|
|
10
|
+
// In browsers e.g. errors (and some other objects) are hard to unwrap via the API
|
|
12
11
|
// So we just stringify everything instead on the sender side
|
|
13
|
-
|
|
12
|
+
// In barebone, we don't want console.log({x:10}) to print "[Object object]"", we want "{ x: 10 }"
|
|
13
|
+
if (process.env.EXODUS_TEST_IS_BROWSER || process.env.EXODUS_TEST_IS_BAREBONE) {
|
|
14
14
|
const utilFormat = require('exodus-test:util-format')
|
|
15
|
-
for (const type of
|
|
15
|
+
for (const type of consoleKeys) {
|
|
16
16
|
if (!Object.hasOwn(console, type)) continue
|
|
17
17
|
const orig = console[type].bind(console)
|
|
18
18
|
console[type] = (...args) => orig(utilFormat(...args))
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
if (!console.time || !console.timeEnd) {
|
|
23
|
+
const start = new Map()
|
|
24
|
+
const now = globalThis.performance?.now ? performance.now.bind(performance) : Date.now.bind(Date) // d8 and jsc have performance.now()
|
|
25
|
+
const warn = (text) => console.error(`Warning: ${text}`)
|
|
26
|
+
console.time = (key = 'default') => {
|
|
27
|
+
if (start.has(key)) return warn(`Label '${key}' already exists for console.time()`) // Does not reset
|
|
28
|
+
start.set(key, now()) // Start late
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.timeEnd = (key = 'default') => {
|
|
32
|
+
const ms = now() // End early
|
|
33
|
+
if (!start.has(key)) return warn(`No such label '${key}' for console.timeEnd()`)
|
|
34
|
+
console.log(`${key}: ${ms - start.get(key)}ms`)
|
|
35
|
+
start.delete(key)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
if (!globalThis.fetch) {
|
|
23
40
|
globalThis.fetch = () => {
|
|
24
41
|
throw new Error('Fetch not supported')
|
|
@@ -116,40 +133,53 @@ This activity created errors and would have caused tests to fail, but instead tr
|
|
|
116
133
|
|
|
117
134
|
if (
|
|
118
135
|
process.env.EXODUS_TEST_PLATFORM === 'hermes' ||
|
|
119
|
-
(process.env.
|
|
120
|
-
(process.env.EXODUS_TEST_PLATFORM === 'd8' && !globalThis.clearTimeout)
|
|
136
|
+
(process.env.EXODUS_TEST_IS_BAREBONE && !globalThis.clearTimeout)
|
|
121
137
|
) {
|
|
122
138
|
// Ok, we have broken timers, let's hack them around
|
|
123
139
|
let i = 0
|
|
124
140
|
const timers = new Map()
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
141
|
+
const repeating = new Set()
|
|
142
|
+
const { setTimeout: setTimeoutOriginal, clearTimeout: clearTimeoutOriginal } = globalThis
|
|
143
|
+
const dateNow = Date.now.bind(Date)
|
|
144
|
+
const precision = clearTimeoutOriginal ? Infinity : 10 // have to tick this fast for clearTimeout to work
|
|
128
145
|
|
|
129
|
-
|
|
146
|
+
const setTimeout = (fn, time, ...args) => {
|
|
130
147
|
const id = `ht${i++}`
|
|
131
|
-
|
|
148
|
+
let started = dateNow()
|
|
132
149
|
const tick = () => {
|
|
133
150
|
if (!timers.has(id)) return
|
|
134
|
-
const remaining =
|
|
151
|
+
const remaining = started + time - dateNow()
|
|
135
152
|
if (remaining < 0) {
|
|
136
|
-
|
|
153
|
+
if (repeating.has(id)) {
|
|
154
|
+
started = dateNow()
|
|
155
|
+
timers.set(id, setTimeoutOriginal(tick, Math.min(precision, time)))
|
|
156
|
+
} else {
|
|
157
|
+
timers.delete(id)
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
fn(...args)
|
|
138
161
|
} else {
|
|
139
|
-
timers.set(id,
|
|
162
|
+
timers.set(id, setTimeoutOriginal(tick, Math.min(precision, remaining)))
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
|
|
143
|
-
timers.set(id,
|
|
166
|
+
timers.set(id, setTimeoutOriginal(tick, Math.min(precision, time)))
|
|
167
|
+
return id
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
globalThis.setTimeout = setTimeout
|
|
171
|
+
globalThis.setInterval = (fn, time, ...args) => {
|
|
172
|
+
const id = setTimeout(fn, time, ...args)
|
|
173
|
+
repeating.add(id)
|
|
144
174
|
return id
|
|
145
175
|
}
|
|
146
176
|
|
|
147
|
-
globalThis.clearTimeout = (id) => {
|
|
177
|
+
globalThis.clearTimeout = globalThis.clearInterval = (id) => {
|
|
148
178
|
if (!timers.has(id)) return
|
|
149
|
-
|
|
179
|
+
clearTimeoutOriginal?.(timers.get(id))
|
|
150
180
|
timers.delete(id)
|
|
181
|
+
repeating.delete(id)
|
|
151
182
|
}
|
|
152
|
-
// TODO: setInterval, clearInterval
|
|
153
183
|
}
|
|
154
184
|
|
|
155
185
|
if (!globalThis.crypto?.getRandomValues && globalThis.EXODUS_TEST_CRYPTO_ENTROPY) {
|
|
@@ -193,11 +223,7 @@ if (globalThis.crypto?.getRandomValues && !globalThis.crypto?.randomUUID) {
|
|
|
193
223
|
|
|
194
224
|
if (!globalThis.crypto.subtle) globalThis.crypto.subtle = {} // For getRandomValues detection
|
|
195
225
|
|
|
196
|
-
if (
|
|
197
|
-
process.env.EXODUS_TEST_PLATFORM === 'hermes' ||
|
|
198
|
-
process.env.EXODUS_TEST_PLATFORM === 'jsc' ||
|
|
199
|
-
process.env.EXODUS_TEST_PLATFORM === 'd8'
|
|
200
|
-
) {
|
|
226
|
+
if (process.env.EXODUS_TEST_IS_BAREBONE) {
|
|
201
227
|
if (!globalThis.URLSearchParams) globalThis.URLSearchParams = require('@ungap/url-search-params')
|
|
202
228
|
if (!globalThis.TextEncoder || !globalThis.TextDecoder) {
|
|
203
229
|
const { TextEncoder, TextDecoder } = require('exodus-test:text-encoding-utf')
|
|
@@ -56,8 +56,7 @@ function TextDecoder(encoding = UTF8, options = {}) {
|
|
|
56
56
|
encoding = normalizeEncoding(encoding)
|
|
57
57
|
assertUTF8orUTF16LE(encoding)
|
|
58
58
|
|
|
59
|
-
const { fatal =
|
|
60
|
-
if (fatal !== true) throw new Error('disabling "fatal" mode is not supported')
|
|
59
|
+
const { fatal = false, ignoreBOM = false, stream = false } = options
|
|
61
60
|
if (ignoreBOM !== false) throw new Error('option "ignoreBOM" is not supported')
|
|
62
61
|
if (stream !== false) throw new Error('option "stream" is not supported')
|
|
63
62
|
|
|
@@ -67,11 +66,25 @@ function TextDecoder(encoding = UTF8, options = {}) {
|
|
|
67
66
|
defineFinal(this, 'ignoreBOM', ignoreBOM)
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
// Note: https://npmjs.com/package/buffer has a bug
|
|
70
|
+
// Buffer.from([0xf0, 0x90, 0x80]).toString().length should be 1, but it is 3 in https://npmjs.com/package/buffer
|
|
71
|
+
// Buffer.from([0xf0, 0x80, 0x80]).toString().length should be 3, see https://github.com/nodejs/node/issues/16894
|
|
70
72
|
TextDecoder.prototype.decode = function (buf) {
|
|
71
73
|
if (buf === undefined) return ''
|
|
72
74
|
assertBufferSource(buf)
|
|
73
75
|
if (!Buffer.isBuffer(buf)) buf = Buffer.from(buf)
|
|
74
|
-
|
|
76
|
+
const res = buf.toString(this.encoding)
|
|
77
|
+
if (this.fatal && res.includes('\uFFFD')) {
|
|
78
|
+
// We have a replacement symbol, recheck if output matches input
|
|
79
|
+
const reconstructed = Buffer.from(res, this.encoding)
|
|
80
|
+
if (Buffer.compare(buf, reconstructed) !== 0) {
|
|
81
|
+
const err = new TypeError('The encoded data was not valid for encoding utf-8')
|
|
82
|
+
err.code = 'ERR_ENCODING_INVALID_ENCODED_DATA'
|
|
83
|
+
throw err
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return res
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
module.exports = { TextEncoder, TextDecoder }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
isatty: () => false,
|
|
3
|
+
// Not arrows as those are classes and can be called with new
|
|
4
|
+
ReadStream() {
|
|
5
|
+
throw new Error('tty.ReadStream unsupported in bundled mode')
|
|
6
|
+
},
|
|
7
|
+
WriteStream() {
|
|
8
|
+
throw new Error('tty.WriteStream unsupported in bundled mode')
|
|
9
|
+
},
|
|
10
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
const { inspect: inspectOrig, isString, isNull, isObject } = require('util/') // dep
|
|
2
2
|
|
|
3
3
|
// Print errors without square brackets
|
|
4
|
-
const
|
|
4
|
+
const trim = (x) => x.trim()
|
|
5
|
+
const validLine = (x) => x && x !== '@'
|
|
6
|
+
const padLine = (line) => ` ${line}`
|
|
7
|
+
const pad = (stack) => stack.split('\n').map(trim).filter(validLine).map(padLine).join('\n')
|
|
8
|
+
const errorStr = (e) => (e.stack.startsWith(`${e}\n`) ? e.stack : `${e}\n${pad(e.stack)}`.trimEnd())
|
|
9
|
+
const inspect = (obj, opts) => (obj instanceof Error ? errorStr(obj) : inspectOrig(obj, opts))
|
|
5
10
|
|
|
6
11
|
// Patched impl from require('util'), added %i
|
|
7
12
|
const formatRegExp = /%[%dijs]/g
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/test",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.70",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"bundler/modules/jest-util.js",
|
|
61
61
|
"bundler/modules/node-buffer.cjs",
|
|
62
62
|
"bundler/modules/text-encoding-utf.cjs",
|
|
63
|
+
"bundler/modules/tty.cjs",
|
|
63
64
|
"bundler/modules/url.cjs",
|
|
64
65
|
"bundler/modules/util-format.cjs",
|
|
65
66
|
"bundler/modules/ws.cjs",
|
|
@@ -130,7 +131,7 @@
|
|
|
130
131
|
"@babel/plugin-transform-private-methods": "^7.0.0",
|
|
131
132
|
"@babel/register": "^7.0.0",
|
|
132
133
|
"@chalker/queue": "^1.0.0",
|
|
133
|
-
"@exodus/replay": "^1.0.0-rc.
|
|
134
|
+
"@exodus/replay": "^1.0.0-rc.6",
|
|
134
135
|
"@ungap/url-search-params": "^0.2.2",
|
|
135
136
|
"amaro": "^0.0.5",
|
|
136
137
|
"assert": "^2.1.0",
|
|
@@ -148,7 +149,7 @@
|
|
|
148
149
|
"jsdom": "^24.1.0",
|
|
149
150
|
"os-browserify": "^0.3.0",
|
|
150
151
|
"path-browserify": "^1.0.1",
|
|
151
|
-
"playwright-core": "^1.
|
|
152
|
+
"playwright-core": "^1.52.0",
|
|
152
153
|
"pretty-format": "^29.7.0",
|
|
153
154
|
"puppeteer-core": "^24.6.0",
|
|
154
155
|
"querystring-es3": "^0.2.1",
|
package/src/engine.pure.cjs
CHANGED
|
@@ -84,6 +84,10 @@ class Context {
|
|
|
84
84
|
if (!this.#hooks?.[type]) return
|
|
85
85
|
for (const hook of this.#hooks[type]) await runFunction(hook, context)
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
diagnostic(message) {
|
|
89
|
+
console.log(`ℹ DIAGNOSTIC ${message}`)
|
|
90
|
+
}
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
function enterContext(name, options) {
|
package/src/jest.js
CHANGED
|
@@ -231,7 +231,9 @@ export const jest = {
|
|
|
231
231
|
fetchRecord,
|
|
232
232
|
fetchReplay,
|
|
233
233
|
fetchNoop: () => {
|
|
234
|
-
|
|
234
|
+
// We can't use pure noop, it will break chained fetch().then(), so let's reject
|
|
235
|
+
const fetch = () => Promise.reject(new Error('fetch is disabled by mock.fetchNoop()'))
|
|
236
|
+
globalThis.fetch = jest.fn(fetch)
|
|
235
237
|
return globalThis.fetch
|
|
236
238
|
},
|
|
237
239
|
websocketRecord,
|