codeceptjs 4.0.0-rc.2 → 4.0.0-rc.20
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 +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1187 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +159 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -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 +230 -0
- package/docs/continuous-integration.md +497 -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 +136 -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/examples.md +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -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/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +26 -23
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +248 -269
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +2 -0
- package/lib/command/run-workers.js +2 -0
- package/lib/command/run.js +1 -1
- package/lib/command/workers/runTests.js +10 -10
- package/lib/config.js +77 -4
- package/lib/container.js +114 -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 +4 -3
- 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 +228 -162
- package/lib/helper/Puppeteer.js +208 -76
- package/lib/helper/WebDriver.js +173 -68
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- 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 +126 -3
- package/lib/mocha/cli.js +14 -2
- package/lib/mocha/factory.js +7 -2
- 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 +453 -0
- package/lib/plugin/analyze.js +1 -1
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +44 -1
- package/lib/plugin/pageInfo.js +53 -49
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/screencast.js +287 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -171
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +3 -2
- package/lib/step/config.js +15 -2
- package/lib/step/record.js +2 -2
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- 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.js +77 -3
- package/lib/workers.js +52 -22
- package/package.json +19 -13
- package/typings/index.d.ts +19 -5
- 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/dontSeeCurrentPathEquals.mustache +0 -10
- 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/seeCurrentPathEquals.mustache +0 -10
- 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/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/typings/promiseBasedTypes.d.ts +0 -9469
- package/typings/types.d.ts +0 -11402
|
@@ -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
|
@@ -381,9 +381,121 @@ class Locator {
|
|
|
381
381
|
return new Locator({ xpath })
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Find an element with all of the provided CSS classes (word-exact match).
|
|
386
|
+
* Accepts variadic class names; all must be present.
|
|
387
|
+
*
|
|
388
|
+
* Example:
|
|
389
|
+
* locate('button').withClass('btn-primary', 'btn-lg')
|
|
390
|
+
*
|
|
391
|
+
* @param {...string} classes
|
|
392
|
+
* @returns {Locator}
|
|
393
|
+
*/
|
|
394
|
+
withClass(...classes) {
|
|
395
|
+
if (!classes.length) return this
|
|
396
|
+
const predicates = classes.map(c => `contains(concat(' ', normalize-space(@class), ' '), ' ${c} ')`)
|
|
397
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), predicates.join(' and '))
|
|
398
|
+
return new Locator({ xpath })
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Find an element with none of the provided CSS classes.
|
|
403
|
+
*
|
|
404
|
+
* Example:
|
|
405
|
+
* locate('tr').withoutClass('deleted')
|
|
406
|
+
*
|
|
407
|
+
* @param {...string} classes
|
|
408
|
+
* @returns {Locator}
|
|
409
|
+
*/
|
|
410
|
+
withoutClass(...classes) {
|
|
411
|
+
if (!classes.length) return this
|
|
412
|
+
const predicates = classes.map(c => `not(contains(concat(' ', normalize-space(@class), ' '), ' ${c} '))`)
|
|
413
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), predicates.join(' and '))
|
|
414
|
+
return new Locator({ xpath })
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Find an element that does NOT contain the provided text.
|
|
419
|
+
* @param {string} text
|
|
420
|
+
* @returns {Locator}
|
|
421
|
+
*/
|
|
422
|
+
withoutText(text) {
|
|
423
|
+
text = xpathLocator.literal(text)
|
|
424
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), `not(contains(., ${text}))`)
|
|
425
|
+
return new Locator({ xpath })
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Find an element that does NOT have any of the provided attribute/value pairs.
|
|
430
|
+
* @param {Object.<string, string>} attributes
|
|
431
|
+
* @returns {Locator}
|
|
432
|
+
*/
|
|
433
|
+
withoutAttr(attributes) {
|
|
434
|
+
const operands = []
|
|
435
|
+
for (const attr of Object.keys(attributes)) {
|
|
436
|
+
operands.push(`not(@${attr} = ${xpathLocator.literal(attributes[attr])})`)
|
|
437
|
+
}
|
|
438
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), operands.join(' and '))
|
|
439
|
+
return new Locator({ xpath })
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Find an element that has no direct child matching the provided locator.
|
|
444
|
+
* @param {CodeceptJS.LocatorOrString} locator
|
|
445
|
+
* @returns {Locator}
|
|
446
|
+
*/
|
|
447
|
+
withoutChild(locator) {
|
|
448
|
+
const xpath = sprintf('%s[not(./child::%s)]', this.toXPath(), convertToSubSelector(locator))
|
|
449
|
+
return new Locator({ xpath })
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Find an element that has no descendant matching the provided locator.
|
|
454
|
+
*
|
|
455
|
+
* Example:
|
|
456
|
+
* locate('button').withoutDescendant('svg')
|
|
457
|
+
*
|
|
458
|
+
* @param {CodeceptJS.LocatorOrString} locator
|
|
459
|
+
* @returns {Locator}
|
|
460
|
+
*/
|
|
461
|
+
withoutDescendant(locator) {
|
|
462
|
+
const xpath = sprintf('%s[not(./descendant::%s)]', this.toXPath(), convertToSubSelector(locator))
|
|
463
|
+
return new Locator({ xpath })
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Append a raw XPath predicate. Escape hatch for expressions not covered by the DSL.
|
|
468
|
+
* Argument is inserted as-is inside `[ ]`; quoting/escaping is the caller's responsibility.
|
|
469
|
+
*
|
|
470
|
+
* Example:
|
|
471
|
+
* locate('input').and('@type="text" or @type="email"')
|
|
472
|
+
*
|
|
473
|
+
* @param {string} xpathExpression
|
|
474
|
+
* @returns {Locator}
|
|
475
|
+
*/
|
|
476
|
+
and(xpathExpression) {
|
|
477
|
+
const xpath = sprintf('%s[%s]', this.toXPath(), xpathExpression)
|
|
478
|
+
return new Locator({ xpath })
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Append a negated raw XPath predicate: `[not(expr)]`.
|
|
483
|
+
*
|
|
484
|
+
* Example:
|
|
485
|
+
* locate('button').andNot('.//svg') // button without a descendant svg
|
|
486
|
+
*
|
|
487
|
+
* @param {string} xpathExpression
|
|
488
|
+
* @returns {Locator}
|
|
489
|
+
*/
|
|
490
|
+
andNot(xpathExpression) {
|
|
491
|
+
const xpath = sprintf('%s[not(%s)]', this.toXPath(), xpathExpression)
|
|
492
|
+
return new Locator({ xpath })
|
|
493
|
+
}
|
|
494
|
+
|
|
384
495
|
/**
|
|
385
496
|
* @param {String} text
|
|
386
497
|
* @returns {Locator}
|
|
498
|
+
* @deprecated Use {@link Locator#withClass} for word-exact class matching, or {@link Locator#withAttrContains} for substring matching.
|
|
387
499
|
*/
|
|
388
500
|
withClassAttr(text) {
|
|
389
501
|
const xpath = sprintf('%s[%s]', this.toXPath(), `contains(@class, '${text}')`)
|
|
@@ -477,15 +589,26 @@ Locator.clickable = {
|
|
|
477
589
|
`.//button[./@name = ${literal}]`,
|
|
478
590
|
`.//*[@aria-label = ${literal}]`,
|
|
479
591
|
`.//*[@title = ${literal}]`,
|
|
480
|
-
`.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id
|
|
592
|
+
`.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
|
|
481
593
|
`.//*[@role='button'][normalize-space(.)=${literal}]`,
|
|
594
|
+
`.//*[@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})]`,
|
|
482
595
|
]),
|
|
483
596
|
|
|
484
597
|
/**
|
|
485
598
|
* @param {string} literal
|
|
486
599
|
* @returns {string}
|
|
487
600
|
*/
|
|
488
|
-
self: literal =>
|
|
601
|
+
self: literal => {
|
|
602
|
+
// Narrowest-match: prefer the deepest descendant whose string-value contains the literal.
|
|
603
|
+
// Falling back to `self` without the `not(descendant...)` guard would match a container
|
|
604
|
+
// whose concatenated text happens to include the literal (e.g. a <ul role="tablist"> whose
|
|
605
|
+
// tab labels all sit in its string-value) and click the container itself.
|
|
606
|
+
const narrowest = `contains(normalize-space(string(.)), ${literal}) and not(.//*[contains(normalize-space(string(.)), ${literal})])`
|
|
607
|
+
return xpathLocator.combine([
|
|
608
|
+
`.//*[${narrowest}]`,
|
|
609
|
+
`./self::*[${narrowest} or contains(normalize-space(@value), ${literal})]`,
|
|
610
|
+
])
|
|
611
|
+
},
|
|
489
612
|
}
|
|
490
613
|
|
|
491
614
|
Locator.field = {
|
|
@@ -509,7 +632,7 @@ Locator.field = {
|
|
|
509
632
|
`.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
|
|
510
633
|
`.//*[@aria-label = ${literal}]`,
|
|
511
634
|
`.//*[@title = ${literal}]`,
|
|
512
|
-
`.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id
|
|
635
|
+
`.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
|
|
513
636
|
]),
|
|
514
637
|
|
|
515
638
|
/**
|
package/lib/mocha/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ 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'
|
|
12
13
|
import { fixErrorStack } from '../utils/typescript.js'
|
|
13
14
|
|
|
@@ -41,7 +42,7 @@ class Cli extends Base {
|
|
|
41
42
|
if (opts.verbose) level = 3
|
|
42
43
|
output.level(level)
|
|
43
44
|
output.print(`CodeceptJS v${codeceptVersion} ${output.standWithUkraine()}`)
|
|
44
|
-
output.print(`Using test root "${
|
|
45
|
+
output.print(`Using test root "${store.codeceptDir}"`)
|
|
45
46
|
|
|
46
47
|
const showSteps = level >= 1
|
|
47
48
|
|
|
@@ -202,7 +203,18 @@ class Cli extends Base {
|
|
|
202
203
|
|
|
203
204
|
// failures
|
|
204
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
|
+
|
|
205
216
|
// append step traces
|
|
217
|
+
const Container = await getContainer()
|
|
206
218
|
this.failures = this.failures.map(test => {
|
|
207
219
|
// we will change the stack trace, so we need to clone the test
|
|
208
220
|
const err = test.err
|
|
@@ -265,7 +277,7 @@ class Cli extends Base {
|
|
|
265
277
|
}
|
|
266
278
|
|
|
267
279
|
try {
|
|
268
|
-
const fileMapping =
|
|
280
|
+
const fileMapping = Container?.tsFileMapping?.()
|
|
269
281
|
if (fileMapping) {
|
|
270
282
|
fixErrorStack(err, fileMapping)
|
|
271
283
|
}
|
package/lib/mocha/factory.js
CHANGED
|
@@ -8,6 +8,7 @@ import output from '../output.js'
|
|
|
8
8
|
import scenarioUiFunction from './ui.js'
|
|
9
9
|
import { initMochaGlobals } from '../globals.js'
|
|
10
10
|
import { fixErrorStack } from '../utils/typescript.js'
|
|
11
|
+
import container from '../container.js'
|
|
11
12
|
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url)
|
|
13
14
|
const __dirname = fsPath.dirname(__filename)
|
|
@@ -16,7 +17,11 @@ let mocha
|
|
|
16
17
|
|
|
17
18
|
class MochaFactory {
|
|
18
19
|
static create(config, opts) {
|
|
19
|
-
|
|
20
|
+
const merged = Object.assign({}, config, opts)
|
|
21
|
+
mocha = new Mocha(merged)
|
|
22
|
+
if (merged.cleanReferencesAfterRun !== true) {
|
|
23
|
+
mocha.cleanReferencesAfterRun(false)
|
|
24
|
+
}
|
|
20
25
|
output.process(opts.child)
|
|
21
26
|
mocha.ui(scenarioUiFunction)
|
|
22
27
|
|
|
@@ -35,7 +40,7 @@ class MochaFactory {
|
|
|
35
40
|
// Handle ECONNREFUSED without dynamic import for now
|
|
36
41
|
err = new Error('Connection refused: ' + err.toString())
|
|
37
42
|
}
|
|
38
|
-
const fileMapping =
|
|
43
|
+
const fileMapping = container?.tsFileMapping?.()
|
|
39
44
|
if (fileMapping) {
|
|
40
45
|
fixErrorStack(err, fileMapping)
|
|
41
46
|
}
|
package/lib/mocha/inject.js
CHANGED
|
@@ -5,7 +5,7 @@ const getInjectedArguments = async (fn, test, suite) => {
|
|
|
5
5
|
const container = containerModule.default || containerModule
|
|
6
6
|
|
|
7
7
|
const testArgs = {}
|
|
8
|
-
const params = getParams(fn) || []
|
|
8
|
+
const params = getParams(fn, { warnOnLegacyFormat: true }) || []
|
|
9
9
|
const objects = container.support()
|
|
10
10
|
|
|
11
11
|
for (const key of params) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isAsyncFunction } from '../utils.js'
|
|
2
|
+
import store from '../store.js'
|
|
2
3
|
|
|
3
4
|
/** @class */
|
|
4
5
|
class ScenarioConfig {
|
|
@@ -40,7 +41,7 @@ class ScenarioConfig {
|
|
|
40
41
|
* @returns {this}
|
|
41
42
|
*/
|
|
42
43
|
retry(retries) {
|
|
43
|
-
if (
|
|
44
|
+
if (store.scenarioOnly) retries = -retries
|
|
44
45
|
this.test.retries(retries)
|
|
45
46
|
return this
|
|
46
47
|
}
|
package/lib/mocha/ui.js
CHANGED
|
@@ -9,13 +9,12 @@ import { HookConfig, AfterSuiteHook, AfterHook, BeforeSuiteHook, BeforeHook } fr
|
|
|
9
9
|
import { initMochaGlobals } from '../globals.js'
|
|
10
10
|
import common from 'mocha/lib/interfaces/common.js'
|
|
11
11
|
import container from '../container.js'
|
|
12
|
+
import store from '../store.js'
|
|
12
13
|
|
|
13
14
|
const setContextTranslation = context => {
|
|
14
|
-
|
|
15
|
-
const containerToUse = global.container || container
|
|
16
|
-
if (!containerToUse) return
|
|
15
|
+
if (!container) return
|
|
17
16
|
|
|
18
|
-
const translation =
|
|
17
|
+
const translation = container.translation?.() || container.translation
|
|
19
18
|
const contexts = translation?.value?.('contexts')
|
|
20
19
|
|
|
21
20
|
if (contexts) {
|
|
@@ -119,7 +118,7 @@ export default function (suite) {
|
|
|
119
118
|
context.Feature.only = function (title, opts) {
|
|
120
119
|
const reString = `^${escapeRe(`${title}:`)}`
|
|
121
120
|
mocha.grep(new RegExp(reString))
|
|
122
|
-
|
|
121
|
+
store.featureOnly = true
|
|
123
122
|
return context.Feature(title, opts)
|
|
124
123
|
}
|
|
125
124
|
|
|
@@ -171,7 +170,7 @@ export default function (suite) {
|
|
|
171
170
|
context.Scenario.only = function (title, opts, fn) {
|
|
172
171
|
const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`
|
|
173
172
|
mocha.grep(new RegExp(reString))
|
|
174
|
-
|
|
173
|
+
store.scenarioOnly = true
|
|
175
174
|
return addScenario(title, opts, fn)
|
|
176
175
|
}
|
|
177
176
|
|
package/lib/parser.js
CHANGED
|
@@ -14,11 +14,11 @@ export const getParamsToString = function (fn) {
|
|
|
14
14
|
return getParams(newFn).join(', ')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function getParams(fn) {
|
|
17
|
+
function getParams(fn, { warnOnLegacyFormat = false } = {}) {
|
|
18
18
|
if (fn.isSinonProxy) return []
|
|
19
19
|
try {
|
|
20
20
|
const reflected = parser.parse(fn)
|
|
21
|
-
if (reflected.args.length > 1 || reflected.args[0] === 'I') {
|
|
21
|
+
if (warnOnLegacyFormat && (reflected.args.length > 1 || reflected.args[0] === 'I')) {
|
|
22
22
|
output.error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up')
|
|
23
23
|
}
|
|
24
24
|
if (reflected.destructuredArgs.length > 0) reflected.args = [...reflected.destructuredArgs]
|
package/lib/pause.js
CHANGED
|
@@ -18,6 +18,8 @@ let nextStep
|
|
|
18
18
|
let finish
|
|
19
19
|
let next
|
|
20
20
|
let registeredVariables = {}
|
|
21
|
+
let externalHandler = null
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* Pauses test execution and starts interactive shell
|
|
23
25
|
* @param {Object<string, *>} [passedObject]
|
|
@@ -37,10 +39,10 @@ const pause = function (passedObject = {}) {
|
|
|
37
39
|
})
|
|
38
40
|
|
|
39
41
|
event.dispatcher.on(event.test.finished, () => {
|
|
40
|
-
finish()
|
|
42
|
+
if (typeof finish === 'function') finish()
|
|
41
43
|
recorder.session.restore('pause')
|
|
42
|
-
rl.close()
|
|
43
|
-
history.save()
|
|
44
|
+
if (rl) rl.close()
|
|
45
|
+
if (!externalHandler) history.save()
|
|
44
46
|
})
|
|
45
47
|
|
|
46
48
|
recorder.add('Start new session', () => pauseSession(passedObject))
|
|
@@ -49,6 +51,15 @@ const pause = function (passedObject = {}) {
|
|
|
49
51
|
function pauseSession(passedObject = {}) {
|
|
50
52
|
registeredVariables = passedObject
|
|
51
53
|
recorder.session.start('pause')
|
|
54
|
+
|
|
55
|
+
if (externalHandler) {
|
|
56
|
+
store.onPause = true
|
|
57
|
+
return externalHandler({ registeredVariables }).then(() => {
|
|
58
|
+
store.onPause = false
|
|
59
|
+
recorder.session.restore('pause')
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
if (!next) {
|
|
53
64
|
let vars = Object.keys(registeredVariables).join(', ')
|
|
54
65
|
if (vars) vars = `(vars: ${vars})`
|
|
@@ -234,5 +245,28 @@ function registerVariable(name, value) {
|
|
|
234
245
|
registeredVariables[name] = value
|
|
235
246
|
}
|
|
236
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Hook for external pause drivers (e.g. the MCP server). When set, pauseSession
|
|
250
|
+
* delegates to the handler instead of opening a readline REPL. The handler
|
|
251
|
+
* receives `{ registeredVariables }` and returns a Promise that resolves when
|
|
252
|
+
* the driver decides to continue (resume) or step.
|
|
253
|
+
*
|
|
254
|
+
* The driver controls step-vs-resume by mutating `next` via setNextStep before
|
|
255
|
+
* resolving its Promise.
|
|
256
|
+
*/
|
|
257
|
+
function setPauseHandler(handler) {
|
|
258
|
+
externalHandler = handler
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Trigger a one-shot pause from outside the test (e.g. the MCP server,
|
|
263
|
+
* pausing the test at a specific step index without modifying the test).
|
|
264
|
+
* Schedules pauseSession through the recorder so it slots between steps.
|
|
265
|
+
*/
|
|
266
|
+
function pauseNow(passedObject = {}) {
|
|
267
|
+
if (store.dryRun) return
|
|
268
|
+
recorder.add('Triggered pause', () => pauseSession(passedObject))
|
|
269
|
+
}
|
|
270
|
+
|
|
237
271
|
export default pause
|
|
238
|
-
export { registerVariable }
|
|
272
|
+
export { registerVariable, setPauseHandler, pauseNow }
|