codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.10
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/README.md +39 -27
- package/bin/codecept.js +2 -2
- package/bin/mcp-server.js +610 -0
- package/docs/webapi/appendField.mustache +5 -0
- package/docs/webapi/attachFile.mustache +12 -0
- package/docs/webapi/checkOption.mustache +1 -1
- package/docs/webapi/clearField.mustache +5 -0
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/dontSeeElement.mustache +4 -0
- package/docs/webapi/dontSeeInField.mustache +5 -0
- package/docs/webapi/fillField.mustache +5 -0
- package/docs/webapi/moveCursorTo.mustache +5 -1
- package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/seeElement.mustache +4 -0
- package/docs/webapi/seeInField.mustache +5 -0
- package/docs/webapi/selectOption.mustache +5 -0
- package/docs/webapi/uncheckOption.mustache +1 -1
- package/lib/actor.js +12 -8
- package/lib/codecept.js +51 -18
- package/lib/command/definitions.js +14 -7
- package/lib/command/init.js +2 -4
- package/lib/command/run-workers.js +13 -2
- package/lib/command/workers/runTests.js +121 -9
- package/lib/config.js +24 -33
- package/lib/container.js +177 -28
- package/lib/element/WebElement.js +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +339 -505
- package/lib/helper/Puppeteer.js +324 -89
- package/lib/helper/REST.js +15 -9
- package/lib/helper/WebDriver.js +311 -81
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/html.js +14 -1
- package/lib/listener/config.js +11 -3
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +2 -14
- package/lib/locator.js +32 -0
- package/lib/mocha/cli.js +16 -0
- package/lib/mocha/factory.js +7 -27
- package/lib/mocha/gherkin.js +4 -4
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/auth.js +2 -1
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/step/base.js +14 -1
- package/lib/step/config.js +15 -2
- package/lib/step/meta.js +18 -1
- package/lib/step/record.js +9 -1
- package/lib/utils/loaderCheck.js +162 -0
- package/lib/utils/typescript.js +449 -0
- package/lib/utils.js +48 -0
- package/lib/workers.js +163 -54
- package/package.json +43 -32
- package/typings/index.d.ts +120 -4
- package/lib/helper/extras/PlaywrightLocator.js +0 -110
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -11011
- package/typings/types.d.ts +0 -13073
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Locator from '../../locator.js'
|
|
2
|
+
|
|
3
|
+
class MultipleElementsFound extends Error {
|
|
4
|
+
constructor(locator, webElements) {
|
|
5
|
+
const locatorStr = (typeof locator === 'object' && !(locator instanceof Locator))
|
|
6
|
+
? new Locator(locator).toString()
|
|
7
|
+
: String(locator)
|
|
8
|
+
super(`Multiple elements (${webElements.length}) found for "${locatorStr}" in strict mode. Call fetchDetails() for full information.`)
|
|
9
|
+
this.name = 'MultipleElementsFound'
|
|
10
|
+
this.locator = locator
|
|
11
|
+
this.webElements = webElements
|
|
12
|
+
this.count = webElements.length
|
|
13
|
+
this._detailsFetched = false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async fetchDetails() {
|
|
17
|
+
if (this._detailsFetched) return
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const items = []
|
|
21
|
+
const maxToShow = Math.min(this.count, 10)
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < maxToShow; i++) {
|
|
24
|
+
const webEl = this.webElements[i]
|
|
25
|
+
try {
|
|
26
|
+
const xpath = await webEl.toAbsoluteXPath()
|
|
27
|
+
const html = await webEl.toSimplifiedHTML()
|
|
28
|
+
items.push(` ${i + 1}. > ${xpath}\n ${html}`)
|
|
29
|
+
} catch (err) {
|
|
30
|
+
items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.count > 10) {
|
|
35
|
+
items.push(` ... and ${this.count - 10} more`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const locatorStr = (typeof this.locator === 'object' && !(this.locator instanceof Locator))
|
|
39
|
+
? new Locator(this.locator).toString()
|
|
40
|
+
: String(this.locator)
|
|
41
|
+
this.message = `Multiple elements (${this.count}) found for "${locatorStr}" in strict mode.\n` +
|
|
42
|
+
items.join('\n') +
|
|
43
|
+
`\nUse a more specific locator or use grabWebElements() to handle multiple elements.`
|
|
44
|
+
} catch (err) {
|
|
45
|
+
this.message = `Multiple elements (${this.count}) found. Failed to fetch details: ${err.message}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this._detailsFetched = true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default MultipleElementsFound
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import store from '../../store.js'
|
|
2
|
+
import output from '../../output.js'
|
|
3
|
+
import WebElement from '../../element/WebElement.js'
|
|
4
|
+
import MultipleElementsFound from '../errors/MultipleElementsFound.js'
|
|
5
|
+
|
|
6
|
+
function resolveElementIndex(value) {
|
|
7
|
+
if (value === 'first') return 1
|
|
8
|
+
if (value === 'last') return -1
|
|
9
|
+
return value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isStrictStep(opts, helper) {
|
|
13
|
+
if (opts?.exact === true || opts?.strictMode === true) return true
|
|
14
|
+
if (opts?.exact === false || opts?.strictMode === false) return false
|
|
15
|
+
return helper.options.strict
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function selectElement(els, locator, helper) {
|
|
19
|
+
const opts = store.currentStep?.opts
|
|
20
|
+
const rawIndex = opts?.elementIndex
|
|
21
|
+
const elementIndex = resolveElementIndex(rawIndex)
|
|
22
|
+
|
|
23
|
+
if (elementIndex != null) {
|
|
24
|
+
if (els.length === 1) return els[0]
|
|
25
|
+
|
|
26
|
+
if (!Number.isInteger(elementIndex) || elementIndex === 0) {
|
|
27
|
+
throw new Error(`elementIndex must be a non-zero integer or 'first'/'last', got: ${rawIndex}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let idx
|
|
31
|
+
if (elementIndex > 0) {
|
|
32
|
+
idx = elementIndex - 1
|
|
33
|
+
if (idx >= els.length) {
|
|
34
|
+
throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
idx = els.length + elementIndex
|
|
38
|
+
if (idx < 0) {
|
|
39
|
+
throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
output.debug(`[Elements] Using element #${elementIndex} out of ${els.length}`)
|
|
44
|
+
return els[idx]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (isStrictStep(opts, helper)) {
|
|
48
|
+
if (els.length > 1) {
|
|
49
|
+
const webElements = Array.from(els).map(el => new WebElement(el, helper))
|
|
50
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (els.length > 1) output.debug(`[Elements] Using first element out of ${els.length}`)
|
|
55
|
+
return els[0]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { selectElement }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const dropFile = (el, { base64Content, fileName, mimeType }) => {
|
|
2
|
+
const binaryStr = atob(base64Content)
|
|
3
|
+
const bytes = new Uint8Array(binaryStr.length)
|
|
4
|
+
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
|
|
5
|
+
const fileObj = new File([bytes], fileName, { type: mimeType })
|
|
6
|
+
const dataTransfer = new DataTransfer()
|
|
7
|
+
dataTransfer.items.add(fileObj)
|
|
8
|
+
el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
|
|
9
|
+
el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
|
|
10
|
+
el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
|
|
11
|
+
}
|
package/lib/html.js
CHANGED
|
@@ -245,4 +245,17 @@ function splitByChunks(text, chunkSize) {
|
|
|
245
245
|
return chunks.map(chunk => chunk.trim())
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
function simplifyHtmlElement(html, maxLength = 300) {
|
|
249
|
+
try {
|
|
250
|
+
html = removeNonInteractiveElements(html)
|
|
251
|
+
html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
|
|
252
|
+
} catch (e) {
|
|
253
|
+
// keep raw html if minification fails
|
|
254
|
+
}
|
|
255
|
+
if (html.length > maxLength) {
|
|
256
|
+
html = html.slice(0, maxLength) + '...'
|
|
257
|
+
}
|
|
258
|
+
return html
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement }
|
package/lib/listener/config.js
CHANGED
|
@@ -12,15 +12,23 @@ export default function () {
|
|
|
12
12
|
return
|
|
13
13
|
}
|
|
14
14
|
global.__codeceptConfigListenerInitialized = true
|
|
15
|
-
|
|
16
|
-
const helpers = global.container.helpers()
|
|
17
15
|
|
|
18
16
|
enableDynamicConfigFor('suite')
|
|
19
17
|
enableDynamicConfigFor('test')
|
|
20
18
|
|
|
21
19
|
function enableDynamicConfigFor(type) {
|
|
22
20
|
event.dispatcher.on(event[type].before, (context = {}) => {
|
|
21
|
+
// Get helpers dynamically at runtime, not at initialization time
|
|
22
|
+
// This ensures we get the actual helper instances, not placeholders
|
|
23
|
+
const helpers = global.container.helpers()
|
|
24
|
+
|
|
23
25
|
function updateHelperConfig(helper, config) {
|
|
26
|
+
// Guard against undefined or invalid helpers
|
|
27
|
+
if (!helper || !helper.constructor) {
|
|
28
|
+
output.debug(`[${ucfirst(type)} Config] Helper not found or not properly initialized`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
const oldConfig = deepClone(helper.options)
|
|
25
33
|
try {
|
|
26
34
|
helper._setConfig(deepMerge(deepClone(oldConfig), config))
|
|
@@ -41,7 +49,7 @@ export default function () {
|
|
|
41
49
|
for (let name in context.config) {
|
|
42
50
|
const config = context.config[name]
|
|
43
51
|
if (name === '0') {
|
|
44
|
-
// first helper
|
|
52
|
+
// first helper - get dynamically
|
|
45
53
|
name = Object.keys(helpers)[0]
|
|
46
54
|
}
|
|
47
55
|
const helper = helpers[name]
|
|
@@ -5,16 +5,27 @@ import { isNotSet } from '../utils.js'
|
|
|
5
5
|
|
|
6
6
|
const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']
|
|
7
7
|
|
|
8
|
+
const RETRY_PRIORITIES = {
|
|
9
|
+
MANUAL_STEP: 100,
|
|
10
|
+
STEP_PLUGIN: 50,
|
|
11
|
+
SCENARIO_CONFIG: 30,
|
|
12
|
+
FEATURE_CONFIG: 20,
|
|
13
|
+
HOOK_CONFIG: 10,
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export default function () {
|
|
9
17
|
event.dispatcher.on(event.suite.before, suite => {
|
|
10
18
|
let retryConfig = Config.get('retry')
|
|
11
19
|
if (!retryConfig) return
|
|
12
20
|
|
|
13
21
|
if (Number.isInteger(+retryConfig)) {
|
|
14
|
-
// is number
|
|
15
22
|
const retryNum = +retryConfig
|
|
16
23
|
output.log(`Retries: ${retryNum}`)
|
|
17
|
-
|
|
24
|
+
|
|
25
|
+
if (suite.retries() === -1 || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
|
|
26
|
+
suite.retries(retryNum)
|
|
27
|
+
suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
|
|
28
|
+
}
|
|
18
29
|
return
|
|
19
30
|
}
|
|
20
31
|
|
|
@@ -30,11 +41,18 @@ export default function () {
|
|
|
30
41
|
hooks
|
|
31
42
|
.filter(hook => !!config[hook])
|
|
32
43
|
.forEach(hook => {
|
|
33
|
-
|
|
44
|
+
const retryKey = `retry${hook}`
|
|
45
|
+
if (isNotSet(suite.opts[retryKey])) {
|
|
46
|
+
suite.opts[retryKey] = config[hook]
|
|
47
|
+
suite.opts[`${retryKey}Priority`] = RETRY_PRIORITIES.HOOK_CONFIG
|
|
48
|
+
}
|
|
34
49
|
})
|
|
35
50
|
|
|
36
51
|
if (config.Feature) {
|
|
37
|
-
if (
|
|
52
|
+
if (suite.retries() === -1 || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
|
|
53
|
+
suite.retries(config.Feature)
|
|
54
|
+
suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
|
|
55
|
+
}
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
output.log(`Retries: ${JSON.stringify(config)}`)
|
|
@@ -46,7 +64,10 @@ export default function () {
|
|
|
46
64
|
if (!retryConfig) return
|
|
47
65
|
|
|
48
66
|
if (Number.isInteger(+retryConfig)) {
|
|
49
|
-
if (test.retries() === -1)
|
|
67
|
+
if (test.retries() === -1) {
|
|
68
|
+
test.retries(retryConfig)
|
|
69
|
+
test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
|
|
70
|
+
}
|
|
50
71
|
return
|
|
51
72
|
}
|
|
52
73
|
|
|
@@ -62,9 +83,14 @@ export default function () {
|
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
if (config.Scenario) {
|
|
65
|
-
if (test.retries() === -1
|
|
86
|
+
if (test.retries() === -1 || (test.opts.retryPriority || 0) <= RETRY_PRIORITIES.SCENARIO_CONFIG) {
|
|
87
|
+
test.retries(config.Scenario)
|
|
88
|
+
test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
|
|
89
|
+
}
|
|
66
90
|
output.log(`Retries: ${config.Scenario}`)
|
|
67
91
|
}
|
|
68
92
|
}
|
|
69
93
|
})
|
|
70
94
|
}
|
|
95
|
+
|
|
96
|
+
export { RETRY_PRIORITIES }
|
package/lib/listener/helpers.js
CHANGED
|
@@ -73,30 +73,18 @@ export default function () {
|
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
event.dispatcher.on(event.all.result, () => {
|
|
76
|
-
// Skip _finishTest for all helpers if any browser helper restarts to avoid double cleanup
|
|
77
|
-
const hasBrowserRestart = Object.values(helpers).some(helper =>
|
|
78
|
-
(helper.config && (helper.config.restart === 'browser' || helper.config.restart === 'context' || helper.config.restart === true)) ||
|
|
79
|
-
(helper.options && (helper.options.restart === 'browser' || helper.options.restart === 'context' || helper.options.restart === true))
|
|
80
|
-
)
|
|
81
|
-
|
|
82
76
|
Object.keys(helpers).forEach(key => {
|
|
83
77
|
const helper = helpers[key]
|
|
84
|
-
if (helper._finishTest
|
|
78
|
+
if (helper._finishTest) {
|
|
85
79
|
recorder.add(`hook ${key}._finishTest()`, () => helper._finishTest(), true, false)
|
|
86
80
|
}
|
|
87
81
|
})
|
|
88
82
|
})
|
|
89
83
|
|
|
90
84
|
event.dispatcher.on(event.all.after, () => {
|
|
91
|
-
// Skip _cleanup for all helpers if any browser helper restarts to avoid double cleanup
|
|
92
|
-
const hasBrowserRestart = Object.values(helpers).some(helper =>
|
|
93
|
-
(helper.config && (helper.config.restart === 'browser' || helper.config.restart === 'context' || helper.config.restart === true)) ||
|
|
94
|
-
(helper.options && (helper.options.restart === 'browser' || helper.options.restart === 'context' || helper.options.restart === true))
|
|
95
|
-
)
|
|
96
|
-
|
|
97
85
|
Object.keys(helpers).forEach(key => {
|
|
98
86
|
const helper = helpers[key]
|
|
99
|
-
if (helper._cleanup
|
|
87
|
+
if (helper._cleanup) {
|
|
100
88
|
recorder.add(`hook ${key}._cleanup()`, () => helper._cleanup(), true, false)
|
|
101
89
|
}
|
|
102
90
|
})
|
package/lib/locator.js
CHANGED
|
@@ -40,6 +40,11 @@ class Locator {
|
|
|
40
40
|
return
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Try to parse JSON strings that look like objects
|
|
44
|
+
if (this.parsedJsonAsString(locator)) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
this.type = defaultType || 'fuzzy'
|
|
44
49
|
this.output = locator
|
|
45
50
|
this.value = locator
|
|
@@ -89,6 +94,33 @@ class Locator {
|
|
|
89
94
|
return { [this.type]: this.value }
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
parsedJsonAsString(locator) {
|
|
98
|
+
if (typeof locator !== 'string') {
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const trimmed = locator.trim()
|
|
103
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(trimmed)
|
|
109
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
110
|
+
this.locator = parsed
|
|
111
|
+
this.type = Object.keys(parsed)[0]
|
|
112
|
+
this.value = parsed[this.type]
|
|
113
|
+
this.strict = true
|
|
114
|
+
|
|
115
|
+
Locator.filters.forEach(f => f(parsed, this))
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
// continue with normal string processing
|
|
120
|
+
}
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
|
|
92
124
|
/**
|
|
93
125
|
* @returns {string}
|
|
94
126
|
*/
|
package/lib/mocha/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import event from '../event.js'
|
|
|
9
9
|
import AssertionFailedError from '../assert/error.js'
|
|
10
10
|
import output from '../output.js'
|
|
11
11
|
import test, { cloneTest } from './test.js'
|
|
12
|
+
import { fixErrorStack } from '../utils/typescript.js'
|
|
12
13
|
|
|
13
14
|
// Get version from package.json to avoid circular dependency
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -201,6 +202,16 @@ class Cli extends Base {
|
|
|
201
202
|
|
|
202
203
|
// failures
|
|
203
204
|
if (stats.failures) {
|
|
205
|
+
for (const test of this.failures) {
|
|
206
|
+
if (test.err && typeof test.err.fetchDetails === 'function') {
|
|
207
|
+
try {
|
|
208
|
+
await test.err.fetchDetails()
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// ignore fetch errors
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
204
215
|
// append step traces
|
|
205
216
|
this.failures = this.failures.map(test => {
|
|
206
217
|
// we will change the stack trace, so we need to clone the test
|
|
@@ -264,6 +275,11 @@ class Cli extends Base {
|
|
|
264
275
|
}
|
|
265
276
|
|
|
266
277
|
try {
|
|
278
|
+
const fileMapping = global.container?.tsFileMapping?.()
|
|
279
|
+
if (fileMapping) {
|
|
280
|
+
fixErrorStack(err, fileMapping)
|
|
281
|
+
}
|
|
282
|
+
|
|
267
283
|
let stack = err.stack
|
|
268
284
|
stack = (stack || '').replace(originalMessage, '')
|
|
269
285
|
stack = stack ? stack.split('\n') : []
|
package/lib/mocha/factory.js
CHANGED
|
@@ -7,6 +7,7 @@ import gherkinParser, { loadTranslations } from './gherkin.js'
|
|
|
7
7
|
import output from '../output.js'
|
|
8
8
|
import scenarioUiFunction from './ui.js'
|
|
9
9
|
import { initMochaGlobals } from '../globals.js'
|
|
10
|
+
import { fixErrorStack } from '../utils/typescript.js'
|
|
10
11
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url)
|
|
12
13
|
const __dirname = fsPath.dirname(__filename)
|
|
@@ -34,6 +35,10 @@ class MochaFactory {
|
|
|
34
35
|
// Handle ECONNREFUSED without dynamic import for now
|
|
35
36
|
err = new Error('Connection refused: ' + err.toString())
|
|
36
37
|
}
|
|
38
|
+
const fileMapping = global.container?.tsFileMapping?.()
|
|
39
|
+
if (fileMapping) {
|
|
40
|
+
fixErrorStack(err, fileMapping)
|
|
41
|
+
}
|
|
37
42
|
output.error(err)
|
|
38
43
|
output.print(err.stack)
|
|
39
44
|
process.exit(1)
|
|
@@ -62,34 +67,9 @@ class MochaFactory {
|
|
|
62
67
|
const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
|
|
63
68
|
this.files = this.files.filter(file => !file.match(/\.feature$/))
|
|
64
69
|
|
|
65
|
-
// Load JavaScript test files using
|
|
70
|
+
// Load JavaScript test files using original loadFiles
|
|
66
71
|
if (jsFiles.length > 0) {
|
|
67
|
-
|
|
68
|
-
// Try original loadFiles first for compatibility
|
|
69
|
-
originalLoadFiles.call(this, fn)
|
|
70
|
-
} catch (e) {
|
|
71
|
-
// If original loadFiles fails, load ESM files manually
|
|
72
|
-
if (e.message.includes('not in cache') || e.message.includes('ESM') || e.message.includes('getStatus')) {
|
|
73
|
-
// Load ESM files by importing them synchronously using top-level await workaround
|
|
74
|
-
for (const file of jsFiles) {
|
|
75
|
-
try {
|
|
76
|
-
// Convert file path to file:// URL for dynamic import
|
|
77
|
-
const fileUrl = `file://${file}`
|
|
78
|
-
// Use import() but don't await it - let it load in the background
|
|
79
|
-
import(fileUrl).catch(importErr => {
|
|
80
|
-
// If dynamic import fails, the file may have syntax errors or other issues
|
|
81
|
-
console.error(`Failed to load test file ${file}:`, importErr.message)
|
|
82
|
-
})
|
|
83
|
-
if (fn) fn()
|
|
84
|
-
} catch (fileErr) {
|
|
85
|
-
console.error(`Error processing test file ${file}:`, fileErr.message)
|
|
86
|
-
if (fn) fn(fileErr)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
throw e
|
|
91
|
-
}
|
|
92
|
-
}
|
|
72
|
+
originalLoadFiles.call(this, fn)
|
|
93
73
|
}
|
|
94
74
|
|
|
95
75
|
// add ids for each test and check uniqueness
|
package/lib/mocha/gherkin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin'
|
|
2
2
|
import { IdGenerator } from '@cucumber/messages'
|
|
3
3
|
import { Context, Suite } from 'mocha'
|
|
4
4
|
import debug from 'debug'
|
|
@@ -15,9 +15,9 @@ import { createTest } from './test.js'
|
|
|
15
15
|
import { matchStep } from './bdd.js'
|
|
16
16
|
|
|
17
17
|
const uuidFn = IdGenerator.uuid()
|
|
18
|
-
const builder = new
|
|
19
|
-
const matcher = new
|
|
20
|
-
const parser = new
|
|
18
|
+
const builder = new AstBuilder(uuidFn)
|
|
19
|
+
const matcher = new GherkinClassicTokenMatcher()
|
|
20
|
+
const parser = new Parser(builder, matcher)
|
|
21
21
|
parser.stopAtFirstError = false
|
|
22
22
|
|
|
23
23
|
const gherkinParser = (text, file) => {
|
package/lib/mocha/test.js
CHANGED
|
@@ -154,14 +154,16 @@ function cloneTest(test) {
|
|
|
154
154
|
function testToFileName(test, { suffix = '', unique = false } = {}) {
|
|
155
155
|
let fileName = test.title
|
|
156
156
|
|
|
157
|
-
if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime() / 1000)}`
|
|
158
|
-
if (suffix) fileName = `${fileName}_${suffix}`
|
|
159
157
|
// remove tags with empty string (disable for now)
|
|
160
158
|
// fileName = fileName.replace(/\@\w+/g, '')
|
|
161
159
|
fileName = fileName.slice(0, 100)
|
|
162
160
|
if (fileName.indexOf('{') !== -1) {
|
|
163
161
|
fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim()
|
|
164
162
|
}
|
|
163
|
+
|
|
164
|
+
// Apply unique suffix AFTER removing data part to ensure uniqueness
|
|
165
|
+
if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime())}`
|
|
166
|
+
if (suffix) fileName = `${fileName}_${suffix}`
|
|
165
167
|
if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') fileName = clearString(`${test.title}_${test.ctx.test.title}`)
|
|
166
168
|
// TODO: add suite title to file name
|
|
167
169
|
// if (test.parent && test.parent.title) {
|
package/lib/output.js
CHANGED
|
@@ -222,12 +222,10 @@ const output = {
|
|
|
222
222
|
/**
|
|
223
223
|
* @param {Mocha.Test} test
|
|
224
224
|
*/
|
|
225
|
-
|
|
226
225
|
started(test) {
|
|
227
226
|
if (outputLevel < 1) return
|
|
228
227
|
print(` ${colors.dim.bold('Scenario()')}`)
|
|
229
228
|
},
|
|
230
|
-
|
|
231
229
|
/**
|
|
232
230
|
* @param {Mocha.Test} test
|
|
233
231
|
*/
|
|
@@ -273,10 +271,12 @@ const output = {
|
|
|
273
271
|
},
|
|
274
272
|
|
|
275
273
|
/**
|
|
274
|
+
* Prints the stats of a test run to the console.
|
|
276
275
|
* @param {number} passed
|
|
277
276
|
* @param {number} failed
|
|
278
277
|
* @param {number} skipped
|
|
279
278
|
* @param {number|string} duration
|
|
279
|
+
* @param {number} [failedHooks]
|
|
280
280
|
*/
|
|
281
281
|
result(passed, failed, skipped, duration, failedHooks = 0) {
|
|
282
282
|
let style = colors.bgGreen
|