@remix-run/test 0.2.0 → 0.3.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.
Files changed (60) hide show
  1. package/README.md +39 -33
  2. package/dist/app/client/entry.js +4 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +68 -23
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/lib/config.d.ts +35 -21
  8. package/dist/lib/config.d.ts.map +1 -1
  9. package/dist/lib/config.js +73 -33
  10. package/dist/lib/fake-timers.d.ts +7 -0
  11. package/dist/lib/fake-timers.d.ts.map +1 -1
  12. package/dist/lib/fake-timers.js +27 -8
  13. package/dist/lib/import-module.d.ts.map +1 -1
  14. package/dist/lib/import-module.js +11 -2
  15. package/dist/lib/reporters/dot.d.ts.map +1 -1
  16. package/dist/lib/reporters/dot.js +10 -0
  17. package/dist/lib/reporters/files.d.ts.map +1 -1
  18. package/dist/lib/reporters/files.js +10 -0
  19. package/dist/lib/reporters/spec.d.ts.map +1 -1
  20. package/dist/lib/reporters/spec.js +10 -0
  21. package/dist/lib/reporters/tap.d.ts.map +1 -1
  22. package/dist/lib/reporters/tap.js +10 -0
  23. package/dist/lib/runner-browser.d.ts.map +1 -1
  24. package/dist/lib/runner-browser.js +6 -0
  25. package/dist/lib/runner.d.ts +18 -1
  26. package/dist/lib/runner.d.ts.map +1 -1
  27. package/dist/lib/runner.js +187 -38
  28. package/dist/lib/worker-e2e-file.d.ts +11 -0
  29. package/dist/lib/worker-e2e-file.d.ts.map +1 -0
  30. package/dist/lib/worker-e2e-file.js +69 -0
  31. package/dist/lib/worker-e2e.js +11 -47
  32. package/dist/lib/worker-process.d.ts +2 -0
  33. package/dist/lib/worker-process.d.ts.map +1 -0
  34. package/dist/lib/worker-process.js +55 -0
  35. package/dist/lib/worker-results.d.ts +3 -0
  36. package/dist/lib/worker-results.d.ts.map +1 -0
  37. package/dist/lib/worker-results.js +20 -0
  38. package/dist/lib/worker-server.d.ts +10 -0
  39. package/dist/lib/worker-server.d.ts.map +1 -0
  40. package/dist/lib/worker-server.js +113 -0
  41. package/dist/lib/worker.js +6 -55
  42. package/package.json +4 -4
  43. package/src/app/client/entry.ts +4 -0
  44. package/src/cli.ts +91 -28
  45. package/src/index.ts +1 -1
  46. package/src/lib/config.ts +124 -58
  47. package/src/lib/fake-timers.ts +33 -8
  48. package/src/lib/import-module.ts +12 -2
  49. package/src/lib/reporters/dot.ts +9 -0
  50. package/src/lib/reporters/files.ts +9 -0
  51. package/src/lib/reporters/spec.ts +9 -0
  52. package/src/lib/reporters/tap.ts +9 -0
  53. package/src/lib/runner-browser.ts +6 -0
  54. package/src/lib/runner.ts +253 -50
  55. package/src/lib/worker-e2e-file.ts +98 -0
  56. package/src/lib/worker-e2e.ts +14 -51
  57. package/src/lib/worker-process.ts +69 -0
  58. package/src/lib/worker-results.ts +22 -0
  59. package/src/lib/worker-server.ts +123 -0
  60. package/src/lib/worker.ts +7 -47
@@ -6,10 +6,17 @@ import type { Counts, TestResult, TestResults } from './results.ts'
6
6
 
7
7
  export class FilesReporter implements Reporter {
8
8
  #failures: { suiteName: string; name: string; error: TestResult['error'] }[] = []
9
+ #files = new Set<string>()
10
+ #suites = new Set<string>()
9
11
 
10
12
  onSectionStart(_label: string) {}
11
13
 
12
14
  onResult(results: TestResults, env?: string) {
15
+ for (let test of results.tests) {
16
+ if (test.filePath) this.#files.add(test.filePath)
17
+ if (test.suiteName) this.#suites.add(test.suiteName)
18
+ }
19
+
13
20
  let filePath = results.tests[0]?.filePath
14
21
  let fileName = filePath ? path.relative(process.cwd(), filePath) : '(unknown)'
15
22
  let envLabel = env ? ` ${colors.dim(`[${env}]`)}` : ''
@@ -66,6 +73,8 @@ export class FilesReporter implements Reporter {
66
73
  let { passed, failed, skipped, todo } = counts
67
74
  let info = colors.cyan('ℹ')
68
75
  console.log()
76
+ console.log(`${info} files ${this.#files.size}`)
77
+ console.log(`${info} suites ${this.#suites.size}`)
69
78
  console.log(`${info} tests ${passed + failed + skipped + todo}`)
70
79
  console.log(`${info} pass ${passed}`)
71
80
  console.log(`${info} fail ${failed}`)
@@ -5,12 +5,19 @@ import type { Counts, TestResult, TestResults } from './results.ts'
5
5
 
6
6
  export class SpecReporter implements Reporter {
7
7
  #failures: { suiteName: string; name: string; error: TestResult['error'] }[] = []
8
+ #files = new Set<string>()
9
+ #suites = new Set<string>()
8
10
 
9
11
  onSectionStart(label: string) {
10
12
  console.log(label)
11
13
  }
12
14
 
13
15
  onResult(results: TestResults, env?: string) {
16
+ for (let test of results.tests) {
17
+ if (test.filePath) this.#files.add(test.filePath)
18
+ if (test.suiteName) this.#suites.add(test.suiteName)
19
+ }
20
+
14
21
  let suiteMap = new Map<string, TestResult[]>()
15
22
  for (let test of results.tests) {
16
23
  let suite = test.suiteName || 'Global'
@@ -163,6 +170,8 @@ export class SpecReporter implements Reporter {
163
170
  let { passed, failed, skipped, todo } = counts
164
171
  let info = colors.cyan('ℹ')
165
172
  console.log()
173
+ console.log(`${info} files ${this.#files.size}`)
174
+ console.log(`${info} suites ${this.#suites.size}`)
166
175
  console.log(`${info} tests ${passed + failed + skipped + todo}`)
167
176
  console.log(`${info} pass ${passed}`)
168
177
  console.log(`${info} fail ${failed}`)
@@ -5,6 +5,8 @@ import type { Counts, TestResults } from './results.ts'
5
5
  export class TapReporter implements Reporter {
6
6
  #counter = 0
7
7
  #total = 0
8
+ #files = new Set<string>()
9
+ #suites = new Set<string>()
8
10
 
9
11
  onSectionStart(_label: string) {}
10
12
 
@@ -15,6 +17,11 @@ export class TapReporter implements Reporter {
15
17
 
16
18
  let envComment = env ? ` # ${env}` : ''
17
19
 
20
+ for (let test of results.tests) {
21
+ if (test.filePath) this.#files.add(test.filePath)
22
+ if (test.suiteName) this.#suites.add(test.suiteName)
23
+ }
24
+
18
25
  for (let test of results.tests) {
19
26
  this.#counter++
20
27
  this.#total++
@@ -48,6 +55,8 @@ export class TapReporter implements Reporter {
48
55
  onSummary(counts: Counts, durationMs: number) {
49
56
  let { passed, failed, skipped, todo } = counts
50
57
  console.log(`1..${this.#total}`)
58
+ console.log(`# files ${this.#files.size}`)
59
+ console.log(`# suites ${this.#suites.size}`)
51
60
  console.log(`# tests ${passed + failed + skipped + todo}`)
52
61
  console.log(`# pass ${passed}`)
53
62
  console.log(`# fail ${failed}`)
@@ -61,6 +61,12 @@ export async function runBrowserTests(options: TestRunOptions): Promise<{
61
61
  getPlaywrightLaunchOptions(options.playwrightUseOpts),
62
62
  )
63
63
  page = await browser.newPage(getPlaywrightPageOptions(options.playwrightUseOpts))
64
+ // Cap how long we'll wait for a browser-test file to signal completion.
65
+ // Playwright's default is 30s; bumping to 60s buys headroom for slower
66
+ // suites without letting a hung test hide forever. Plumb this through
67
+ // config later if anyone needs to tune it.
68
+ page.setDefaultTimeout(90_000)
69
+ page.setDefaultNavigationTimeout(90_000)
64
70
 
65
71
  if (options.console) {
66
72
  page.on('console', (msg) => console.log(`${colors.dim('[browser console]')} ${msg.text()}`))
package/src/lib/runner.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { fork, type ChildProcess } from 'node:child_process'
1
2
  import * as fsp from 'node:fs/promises'
2
3
  import * as path from 'node:path'
3
- import { pathToFileURL } from 'node:url'
4
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
5
  import { Worker } from 'node:worker_threads'
5
- import { IS_RUNNING_FROM_SRC } from './config.ts'
6
+ import { IS_RUNNING_FROM_SRC, type RemixTestPool } from './config.ts'
6
7
  import {
7
8
  collectCoverageMapFromPlaywright,
8
9
  collectServerCoverageMap,
@@ -19,6 +20,22 @@ import type { Counts, TestResults } from './reporters/results.ts'
19
20
  const ext = IS_RUNNING_FROM_SRC ? '.ts' : '.js'
20
21
  const workerUrl = new URL(`./worker${ext}`, import.meta.url)
21
22
  const workerE2EUrl = new URL(`./worker-e2e${ext}`, import.meta.url)
23
+ const workerProcessUrl = new URL(`./worker-process${ext}`, import.meta.url)
24
+ const DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS = 10_000
25
+
26
+ interface WorkerRun {
27
+ finished: Promise<void>
28
+ exited: Promise<number | null>
29
+ terminate(): Promise<boolean>
30
+ }
31
+
32
+ interface RunFileOptions {
33
+ cwd?: string
34
+ coverage?: CoverageConfig
35
+ open?: boolean
36
+ playwrightUseOpts?: PlaywrightUseOpts
37
+ pool?: RemixTestPool
38
+ }
22
39
 
23
40
  export async function runServerTests(
24
41
  files: string[],
@@ -31,12 +48,15 @@ export async function runServerTests(
31
48
  playwrightUseOpts?: PlaywrightUseOpts
32
49
  projectName?: string
33
50
  coverage?: CoverageConfig
51
+ workerShutdownTimeoutMs?: number
52
+ pool?: RemixTestPool
34
53
  } = {},
35
54
  ): Promise<Counts & { coverageMap: CoverageMap | null }> {
36
55
  let counts: Counts = { passed: 0, failed: 0, skipped: 0, todo: 0 }
37
56
  let coverageMap: CoverageMap | null = null
38
57
  let cwd = options.cwd ?? process.cwd()
39
58
  let envLabel = options.projectName ? `${type}:${options.projectName}` : type
59
+ let pool = options.pool ?? 'forks'
40
60
 
41
61
  function accumulate(results: TestResults, file: string) {
42
62
  reporter.onResult(
@@ -56,7 +76,7 @@ export async function runServerTests(
56
76
  files,
57
77
  concurrency,
58
78
  (file) =>
59
- runFileInWorker(
79
+ runFileInPool(
60
80
  file,
61
81
  type,
62
82
  (results) => {
@@ -67,10 +87,13 @@ export async function runServerTests(
67
87
  },
68
88
  {
69
89
  ...options,
90
+ pool,
70
91
  playwrightUseOpts: options.playwrightUseOpts,
71
92
  },
72
93
  ),
73
94
  () => counts.failed++,
95
+ !options.open,
96
+ options.workerShutdownTimeoutMs ?? DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS,
74
97
  )
75
98
 
76
99
  if (options.coverage && allBrowserCoverageEntries.length > 0) {
@@ -92,8 +115,14 @@ export async function runServerTests(
92
115
  await runInConcurrentWorkers(
93
116
  files,
94
117
  concurrency,
95
- (file) => runFileInWorker(file, type, (results) => accumulate(results, file), options),
118
+ (file) =>
119
+ runFileInPool(file, type, (results) => accumulate(results, file), {
120
+ ...options,
121
+ pool,
122
+ }),
96
123
  () => counts.failed++,
124
+ true,
125
+ options.workerShutdownTimeoutMs ?? DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS,
97
126
  )
98
127
 
99
128
  if (coverageDataDir) {
@@ -109,8 +138,10 @@ export async function runServerTests(
109
138
  async function runInConcurrentWorkers(
110
139
  files: string[],
111
140
  concurrency: number,
112
- runFile: (file: string) => Promise<void>,
141
+ runFile: (file: string) => WorkerRun,
113
142
  onError: () => void,
143
+ terminateWhenFinished: boolean,
144
+ workerShutdownTimeoutMs: number,
114
145
  ): Promise<void> {
115
146
  let index = 0
116
147
  let active = 0
@@ -122,22 +153,42 @@ async function runInConcurrentWorkers(
122
153
  index++
123
154
  active++
124
155
 
125
- runFile(file).then(
126
- () => {
127
- active--
128
- if (index < files.length) {
129
- dispatch()
130
- } else if (active === 0) {
131
- resolve()
156
+ let run = runFile(file)
157
+
158
+ function complete() {
159
+ active--
160
+ if (index < files.length) {
161
+ dispatch()
162
+ } else if (active === 0) {
163
+ resolve()
164
+ }
165
+ }
166
+
167
+ run.finished.then(
168
+ async () => {
169
+ try {
170
+ if (terminateWhenFinished) {
171
+ let exited = await waitForWorkerExit(run.exited, workerShutdownTimeoutMs)
172
+ if (!exited) {
173
+ let terminated = await run.terminate()
174
+ if (!terminated) {
175
+ onError()
176
+ }
177
+ }
178
+ }
179
+ } finally {
180
+ complete()
132
181
  }
133
182
  },
134
- (err) => {
135
- console.error(`Error running ${file}:`, err.message)
136
- console.error(err)
137
- onError()
138
- active--
139
- if (active === 0 && index >= files.length) resolve()
140
- else dispatch()
183
+ async (err) => {
184
+ try {
185
+ console.error(`Error running ${file}:`, err instanceof Error ? err.message : err)
186
+ console.error(err)
187
+ onError()
188
+ await run.terminate()
189
+ } finally {
190
+ complete()
191
+ }
141
192
  },
142
193
  )
143
194
  }
@@ -149,41 +200,193 @@ async function runInConcurrentWorkers(
149
200
  })
150
201
  }
151
202
 
152
- function runFileInWorker(
203
+ function waitForWorkerExit(exited: Promise<number | null>, timeoutMs: number): Promise<boolean> {
204
+ return new Promise((resolve) => {
205
+ let timeout = setTimeout(() => resolve(false), timeoutMs)
206
+ exited.then(() => {
207
+ clearTimeout(timeout)
208
+ resolve(true)
209
+ })
210
+ })
211
+ }
212
+
213
+ function runFileInPool(
153
214
  file: string,
154
215
  type: 'server' | 'e2e',
155
216
  onResults: (results: TestResults) => void,
156
- options: {
157
- cwd?: string
158
- coverage?: CoverageConfig
159
- open?: boolean
160
- playwrightUseOpts?: PlaywrightUseOpts
161
- } = {},
162
- ): Promise<void> {
163
- return new Promise((resolve, reject) => {
164
- let worker =
165
- type === 'e2e'
166
- ? new Worker(workerE2EUrl, {
167
- workerData: {
168
- file: pathToFileURL(file).href,
169
- type,
170
- coverage: options.coverage,
171
- open: options.open,
172
- playwrightUseOpts: options.playwrightUseOpts,
173
- },
174
- })
175
- : new Worker(workerUrl, {
176
- workerData: {
177
- file: pathToFileURL(file).href,
178
- type,
179
- coverage: options.coverage,
180
- },
181
- })
182
- worker.once('message', (msg: TestResults) => onResults(msg))
217
+ options: RunFileOptions,
218
+ ): WorkerRun {
219
+ return options.pool === 'threads'
220
+ ? runFileInWorker(file, type, onResults, options)
221
+ : runFileInProcess(file, type, onResults, options)
222
+ }
223
+
224
+ export function runFileInWorker(
225
+ file: string,
226
+ type: 'server' | 'e2e',
227
+ onResults: (results: TestResults) => void,
228
+ options: RunFileOptions = {},
229
+ ): WorkerRun {
230
+ let receivedResults = false
231
+ let worker =
232
+ type === 'e2e'
233
+ ? new Worker(workerE2EUrl, {
234
+ workerData: {
235
+ file: pathToFileURL(file).href,
236
+ type,
237
+ coverage: options.coverage,
238
+ open: options.open,
239
+ playwrightUseOpts: options.playwrightUseOpts,
240
+ },
241
+ })
242
+ : new Worker(workerUrl, {
243
+ workerData: {
244
+ file: pathToFileURL(file).href,
245
+ type,
246
+ coverage: options.coverage,
247
+ },
248
+ })
249
+
250
+ let exited = new Promise<number>((resolve) => {
251
+ worker.once('exit', (code) => resolve(code))
252
+ })
253
+
254
+ let finished = new Promise<void>((resolve, reject) => {
255
+ worker.once('message', (msg: TestResults) => {
256
+ receivedResults = true
257
+ try {
258
+ onResults(msg)
259
+ } catch (error) {
260
+ reject(error)
261
+ return
262
+ }
263
+ if (!options.open) {
264
+ resolve()
265
+ }
266
+ })
183
267
  worker.once('error', reject)
184
- worker.once('exit', (code) => {
185
- if (code !== 0) reject(new Error(`Worker exited with code ${code}`))
186
- else resolve()
268
+ exited.then((code) => {
269
+ if (receivedResults || code === 0) {
270
+ resolve()
271
+ } else {
272
+ reject(new Error(`Worker exited with code ${code}`))
273
+ }
274
+ })
275
+ })
276
+
277
+ return {
278
+ finished,
279
+ exited,
280
+ async terminate() {
281
+ try {
282
+ await worker.terminate()
283
+ return true
284
+ } catch (err) {
285
+ console.error(
286
+ `Error terminating worker for ${file}:`,
287
+ err instanceof Error ? err.message : err,
288
+ )
289
+ console.error(err)
290
+ return false
291
+ }
292
+ },
293
+ }
294
+ }
295
+
296
+ function runFileInProcess(
297
+ file: string,
298
+ type: 'server' | 'e2e',
299
+ onResults: (results: TestResults) => void,
300
+ options: RunFileOptions = {},
301
+ ): WorkerRun {
302
+ let receivedResults = false
303
+ let child = fork(fileURLToPath(workerProcessUrl), [], {
304
+ serialization: 'advanced',
305
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
306
+ })
307
+
308
+ let exited = new Promise<number | null>((resolve) => {
309
+ child.once('exit', (code) => resolve(code))
310
+ })
311
+
312
+ let finished = new Promise<void>((resolve, reject) => {
313
+ child.once('message', (msg: unknown) => {
314
+ if (!isTestResults(msg)) {
315
+ reject(new Error('Test worker process sent invalid results'))
316
+ return
317
+ }
318
+
319
+ receivedResults = true
320
+ try {
321
+ onResults(msg)
322
+ } catch (error) {
323
+ reject(error)
324
+ return
325
+ }
326
+ if (!options.open) {
327
+ resolve()
328
+ }
187
329
  })
330
+ child.once('error', reject)
331
+ exited.then((code) => {
332
+ if (receivedResults || code === 0) {
333
+ resolve()
334
+ } else {
335
+ reject(new Error(`Worker process exited with code ${code}`))
336
+ }
337
+ })
338
+ child.send(
339
+ {
340
+ file: pathToFileURL(file).href,
341
+ type,
342
+ coverage: options.coverage,
343
+ open: options.open,
344
+ playwrightUseOpts: options.playwrightUseOpts,
345
+ },
346
+ (error) => {
347
+ if (error) {
348
+ reject(error)
349
+ }
350
+ },
351
+ )
188
352
  })
353
+
354
+ return {
355
+ finished,
356
+ exited,
357
+ terminate: () => terminateChildProcess(child, exited),
358
+ }
359
+ }
360
+
361
+ async function terminateChildProcess(
362
+ child: ChildProcess,
363
+ exited: Promise<number | null>,
364
+ ): Promise<boolean> {
365
+ if (child.exitCode !== null || child.signalCode !== null) {
366
+ return true
367
+ }
368
+
369
+ if (!child.kill()) {
370
+ return false
371
+ }
372
+
373
+ return await waitForWorkerExit(exited, 5_000)
374
+ }
375
+
376
+ function isTestResults(value: unknown): value is TestResults {
377
+ if (!isRecord(value)) {
378
+ return false
379
+ }
380
+
381
+ return (
382
+ typeof value.passed === 'number' &&
383
+ typeof value.failed === 'number' &&
384
+ typeof value.skipped === 'number' &&
385
+ typeof value.todo === 'number' &&
386
+ Array.isArray(value.tests)
387
+ )
388
+ }
389
+
390
+ function isRecord(value: unknown): value is Record<string, unknown> {
391
+ return typeof value === 'object' && value !== null
189
392
  }
@@ -0,0 +1,98 @@
1
+ import { runTests } from './executor.ts'
2
+ import { importModule } from './import-module.ts'
3
+ import {
4
+ getBrowserLauncher,
5
+ getPlaywrightLaunchOptions,
6
+ getPlaywrightPageOptions,
7
+ type PlaywrightUseOpts,
8
+ } from './playwright.ts'
9
+ import type { CoverageConfig } from './coverage.ts'
10
+ import type { TestResults } from './reporters/results.ts'
11
+ import { createFailedResults } from './worker-results.ts'
12
+ import { isRecord, parseCoverageConfig } from './worker-server.ts'
13
+
14
+ export interface E2ETestWorkerData {
15
+ file: string
16
+ coverage?: CoverageConfig
17
+ open?: boolean
18
+ playwrightUseOpts?: PlaywrightUseOpts
19
+ }
20
+
21
+ export async function runE2ETestFile(
22
+ value: unknown,
23
+ onOpenResults?: (results: TestResults) => void | Promise<void>,
24
+ ): Promise<TestResults | undefined> {
25
+ try {
26
+ let workerData = parseE2ETestWorkerData(value)
27
+
28
+ await importModule(workerData.file, import.meta)
29
+
30
+ let launcher = await getBrowserLauncher(workerData.playwrightUseOpts)
31
+ let opts = getPlaywrightLaunchOptions(workerData.playwrightUseOpts)
32
+ let browser = await launcher.launch(opts)
33
+ let browserClosed = false
34
+
35
+ try {
36
+ let results = await runTests({
37
+ browser,
38
+ open: workerData.open ?? false,
39
+ playwrightPageOptions: getPlaywrightPageOptions(workerData.playwrightUseOpts),
40
+ coverage: !!workerData.coverage,
41
+ })
42
+
43
+ if (workerData.open) {
44
+ await onOpenResults?.(results)
45
+ console.log('\nBrowser is open. Press Ctrl+C to close.')
46
+ await new Promise<void>((resolve) => browser.on('disconnected', () => resolve()))
47
+ return undefined
48
+ }
49
+
50
+ await browser.close()
51
+ browserClosed = true
52
+ return results
53
+ } finally {
54
+ if (!browserClosed) {
55
+ await browser.close()
56
+ }
57
+ }
58
+ } catch (error) {
59
+ return createFailedResults(error)
60
+ }
61
+ }
62
+
63
+ function parseE2ETestWorkerData(value: unknown): E2ETestWorkerData {
64
+ if (!isRecord(value) || typeof value.file !== 'string') {
65
+ throw new Error('Invalid E2E test worker data')
66
+ }
67
+
68
+ return {
69
+ file: value.file,
70
+ coverage: parseCoverageConfig(value.coverage),
71
+ open: parseBoolean(value.open, 'open'),
72
+ playwrightUseOpts: parsePlaywrightUseOpts(value.playwrightUseOpts),
73
+ }
74
+ }
75
+
76
+ function parseBoolean(value: unknown, name: string): boolean | undefined {
77
+ if (value === undefined) {
78
+ return undefined
79
+ }
80
+
81
+ if (typeof value !== 'boolean') {
82
+ throw new Error(`Invalid E2E test worker ${name}`)
83
+ }
84
+
85
+ return value
86
+ }
87
+
88
+ function parsePlaywrightUseOpts(value: unknown): PlaywrightUseOpts | undefined {
89
+ if (value === undefined) {
90
+ return undefined
91
+ }
92
+
93
+ if (!isRecord(value)) {
94
+ throw new Error('Invalid E2E test worker playwright options')
95
+ }
96
+
97
+ return value as PlaywrightUseOpts
98
+ }
@@ -1,54 +1,17 @@
1
- import { workerData, parentPort } from 'node:worker_threads'
2
- import { runTests } from './executor.ts'
3
- import { importModule } from './import-module.ts'
4
- import {
5
- getBrowserLauncher,
6
- getPlaywrightLaunchOptions,
7
- getPlaywrightPageOptions,
8
- } from './playwright.ts'
9
- import type { TestResults } from './reporters/results.ts'
1
+ import { parentPort, workerData } from 'node:worker_threads'
2
+ import { runE2ETestFile } from './worker-e2e-file.ts'
10
3
 
11
- try {
12
- await importModule(workerData.file, import.meta)
4
+ if (!parentPort) {
5
+ throw new Error('E2E test worker is missing a parent port')
6
+ }
7
+
8
+ const port = parentPort
9
+ const results = await runE2ETestFile(workerData, (openResults) => {
10
+ port.postMessage(openResults)
11
+ })
13
12
 
14
- let launcher = await getBrowserLauncher(workerData.playwrightUseOpts)
15
- let opts = getPlaywrightLaunchOptions(workerData.playwrightUseOpts)
16
- let browser = await launcher.launch(opts)
17
- try {
18
- let results = await runTests({
19
- browser,
20
- open: workerData.open,
21
- playwrightPageOptions: getPlaywrightPageOptions(workerData.playwrightUseOpts),
22
- coverage: workerData.coverage,
23
- })
24
- parentPort!.postMessage(results)
25
- if (workerData.open) {
26
- console.log('\nBrowser is open. Press Ctrl+C to close.')
27
- await new Promise<void>((resolve) => browser.on('disconnected', () => resolve()))
28
- }
29
- } finally {
30
- await browser.close()
31
- }
32
- process.exit(0)
33
- } catch (e) {
34
- let results: TestResults = {
35
- passed: 0,
36
- failed: 1,
37
- skipped: 0,
38
- todo: 0,
39
- tests: [
40
- {
41
- name: '',
42
- suiteName: '',
43
- status: 'failed',
44
- duration: 0,
45
- error: {
46
- message: e instanceof Error ? e.message : String(e),
47
- stack: e instanceof Error ? e.stack : undefined,
48
- },
49
- },
50
- ],
51
- }
52
- parentPort!.postMessage(results)
53
- process.exit(0)
13
+ if (results) {
14
+ port.postMessage(results)
54
15
  }
16
+
17
+ process.exit(0)