codeceptjs 4.0.2-beta.8 → 4.0.2
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 -28
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1189 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +489 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/architecture.md +235 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +185 -0
- package/docs/continuous-integration.md +431 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +107 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/environment-variables.md +131 -0
- package/docs/examples.md +160 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1419 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/Playwright.md +2970 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2583 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/WebDriver.md +2639 -0
- package/docs/hooks.md +148 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +121 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migrate-from-cypress.md +98 -0
- package/docs/migrate-from-java.md +108 -0
- package/docs/migrate-from-protractor.md +101 -0
- package/docs/migrate-from-testcafe.md +99 -0
- package/docs/migration-4.md +745 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +187 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins/aiTrace.md +49 -0
- package/docs/plugins/analyze.md +66 -0
- package/docs/plugins/auth.md +241 -0
- package/docs/plugins/autoDelay.md +48 -0
- package/docs/plugins/browser.md +41 -0
- package/docs/plugins/coverage.md +39 -0
- package/docs/plugins/customLocator.md +119 -0
- package/docs/plugins/customReporter.md +16 -0
- package/docs/plugins/expose.md +75 -0
- package/docs/plugins/heal.md +44 -0
- package/docs/plugins/junitReporter.md +51 -0
- package/docs/plugins/pageInfo.md +34 -0
- package/docs/plugins/pause.md +43 -0
- package/docs/plugins/pauseOnFail.md +18 -0
- package/docs/plugins/retryFailedStep.md +75 -0
- package/docs/plugins/screencast.md +55 -0
- package/docs/plugins/screenshot.md +58 -0
- package/docs/plugins/screenshotOnFail.md +18 -0
- package/docs/plugins/stepTimeout.md +65 -0
- package/docs/plugins.md +87 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/reports.md +195 -0
- package/docs/retry.md +311 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/store.md +94 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +323 -0
- package/docs/typescript.md +159 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +641 -0
- package/docs/within.md +55 -0
- package/lib/actor.js +1 -36
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +34 -25
- package/lib/command/check.js +2 -1
- package/lib/command/definitions.js +6 -7
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +3 -1
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +249 -270
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -1
- package/lib/command/run-workers.js +2 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +91 -37
- package/lib/config.js +96 -18
- package/lib/container.js +115 -17
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +246 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +7 -4
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +358 -467
- package/lib/helper/Puppeteer.js +335 -192
- package/lib/helper/WebDriver.js +324 -111
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +7 -107
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/history.js +3 -2
- package/lib/html.js +103 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/globalRetry.js +32 -6
- 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 +158 -16
- package/lib/mocha/cli.js +19 -1
- package/lib/mocha/factory.js +11 -1
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +457 -0
- package/lib/plugin/analyze.js +9 -9
- package/lib/plugin/auth.js +5 -4
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +47 -3
- package/lib/plugin/junitReporter.js +303 -0
- package/lib/plugin/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +11 -33
- package/lib/plugin/retryFailedStep.js +43 -32
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +558 -0
- package/lib/plugin/screenshotOnFail.js +9 -170
- package/lib/plugin/stepTimeout.js +3 -2
- package/lib/recorder.js +1 -1
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +10 -9
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +15 -2
- package/lib/step/helper.js +4 -4
- package/lib/step/meta.js +3 -3
- package/lib/step/record.js +5 -5
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/loaderCheck.js +28 -0
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils/typescript.js +188 -23
- package/lib/utils.js +77 -3
- package/lib/workers.js +65 -40
- package/package.json +35 -30
- package/typings/index.d.ts +119 -8
- package/typings/promiseBasedTypes.d.ts +3158 -6065
- package/typings/types.d.ts +3453 -6494
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -9
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -16
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -12
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -21
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/helper/Mochawesome.js +0 -96
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
- package/lib/helper/extras/React.js +0 -65
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/plugin/stepByStepReport.js +0 -427
- package/lib/plugin/subtitles.js +0 -89
- package/lib/retryCoordinator.js +0 -207
package/lib/html.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { parse, serialize } from 'parse5'
|
|
2
2
|
import { minify } from 'html-minifier-terser'
|
|
3
|
+
import beautify from 'js-beautify'
|
|
4
|
+
|
|
5
|
+
const { html: html_beautify } = beautify
|
|
3
6
|
|
|
4
7
|
async function minifyHtml(html) {
|
|
5
8
|
return minify(html, {
|
|
@@ -14,6 +17,62 @@ async function minifyHtml(html) {
|
|
|
14
17
|
})
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
const TRASH_HTML_CLASSES = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
|
|
21
|
+
|
|
22
|
+
function isTrashClass(className) {
|
|
23
|
+
if (!className) return true
|
|
24
|
+
if (/\d/.test(className)) return true
|
|
25
|
+
if (TRASH_HTML_CLASSES.test(className)) return true
|
|
26
|
+
if (/(:|__)/.test(className)) return true
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function filterClassValue(value) {
|
|
31
|
+
return (value || '')
|
|
32
|
+
.split(/\s+/)
|
|
33
|
+
.filter(c => c && !isTrashClass(c))
|
|
34
|
+
.join(' ')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DROP_TAGS = new Set(['style', 'noscript'])
|
|
38
|
+
const DROP_ATTRS = new Set(['style'])
|
|
39
|
+
|
|
40
|
+
function cleanHtml(html) {
|
|
41
|
+
const document = parse(html)
|
|
42
|
+
|
|
43
|
+
function walk(node) {
|
|
44
|
+
if (!node) return false
|
|
45
|
+
|
|
46
|
+
if (DROP_TAGS.has(node.nodeName) || (node.nodeName === 'script' && !(node.attrs || []).some(a => a.name === 'src'))) {
|
|
47
|
+
const parent = node.parentNode
|
|
48
|
+
const idx = parent.childNodes.indexOf(node)
|
|
49
|
+
if (idx >= 0) parent.childNodes.splice(idx, 1)
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (node.attrs) {
|
|
54
|
+
node.attrs = node.attrs.filter(attr => {
|
|
55
|
+
if (DROP_ATTRS.has(attr.name)) return false
|
|
56
|
+
if (attr.name === 'class') {
|
|
57
|
+
attr.value = filterClassValue(attr.value)
|
|
58
|
+
if (!attr.value) return false
|
|
59
|
+
}
|
|
60
|
+
return true
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (node.childNodes) {
|
|
65
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
66
|
+
walk(node.childNodes[i])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
walk(document)
|
|
73
|
+
return serialize(document)
|
|
74
|
+
}
|
|
75
|
+
|
|
17
76
|
const defaultHtmlOpts = {
|
|
18
77
|
interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'option'],
|
|
19
78
|
textElements: ['label', 'h1', 'h2'],
|
|
@@ -28,7 +87,6 @@ function removeNonInteractiveElements(html, opts = {}) {
|
|
|
28
87
|
// Parse the HTML into a document tree
|
|
29
88
|
const document = parse(html)
|
|
30
89
|
|
|
31
|
-
const trashHtmlClasses = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
|
|
32
90
|
// Array to store interactive elements
|
|
33
91
|
const removeElements = ['path', 'script']
|
|
34
92
|
|
|
@@ -103,21 +161,10 @@ function removeNonInteractiveElements(html, opts = {}) {
|
|
|
103
161
|
if (node.attrs) {
|
|
104
162
|
// Filter and keep allowed attributes, accessibility attributes
|
|
105
163
|
node.attrs = node.attrs.filter(attr => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Remove classes containing digits
|
|
109
|
-
attr.value = value
|
|
110
|
-
.split(' ')
|
|
111
|
-
// remove classes containing digits/
|
|
112
|
-
.filter(className => !/\d/.test(className))
|
|
113
|
-
// remove popular trash classes
|
|
114
|
-
.filter(className => !className.match(trashHtmlClasses))
|
|
115
|
-
// remove classes with : and __ in them
|
|
116
|
-
.filter(className => !className.match(/(:|__)/))
|
|
117
|
-
.join(' ')
|
|
164
|
+
if (attr.name === 'class') {
|
|
165
|
+
attr.value = filterClassValue(attr.value)
|
|
118
166
|
}
|
|
119
|
-
|
|
120
|
-
return allowedAttrs.includes(name)
|
|
167
|
+
return allowedAttrs.includes(attr.name)
|
|
121
168
|
})
|
|
122
169
|
}
|
|
123
170
|
|
|
@@ -245,4 +292,44 @@ function splitByChunks(text, chunkSize) {
|
|
|
245
292
|
return chunks.map(chunk => chunk.trim())
|
|
246
293
|
}
|
|
247
294
|
|
|
248
|
-
|
|
295
|
+
function simplifyHtmlElement(html, maxLength = 300) {
|
|
296
|
+
try {
|
|
297
|
+
html = removeNonInteractiveElements(html)
|
|
298
|
+
html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
|
|
299
|
+
} catch (e) {
|
|
300
|
+
// keep raw html if minification fails
|
|
301
|
+
}
|
|
302
|
+
if (html.length > maxLength) {
|
|
303
|
+
html = html.slice(0, maxLength) + '...'
|
|
304
|
+
}
|
|
305
|
+
return html
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function formatHtml(html) {
|
|
309
|
+
let processed = html
|
|
310
|
+
try {
|
|
311
|
+
processed = await minifyHtml(processed)
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// keep raw html if minification fails
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
processed = cleanHtml(processed)
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// keep minified html if cleaning fails
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
return html_beautify(processed, {
|
|
322
|
+
indent_size: 2,
|
|
323
|
+
wrap_line_length: 0,
|
|
324
|
+
preserve_newlines: false,
|
|
325
|
+
end_with_newline: false,
|
|
326
|
+
// Force every element onto its own line so line numbers in trace HTML
|
|
327
|
+
// map 1:1 to elements (consumed by codeceptq for AI/agent debugging).
|
|
328
|
+
inline: [],
|
|
329
|
+
})
|
|
330
|
+
} catch (e) {
|
|
331
|
+
return processed
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement, formatHtml, cleanHtml, isTrashClass }
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,10 @@ import heal from './heal.js'
|
|
|
23
23
|
import ai from './ai.js'
|
|
24
24
|
import Workers from './workers.js'
|
|
25
25
|
import Secret, { secret } from './secret.js'
|
|
26
|
+
import session from './session.js'
|
|
27
|
+
|
|
28
|
+
const inject = (name) => container.support(name)
|
|
29
|
+
const locate = (query) => locator.build(query)
|
|
26
30
|
|
|
27
31
|
export default {
|
|
28
32
|
/** @type {typeof CodeceptJS.Codecept} */
|
|
@@ -67,7 +71,11 @@ export default {
|
|
|
67
71
|
Secret,
|
|
68
72
|
/** @type {typeof CodeceptJS.secret} */
|
|
69
73
|
secret,
|
|
74
|
+
|
|
75
|
+
session,
|
|
76
|
+
inject,
|
|
77
|
+
locate,
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
// Named exports for ESM compatibility
|
|
73
|
-
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret }
|
|
81
|
+
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, session, inject, locate }
|
package/lib/listener/config.js
CHANGED
|
@@ -2,16 +2,18 @@ import event from '../event.js'
|
|
|
2
2
|
import recorder from '../recorder.js'
|
|
3
3
|
import { deepMerge, deepClone, ucfirst } from '../utils.js'
|
|
4
4
|
import output from '../output.js'
|
|
5
|
+
import container from '../container.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Enable Helpers to listen to test events
|
|
8
9
|
*/
|
|
10
|
+
let initialized = false
|
|
11
|
+
|
|
9
12
|
export default function () {
|
|
10
|
-
|
|
11
|
-
if (global.__codeceptConfigListenerInitialized) {
|
|
13
|
+
if (initialized) {
|
|
12
14
|
return
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
initialized = true
|
|
15
17
|
|
|
16
18
|
enableDynamicConfigFor('suite')
|
|
17
19
|
enableDynamicConfigFor('test')
|
|
@@ -20,7 +22,7 @@ export default function () {
|
|
|
20
22
|
event.dispatcher.on(event[type].before, (context = {}) => {
|
|
21
23
|
// Get helpers dynamically at runtime, not at initialization time
|
|
22
24
|
// This ensures we get the actual helper instances, not placeholders
|
|
23
|
-
const helpers =
|
|
25
|
+
const helpers = container.helpers()
|
|
24
26
|
|
|
25
27
|
function updateHelperConfig(helper, config) {
|
|
26
28
|
// Guard against undefined or invalid helpers
|
package/lib/listener/emptyRun.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import figures from 'figures'
|
|
2
2
|
import event from '../event.js'
|
|
3
3
|
import output from '../output.js'
|
|
4
|
+
import container from '../container.js'
|
|
4
5
|
import { searchWithFusejs } from '../utils.js'
|
|
5
6
|
|
|
6
7
|
export default function () {
|
|
@@ -12,7 +13,7 @@ export default function () {
|
|
|
12
13
|
|
|
13
14
|
event.dispatcher.on(event.all.result, () => {
|
|
14
15
|
if (isEmptyRun) {
|
|
15
|
-
const mocha =
|
|
16
|
+
const mocha = container.mocha()
|
|
16
17
|
|
|
17
18
|
if (mocha.options.grep) {
|
|
18
19
|
output.print()
|
|
@@ -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
|
@@ -3,11 +3,12 @@ import event from '../event.js'
|
|
|
3
3
|
import recorder from '../recorder.js'
|
|
4
4
|
import store from '../store.js'
|
|
5
5
|
import output from '../output.js'
|
|
6
|
+
import container from '../container.js'
|
|
6
7
|
/**
|
|
7
8
|
* Enable Helpers to listen to test events
|
|
8
9
|
*/
|
|
9
10
|
export default function () {
|
|
10
|
-
const helpers =
|
|
11
|
+
const helpers = container.helpers()
|
|
11
12
|
|
|
12
13
|
const runHelpersHook = (hook, param) => {
|
|
13
14
|
if (store.dryRun) return
|
|
@@ -29,11 +30,13 @@ export default function () {
|
|
|
29
30
|
event.dispatcher.on(event.suite.before, suite => {
|
|
30
31
|
// if (suite.parent) return; // only for root suite
|
|
31
32
|
runAsyncHelpersHook('_beforeSuite', suite, true)
|
|
33
|
+
recorder.catch()
|
|
32
34
|
})
|
|
33
35
|
|
|
34
36
|
event.dispatcher.on(event.suite.after, suite => {
|
|
35
37
|
// if (suite.parent) return; // only for root suite
|
|
36
38
|
runAsyncHelpersHook('_afterSuite', suite, true)
|
|
39
|
+
recorder.catch()
|
|
37
40
|
})
|
|
38
41
|
|
|
39
42
|
event.dispatcher.on(event.test.started, test => {
|
package/lib/listener/mocha.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import event from '../event.js'
|
|
2
|
+
import container from '../container.js'
|
|
2
3
|
|
|
3
4
|
export default function () {
|
|
4
5
|
let mocha
|
|
5
6
|
|
|
6
7
|
event.dispatcher.on(event.all.before, () => {
|
|
7
|
-
mocha =
|
|
8
|
+
mocha = container.mocha()
|
|
8
9
|
})
|
|
9
10
|
|
|
10
11
|
event.dispatcher.on(event.test.passed, test => {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import event from '../event.js'
|
|
2
|
+
import recorder from '../recorder.js'
|
|
3
|
+
import store from '../store.js'
|
|
4
|
+
import container from '../container.js'
|
|
5
|
+
import { resetBeforeCalledSet, getBeforeCalledSet } from '../container.js'
|
|
6
|
+
|
|
7
|
+
export default function () {
|
|
8
|
+
const runAsyncSupportHook = (hook, param, force) => {
|
|
9
|
+
if (store.dryRun) return
|
|
10
|
+
const support = container.supportObjects()
|
|
11
|
+
Object.keys(support).forEach(key => {
|
|
12
|
+
if (key === 'I') return
|
|
13
|
+
const obj = support[key]
|
|
14
|
+
if (!obj || typeof obj !== 'object' || !obj[hook]) return
|
|
15
|
+
recorder.add(`pageobject ${key}.${hook}()`, () => obj[hook](param), force, false)
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
event.dispatcher.on(event.test.started, () => {
|
|
20
|
+
resetBeforeCalledSet()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
event.dispatcher.on(event.test.after, () => {
|
|
24
|
+
if (store.dryRun) return
|
|
25
|
+
const support = container.supportObjects()
|
|
26
|
+
const called = getBeforeCalledSet()
|
|
27
|
+
called.forEach(name => {
|
|
28
|
+
const obj = support[name]
|
|
29
|
+
if (obj && obj._after) {
|
|
30
|
+
recorder.add(`pageobject ${name}._after()`, () => obj._after(), true, false)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
recorder.catchWithoutStop(() => {})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
event.dispatcher.on(event.suite.after, suite => {
|
|
37
|
+
runAsyncSupportHook('_afterSuite', suite, true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
41
|
+
runAsyncSupportHook('_beforeSuite', suite, true)
|
|
42
|
+
})
|
|
43
|
+
}
|
package/lib/listener/result.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import event from '../event.js'
|
|
2
|
+
import container from '../container.js'
|
|
2
3
|
|
|
3
4
|
export default function () {
|
|
4
5
|
event.dispatcher.on(event.hook.failed, err => {
|
|
5
|
-
|
|
6
|
+
container.result().addStats({ failedHooks: 1 })
|
|
6
7
|
})
|
|
7
8
|
|
|
8
9
|
event.dispatcher.on(event.test.before, test => {
|
|
9
|
-
|
|
10
|
+
container.result().addTest(test)
|
|
10
11
|
})
|
|
11
12
|
}
|
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
|
|
@@ -53,9 +58,6 @@ class Locator {
|
|
|
53
58
|
if (isShadow(locator)) {
|
|
54
59
|
this.type = 'shadow'
|
|
55
60
|
}
|
|
56
|
-
if (isPlaywrightLocator(locator)) {
|
|
57
|
-
this.type = 'pw'
|
|
58
|
-
}
|
|
59
61
|
|
|
60
62
|
Locator.filters.forEach(f => f(locator, this))
|
|
61
63
|
}
|
|
@@ -89,6 +91,33 @@ class Locator {
|
|
|
89
91
|
return { [this.type]: this.value }
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
parsedJsonAsString(locator) {
|
|
95
|
+
if (typeof locator !== 'string') {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const trimmed = locator.trim()
|
|
100
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(trimmed)
|
|
106
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
107
|
+
this.locator = parsed
|
|
108
|
+
this.type = Object.keys(parsed)[0]
|
|
109
|
+
this.value = parsed[this.type]
|
|
110
|
+
this.strict = true
|
|
111
|
+
|
|
112
|
+
Locator.filters.forEach(f => f(parsed, this))
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// continue with normal string processing
|
|
117
|
+
}
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
92
121
|
/**
|
|
93
122
|
* @returns {string}
|
|
94
123
|
*/
|
|
@@ -349,9 +378,121 @@ class Locator {
|
|
|
349
378
|
return new Locator({ xpath })
|
|
350
379
|
}
|
|
351
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Find an element with all of the provided CSS classes (word-exact match).
|
|
383
|
+
* Accepts variadic class names; all must be present.
|
|
384
|
+
*
|
|
385
|
+
* Example:
|
|
386
|
+
* locate('button').withClass('btn-primary', 'btn-lg')
|
|
387
|
+
*
|
|
388
|
+
* @param {...string} classes
|
|
389
|
+
* @returns {Locator}
|
|
390
|
+
*/
|
|
391
|
+
withClass(...classes) {
|
|
392
|
+
if (!classes.length) return this
|
|
393
|
+
const predicates = classes.map(c => `contains(concat(' ', normalize-space(@class), ' '), ' ${c} ')`)
|
|
394
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), predicates.join(' and '))
|
|
395
|
+
return new Locator({ xpath })
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Find an element with none of the provided CSS classes.
|
|
400
|
+
*
|
|
401
|
+
* Example:
|
|
402
|
+
* locate('tr').withoutClass('deleted')
|
|
403
|
+
*
|
|
404
|
+
* @param {...string} classes
|
|
405
|
+
* @returns {Locator}
|
|
406
|
+
*/
|
|
407
|
+
withoutClass(...classes) {
|
|
408
|
+
if (!classes.length) return this
|
|
409
|
+
const predicates = classes.map(c => `not(contains(concat(' ', normalize-space(@class), ' '), ' ${c} '))`)
|
|
410
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), predicates.join(' and '))
|
|
411
|
+
return new Locator({ xpath })
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Find an element that does NOT contain the provided text.
|
|
416
|
+
* @param {string} text
|
|
417
|
+
* @returns {Locator}
|
|
418
|
+
*/
|
|
419
|
+
withoutText(text) {
|
|
420
|
+
text = xpathLocator.literal(text)
|
|
421
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), `not(contains(., ${text}))`)
|
|
422
|
+
return new Locator({ xpath })
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Find an element that does NOT have any of the provided attribute/value pairs.
|
|
427
|
+
* @param {Object.<string, string>} attributes
|
|
428
|
+
* @returns {Locator}
|
|
429
|
+
*/
|
|
430
|
+
withoutAttr(attributes) {
|
|
431
|
+
const operands = []
|
|
432
|
+
for (const attr of Object.keys(attributes)) {
|
|
433
|
+
operands.push(`not(@${attr} = ${xpathLocator.literal(attributes[attr])})`)
|
|
434
|
+
}
|
|
435
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), operands.join(' and '))
|
|
436
|
+
return new Locator({ xpath })
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Find an element that has no direct child matching the provided locator.
|
|
441
|
+
* @param {CodeceptJS.LocatorOrString} locator
|
|
442
|
+
* @returns {Locator}
|
|
443
|
+
*/
|
|
444
|
+
withoutChild(locator) {
|
|
445
|
+
const xpath = sprintf('%s[not(./child::%s)]', this.toXPath(), convertToSubSelector(locator))
|
|
446
|
+
return new Locator({ xpath })
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Find an element that has no descendant matching the provided locator.
|
|
451
|
+
*
|
|
452
|
+
* Example:
|
|
453
|
+
* locate('button').withoutDescendant('svg')
|
|
454
|
+
*
|
|
455
|
+
* @param {CodeceptJS.LocatorOrString} locator
|
|
456
|
+
* @returns {Locator}
|
|
457
|
+
*/
|
|
458
|
+
withoutDescendant(locator) {
|
|
459
|
+
const xpath = sprintf('%s[not(./descendant::%s)]', this.toXPath(), convertToSubSelector(locator))
|
|
460
|
+
return new Locator({ xpath })
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Append a raw XPath predicate. Escape hatch for expressions not covered by the DSL.
|
|
465
|
+
* Argument is inserted as-is inside `[ ]`; quoting/escaping is the caller's responsibility.
|
|
466
|
+
*
|
|
467
|
+
* Example:
|
|
468
|
+
* locate('input').and('@type="text" or @type="email"')
|
|
469
|
+
*
|
|
470
|
+
* @param {string} xpathExpression
|
|
471
|
+
* @returns {Locator}
|
|
472
|
+
*/
|
|
473
|
+
and(xpathExpression) {
|
|
474
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), xpathExpression)
|
|
475
|
+
return new Locator({ xpath })
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Append a negated raw XPath predicate: `[not(expr)]`.
|
|
480
|
+
*
|
|
481
|
+
* Example:
|
|
482
|
+
* locate('button').andNot('.//svg') // button without a descendant svg
|
|
483
|
+
*
|
|
484
|
+
* @param {string} xpathExpression
|
|
485
|
+
* @returns {Locator}
|
|
486
|
+
*/
|
|
487
|
+
andNot(xpathExpression) {
|
|
488
|
+
const xpath = sprintf('%s[not(%s)]', this.toXPath(), xpathExpression)
|
|
489
|
+
return new Locator({ xpath })
|
|
490
|
+
}
|
|
491
|
+
|
|
352
492
|
/**
|
|
353
493
|
* @param {String} text
|
|
354
494
|
* @returns {Locator}
|
|
495
|
+
* @deprecated Use {@link Locator#withClass} for word-exact class matching, or {@link Locator#withAttrContains} for substring matching.
|
|
355
496
|
*/
|
|
356
497
|
withClassAttr(text) {
|
|
357
498
|
const xpath = sprintf('%s[%s]', this.toXPath(), `contains(@class, '${text}')`)
|
|
@@ -445,15 +586,26 @@ Locator.clickable = {
|
|
|
445
586
|
`.//button[./@name = ${literal}]`,
|
|
446
587
|
`.//*[@aria-label = ${literal}]`,
|
|
447
588
|
`.//*[@title = ${literal}]`,
|
|
448
|
-
`.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id
|
|
589
|
+
`.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
|
|
449
590
|
`.//*[@role='button'][normalize-space(.)=${literal}]`,
|
|
591
|
+
`.//*[@role='tab' or @role='link' or @role='menuitem' or @role='menuitemcheckbox' or @role='menuitemradio' or @role='option' or @role='treeitem'][contains(normalize-space(string(.)), ${literal})]`,
|
|
450
592
|
]),
|
|
451
593
|
|
|
452
594
|
/**
|
|
453
595
|
* @param {string} literal
|
|
454
596
|
* @returns {string}
|
|
455
597
|
*/
|
|
456
|
-
self: literal =>
|
|
598
|
+
self: literal => {
|
|
599
|
+
// Narrowest-match: prefer the deepest descendant whose string-value contains the literal.
|
|
600
|
+
// Falling back to `self` without the `not(descendant...)` guard would match a container
|
|
601
|
+
// whose concatenated text happens to include the literal (e.g. a <ul role="tablist"> whose
|
|
602
|
+
// tab labels all sit in its string-value) and click the container itself.
|
|
603
|
+
const narrowest = `contains(normalize-space(string(.)), ${literal}) and not(.//*[contains(normalize-space(string(.)), ${literal})])`
|
|
604
|
+
return xpathLocator.combine([
|
|
605
|
+
`.//*[${narrowest}]`,
|
|
606
|
+
`./self::*[${narrowest} or contains(normalize-space(@value), ${literal})]`,
|
|
607
|
+
])
|
|
608
|
+
},
|
|
457
609
|
}
|
|
458
610
|
|
|
459
611
|
Locator.field = {
|
|
@@ -477,7 +629,7 @@ Locator.field = {
|
|
|
477
629
|
`.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
|
|
478
630
|
`.//*[@aria-label = ${literal}]`,
|
|
479
631
|
`.//*[@title = ${literal}]`,
|
|
480
|
-
`.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id
|
|
632
|
+
`.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
|
|
481
633
|
]),
|
|
482
634
|
|
|
483
635
|
/**
|
|
@@ -598,16 +750,6 @@ function removePrefix(xpath) {
|
|
|
598
750
|
return xpath.replace(/^(\.|\/)+/, '')
|
|
599
751
|
}
|
|
600
752
|
|
|
601
|
-
/**
|
|
602
|
-
* @private
|
|
603
|
-
* check if the locator is a Playwright locator
|
|
604
|
-
* @param {string} locator
|
|
605
|
-
* @returns {boolean}
|
|
606
|
-
*/
|
|
607
|
-
function isPlaywrightLocator(locator) {
|
|
608
|
-
return locator.includes('_react') || locator.includes('_vue')
|
|
609
|
-
}
|
|
610
|
-
|
|
611
753
|
/**
|
|
612
754
|
* @private
|
|
613
755
|
* check if the locator is a role locator
|
package/lib/mocha/cli.js
CHANGED
|
@@ -8,7 +8,9 @@ import { dirname, join } from 'path'
|
|
|
8
8
|
import event from '../event.js'
|
|
9
9
|
import AssertionFailedError from '../assert/error.js'
|
|
10
10
|
import output from '../output.js'
|
|
11
|
+
import store from '../store.js'
|
|
11
12
|
import test, { cloneTest } from './test.js'
|
|
13
|
+
import { fixErrorStack } from '../utils/typescript.js'
|
|
12
14
|
|
|
13
15
|
// Get version from package.json to avoid circular dependency
|
|
14
16
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -40,7 +42,7 @@ class Cli extends Base {
|
|
|
40
42
|
if (opts.verbose) level = 3
|
|
41
43
|
output.level(level)
|
|
42
44
|
output.print(`CodeceptJS v${codeceptVersion} ${output.standWithUkraine()}`)
|
|
43
|
-
output.print(`Using test root "${
|
|
45
|
+
output.print(`Using test root "${store.codeceptDir}"`)
|
|
44
46
|
|
|
45
47
|
const showSteps = level >= 1
|
|
46
48
|
|
|
@@ -201,7 +203,18 @@ class Cli extends Base {
|
|
|
201
203
|
|
|
202
204
|
// failures
|
|
203
205
|
if (stats.failures) {
|
|
206
|
+
for (const test of this.failures) {
|
|
207
|
+
if (test.err && typeof test.err.fetchDetails === 'function') {
|
|
208
|
+
try {
|
|
209
|
+
await test.err.fetchDetails()
|
|
210
|
+
} catch (e) {
|
|
211
|
+
// ignore fetch errors
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
204
216
|
// append step traces
|
|
217
|
+
const Container = await getContainer()
|
|
205
218
|
this.failures = this.failures.map(test => {
|
|
206
219
|
// we will change the stack trace, so we need to clone the test
|
|
207
220
|
const err = test.err
|
|
@@ -264,6 +277,11 @@ class Cli extends Base {
|
|
|
264
277
|
}
|
|
265
278
|
|
|
266
279
|
try {
|
|
280
|
+
const fileMapping = Container?.tsFileMapping?.()
|
|
281
|
+
if (fileMapping) {
|
|
282
|
+
fixErrorStack(err, fileMapping)
|
|
283
|
+
}
|
|
284
|
+
|
|
267
285
|
let stack = err.stack
|
|
268
286
|
stack = (stack || '').replace(originalMessage, '')
|
|
269
287
|
stack = stack ? stack.split('\n') : []
|