codeceptjs 4.0.0-rc.10 → 4.0.0-rc.15

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 (57) hide show
  1. package/bin/mcp-server.js +32 -5
  2. package/lib/ai.js +3 -2
  3. package/lib/assertions.js +18 -0
  4. package/lib/codecept.js +6 -6
  5. package/lib/command/check.js +2 -0
  6. package/lib/command/dryRun.js +2 -3
  7. package/lib/command/generate.js +2 -0
  8. package/lib/command/gherkin/snippets.js +5 -4
  9. package/lib/command/init.js +1 -0
  10. package/lib/command/run-multiple.js +2 -0
  11. package/lib/command/run-workers.js +1 -0
  12. package/lib/command/run.js +1 -1
  13. package/lib/command/workers/runTests.js +10 -10
  14. package/lib/container.js +63 -13
  15. package/lib/effects.js +17 -0
  16. package/lib/element/WebElement.js +128 -0
  17. package/lib/globals.js +22 -10
  18. package/lib/heal.js +4 -3
  19. package/lib/helper/ApiDataFactory.js +2 -1
  20. package/lib/helper/FileSystem.js +3 -2
  21. package/lib/helper/GraphQLDataFactory.js +2 -1
  22. package/lib/helper/Playwright.js +22 -17
  23. package/lib/helper/Puppeteer.js +20 -6
  24. package/lib/helper/WebDriver.js +12 -2
  25. package/lib/helper/errors/NonFocusedType.js +8 -0
  26. package/lib/helper/extras/Download.js +45 -0
  27. package/lib/helper/extras/focusCheck.js +43 -0
  28. package/lib/helper/extras/richTextEditor.js +115 -0
  29. package/lib/history.js +3 -2
  30. package/lib/listener/config.js +6 -4
  31. package/lib/listener/emptyRun.js +2 -1
  32. package/lib/listener/helpers.js +4 -1
  33. package/lib/listener/mocha.js +2 -1
  34. package/lib/listener/pageobjects.js +43 -0
  35. package/lib/listener/result.js +3 -2
  36. package/lib/locator.js +112 -0
  37. package/lib/mocha/cli.js +4 -2
  38. package/lib/mocha/factory.js +2 -1
  39. package/lib/mocha/scenarioConfig.js +2 -1
  40. package/lib/mocha/ui.js +5 -6
  41. package/lib/plugin/aiTrace.js +4 -3
  42. package/lib/plugin/analyze.js +1 -1
  43. package/lib/plugin/auth.js +3 -3
  44. package/lib/plugin/pageInfo.js +2 -1
  45. package/lib/plugin/pauseOn.js +167 -0
  46. package/lib/plugin/screenshotOnFail.js +3 -4
  47. package/lib/plugin/stepByStepReport.js +5 -4
  48. package/lib/rerun.js +2 -1
  49. package/lib/result.js +2 -1
  50. package/lib/step/base.js +3 -2
  51. package/lib/step/record.js +1 -1
  52. package/lib/store.js +72 -3
  53. package/lib/translation.js +2 -1
  54. package/lib/utils/mask_data.js +2 -1
  55. package/lib/utils.js +4 -3
  56. package/lib/workers.js +2 -0
  57. package/package.json +5 -3
@@ -0,0 +1,167 @@
1
+ import event from '../event.js'
2
+ import pause from '../pause.js'
3
+ import recorder from '../recorder.js'
4
+ import Container from '../container.js'
5
+ import output from '../output.js'
6
+
7
+ const supportedHelpers = Container.STANDARD_ACTING_HELPERS
8
+
9
+ /**
10
+ * Pauses test execution in different modes. Unlike `pauseOnFail`, this plugin supports
11
+ * multiple triggers for pausing and is controlled via CLI arguments.
12
+ *
13
+ * Enable it via `-p` option with a mode:
14
+ *
15
+ * ```
16
+ * npx codeceptjs run -p pauseOn:fail
17
+ * npx codeceptjs run -p pauseOn:step
18
+ * npx codeceptjs run -p pauseOn:file:tests/login_test.js
19
+ * npx codeceptjs run -p pauseOn:file:tests/login_test.js:43
20
+ * npx codeceptjs run -p pauseOn:url:/users/*
21
+ * ```
22
+ *
23
+ * #### Modes
24
+ *
25
+ * * **fail** — pause when a step fails (same as `pauseOnFail` plugin)
26
+ * * **step** — pause before first step, use `next` to advance step-by-step
27
+ * * **file** — pause when execution reaches a specific file (and optionally line)
28
+ * * **url** — pause when the browser URL matches a pattern (supports `*` wildcards)
29
+ *
30
+ */
31
+ export default function (config = {}) {
32
+ const args = config._args || []
33
+ const mode = args[0] || 'fail'
34
+
35
+ switch (mode) {
36
+ case 'fail':
37
+ return initFailMode()
38
+ case 'step':
39
+ return initStepMode()
40
+ case 'file':
41
+ return initFileMode(args.slice(1))
42
+ case 'url':
43
+ return initUrlMode(args.slice(1))
44
+ default:
45
+ output.error(`pauseOn: unknown mode "${mode}". Available: fail, step, file, url`)
46
+ }
47
+ }
48
+
49
+ function initFailMode() {
50
+ let failed = false
51
+
52
+ event.dispatcher.on(event.test.started, () => {
53
+ failed = false
54
+ })
55
+
56
+ event.dispatcher.on(event.step.failed, () => {
57
+ failed = true
58
+ })
59
+
60
+ event.dispatcher.on(event.test.after, () => {
61
+ if (failed) pause()
62
+ })
63
+ }
64
+
65
+ function initStepMode() {
66
+ let activated = false
67
+
68
+ event.dispatcher.on(event.test.before, () => {
69
+ if (activated) return
70
+ activated = true
71
+ recorder.add('pauseOn:step', () => pause())
72
+ })
73
+ }
74
+
75
+ function initFileMode(fileArgs) {
76
+ if (fileArgs.length === 0) {
77
+ output.error('pauseOn:file requires a path. Usage: -p pauseOn:file:<path>[:<line>]')
78
+ return
79
+ }
80
+
81
+ const targetFile = fileArgs[0]
82
+ const targetLine = fileArgs[1] ? parseInt(fileArgs[1], 10) : null
83
+ let paused = false
84
+
85
+ event.dispatcher.on(event.step.before, (step) => {
86
+ if (paused) return
87
+
88
+ const stepLine = step.line()
89
+ if (!stepLine) return
90
+
91
+ const match = parseStepLine(stepLine)
92
+ if (!match) return
93
+
94
+ const fileMatches = match.file.includes(targetFile) || match.file.endsWith(targetFile)
95
+ if (!fileMatches) return
96
+
97
+ if (targetLine !== null && match.line !== targetLine) return
98
+
99
+ paused = true
100
+ recorder.add('pauseOn:file', () => pause())
101
+ })
102
+ }
103
+
104
+ function initUrlMode(urlArgs) {
105
+ if (urlArgs.length === 0) {
106
+ output.error('pauseOn:url requires a pattern. Usage: -p pauseOn:url:<pattern>')
107
+ return
108
+ }
109
+
110
+ const urlPattern = urlArgs.join(':')
111
+
112
+ const helpers = Container.helpers()
113
+ let helper = null
114
+ for (const helperName of supportedHelpers) {
115
+ if (Object.keys(helpers).indexOf(helperName) > -1) {
116
+ helper = helpers[helperName]
117
+ }
118
+ }
119
+
120
+ if (!helper) {
121
+ output.error('pauseOn:url requires a browser helper (Playwright, WebDriver, Puppeteer, Appium)')
122
+ return
123
+ }
124
+
125
+ const regex = patternToRegex(urlPattern)
126
+ let paused = false
127
+
128
+ event.dispatcher.on(event.step.after, () => {
129
+ if (paused) return
130
+
131
+ recorder.add('pauseOn:url check', async () => {
132
+ if (paused) return
133
+ try {
134
+ const currentUrl = await helper.grabCurrentUrl()
135
+ if (regex.test(currentUrl)) {
136
+ paused = true
137
+ return pause()
138
+ }
139
+ } catch (err) {
140
+ // page may not be loaded yet
141
+ }
142
+ })
143
+ })
144
+ }
145
+
146
+ function parseStepLine(stepLine) {
147
+ let line = stepLine.trim()
148
+ if (line.startsWith('at ')) line = line.substring(3).trim()
149
+
150
+ const lastColon = line.lastIndexOf(':')
151
+ if (lastColon < 0) return null
152
+ const secondLastColon = line.lastIndexOf(':', lastColon - 1)
153
+ if (secondLastColon < 0) return null
154
+
155
+ const file = line.substring(0, secondLastColon)
156
+ const lineNum = parseInt(line.substring(secondLastColon + 1, lastColon), 10)
157
+
158
+ if (isNaN(lineNum)) return null
159
+
160
+ return { file, line: lineNum }
161
+ }
162
+
163
+ function patternToRegex(pattern) {
164
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
165
+ const regexStr = escaped.replace(/\*/g, '.*')
166
+ return new RegExp(regexStr)
167
+ }
@@ -8,6 +8,7 @@ import recorder from '../recorder.js'
8
8
  import event from '../event.js'
9
9
 
10
10
  import output from '../output.js'
11
+ import store from '../store.js'
11
12
 
12
13
  import { fileExists } from '../utils.js'
13
14
  import Codeceptjs from '../index.js'
@@ -94,7 +95,7 @@ export default function (config) {
94
95
  } else {
95
96
  fileName = `${testToFileName(test, { suffix: '', unique: false })}.failed.png`
96
97
  }
97
- const quietMode = !('output_dir' in global) || !global.output_dir
98
+ const quietMode = !store.outputDir
98
99
  if (!quietMode) {
99
100
  output.plugin('screenshotOnFail', 'Test failed, try to save a screenshot')
100
101
  }
@@ -139,9 +140,7 @@ export default function (config) {
139
140
  await Promise.race([screenshotPromise, timeoutPromise])
140
141
 
141
142
  if (!test.artifacts) test.artifacts = {}
142
- // Some unit tests may not define global.output_dir; avoid throwing when it is undefined
143
- // Detect output directory safely (may not be initialized in narrow unit tests)
144
- const baseOutputDir = 'output_dir' in global && typeof global.output_dir === 'string' && global.output_dir ? global.output_dir : null
143
+ const baseOutputDir = store.outputDir || null
145
144
  if (baseOutputDir) {
146
145
  test.artifacts.screenshot = path.join(baseOutputDir, fileName)
147
146
  if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) {
@@ -4,8 +4,9 @@ import figures from 'figures'
4
4
  import fs from 'fs'
5
5
  import { mkdirp } from 'mkdirp'
6
6
  import path from 'path'
7
- import cheerio from 'cheerio'
7
+ import * as cheerio from 'cheerio'
8
8
 
9
+ import store from '../store.js'
9
10
  import Container from '../container.js'
10
11
  import recorder from '../recorder.js'
11
12
  import event from '../event.js'
@@ -19,7 +20,7 @@ const defaultConfig = {
19
20
  animateSlides: true,
20
21
  ignoreSteps: [],
21
22
  fullPageScreenshots: false,
22
- output: global.output_dir,
23
+ output: store.outputDir,
23
24
  screenshotsForAllureReport: false,
24
25
  disableScreenshotOnFail: true,
25
26
  }
@@ -87,7 +88,7 @@ export default function (config) {
87
88
 
88
89
  const recordedTests = {}
89
90
  const pad = '0000'
90
- const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output
91
+ const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
91
92
 
92
93
  event.dispatcher.on(event.suite.before, suite => {
93
94
  stepNum = -1
@@ -198,7 +199,7 @@ export default function (config) {
198
199
  // Ignore steps from BeforeSuite function
199
200
  if (scenarioFailed && config.disableScreenshotOnFail) return
200
201
  if (step.metaStep && step.metaStep.name === 'BeforeSuite') return
201
- if (!step.test) return // Ignore steps from AfterSuite
202
+ if (!currentTest) return // Ignore steps from AfterSuite
202
203
 
203
204
  const fileName = `${pad.substring(0, pad.length - stepNum.toString().length) + stepNum.toString()}.png`
204
205
  if (step.status === 'failed') {
package/lib/rerun.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fsPath from 'path'
2
+ import store from './store.js'
2
3
  import container from './container.js'
3
4
  import event from './event.js'
4
5
  import BaseCodecept from './codecept.js'
@@ -29,7 +30,7 @@ class CodeceptRerunner extends BaseCodecept {
29
30
  let filesToRun = this.testFiles
30
31
  if (test) {
31
32
  if (!fsPath.isAbsolute(test)) {
32
- test = fsPath.join(global.codecept_dir, test)
33
+ test = fsPath.join(store.codeceptDir, test)
33
34
  }
34
35
  filesToRun = this.testFiles.filter(t => fsPath.basename(t, '.js') === test || t === test)
35
36
  }
package/lib/result.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { serializeTest } from './mocha/test.js'
4
+ import store from './store.js'
4
5
 
5
6
  /**
6
7
  * @typedef {Object} Stats Statistics for a test result.
@@ -212,7 +213,7 @@ class Result {
212
213
  */
213
214
  save(fileName) {
214
215
  if (!fileName) fileName = 'result.json'
215
- fs.writeFileSync(path.join(global.output_dir, fileName), JSON.stringify(this.simplify(), null, 2))
216
+ fs.writeFileSync(path.join(store.outputDir, fileName), JSON.stringify(this.simplify(), null, 2))
216
217
  }
217
218
 
218
219
  /**
package/lib/step/base.js CHANGED
@@ -3,6 +3,7 @@ import Secret from '../secret.js'
3
3
  import { getCurrentTimeout } from '../timeout.js'
4
4
  import { ucfirst, humanizeString, serializeError } from '../utils.js'
5
5
  import recordStep from './record.js'
6
+ import store from '../store.js'
6
7
 
7
8
  const STACK_LINE = 5
8
9
 
@@ -148,11 +149,11 @@ class Step {
148
149
  const lines = this.stack.split('\n')
149
150
  if (lines[STACK_LINE]) {
150
151
  let line = lines[STACK_LINE].trim()
151
- .replace(global.codecept_dir || '', '.')
152
+ .replace(store.codeceptDir || '', '.')
152
153
  .trim()
153
154
 
154
155
  // Map .temp.mjs back to original .ts files using container's tsFileMapping
155
- const fileMapping = global.container?.tsFileMapping?.()
156
+ const fileMapping = store.tsFileMapping
156
157
  if (line.includes('.temp.mjs') && fileMapping) {
157
158
  for (const [tsFile, mjsFile] of fileMapping.entries()) {
158
159
  if (line.includes(mjsFile)) {
@@ -63,7 +63,7 @@ function recordStep(step, args) {
63
63
  step.endTime = +Date.now()
64
64
 
65
65
  // Fix error stack to point to original .ts files (lazy import to avoid circular dependency)
66
- const fileMapping = global.container?.tsFileMapping?.()
66
+ const fileMapping = store.tsFileMapping
67
67
  if (fileMapping) {
68
68
  fixErrorStack(err, fileMapping)
69
69
  }
package/lib/store.js CHANGED
@@ -1,8 +1,34 @@
1
1
  /**
2
- * global values for current session
2
+ * Global store for current session
3
3
  * @namespace
4
4
  */
5
5
  const store = {
6
+ // --- Required (set once via initialize(), immutable after) ---
7
+
8
+ /** @type {string | null} */
9
+ _codeceptDir: null,
10
+ /** @type {string | null} */
11
+ _outputDir: null,
12
+
13
+ get codeceptDir() {
14
+ return this._codeceptDir || global.codecept_dir || null
15
+ },
16
+ set codeceptDir(val) {
17
+ this._codeceptDir = val
18
+ },
19
+
20
+ get outputDir() {
21
+ return this._outputDir || global.output_dir || null
22
+ },
23
+ set outputDir(val) {
24
+ this._outputDir = val
25
+ },
26
+
27
+ /** @type {boolean} */
28
+ workerMode: false,
29
+
30
+ // --- Session config (per-session, mutable, set at session start) ---
31
+
6
32
  /**
7
33
  * If we are in --debug mode
8
34
  * @type {boolean}
@@ -27,20 +53,63 @@ const store = {
27
53
  * @type {boolean}
28
54
  */
29
55
  dryRun: false,
56
+
57
+ /**
58
+ * Feature.only() was used
59
+ * @type {boolean}
60
+ */
61
+ featureOnly: false,
62
+
63
+ /**
64
+ * Scenario.only() was used
65
+ * @type {boolean}
66
+ */
67
+ scenarioOnly: false,
68
+
69
+ /**
70
+ * Mask sensitive data config
71
+ * @type {boolean|object}
72
+ */
73
+ maskSensitiveData: false,
74
+
75
+ /**
76
+ * noGlobals mode — user imports everything
77
+ * @type {boolean}
78
+ */
79
+ noGlobals: false,
80
+
81
+ // --- State (tracks current execution, changes constantly) ---
82
+
30
83
  /**
31
84
  * If we are in pause mode
32
85
  * @type {boolean}
33
86
  */
34
87
  onPause: false,
35
88
 
36
- // current object states
37
-
38
89
  /** @type {CodeceptJS.Test | null} */
39
90
  currentTest: null,
40
91
  /** @type {CodeceptJS.Step | null} */
41
92
  currentStep: null,
42
93
  /** @type {CodeceptJS.Suite | null} */
43
94
  currentSuite: null,
95
+
96
+ /** @type {Map<string, string> | null} */
97
+ tsFileMapping: null,
98
+
99
+ /**
100
+ * Initialize required store fields.
101
+ * These values cannot be overwritten after initialization.
102
+ * @param {object} opts
103
+ * @param {string} opts.codeceptDir - root directory of tests
104
+ * @param {string} opts.outputDir - resolved output directory
105
+ */
106
+ initialize(opts) {
107
+ if (!opts.codeceptDir) throw new Error('codeceptDir is required')
108
+ if (!opts.outputDir) throw new Error('outputDir is required')
109
+
110
+ this._codeceptDir = opts.codeceptDir
111
+ this._outputDir = opts.outputDir
112
+ },
44
113
  }
45
114
 
46
115
  export default store
@@ -1,6 +1,7 @@
1
1
  import merge from 'lodash.merge'
2
2
  import path from 'path'
3
3
  import { createRequire } from 'module'
4
+ import store from './store.js'
4
5
 
5
6
  const defaultVocabulary = {
6
7
  I: 'I',
@@ -15,7 +16,7 @@ class Translation {
15
16
 
16
17
  loadVocabulary(vocabularyFile) {
17
18
  if (!vocabularyFile) return
18
- const filePath = path.join(global.codecept_dir, vocabularyFile)
19
+ const filePath = path.join(store.codeceptDir, vocabularyFile)
19
20
 
20
21
  try {
21
22
  const require = createRequire(import.meta.url)
@@ -1,4 +1,5 @@
1
1
  import { maskSensitiveData } from 'invisi-data'
2
+ import store from '../store.js'
2
3
 
3
4
  /**
4
5
  * Mask sensitive data utility for CodeceptJS
@@ -33,7 +34,7 @@ export function maskData(input, config) {
33
34
  * @returns {boolean|object} - Current masking configuration
34
35
  */
35
36
  export function getMaskConfig() {
36
- return global.maskSensitiveData || false
37
+ return store.maskSensitiveData || global.maskSensitiveData || false
37
38
  }
38
39
 
39
40
  /**
package/lib/utils.js CHANGED
@@ -7,6 +7,7 @@ import getFunctionArguments from 'fn-args'
7
7
  import deepClone from 'lodash.clonedeep'
8
8
  import merge from 'lodash.merge'
9
9
  import { convertColorToRGBA, isColorProperty } from './colorUtils.js'
10
+ import store from './store.js'
10
11
  import Fuse from 'fuse.js'
11
12
  import crypto from 'crypto'
12
13
  import jsBeautify from 'js-beautify'
@@ -335,13 +336,13 @@ export const screenshotOutputFolder = function (fileName) {
335
336
  const fileSep = path.sep
336
337
 
337
338
  if (!fileName.includes(fileSep) || fileName.includes('record_')) {
338
- return path.resolve(global.output_dir, fileName)
339
+ return path.resolve(store.outputDir, fileName)
339
340
  }
340
- return path.resolve(global.codecept_dir, fileName)
341
+ return path.resolve(store.codeceptDir, fileName)
341
342
  }
342
343
 
343
344
  export const relativeDir = function (fileName) {
344
- return fileName.replace(global.codecept_dir, '').replace(/^\//, '')
345
+ return fileName.replace(store.codeceptDir, '').replace(/^\//, '')
345
346
  }
346
347
 
347
348
  export const beautify = function (code) {
package/lib/workers.js CHANGED
@@ -20,6 +20,7 @@ import event from './event.js'
20
20
  import { deserializeTest } from './mocha/test.js'
21
21
  import { deserializeSuite } from './mocha/suite.js'
22
22
  import recorder from './recorder.js'
23
+ import store from './store.js'
23
24
  import runHook from './hooks.js'
24
25
  import WorkerStorage from './workerStorage.js'
25
26
  import { createRuns } from './command/run-multiple/collection.js'
@@ -504,6 +505,7 @@ class Workers extends EventEmitter {
504
505
  await this._ensureInitialized()
505
506
  recorder.startUnlessRunning()
506
507
  event.dispatcher.emit(event.workers.before)
508
+ store.workerMode = true
507
509
  process.env.RUNS_WITH_WORKERS = 'true'
508
510
 
509
511
  // Create workers and set up message handlers immediately (not in recorder queue)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.0-rc.10",
3
+ "version": "4.0.0-rc.15",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [
@@ -41,7 +41,8 @@
41
41
  "./els": "./lib/els.js",
42
42
  "./effects": "./lib/effects.js",
43
43
  "./steps": "./lib/steps.js",
44
- "./store": "./lib/store.js"
44
+ "./store": "./lib/store.js",
45
+ "./assertions": "./lib/assertions.js"
45
46
  },
46
47
  "bin": {
47
48
  "codeceptjs": "./bin/codecept.js",
@@ -214,6 +215,7 @@
214
215
  }
215
216
  },
216
217
  "overrides": {
217
- "tmp": "0.2.5"
218
+ "tmp": "0.2.5",
219
+ "js-yaml": "^4.1.1"
218
220
  }
219
221
  }