codeceptjs 4.0.0-rc.11 → 4.0.0-rc.16
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/lib/ai.js +3 -2
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +6 -6
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +2 -3
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +1 -0
- package/lib/command/run-multiple.js +2 -0
- package/lib/command/run-workers.js +1 -0
- package/lib/command/run.js +1 -1
- package/lib/command/workers/runTests.js +10 -10
- package/lib/container.js +63 -13
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +128 -0
- package/lib/globals.js +22 -10
- package/lib/heal.js +4 -3
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +17 -16
- package/lib/helper/Puppeteer.js +16 -6
- package/lib/helper/WebDriver.js +8 -2
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/history.js +3 -2
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +112 -0
- package/lib/mocha/cli.js +4 -2
- package/lib/mocha/factory.js +2 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/plugin/aiTrace.js +4 -3
- package/lib/plugin/analyze.js +1 -1
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/pageInfo.js +2 -1
- package/lib/plugin/pauseOn.js +167 -0
- package/lib/plugin/screenshotOnFail.js +3 -4
- package/lib/plugin/stepByStepReport.js +5 -4
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +3 -2
- package/lib/step/record.js +1 -1
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils.js +4 -3
- package/lib/workers.js +2 -0
- package/package.json +5 -3
package/lib/ai.js
CHANGED
|
@@ -7,6 +7,7 @@ import { generateText } from 'ai'
|
|
|
7
7
|
import { fileURLToPath } from 'url'
|
|
8
8
|
import path from 'path'
|
|
9
9
|
import { fileExists } from './utils.js'
|
|
10
|
+
import store from './store.js'
|
|
10
11
|
|
|
11
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
13
|
|
|
@@ -24,8 +25,8 @@ async function loadPrompts() {
|
|
|
24
25
|
for (const name of promptNames) {
|
|
25
26
|
let promptPath
|
|
26
27
|
|
|
27
|
-
if (
|
|
28
|
-
promptPath = path.join(
|
|
28
|
+
if (store.codeceptDir) {
|
|
29
|
+
promptPath = path.join(store.codeceptDir, `prompts/${name}.js`)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
if (!promptPath || !fileExists(promptPath)) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Assertion from './assert.js'
|
|
2
|
+
import { equals, urlEquals, fileEquals } from './assert/equal.js'
|
|
3
|
+
import { includes, fileIncludes } from './assert/include.js'
|
|
4
|
+
import { empty } from './assert/empty.js'
|
|
5
|
+
import { truth } from './assert/truth.js'
|
|
6
|
+
|
|
7
|
+
export { Assertion, equals, urlEquals, fileEquals, includes, fileIncludes, empty, truth }
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
Assertion,
|
|
11
|
+
equals,
|
|
12
|
+
urlEquals,
|
|
13
|
+
fileEquals,
|
|
14
|
+
includes,
|
|
15
|
+
fileIncludes,
|
|
16
|
+
empty,
|
|
17
|
+
truth,
|
|
18
|
+
}
|
package/lib/codecept.js
CHANGED
|
@@ -21,6 +21,7 @@ import { emptyFolder } from './utils.js'
|
|
|
21
21
|
import { initCodeceptGlobals } from './globals.js'
|
|
22
22
|
import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
|
|
23
23
|
import recorder from './recorder.js'
|
|
24
|
+
import store from './store.js'
|
|
24
25
|
|
|
25
26
|
import storeListener from './listener/store.js'
|
|
26
27
|
import stepsListener from './listener/steps.js'
|
|
@@ -71,7 +72,7 @@ class Codecept {
|
|
|
71
72
|
} else {
|
|
72
73
|
// For npm packages, resolve from the user's directory
|
|
73
74
|
// This ensures packages like tsx are found in user's node_modules
|
|
74
|
-
const userDir =
|
|
75
|
+
const userDir = store.codeceptDir || process.cwd()
|
|
75
76
|
|
|
76
77
|
try {
|
|
77
78
|
// Use createRequire to resolve from user's directory
|
|
@@ -102,8 +103,6 @@ class Codecept {
|
|
|
102
103
|
await this.requireModules(this.requiringModules)
|
|
103
104
|
// initializing listeners
|
|
104
105
|
await container.create(this.config, this.opts)
|
|
105
|
-
// Store container globally for easy access
|
|
106
|
-
global.container = container
|
|
107
106
|
await this.runHooks()
|
|
108
107
|
}
|
|
109
108
|
|
|
@@ -129,6 +128,7 @@ class Codecept {
|
|
|
129
128
|
'./listener/config.js',
|
|
130
129
|
'./listener/result.js',
|
|
131
130
|
'./listener/helpers.js',
|
|
131
|
+
'./listener/pageobjects.js',
|
|
132
132
|
'./listener/globalTimeout.js',
|
|
133
133
|
'./listener/globalRetry.js',
|
|
134
134
|
'./listener/retryEnhancer.js',
|
|
@@ -171,7 +171,7 @@ class Codecept {
|
|
|
171
171
|
*/
|
|
172
172
|
loadTests(pattern) {
|
|
173
173
|
const options = {
|
|
174
|
-
cwd:
|
|
174
|
+
cwd: store.codeceptDir,
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
let patterns = [pattern]
|
|
@@ -203,7 +203,7 @@ class Codecept {
|
|
|
203
203
|
globSync(pattern, options).forEach(file => {
|
|
204
204
|
if (file.includes('node_modules')) return
|
|
205
205
|
if (!fsPath.isAbsolute(file)) {
|
|
206
|
-
file = fsPath.join(
|
|
206
|
+
file = fsPath.join(store.codeceptDir, file)
|
|
207
207
|
}
|
|
208
208
|
if (!this.testFiles.includes(fsPath.resolve(file))) {
|
|
209
209
|
this.testFiles.push(fsPath.resolve(file))
|
|
@@ -293,7 +293,7 @@ class Codecept {
|
|
|
293
293
|
|
|
294
294
|
if (test) {
|
|
295
295
|
if (!fsPath.isAbsolute(test)) {
|
|
296
|
-
test = fsPath.join(
|
|
296
|
+
test = fsPath.join(store.codeceptDir, test)
|
|
297
297
|
}
|
|
298
298
|
const testBasename = fsPath.basename(test, '.js')
|
|
299
299
|
const testFeatureBasename = fsPath.basename(test, '.feature')
|
package/lib/command/check.js
CHANGED
|
@@ -135,6 +135,7 @@ export default async function (options) {
|
|
|
135
135
|
printCheck('plugins', checks['plugins'], Object.keys(Container.plugins()).join(', '))
|
|
136
136
|
|
|
137
137
|
if (Object.keys(helpers).length) {
|
|
138
|
+
store.dryRun = false
|
|
138
139
|
const suite = Container.mocha().suite
|
|
139
140
|
const test = createTest('test', () => {})
|
|
140
141
|
checks.setup = true
|
|
@@ -154,7 +155,6 @@ export default async function (options) {
|
|
|
154
155
|
checks.teardown = true
|
|
155
156
|
for (const helper of Object.values(helpers).reverse()) {
|
|
156
157
|
try {
|
|
157
|
-
if (helper._passed) await helper._passed(test)
|
|
158
158
|
if (helper._after) await helper._after(test)
|
|
159
159
|
if (helper._finishTest) await helper._finishTest(suite)
|
|
160
160
|
if (helper._afterSuite) await helper._afterSuite(suite)
|
|
@@ -166,6 +166,7 @@ export default async function (options) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
printCheck('Helpers After', checks['teardown'], standardActingHelpers.some(h => Object.keys(helpers).includes(h)) ? 'Closing browser' : '')
|
|
169
|
+
store.dryRun = true
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
try {
|
package/lib/command/dryRun.js
CHANGED
|
@@ -20,8 +20,7 @@ export default async function (test, options) {
|
|
|
20
20
|
if (config.plugins) {
|
|
21
21
|
// disable all plugins by default, they can be enabled with -p option
|
|
22
22
|
for (const plugin in config.plugins) {
|
|
23
|
-
|
|
24
|
-
config.plugins[plugin].enabled = options.plugins === 'all'
|
|
23
|
+
config.plugins[plugin].enabled = false
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -50,7 +49,7 @@ async function printTests(files) {
|
|
|
50
49
|
const { default: figures } = await import('figures')
|
|
51
50
|
const { default: colors } = await import('chalk')
|
|
52
51
|
|
|
53
|
-
output.print(output.styles.debug(`Tests from ${
|
|
52
|
+
output.print(output.styles.debug(`Tests from ${store.codeceptDir}:`))
|
|
54
53
|
output.print()
|
|
55
54
|
|
|
56
55
|
const mocha = Container.mocha()
|
package/lib/command/generate.js
CHANGED
|
@@ -5,6 +5,7 @@ import { mkdirp } from 'mkdirp'
|
|
|
5
5
|
import path from 'path'
|
|
6
6
|
import { fileExists, ucfirst, lcfirst, beautify } from '../utils.js'
|
|
7
7
|
import output from '../output.js'
|
|
8
|
+
import store from '../store.js'
|
|
8
9
|
import generateDefinitions from './definitions.js'
|
|
9
10
|
import { getConfig, getTestRoot, safeFileWrite, readConfig } from './utils.js'
|
|
10
11
|
|
|
@@ -20,6 +21,7 @@ Scenario('test something', async ({ {{actor}} }) => {
|
|
|
20
21
|
// generates empty test
|
|
21
22
|
export async function test(genPath) {
|
|
22
23
|
const testsPath = getTestRoot(genPath)
|
|
24
|
+
store.codeceptDir = testsPath
|
|
23
25
|
global.codecept_dir = testsPath
|
|
24
26
|
const config = await getConfig(testsPath)
|
|
25
27
|
if (!config) return
|
|
@@ -8,6 +8,7 @@ import fsPath from 'path'
|
|
|
8
8
|
import { getConfig, getTestRoot } from '../utils.js'
|
|
9
9
|
import Codecept from '../../codecept.js'
|
|
10
10
|
import output from '../../output.js'
|
|
11
|
+
import store from '../../store.js'
|
|
11
12
|
import { matchStep } from '../../mocha/bdd.js'
|
|
12
13
|
|
|
13
14
|
const uuidFn = IdGenerator.uuid()
|
|
@@ -43,9 +44,9 @@ export default async function (genPath, options) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const files = []
|
|
46
|
-
globSync(options.feature || config.gherkin.features, { cwd: options.feature ? '.' :
|
|
47
|
+
globSync(options.feature || config.gherkin.features, { cwd: options.feature ? '.' : store.codeceptDir }).forEach(file => {
|
|
47
48
|
if (!fsPath.isAbsolute(file)) {
|
|
48
|
-
file = fsPath.join(
|
|
49
|
+
file = fsPath.join(store.codeceptDir, file)
|
|
49
50
|
}
|
|
50
51
|
files.push(fsPath.resolve(file))
|
|
51
52
|
})
|
|
@@ -92,7 +93,7 @@ export default async function (genPath, options) {
|
|
|
92
93
|
if (child.scenario.keyword === 'Scenario Outline') continue // skip scenario outline
|
|
93
94
|
parseSteps(child.scenario.steps)
|
|
94
95
|
.map(step => {
|
|
95
|
-
return Object.assign(step, { file: file.replace(
|
|
96
|
+
return Object.assign(step, { file: file.replace(store.codeceptDir, '').slice(1) })
|
|
96
97
|
})
|
|
97
98
|
.map(step => newSteps.set(`${step.type}(${step})`, step))
|
|
98
99
|
}
|
|
@@ -107,7 +108,7 @@ export default async function (genPath, options) {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
if (!fsPath.isAbsolute(stepFile)) {
|
|
110
|
-
stepFile = fsPath.join(
|
|
111
|
+
stepFile = fsPath.join(store.codeceptDir, stepFile)
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
const snippets = [...newSteps.values()]
|
package/lib/command/init.js
CHANGED
|
@@ -8,6 +8,7 @@ import event from '../event.js'
|
|
|
8
8
|
import { createRuns } from './run-multiple/collection.js'
|
|
9
9
|
import { clearString, replaceValueDeep } from '../utils.js'
|
|
10
10
|
import { getConfig, getTestRoot, fail } from './utils.js'
|
|
11
|
+
import store from '../store.js'
|
|
11
12
|
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url)
|
|
13
14
|
const __dirname = path.dirname(__filename)
|
|
@@ -35,6 +36,7 @@ export default async function (selectedRuns, options) {
|
|
|
35
36
|
const configFile = options.config
|
|
36
37
|
|
|
37
38
|
const testRoot = getTestRoot(configFile)
|
|
39
|
+
store.codeceptDir = testRoot
|
|
38
40
|
global.codecept_dir = testRoot
|
|
39
41
|
|
|
40
42
|
// copy opts to run
|
|
@@ -41,6 +41,7 @@ export default async function (workerCount, selectedRuns, options) {
|
|
|
41
41
|
output.print(`CodeceptJS v${Codecept.version()} ${output.standWithUkraine()}`)
|
|
42
42
|
output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`)
|
|
43
43
|
store.hasWorkers = true
|
|
44
|
+
store.workerMode = true
|
|
44
45
|
process.env.RUNS_WITH_WORKERS = 'true'
|
|
45
46
|
|
|
46
47
|
const workers = new Workers(numberOfWorkers, config)
|
package/lib/command/run.js
CHANGED
|
@@ -21,8 +21,8 @@ const { options, tests, testRoot, workerIndex, poolMode } = workerData
|
|
|
21
21
|
|
|
22
22
|
// Global error handlers to catch critical errors but not test failures
|
|
23
23
|
process.on('uncaughtException', (err) => {
|
|
24
|
-
if (
|
|
25
|
-
const fileMapping =
|
|
24
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
25
|
+
const fileMapping = container.tsFileMapping()
|
|
26
26
|
if (fileMapping) {
|
|
27
27
|
fixErrorStack(err, fileMapping)
|
|
28
28
|
}
|
|
@@ -40,8 +40,8 @@ process.on('uncaughtException', (err) => {
|
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
process.on('unhandledRejection', (reason, promise) => {
|
|
43
|
-
if (reason && typeof reason === 'object' && reason.stack &&
|
|
44
|
-
const fileMapping =
|
|
43
|
+
if (reason && typeof reason === 'object' && reason.stack && container?.tsFileMapping && fixErrorStack) {
|
|
44
|
+
const fileMapping = container.tsFileMapping()
|
|
45
45
|
if (fileMapping) {
|
|
46
46
|
fixErrorStack(reason, fileMapping)
|
|
47
47
|
}
|
|
@@ -163,8 +163,8 @@ initPromise = (async function () {
|
|
|
163
163
|
// IMPORTANT: await is required here since getConfig is async
|
|
164
164
|
baseConfig = await getConfig(options.config || testRoot)
|
|
165
165
|
} catch (configErr) {
|
|
166
|
-
if (
|
|
167
|
-
const fileMapping =
|
|
166
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
167
|
+
const fileMapping = container.tsFileMapping()
|
|
168
168
|
if (fileMapping) {
|
|
169
169
|
fixErrorStack(configErr, fileMapping)
|
|
170
170
|
}
|
|
@@ -185,8 +185,8 @@ initPromise = (async function () {
|
|
|
185
185
|
try {
|
|
186
186
|
await codecept.init(testRoot)
|
|
187
187
|
} catch (initErr) {
|
|
188
|
-
if (
|
|
189
|
-
const fileMapping =
|
|
188
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
189
|
+
const fileMapping = container.tsFileMapping()
|
|
190
190
|
if (fileMapping) {
|
|
191
191
|
fixErrorStack(initErr, fileMapping)
|
|
192
192
|
}
|
|
@@ -218,8 +218,8 @@ initPromise = (async function () {
|
|
|
218
218
|
parentPort?.close()
|
|
219
219
|
}
|
|
220
220
|
} catch (err) {
|
|
221
|
-
if (
|
|
222
|
-
const fileMapping =
|
|
221
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
222
|
+
const fileMapping = container.tsFileMapping()
|
|
223
223
|
if (fileMapping) {
|
|
224
224
|
fixErrorStack(err, fileMapping)
|
|
225
225
|
}
|
package/lib/container.js
CHANGED
|
@@ -18,6 +18,11 @@ import actorFactory from './actor.js'
|
|
|
18
18
|
|
|
19
19
|
let asyncHelperPromise
|
|
20
20
|
|
|
21
|
+
let beforeCalledSet = new Set()
|
|
22
|
+
|
|
23
|
+
export function getBeforeCalledSet() { return beforeCalledSet }
|
|
24
|
+
export function resetBeforeCalledSet() { beforeCalledSet = new Set() }
|
|
25
|
+
|
|
21
26
|
let container = {
|
|
22
27
|
helpers: {},
|
|
23
28
|
support: {},
|
|
@@ -150,10 +155,23 @@ class Container {
|
|
|
150
155
|
if (!name) {
|
|
151
156
|
return container.proxySupport
|
|
152
157
|
}
|
|
153
|
-
|
|
158
|
+
if (typeof container.support[name] === 'function') {
|
|
159
|
+
return container.support[name]
|
|
160
|
+
}
|
|
154
161
|
return container.proxySupport[name]
|
|
155
162
|
}
|
|
156
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Get raw (non-proxied) support objects for direct access.
|
|
166
|
+
* Used by listeners to call lifecycle hooks without MetaStep wrapping.
|
|
167
|
+
*
|
|
168
|
+
* @api
|
|
169
|
+
* @returns {object}
|
|
170
|
+
*/
|
|
171
|
+
static supportObjects() {
|
|
172
|
+
return container.support
|
|
173
|
+
}
|
|
174
|
+
|
|
157
175
|
/**
|
|
158
176
|
* Get all helpers or get a helper by name
|
|
159
177
|
*
|
|
@@ -183,7 +201,7 @@ class Container {
|
|
|
183
201
|
* @api
|
|
184
202
|
*/
|
|
185
203
|
static tsFileMapping() {
|
|
186
|
-
return
|
|
204
|
+
return store.tsFileMapping
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
/**
|
|
@@ -426,11 +444,11 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
|
|
|
426
444
|
tempJsFile = allTempFiles
|
|
427
445
|
fileMapping = mapping
|
|
428
446
|
// Store file mapping in container for runtime error fixing (merge with existing)
|
|
429
|
-
if (!
|
|
430
|
-
|
|
447
|
+
if (!store.tsFileMapping) {
|
|
448
|
+
store.tsFileMapping = new Map()
|
|
431
449
|
}
|
|
432
450
|
for (const [key, value] of mapping.entries()) {
|
|
433
|
-
|
|
451
|
+
store.tsFileMapping.set(key, value)
|
|
434
452
|
}
|
|
435
453
|
} catch (tsError) {
|
|
436
454
|
throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
@@ -542,6 +560,19 @@ function createSupportObjects(config) {
|
|
|
542
560
|
let currentValue = currentObject[prop]
|
|
543
561
|
|
|
544
562
|
if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
|
|
563
|
+
if (prop.toString().charAt(0) !== '_' && currentObject._before && !beforeCalledSet.has(name)) {
|
|
564
|
+
beforeCalledSet.add(name)
|
|
565
|
+
const originalValue = currentValue
|
|
566
|
+
const wrappedValue = async function (...args) {
|
|
567
|
+
await currentObject._before()
|
|
568
|
+
return originalValue.apply(currentObject, args)
|
|
569
|
+
}
|
|
570
|
+
const ms = new MetaStep(name, prop)
|
|
571
|
+
ms.setContext(currentObject)
|
|
572
|
+
debug(`metastep is created for ${name}.${prop.toString()}() (with _before)`)
|
|
573
|
+
return ms.run.bind(ms, asyncWrapper(wrappedValue))
|
|
574
|
+
}
|
|
575
|
+
|
|
545
576
|
const ms = new MetaStep(name, prop)
|
|
546
577
|
ms.setContext(currentObject)
|
|
547
578
|
if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
|
|
@@ -600,6 +631,8 @@ function createSupportObjects(config) {
|
|
|
600
631
|
let value
|
|
601
632
|
if (container.sharedKeys.has(prop) && prop in container.support) {
|
|
602
633
|
value = container.support[prop]
|
|
634
|
+
} else if (prop in container.support && typeof container.support[prop] === 'function') {
|
|
635
|
+
value = container.support[prop]
|
|
603
636
|
} else {
|
|
604
637
|
value = lazyLoad(prop)
|
|
605
638
|
}
|
|
@@ -614,6 +647,9 @@ function createSupportObjects(config) {
|
|
|
614
647
|
if (container.sharedKeys.has(key) && key in container.support) {
|
|
615
648
|
return container.support[key]
|
|
616
649
|
}
|
|
650
|
+
if (key in container.support && typeof container.support[key] === 'function') {
|
|
651
|
+
return container.support[key]
|
|
652
|
+
}
|
|
617
653
|
return lazyLoad(key)
|
|
618
654
|
},
|
|
619
655
|
},
|
|
@@ -654,14 +690,28 @@ async function loadPluginFallback(modulePath, config) {
|
|
|
654
690
|
async function createPlugins(config, options = {}) {
|
|
655
691
|
const plugins = {}
|
|
656
692
|
|
|
657
|
-
const
|
|
693
|
+
const pluginOptionMap = new Map()
|
|
694
|
+
for (const token of (options.plugins || '').split(',').filter(Boolean)) {
|
|
695
|
+
const parts = token.split(':')
|
|
696
|
+
pluginOptionMap.set(parts[0], parts.slice(1))
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
for (const [name] of pluginOptionMap) {
|
|
700
|
+
if (!config[name]) config[name] = {}
|
|
701
|
+
}
|
|
702
|
+
|
|
658
703
|
for (const pluginName in config) {
|
|
659
704
|
if (!config[pluginName]) config[pluginName] = {}
|
|
660
705
|
const pluginConfig = config[pluginName]
|
|
661
|
-
|
|
706
|
+
const enabledByCli = pluginOptionMap.has(pluginName)
|
|
707
|
+
if (!pluginConfig.enabled && !enabledByCli) {
|
|
662
708
|
continue // plugin is disabled
|
|
663
709
|
}
|
|
664
710
|
|
|
711
|
+
if (enabledByCli && pluginOptionMap.get(pluginName).length > 0) {
|
|
712
|
+
pluginConfig._args = pluginOptionMap.get(pluginName)
|
|
713
|
+
}
|
|
714
|
+
|
|
665
715
|
// Generic workers gate:
|
|
666
716
|
// - runInWorker / runInWorkers controls plugin execution inside worker threads.
|
|
667
717
|
// - runInParent / runInMain can disable plugin in workers parent process.
|
|
@@ -672,7 +722,7 @@ async function createPlugins(config, options = {}) {
|
|
|
672
722
|
continue
|
|
673
723
|
}
|
|
674
724
|
|
|
675
|
-
if (!options.child &&
|
|
725
|
+
if (!options.child && store.workerMode && !runInParent) {
|
|
676
726
|
continue
|
|
677
727
|
}
|
|
678
728
|
let module
|
|
@@ -681,7 +731,7 @@ async function createPlugins(config, options = {}) {
|
|
|
681
731
|
module = pluginConfig.require
|
|
682
732
|
if (module.startsWith('.')) {
|
|
683
733
|
// local
|
|
684
|
-
module = path.resolve(
|
|
734
|
+
module = path.resolve(store.codeceptDir, module) // custom plugin
|
|
685
735
|
}
|
|
686
736
|
} else {
|
|
687
737
|
module = `./plugin/${pluginName}.js`
|
|
@@ -716,7 +766,7 @@ async function loadGherkinStepsAsync(paths) {
|
|
|
716
766
|
bddModule.clearCurrentStepFile()
|
|
717
767
|
}
|
|
718
768
|
} else {
|
|
719
|
-
const folderPath = paths.startsWith('.') ? normalizeAndJoin(
|
|
769
|
+
const folderPath = paths.startsWith('.') ? normalizeAndJoin(store.codeceptDir, paths) : ''
|
|
720
770
|
if (folderPath !== '') {
|
|
721
771
|
const files = globSync(folderPath)
|
|
722
772
|
for (const file of files) {
|
|
@@ -764,7 +814,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
|
|
|
764
814
|
}
|
|
765
815
|
}
|
|
766
816
|
if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
|
|
767
|
-
modulePath = path.join(
|
|
817
|
+
modulePath = path.join(store.codeceptDir, modulePath)
|
|
768
818
|
}
|
|
769
819
|
try {
|
|
770
820
|
// Use dynamic import for both ESM and CJS modules
|
|
@@ -888,7 +938,7 @@ async function loadTranslation(locale, vocabularies) {
|
|
|
888
938
|
const langs = await Translation.getLangs()
|
|
889
939
|
if (langs[locale]) {
|
|
890
940
|
translation = new Translation(langs[locale])
|
|
891
|
-
} else if (fileExists(path.join(
|
|
941
|
+
} else if (fileExists(path.join(store.codeceptDir, locale))) {
|
|
892
942
|
// get from a provided file instead
|
|
893
943
|
translation = Translation.createDefault()
|
|
894
944
|
translation.loadVocabulary(locale)
|
|
@@ -905,7 +955,7 @@ function getHelperModuleName(helperName, config) {
|
|
|
905
955
|
// classical require
|
|
906
956
|
if (config[helperName].require) {
|
|
907
957
|
if (config[helperName].require.startsWith('.')) {
|
|
908
|
-
let helperPath = path.resolve(
|
|
958
|
+
let helperPath = path.resolve(store.codeceptDir, config[helperName].require)
|
|
909
959
|
// Add .js extension if not present for ESM compatibility
|
|
910
960
|
if (!path.extname(helperPath)) {
|
|
911
961
|
helperPath += '.js'
|
package/lib/effects.js
CHANGED
|
@@ -4,6 +4,7 @@ import store from './store.js'
|
|
|
4
4
|
import event from './event.js'
|
|
5
5
|
import container from './container.js'
|
|
6
6
|
import MetaStep from './step/meta.js'
|
|
7
|
+
import { empty } from './assert/empty.js'
|
|
7
8
|
import { isAsyncFunction } from './utils.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -111,6 +112,11 @@ class WithinStep extends MetaStep {
|
|
|
111
112
|
*
|
|
112
113
|
* @throws Will handle errors that occur during the callback execution. Errors are logged and attached as notes to the test.
|
|
113
114
|
*/
|
|
115
|
+
let hopeThatFailures = []
|
|
116
|
+
event.dispatcher.on(event.test.before, () => {
|
|
117
|
+
hopeThatFailures = []
|
|
118
|
+
})
|
|
119
|
+
|
|
114
120
|
async function hopeThat(callback) {
|
|
115
121
|
if (store.dryRun) return
|
|
116
122
|
const sessionName = 'hopeThat'
|
|
@@ -131,6 +137,7 @@ async function hopeThat(callback) {
|
|
|
131
137
|
result = false
|
|
132
138
|
const msg = err.inspect ? err.inspect() : err.toString()
|
|
133
139
|
output.debug(`Unsuccessful assertion > ${msg}`)
|
|
140
|
+
hopeThatFailures.push(msg)
|
|
134
141
|
event.dispatcher.once(event.test.finished, test => {
|
|
135
142
|
if (!test.notes) test.notes = []
|
|
136
143
|
test.notes.push({ type: 'conditionalError', text: msg })
|
|
@@ -153,6 +160,16 @@ async function hopeThat(callback) {
|
|
|
153
160
|
)
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Asserts that no `hopeThat` soft assertion has failed in the current test.
|
|
165
|
+
* Call once at the end of a scenario to fail it when any soft assertion failed.
|
|
166
|
+
*/
|
|
167
|
+
hopeThat.noErrors = function () {
|
|
168
|
+
const failures = hopeThatFailures
|
|
169
|
+
hopeThatFailures = []
|
|
170
|
+
empty('soft assertions').assert(failures)
|
|
171
|
+
}
|
|
172
|
+
|
|
156
173
|
/**
|
|
157
174
|
* A CodeceptJS utility function to retry a step or callback multiple times with a specified polling interval.
|
|
158
175
|
*
|
|
@@ -256,6 +256,134 @@ class WebElement {
|
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Run a function in the browser with this element as the first argument.
|
|
261
|
+
* @param {Function} fn Browser-side function. Receives the element, then extra args.
|
|
262
|
+
* @param {...any} args Additional arguments passed to the function
|
|
263
|
+
* @returns {Promise<any>} Value returned by fn
|
|
264
|
+
*/
|
|
265
|
+
async evaluate(fn, ...args) {
|
|
266
|
+
switch (this.helperType) {
|
|
267
|
+
case 'playwright':
|
|
268
|
+
case 'puppeteer':
|
|
269
|
+
return this.element.evaluate(fn, ...args)
|
|
270
|
+
case 'webdriver':
|
|
271
|
+
return this.helper.executeScript(fn, this.element, ...args)
|
|
272
|
+
default:
|
|
273
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Focus the element.
|
|
279
|
+
* @returns {Promise<void>}
|
|
280
|
+
*/
|
|
281
|
+
async focus() {
|
|
282
|
+
switch (this.helperType) {
|
|
283
|
+
case 'playwright':
|
|
284
|
+
return this.element.focus()
|
|
285
|
+
case 'puppeteer':
|
|
286
|
+
if (this.element.focus) return this.element.focus()
|
|
287
|
+
return this.element.evaluate(el => el.focus())
|
|
288
|
+
case 'webdriver':
|
|
289
|
+
return this.helper.executeScript(el => el.focus(), this.element)
|
|
290
|
+
default:
|
|
291
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Type characters via the page/browser keyboard into the focused element.
|
|
297
|
+
* Unlike `type()`, this does not call `.fill()`/`.setValue()`, so it works
|
|
298
|
+
* with contenteditable nodes, iframe bodies, and editor-owned hidden textareas.
|
|
299
|
+
* @param {string} text Text to send
|
|
300
|
+
* @param {Object} [options] Options (e.g. `{ delay }`)
|
|
301
|
+
* @returns {Promise<void>}
|
|
302
|
+
*/
|
|
303
|
+
async typeText(text, options = {}) {
|
|
304
|
+
const s = String(text)
|
|
305
|
+
switch (this.helperType) {
|
|
306
|
+
case 'playwright':
|
|
307
|
+
case 'puppeteer':
|
|
308
|
+
return this.helper.page.keyboard.type(s, options)
|
|
309
|
+
case 'webdriver': {
|
|
310
|
+
const ENTER = '\uE007'
|
|
311
|
+
const parts = s.split('\n')
|
|
312
|
+
for (let i = 0; i < parts.length; i++) {
|
|
313
|
+
if (parts[i]) await this.helper.browser.keys(parts[i])
|
|
314
|
+
if (i < parts.length - 1) await this.helper.browser.keys(ENTER)
|
|
315
|
+
}
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
default:
|
|
319
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Select all content in the focused field and delete it via keyboard input.
|
|
325
|
+
* Sends Ctrl+A and Meta+A (so it works across platforms) followed by Backspace.
|
|
326
|
+
* @returns {Promise<void>}
|
|
327
|
+
*/
|
|
328
|
+
async selectAllAndDelete() {
|
|
329
|
+
switch (this.helperType) {
|
|
330
|
+
case 'playwright':
|
|
331
|
+
await this.helper.page.keyboard.press('Control+a').catch(() => {})
|
|
332
|
+
await this.helper.page.keyboard.press('Meta+a').catch(() => {})
|
|
333
|
+
await this.helper.page.keyboard.press('Backspace')
|
|
334
|
+
return
|
|
335
|
+
case 'puppeteer':
|
|
336
|
+
for (const mod of ['Control', 'Meta']) {
|
|
337
|
+
try {
|
|
338
|
+
await this.helper.page.keyboard.down(mod)
|
|
339
|
+
await this.helper.page.keyboard.press('KeyA')
|
|
340
|
+
await this.helper.page.keyboard.up(mod)
|
|
341
|
+
} catch (e) {}
|
|
342
|
+
}
|
|
343
|
+
await this.helper.page.keyboard.press('Backspace')
|
|
344
|
+
return
|
|
345
|
+
case 'webdriver': {
|
|
346
|
+
const b = this.helper.browser
|
|
347
|
+
await b.keys(['Control', 'a']).catch(() => {})
|
|
348
|
+
await b.keys(['Meta', 'a']).catch(() => {})
|
|
349
|
+
await b.keys(['Backspace'])
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
default:
|
|
353
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Treat this element as an iframe; invoke `fn` with a WebElement wrapping
|
|
359
|
+
* the iframe body. For WebDriver this switches the browser into the frame
|
|
360
|
+
* for the duration of the callback and switches back on exit.
|
|
361
|
+
* @param {(body: WebElement) => Promise<any>} fn
|
|
362
|
+
* @returns {Promise<any>} Return value of fn
|
|
363
|
+
*/
|
|
364
|
+
async inIframe(fn) {
|
|
365
|
+
switch (this.helperType) {
|
|
366
|
+
case 'playwright':
|
|
367
|
+
case 'puppeteer': {
|
|
368
|
+
const frame = await this.element.contentFrame()
|
|
369
|
+
const body = await frame.$('body')
|
|
370
|
+
return fn(new WebElement(body, this.helper))
|
|
371
|
+
}
|
|
372
|
+
case 'webdriver': {
|
|
373
|
+
const browser = this.helper.browser
|
|
374
|
+
await browser.switchFrame(this.element)
|
|
375
|
+
try {
|
|
376
|
+
const body = await browser.$('body')
|
|
377
|
+
return await fn(new WebElement(body, this.helper))
|
|
378
|
+
} finally {
|
|
379
|
+
await browser.switchFrame(null)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
default:
|
|
383
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
259
387
|
/**
|
|
260
388
|
* Find first child element matching the locator
|
|
261
389
|
* @param {string|Object} locator Element locator
|