codeceptjs 4.2.0 → 4.2.1-beta.1

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 (63) hide show
  1. package/README.md +2 -0
  2. package/bin/codecept.js +11 -0
  3. package/docs/agents.md +26 -1
  4. package/docs/ai.md +57 -0
  5. package/docs/alternative-browsers.md +175 -148
  6. package/docs/assertions.md +74 -0
  7. package/docs/bootstrap.md +2 -0
  8. package/docs/commands.md +45 -0
  9. package/docs/configuration.md +37 -0
  10. package/docs/debugging.md +1 -1
  11. package/docs/helpers/ApiDataFactory.md +20 -20
  12. package/docs/helpers/Appium.md +1615 -670
  13. package/docs/helpers/Decision.md +146 -0
  14. package/docs/helpers/GraphQL.md +31 -31
  15. package/docs/helpers/GraphQLDataFactory.md +20 -20
  16. package/docs/helpers/Kitesurf.md +1808 -25
  17. package/docs/helpers/Lightpanda.md +139 -0
  18. package/docs/helpers/Obscura.md +1799 -74
  19. package/docs/helpers/Playwright.md +490 -428
  20. package/docs/helpers/Puppeteer.md +317 -255
  21. package/docs/helpers/REST.md +43 -43
  22. package/docs/helpers/WebDriver.md +237 -175
  23. package/docs/installation.md +16 -0
  24. package/docs/mcp.md +7 -1
  25. package/docs/parallel.md +35 -1
  26. package/docs/plugins/pause.md +2 -1
  27. package/docs/plugins/sitemap.md +48 -0
  28. package/docs/plugins.md +4 -0
  29. package/lib/ai.js +81 -2
  30. package/lib/codecept.js +32 -2
  31. package/lib/command/init.js +22 -0
  32. package/lib/command/initAgent.js +78 -0
  33. package/lib/command/run-multiple.js +3 -0
  34. package/lib/command/run-workers.js +10 -3
  35. package/lib/command/run.js +3 -2
  36. package/lib/command/utils.js +15 -0
  37. package/lib/command/workers/runTests.js +10 -10
  38. package/lib/container.js +4 -2
  39. package/lib/effects.js +19 -1
  40. package/lib/helper/CDPBrowser.js +349 -1384
  41. package/lib/helper/Decision.js +256 -0
  42. package/lib/helper/Kitesurf.js +3 -21
  43. package/lib/helper/Lightpanda.js +137 -0
  44. package/lib/helper/Obscura.js +21 -238
  45. package/lib/helper/Playwright.js +6 -4
  46. package/lib/helper/Puppeteer.js +12 -7
  47. package/lib/helper/WebDriver.js +2 -1
  48. package/lib/listener/result.js +4 -0
  49. package/lib/mocha/bdd.js +19 -1
  50. package/lib/plugin/junitReporter.js +4 -1
  51. package/lib/plugin/pause.js +4 -1
  52. package/lib/plugin/sitemap.js +228 -0
  53. package/lib/recorder.js +19 -18
  54. package/lib/result.js +16 -0
  55. package/lib/step/base.js +12 -0
  56. package/lib/utils.js +2 -10
  57. package/lib/webServer.js +193 -0
  58. package/lib/workers.js +118 -31
  59. package/package.json +4 -2
  60. package/typings/index.d.ts +60 -7
  61. package/typings/promiseBasedTypes.d.ts +772 -824
  62. package/typings/types.d.ts +866 -833
  63. package/docs/helpers/CDPBrowser.md +0 -2138
package/lib/workers.js CHANGED
@@ -20,9 +20,11 @@ import output from './output.js'
20
20
  import event from './event.js'
21
21
  import { deserializeTest } from './mocha/test.js'
22
22
  import { deserializeSuite } from './mocha/suite.js'
23
+ import Step from './step/base.js'
23
24
  import recorder from './recorder.js'
24
25
  import store from './store.js'
25
26
  import runHook from './hooks.js'
27
+ import WebServer from './webServer.js'
26
28
  import WorkerStorage from './workerStorage.js'
27
29
  import { createRuns } from './command/run-multiple/collection.js'
28
30
 
@@ -57,11 +59,23 @@ const populateGroups = numberOfWorkers => {
57
59
  return groups
58
60
  }
59
61
 
60
- const createWorker = (workerObject, isPoolMode = false) => {
62
+ const matchesGrep = (mocha, test) => {
63
+ const { grep, invert } = mocha.options
64
+ if (!grep) return true
65
+ return grep.test(test.fullTitle()) !== Boolean(invert)
66
+ }
67
+
68
+ const withoutEmptyGroups = groups => {
69
+ const nonEmpty = groups.filter(group => group.length)
70
+ return nonEmpty.length ? nonEmpty : groups.slice(0, 1)
71
+ }
72
+
73
+ const createWorker = (workerObject, isPoolMode = false, skipTests = []) => {
61
74
  const worker = new Worker(pathToWorker, {
62
75
  workerData: {
63
76
  options: simplifyObject(workerObject.options),
64
77
  tests: workerObject.tests,
78
+ skipTests,
65
79
  testRoot: workerObject.testRoot,
66
80
  workerIndex: workerObject.workerIndex + 1,
67
81
  poolMode: isPoolMode,
@@ -104,6 +118,8 @@ const simplifyObject = object => {
104
118
  }, {})
105
119
  }
106
120
 
121
+ const deserializeStep = data => Object.assign(new Step(data.title), data)
122
+
107
123
  const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns) => {
108
124
  selectedRuns = options && options.all && config.multiple ? Object.keys(config.multiple) : selectedRuns
109
125
  if (selectedRuns === undefined || !selectedRuns.length || config.multiple === undefined) {
@@ -213,6 +229,7 @@ class WorkerObject {
213
229
  */
214
230
  constructor(workerIndex) {
215
231
  this.workerIndex = workerIndex
232
+ this.serial = false
216
233
  this.options = {}
217
234
  this.tests = []
218
235
  this.testRoot = getTestRoot()
@@ -288,6 +305,7 @@ class Workers extends EventEmitter {
288
305
  this.testGroups = []
289
306
  this.testPool = []
290
307
  this.testPoolInitialized = false
308
+ this.serialTests = []
291
309
  this.isPoolMode = config.by === 'pool'
292
310
  this.activeWorkers = new Map()
293
311
  this.maxWorkers = numberOfWorkers // Track original worker count for pool mode
@@ -312,9 +330,21 @@ class Workers extends EventEmitter {
312
330
 
313
331
  async _initWorkers(numberOfWorkers, config) {
314
332
  await this.splitTestsByGroups(numberOfWorkers, config)
333
+ this.serialTests = await this.createGroupOfSerialTests()
334
+ if (this.serialTests.length && !this.isPoolMode) {
335
+ this.testGroups = this.testGroups.map(group => group.filter(uid => !this.serialTests.includes(uid))).filter(group => group.length)
336
+ }
315
337
  // For function-based grouping, use the actual number of test groups created
316
338
  const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers
317
339
  this.workers = createWorkerObjects(this.testGroups, this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns)
340
+ if (this.serialTests.length) {
341
+ const serialWorkers = createWorkerObjects([this.serialTests], this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns)
342
+ for (const worker of serialWorkers) {
343
+ worker.workerIndex = this.workers.length
344
+ worker.serial = true
345
+ this.workers.push(worker)
346
+ }
347
+ }
318
348
  this.numberOfWorkers = this.workers.length
319
349
  }
320
350
 
@@ -345,7 +375,8 @@ class Workers extends EventEmitter {
345
375
  if (config.by === 'pool') {
346
376
  this.createTestPool(numberOfWorkers)
347
377
  } else {
348
- this.testGroups = config.by === 'suite' ? await this.createGroupsOfSuites(numberOfWorkers) : await this.createGroupsOfTests(numberOfWorkers)
378
+ const groups = config.by === 'suite' ? await this.createGroupsOfSuites(numberOfWorkers) : await this.createGroupsOfTests(numberOfWorkers)
379
+ this.testGroups = withoutEmptyGroups(groups)
349
380
  }
350
381
  }
351
382
  }
@@ -375,17 +406,21 @@ class Workers extends EventEmitter {
375
406
  const mocha = Container.mocha()
376
407
  mocha.files = files
377
408
  await loadTests(mocha)
409
+ this.codecept.filterLastFailedTests(mocha)
378
410
 
379
411
  const groups = populateGroups(numberOfWorkers)
380
412
  let groupCounter = 0
381
413
 
414
+ const tests = []
382
415
  mocha.suite.eachTest(test => {
383
- const i = groupCounter % groups.length
384
- if (test) {
385
- groups[i].push(test.uid)
386
- groupCounter++
387
- }
416
+ if (test) tests.push(test)
388
417
  })
418
+ const selected = tests.filter(test => matchesGrep(mocha, test))
419
+
420
+ for (const test of selected.length ? selected : tests) {
421
+ groups[groupCounter % groups.length].push(test.uid)
422
+ groupCounter++
423
+ }
389
424
 
390
425
  // Clean up after collecting test UIDs
391
426
  mocha.unloadFiles()
@@ -393,6 +428,32 @@ class Workers extends EventEmitter {
393
428
  return groups
394
429
  }
395
430
 
431
+ /**
432
+ * Collects tests matching `--serial` option or `serial` config pattern.
433
+ * These tests are excluded from parallel workers and run one by one after them.
434
+ *
435
+ * @returns {Promise<string[]>}
436
+ */
437
+ async createGroupOfSerialTests() {
438
+ const pattern = this.options.serial || this.codecept?.config.serial
439
+ if (!pattern) return []
440
+ const serial = new RegExp(pattern)
441
+
442
+ Container.createMocha(this.codecept.config.mocha || {}, this.options)
443
+ const mocha = Container.mocha()
444
+ mocha.files = this.codecept.testFiles
445
+ await loadTests(mocha, { reload: true })
446
+
447
+ const group = []
448
+ mocha.suite.eachTest(test => {
449
+ if (test && serial.test(test.fullTitle()) && matchesGrep(mocha, test)) group.push(test.uid)
450
+ })
451
+
452
+ mocha.unloadFiles()
453
+
454
+ return group
455
+ }
456
+
396
457
  /**
397
458
  * @param {Number} numberOfWorkers
398
459
  */
@@ -463,15 +524,15 @@ class Workers extends EventEmitter {
463
524
  const mocha = Container.mocha()
464
525
  mocha.files = files
465
526
  await loadTests(mocha)
527
+ this.codecept.filterLastFailedTests(mocha)
466
528
 
467
- mocha.suite.suites.forEach(suite => {
529
+ const suites = mocha.suite.suites.map(suite => suite.tests.filter(test => test))
530
+ const selected = suites.map(tests => tests.filter(test => matchesGrep(mocha, test))).filter(tests => tests.length)
531
+
532
+ for (const tests of selected.length ? selected : suites) {
468
533
  const i = indexOfSmallestElement(groups)
469
- suite.tests.forEach(test => {
470
- if (test) {
471
- groups[i].push(test.uid)
472
- }
473
- })
474
- })
534
+ tests.forEach(test => groups[i].push(test.uid))
535
+ }
475
536
 
476
537
  // Clean up after collecting test UIDs
477
538
  mocha.unloadFiles()
@@ -490,12 +551,17 @@ class Workers extends EventEmitter {
490
551
 
491
552
  async bootstrapAll() {
492
553
  await this._ensureInitialized()
554
+ await WebServer.startAll(this.codecept.config.webServer)
493
555
  return runHook(this.codecept.config.bootstrapAll, 'bootstrapAll')
494
556
  }
495
557
 
496
558
  async teardownAll() {
497
559
  await this._ensureInitialized()
498
- return runHook(this.codecept.config.teardownAll, 'teardownAll')
560
+ try {
561
+ return await runHook(this.codecept.config.teardownAll, 'teardownAll')
562
+ } finally {
563
+ await WebServer.stopAll()
564
+ }
499
565
  }
500
566
 
501
567
  async run() {
@@ -520,7 +586,8 @@ class Workers extends EventEmitter {
520
586
  : 0
521
587
 
522
588
  for (const worker of this.workers) {
523
- const workerThread = createWorker(worker, this.isPoolMode)
589
+ if (worker.serial) continue
590
+ const workerThread = createWorker(worker, this.isPoolMode, this.serialTests)
524
591
  this._listenWorkerEvents(workerThread)
525
592
  workerThreads.push(workerThread)
526
593
 
@@ -531,6 +598,8 @@ class Workers extends EventEmitter {
531
598
  }
532
599
  }
533
600
 
601
+ if (!workerThreads.length) this._runSerialWorker()
602
+
534
603
  recorder.add('workers started', () => {
535
604
  // Workers are already running, this is just a placeholder step
536
605
  })
@@ -542,6 +611,14 @@ class Workers extends EventEmitter {
542
611
  })
543
612
  }
544
613
 
614
+ _runSerialWorker() {
615
+ const parallelWorkers = this.workers.filter(worker => !worker.serial)
616
+ if (this.closedWorkers < parallelWorkers.length) return
617
+ const worker = this.workers[this.closedWorkers]
618
+ if (!worker) return
619
+ this._listenWorkerEvents(createWorker(worker))
620
+ }
621
+
545
622
  /**
546
623
  * @returns {Array<WorkerObject>}
547
624
  */
@@ -720,20 +797,32 @@ class Workers extends EventEmitter {
720
797
  }
721
798
  break
722
799
  case event.step.finished:
723
- this.emit(event.step.finished, message.data)
724
- event.dispatcher.emit(event.step.finished, message.data)
800
+ {
801
+ const step = deserializeStep(message.data)
802
+ this.emit(event.step.finished, step)
803
+ event.dispatcher.emit(event.step.finished, step)
804
+ }
725
805
  break
726
806
  case event.step.started:
727
- this.emit(event.step.started, message.data)
728
- event.dispatcher.emit(event.step.started, message.data)
807
+ {
808
+ const step = deserializeStep(message.data)
809
+ this.emit(event.step.started, step)
810
+ event.dispatcher.emit(event.step.started, step)
811
+ }
729
812
  break
730
813
  case event.step.passed:
731
- this.emit(event.step.passed, message.data)
732
- event.dispatcher.emit(event.step.passed, message.data)
814
+ {
815
+ const step = deserializeStep(message.data)
816
+ this.emit(event.step.passed, step)
817
+ event.dispatcher.emit(event.step.passed, step)
818
+ }
733
819
  break
734
820
  case event.step.failed:
735
- this.emit(event.step.failed, message.data, message.data.error)
736
- event.dispatcher.emit(event.step.failed, message.data, message.data.error)
821
+ {
822
+ const step = deserializeStep(message.data)
823
+ this.emit(event.step.failed, step, step.error)
824
+ event.dispatcher.emit(event.step.failed, step, step.error)
825
+ }
737
826
  break
738
827
  case event.hook.failed:
739
828
  // Hook failures are already reported as test failures by the worker
@@ -773,14 +862,10 @@ class Workers extends EventEmitter {
773
862
  process.exitCode = 1
774
863
  }
775
864
 
776
- if (this.isPoolMode) {
777
- // Pool mode: finish when all workers have exited and no more tests
778
- if (this.closedWorkers === this.numberOfWorkers) {
779
- this._finishRun()
780
- }
781
- } else if (this.closedWorkers === this.numberOfWorkers) {
782
- // Regular mode: finish when all original workers have exited
865
+ if (this.closedWorkers === this.numberOfWorkers) {
783
866
  this._finishRun()
867
+ } else {
868
+ this._runSerialWorker()
784
869
  }
785
870
  })
786
871
  }
@@ -802,6 +887,7 @@ class Workers extends EventEmitter {
802
887
  // For tests with retries configured, emit all failures + final success
803
888
  // For tests without retries, emit only final state
804
889
  const lastState = states[states.length - 1]
890
+ Container.result().addTest(deserializeTest(lastState.data))
805
891
 
806
892
  // Check if this test had retries by looking for failure followed by success
807
893
  const hasRetryPattern = states.length > 1 && states.some((s, i) => s.isFailed && i < states.length - 1 && !states[i + 1].isFailed)
@@ -829,6 +915,7 @@ class Workers extends EventEmitter {
829
915
 
830
916
  this.emit(event.all.result, Container.result())
831
917
  event.dispatcher.emit(event.workers.result, Container.result())
918
+ if (Container.result().tests.length) Container.result().save()
832
919
  this.emit('end') // internal event
833
920
  }
834
921
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.2.0",
3
+ "version": "4.2.1-beta.1",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [
@@ -80,6 +80,7 @@
80
80
  "test:unit:webbapi:webDriver:noSeleniumServer": "mocha test/helper/WebDriver.noSeleniumServer_test.js --timeout 10000 --reporter @testomatio/reporter/mocha",
81
81
  "test:unit:webbapi:cdpbrowser": "mocha test/helper/CDPBrowser_chrome_test.js --timeout 30000 --reporter @testomatio/reporter/mocha",
82
82
  "test:unit:webbapi:obscura": "mocha test/helper/CDPBrowser_obscura_test.js --timeout 30000 --reporter @testomatio/reporter/mocha",
83
+ "test:unit:webbapi:lightpanda": "mocha test/helper/CDPBrowser_lightpanda_test.js --timeout 30000 --reporter @testomatio/reporter/mocha",
83
84
  "test:unit:expect": "mocha test/helper/Expect_test.js --reporter @testomatio/reporter/mocha",
84
85
  "test:plugin": "mocha test/plugin/plugin_test.js --reporter @testomatio/reporter/mocha",
85
86
  "def": "./runok.cjs def",
@@ -101,7 +102,7 @@
101
102
  "@modelcontextprotocol/sdk": "^1.26.0",
102
103
  "@xmldom/xmldom": "0.9.10",
103
104
  "acorn": "8.15.0",
104
- "ai": "^6.0.43",
105
+ "ai": "7.0.132",
105
106
  "arrify": "3.0.0",
106
107
  "axios": "1.16.1",
107
108
  "chalk": "4.1.2",
@@ -149,6 +150,7 @@
149
150
  "@eslint/js": "9.39.2",
150
151
  "@faker-js/faker": "10.3.0",
151
152
  "@inquirer/testing": "^3.0.3",
153
+ "@openrouter/ai-sdk-provider": "^3.1.0",
152
154
  "@pollyjs/adapter-puppeteer": "6.0.6",
153
155
  "@pollyjs/core": "6.0.6",
154
156
  "@testomatio/reporter": "^2.3.1",
@@ -81,6 +81,21 @@ declare namespace CodeceptJS {
81
81
  }
82
82
  }
83
83
 
84
+ type WebServerConfig = {
85
+ /** Shell command that starts the application, e.g. `npm run start` */
86
+ command: string
87
+ /** URL polled with GET until it responds; any HTTP status counts as ready */
88
+ url: string
89
+ /** Set to `false` to skip starting the server. Defaults to `true` */
90
+ enabled?: boolean
91
+ /** Milliseconds to wait for `url` to respond. Defaults to `60000` */
92
+ timeout?: number
93
+ /** Working directory for the command, relative to the config directory */
94
+ cwd?: string
95
+ /** Extra environment variables for the command */
96
+ env?: Record<string, string>
97
+ }
98
+
84
99
  type MainConfig = {
85
100
  /** Pattern to locate CodeceptJS tests.
86
101
  * Allows to enter glob pattern or an Array<string> of patterns to match tests / test file names.
@@ -132,6 +147,15 @@ declare namespace CodeceptJS {
132
147
  * ```
133
148
  */
134
149
  grep?: string
150
+ /**
151
+ * Pattern for tests that must not run in parallel.
152
+ * In `run-workers` matching tests run one by one after all parallel workers finish.
153
+ *
154
+ * ```js
155
+ * serial: '@serial'
156
+ * ```
157
+ */
158
+ serial?: string | RegExp
135
159
  /**
136
160
  * Enable and configure helpers:
137
161
  *
@@ -359,6 +383,17 @@ declare namespace CodeceptJS {
359
383
  * [Execute JS code after finishing tests in parallel mode](https://codecept.io/bootstrap/#bootstrapall-teardownall)
360
384
  */
361
385
  teardownAll?: (() => Promise<void>) | boolean | string
386
+ /**
387
+ * [Start the application under test](https://codecept.io/configuration/#web-server) before tests run and stop it after.
388
+ *
389
+ * ```js
390
+ * webServer: {
391
+ * command: 'npm run start',
392
+ * url: 'http://localhost:3000',
393
+ * }
394
+ * ```
395
+ */
396
+ webServer?: WebServerConfig | WebServerConfig[]
362
397
 
363
398
  /** Enable [localized test commands](https://codecept.io/translation/) */
364
399
  translation?: string
@@ -433,7 +468,12 @@ declare namespace CodeceptJS {
433
468
  // Could get extended by user generated typings
434
469
  interface Methods extends ActorStatic {}
435
470
  interface I {}
436
- interface IHook {}
471
+ interface IHook {
472
+ (callback: HookCallback): HookConfig
473
+ }
474
+ interface IHookSuite {
475
+ (callback: HookSuiteCallback): HookConfig
476
+ }
437
477
  interface IScenario {}
438
478
  interface IFeature {
439
479
  (title: string, opts?: { [key: string]: any }): FeatureConfig
@@ -473,8 +513,22 @@ declare namespace CodeceptJS {
473
513
 
474
514
  type StringOrSecret = string | CodeceptJS.Secret
475
515
 
516
+ interface GherkinCallback {
517
+ (test: Test): void | Promise<void>
518
+ }
519
+ interface GherkinEventEmitter {}
520
+ interface GherkinHook {
521
+ (callback: GherkinCallback): GherkinEventEmitter
522
+ }
476
523
  interface HookCallback {
477
- (args: SupportObject): void | Promise<void>
524
+ // `suite` has type Mocha.Suite if hook (Background/Before/After) is called before `Feature()`
525
+ // `suite` has type CodeceptJS.Suite if hook is called after `Feature()`
526
+ (args: SupportObject & { suite: Mocha.Suite; test: Test }): void | Promise<void>
527
+ }
528
+ interface HookSuiteCallback {
529
+ // `suite` has type Mocha.Suite if hook (BeforeSuite/AfterSuite) is called before `Feature()`
530
+ // `suite` has type CodeceptJS.Suite if hook is called after `Feature()`
531
+ (args: SupportObject & { suite: Mocha.Suite }): void | Promise<void>
478
532
  }
479
533
  interface Scenario extends IScenario {
480
534
  only: IScenario
@@ -495,9 +549,6 @@ declare namespace CodeceptJS {
495
549
  (title: string, callback?: HookCallback): ScenarioConfig
496
550
  (title: string, opts: { [key: string]: any }, callback: HookCallback): ScenarioConfig
497
551
  }
498
- interface IHook {
499
- (callback: HookCallback): HookConfig
500
- }
501
552
 
502
553
  interface Globals {
503
554
  codeceptjs: typeof codeceptjs
@@ -545,11 +596,13 @@ declare function xData(data: any): CodeceptJS.IData
545
596
  declare function DefineParameterType(options: CodeceptJS.IParameterTypeDefinition<any>): void
546
597
 
547
598
  // Hooks
548
- declare const BeforeSuite: CodeceptJS.IHook
549
- declare const AfterSuite: CodeceptJS.IHook
599
+ declare const BeforeSuite: CodeceptJS.IHookSuite
600
+ declare const AfterSuite: CodeceptJS.IHookSuite
550
601
  declare const Background: CodeceptJS.IHook
551
602
  declare const Before: CodeceptJS.IHook
552
603
  declare const After: CodeceptJS.IHook
604
+ declare const GherkinBefore: CodeceptJS.GherkinHook
605
+ declare const GherkinAfter: CodeceptJS.GherkinHook
553
606
 
554
607
  // Plugins
555
608
  declare const __: any