codeceptjs 4.0.0-rc.9 → 4.0.0
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 +9 -10
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +751 -172
- 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 +743 -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 +198 -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 +7 -7
- 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 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -2
- package/lib/command/run-workers.js +1 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +11 -15
- package/lib/config.js +77 -4
- package/lib/container.js +97 -15
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +194 -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/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +63 -70
- package/lib/helper/Puppeteer.js +20 -109
- package/lib/helper/WebDriver.js +13 -30
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +10 -0
- package/lib/helper/extras/elementSelection.js +10 -3
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/history.js +3 -2
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +126 -16
- package/lib/mocha/cli.js +4 -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 +96 -103
- package/lib/plugin/analyze.js +9 -9
- 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 +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 +15 -13
- 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 +7 -0
- 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/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +29 -3
- package/lib/workers.js +14 -22
- package/package.json +17 -14
- package/typings/index.d.ts +0 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -16
- package/docs/webapi/attachFile.mustache +0 -24
- 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 -14
- 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 -12
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -16
- 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 -21
- 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 -16
- 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 -12
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -17
- 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 -26
- 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/plugin/stepByStepReport.js +0 -431
- package/lib/plugin/subtitles.js +0 -89
package/lib/plugin/heal.js
CHANGED
|
@@ -10,21 +10,30 @@ import output from '../output.js'
|
|
|
10
10
|
import healModule from '../heal.js'
|
|
11
11
|
const heal = healModule.default || healModule
|
|
12
12
|
import store from '../store.js'
|
|
13
|
+
import {
|
|
14
|
+
parsePluginArgs,
|
|
15
|
+
resolveTrigger,
|
|
16
|
+
matchStepFile,
|
|
17
|
+
matchUrl,
|
|
18
|
+
getBrowserHelper,
|
|
19
|
+
} from '../utils/pluginParser.js'
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
const defaultConfig = {
|
|
23
|
+
on: 'fail',
|
|
16
24
|
healLimit: 2,
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
/**
|
|
20
28
|
* Self-healing tests with AI.
|
|
21
29
|
*
|
|
22
|
-
* Read more about
|
|
30
|
+
* Read more about healing in [Self-Healing Tests](https://codecept.io/heal/)
|
|
23
31
|
*
|
|
24
32
|
* ```js
|
|
25
33
|
* plugins: {
|
|
26
34
|
* heal: {
|
|
27
35
|
* enabled: true,
|
|
36
|
+
* on: 'fail',
|
|
28
37
|
* }
|
|
29
38
|
* }
|
|
30
39
|
* ```
|
|
@@ -32,7 +41,17 @@ const defaultConfig = {
|
|
|
32
41
|
* More config options are available:
|
|
33
42
|
*
|
|
34
43
|
* * `healLimit` - how many steps can be healed in a single test (default: 2)
|
|
44
|
+
* * `on` - trigger mode. `fail` (default), `file` (filter to a path), `url` (filter to a URL pattern).
|
|
35
45
|
*
|
|
46
|
+
* #### `on=` modes
|
|
47
|
+
*
|
|
48
|
+
* Heal always runs on step failures; `on=` narrows when it engages.
|
|
49
|
+
*
|
|
50
|
+
* * **fail** — heal any failing step (default)
|
|
51
|
+
* * **file** — heal only failures in `path=...[;line=...]`
|
|
52
|
+
* * **url** — heal only failures when the current URL matches `pattern=...`
|
|
53
|
+
*
|
|
54
|
+
* `on=step` and `on=test` are not supported and are rejected with an error.
|
|
36
55
|
*/
|
|
37
56
|
export default function (config = {}) {
|
|
38
57
|
if (store.debugMode && !process.env.DEBUG) {
|
|
@@ -42,6 +61,13 @@ export default function (config = {}) {
|
|
|
42
61
|
return
|
|
43
62
|
}
|
|
44
63
|
|
|
64
|
+
const cliArgs = parsePluginArgs(config._args)
|
|
65
|
+
const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, {
|
|
66
|
+
name: 'heal',
|
|
67
|
+
validModes: ['fail', 'file', 'url'],
|
|
68
|
+
})
|
|
69
|
+
if (!trigger) return
|
|
70
|
+
|
|
45
71
|
let currentTest = null
|
|
46
72
|
let currentStep = null
|
|
47
73
|
let healedSteps = 0
|
|
@@ -54,6 +80,7 @@ export default function (config = {}) {
|
|
|
54
80
|
event.dispatcher.on(event.test.before, test => {
|
|
55
81
|
currentTest = test
|
|
56
82
|
healedSteps = 0
|
|
83
|
+
healTries = 0
|
|
57
84
|
caughtError = null
|
|
58
85
|
})
|
|
59
86
|
|
|
@@ -65,21 +92,38 @@ export default function (config = {}) {
|
|
|
65
92
|
|
|
66
93
|
if (!heal.hasCorrespondingRecipes(step)) return
|
|
67
94
|
|
|
95
|
+
if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
|
|
96
|
+
|
|
68
97
|
recorder.catchWithoutStop(async err => {
|
|
98
|
+
if (healTries >= config.healLimit) throw err
|
|
69
99
|
isHealing = true
|
|
100
|
+
healTries++
|
|
70
101
|
if (caughtError === err) throw err // avoid double handling
|
|
71
102
|
caughtError = err
|
|
72
103
|
|
|
73
104
|
const test = currentTest
|
|
74
105
|
|
|
106
|
+
if (trigger.on === 'url') {
|
|
107
|
+
try {
|
|
108
|
+
const helper = getBrowserHelper()
|
|
109
|
+
const url = helper && helper.grabCurrentUrl ? await helper.grabCurrentUrl() : null
|
|
110
|
+
if (!matchUrl(url, trigger.pattern)) {
|
|
111
|
+
isHealing = false
|
|
112
|
+
throw err
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
if (e === err) throw e
|
|
116
|
+
isHealing = false
|
|
117
|
+
throw err
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
75
121
|
recorder.session.start('heal')
|
|
76
122
|
|
|
77
123
|
debug('Self-healing started', step.toCode())
|
|
78
124
|
|
|
79
125
|
await heal.healStep(step, err, { test })
|
|
80
126
|
|
|
81
|
-
healTries++
|
|
82
|
-
|
|
83
127
|
recorder.add('close healing session', () => {
|
|
84
128
|
recorder.reset()
|
|
85
129
|
recorder.session.restore('heal')
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import { mkdirp } from 'mkdirp'
|
|
5
|
+
import { DOMImplementation, XMLSerializer } from '@xmldom/xmldom'
|
|
6
|
+
|
|
7
|
+
import event from '../event.js'
|
|
8
|
+
import store from '../store.js'
|
|
9
|
+
import output from '../output.js'
|
|
10
|
+
|
|
11
|
+
const defaultConfig = {
|
|
12
|
+
outputName: 'report.xml',
|
|
13
|
+
output: null,
|
|
14
|
+
testGroupName: 'CodeceptJS',
|
|
15
|
+
attachSteps: true,
|
|
16
|
+
attachMeta: true,
|
|
17
|
+
stepsInFailure: true,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const INVALID_XML_CHARS = new RegExp('[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\uFFFE\\uFFFF]', 'g')
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* Generates a JUnit-compatible XML report after a test run.
|
|
25
|
+
*
|
|
26
|
+
* Unlike Mocha's `mocha-junit-reporter`, this plugin understands CodeceptJS steps and substeps.
|
|
27
|
+
* For every `<testcase>` it includes:
|
|
28
|
+
*
|
|
29
|
+
* * `<properties>` — the test's meta information: every `meta` key from `Scenario('...', { meta })`, plus its `tags` and `retries`
|
|
30
|
+
* * `<system-out>` — an indented step/substep log (substeps are nested under their meta step); only failed steps are marked
|
|
31
|
+
* * `<failure>` — for failed tests: the error message, type, stack trace and (optionally) the step trace
|
|
32
|
+
*
|
|
33
|
+
* The produced file is consumable by Jenkins, GitLab CI, CircleCI, GitHub Actions test reporters, etc.
|
|
34
|
+
*
|
|
35
|
+
* #### Configuration
|
|
36
|
+
*
|
|
37
|
+
* ```js
|
|
38
|
+
* "plugins": {
|
|
39
|
+
* "junitReporter": {
|
|
40
|
+
* "enabled": true
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Possible config options:
|
|
46
|
+
*
|
|
47
|
+
* * `outputName`: file name for the report. Default: `report.xml`.
|
|
48
|
+
* * `output`: directory where the report is stored, relative to the project root. Default: the `output` directory.
|
|
49
|
+
* * `testGroupName`: value of the `name` attribute on the root `<testsuites>` element. Default: `CodeceptJS`.
|
|
50
|
+
* * `attachMeta`: add the test's meta information (`meta` keys, `tags`, `retries`) as `<properties>`. Default: true.
|
|
51
|
+
* * `attachSteps`: add the step/substep log as `<system-out>`. Default: true.
|
|
52
|
+
* * `stepsInFailure`: append the step trace to the `<failure>` body. Default: true.
|
|
53
|
+
*
|
|
54
|
+
* CLI examples:
|
|
55
|
+
*
|
|
56
|
+
* ```
|
|
57
|
+
* npx codeceptjs run -p junitReporter
|
|
58
|
+
* npx codeceptjs run -p junitReporter:outputName=junit.xml
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* > ℹ When running with `run-workers`, steps are serialized between processes and substep nesting is flattened.
|
|
62
|
+
*
|
|
63
|
+
* @param {*} config
|
|
64
|
+
*/
|
|
65
|
+
export default function (config = {}) {
|
|
66
|
+
config = Object.assign({}, defaultConfig, config)
|
|
67
|
+
|
|
68
|
+
let written = false
|
|
69
|
+
|
|
70
|
+
const writeReport = result => {
|
|
71
|
+
if (written) return
|
|
72
|
+
if (!result || !Array.isArray(result.tests)) return
|
|
73
|
+
written = true
|
|
74
|
+
|
|
75
|
+
const dir = config.output ? path.resolve(store.codeceptDir || process.cwd(), config.output) : store.outputDir || process.cwd()
|
|
76
|
+
mkdirp.sync(dir)
|
|
77
|
+
const file = path.join(dir, config.outputName)
|
|
78
|
+
|
|
79
|
+
fs.writeFileSync(file, buildXml(result, config))
|
|
80
|
+
output.plugin('junitReporter', `JUnit report saved to ${file}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
event.dispatcher.on(event.all.result, writeReport)
|
|
84
|
+
event.dispatcher.on(event.workers.result, writeReport)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildXml(result, config) {
|
|
88
|
+
const doc = new DOMImplementation().createDocument(null, null, null)
|
|
89
|
+
const suites = groupBySuite(result.tests)
|
|
90
|
+
|
|
91
|
+
const root = doc.createElement('testsuites')
|
|
92
|
+
setAttr(root, 'name', config.testGroupName)
|
|
93
|
+
setAttr(root, 'tests', result.tests.length)
|
|
94
|
+
setAttr(root, 'failures', countState(result.tests, 'failed'))
|
|
95
|
+
setAttr(root, 'skipped', countSkipped(result.tests))
|
|
96
|
+
setAttr(root, 'errors', 0)
|
|
97
|
+
setAttr(root, 'time', toSeconds(sumDuration(result.tests)))
|
|
98
|
+
setAttr(root, 'timestamp', toIso(result.stats && result.stats.start))
|
|
99
|
+
doc.appendChild(root)
|
|
100
|
+
|
|
101
|
+
suites.forEach((tests, index) => {
|
|
102
|
+
const suite = tests[0] && tests[0].parent
|
|
103
|
+
const suiteName = (suite && suite.title) || 'Tests'
|
|
104
|
+
const suiteFile = (suite && suite.file) || (tests[0] && tests[0].file) || ''
|
|
105
|
+
|
|
106
|
+
const suiteEl = doc.createElement('testsuite')
|
|
107
|
+
setAttr(suiteEl, 'name', suiteName)
|
|
108
|
+
setAttr(suiteEl, 'id', index)
|
|
109
|
+
setAttr(suiteEl, 'tests', tests.length)
|
|
110
|
+
setAttr(suiteEl, 'failures', countState(tests, 'failed'))
|
|
111
|
+
setAttr(suiteEl, 'skipped', countSkipped(tests))
|
|
112
|
+
setAttr(suiteEl, 'errors', 0)
|
|
113
|
+
setAttr(suiteEl, 'time', toSeconds(sumDuration(tests)))
|
|
114
|
+
setAttr(suiteEl, 'timestamp', toIso(suite && suite.startedAt))
|
|
115
|
+
setAttr(suiteEl, 'hostname', os.hostname())
|
|
116
|
+
if (suiteFile) setAttr(suiteEl, 'file', suiteFile)
|
|
117
|
+
root.appendChild(suiteEl)
|
|
118
|
+
|
|
119
|
+
for (const test of tests) {
|
|
120
|
+
suiteEl.appendChild(buildTestCase(doc, test, suiteName, config))
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(doc) + '\n'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildTestCase(doc, test, suiteName, config) {
|
|
128
|
+
const testEl = doc.createElement('testcase')
|
|
129
|
+
setAttr(testEl, 'name', test.title || '(no title)')
|
|
130
|
+
setAttr(testEl, 'classname', suiteName)
|
|
131
|
+
setAttr(testEl, 'time', toSeconds(test.duration || 0))
|
|
132
|
+
const file = test.file || (test.parent && test.parent.file)
|
|
133
|
+
if (file) setAttr(testEl, 'file', file)
|
|
134
|
+
|
|
135
|
+
if (config.attachMeta) {
|
|
136
|
+
const properties = metaProperties(test)
|
|
137
|
+
if (properties.length) {
|
|
138
|
+
const propertiesEl = doc.createElement('properties')
|
|
139
|
+
for (const [name, value] of properties) {
|
|
140
|
+
const prop = doc.createElement('property')
|
|
141
|
+
setAttr(prop, 'name', name)
|
|
142
|
+
setAttr(prop, 'value', value)
|
|
143
|
+
propertiesEl.appendChild(prop)
|
|
144
|
+
}
|
|
145
|
+
testEl.appendChild(propertiesEl)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const flat = flattenSteps(Array.isArray(test.steps) ? test.steps : [])
|
|
150
|
+
|
|
151
|
+
if (test.state === 'skipped' || test.state === 'pending') {
|
|
152
|
+
const skipped = doc.createElement('skipped')
|
|
153
|
+
const reason = skipReason(test)
|
|
154
|
+
if (reason) setAttr(skipped, 'message', reason)
|
|
155
|
+
testEl.appendChild(skipped)
|
|
156
|
+
} else if (test.state === 'failed') {
|
|
157
|
+
const err = test.err || {}
|
|
158
|
+
const failure = doc.createElement('failure')
|
|
159
|
+
setAttr(failure, 'message', err.message || 'Test failed')
|
|
160
|
+
setAttr(failure, 'type', err.name || 'Error')
|
|
161
|
+
let body = err.stack || err.message || 'Test failed'
|
|
162
|
+
if (config.stepsInFailure && flat.length) {
|
|
163
|
+
body += '\n\nSteps:\n' + flat.map(stepLogLine).join('\n')
|
|
164
|
+
}
|
|
165
|
+
failure.appendChild(doc.createTextNode(cleanText(body)))
|
|
166
|
+
testEl.appendChild(failure)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (config.attachSteps && flat.length) {
|
|
170
|
+
const out = doc.createElement('system-out')
|
|
171
|
+
out.appendChild(doc.createTextNode(cleanText(flat.map(stepLogLine).join('\n'))))
|
|
172
|
+
testEl.appendChild(out)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return testEl
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function metaProperties(test) {
|
|
179
|
+
const props = []
|
|
180
|
+
const meta = test.meta || {}
|
|
181
|
+
for (const key of Object.keys(meta)) {
|
|
182
|
+
if (meta[key] === undefined || meta[key] === null) continue
|
|
183
|
+
props.push([key, stringifyMeta(meta[key])])
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(test.tags) && test.tags.length) {
|
|
186
|
+
props.push(['tags', test.tags.join(' ')])
|
|
187
|
+
}
|
|
188
|
+
if (test.retries > 0 || test.retryNum > 0) {
|
|
189
|
+
props.push(['retries', String(test.retryNum || test.retries)])
|
|
190
|
+
}
|
|
191
|
+
return props
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function stringifyMeta(value) {
|
|
195
|
+
if (typeof value === 'string') return value
|
|
196
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
|
197
|
+
try {
|
|
198
|
+
return JSON.stringify(value)
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return String(value)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function flattenSteps(steps) {
|
|
205
|
+
const out = []
|
|
206
|
+
let prevChain = []
|
|
207
|
+
|
|
208
|
+
for (const step of steps) {
|
|
209
|
+
const chain = metaChain(step)
|
|
210
|
+
|
|
211
|
+
let common = 0
|
|
212
|
+
while (common < chain.length && common < prevChain.length && chain[common].key === prevChain[common].key) common++
|
|
213
|
+
|
|
214
|
+
for (let d = common; d < chain.length; d++) {
|
|
215
|
+
out.push({ depth: d, step: chain[d].step })
|
|
216
|
+
}
|
|
217
|
+
out.push({ depth: chain.length, step })
|
|
218
|
+
prevChain = chain
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return out
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function metaChain(step) {
|
|
225
|
+
const chain = []
|
|
226
|
+
let meta = step && step.metaStep
|
|
227
|
+
while (meta) {
|
|
228
|
+
chain.unshift({ step: meta, key: meta })
|
|
229
|
+
meta = meta.metaStep
|
|
230
|
+
}
|
|
231
|
+
if (!chain.length && step && step.parent && step.parent.title) {
|
|
232
|
+
chain.push({ step: { title: step.parent.title, status: step.status }, key: `meta:${step.parent.title}` })
|
|
233
|
+
}
|
|
234
|
+
return chain
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function stepLogLine(entry) {
|
|
238
|
+
const indent = ' '.repeat(entry.depth)
|
|
239
|
+
const mark = entry.step && entry.step.status === 'failed' ? '[FAILED] ' : ''
|
|
240
|
+
return `${indent}${mark}${stepText(entry.step)} (${stepDuration(entry.step)}ms)`
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function stepText(step) {
|
|
244
|
+
if (step && typeof step.toString === 'function' && step.toString !== Object.prototype.toString) return step.toString()
|
|
245
|
+
return (step && step.title) || 'step'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function stepDuration(step) {
|
|
249
|
+
if (!step) return 0
|
|
250
|
+
if (typeof step.duration === 'number' && step.duration >= 0) return step.duration
|
|
251
|
+
if (step.startTime && step.endTime) return Math.max(0, step.endTime - step.startTime)
|
|
252
|
+
return 0
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function groupBySuite(tests) {
|
|
256
|
+
const groups = []
|
|
257
|
+
const byKey = new Map()
|
|
258
|
+
for (const test of tests) {
|
|
259
|
+
const key = test.parent || test
|
|
260
|
+
if (!byKey.has(key)) {
|
|
261
|
+
const list = []
|
|
262
|
+
byKey.set(key, list)
|
|
263
|
+
groups.push(list)
|
|
264
|
+
}
|
|
265
|
+
byKey.get(key).push(test)
|
|
266
|
+
}
|
|
267
|
+
return groups
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function skipReason(test) {
|
|
271
|
+
if (test.opts && test.opts.skipInfo && test.opts.skipInfo.message) return test.opts.skipInfo.message
|
|
272
|
+
if (test.meta && test.meta.skipReason) return test.meta.skipReason
|
|
273
|
+
return ''
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function countState(tests, state) {
|
|
277
|
+
return tests.filter(t => t.state === state).length
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function countSkipped(tests) {
|
|
281
|
+
return tests.filter(t => t.state === 'skipped' || t.state === 'pending').length
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function sumDuration(tests) {
|
|
285
|
+
return tests.reduce((sum, t) => sum + (t.duration || 0), 0)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function toSeconds(ms) {
|
|
289
|
+
return (Math.max(0, ms) / 1000).toFixed(3)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function toIso(value) {
|
|
293
|
+
const date = value ? new Date(value) : new Date()
|
|
294
|
+
return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function cleanText(text) {
|
|
298
|
+
return String(text == null ? '' : text).replace(INVALID_XML_CHARS, '')
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function setAttr(el, name, value) {
|
|
302
|
+
el.setAttribute(name, cleanText(value))
|
|
303
|
+
}
|
package/lib/plugin/pageInfo.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import Container from '../container.js'
|
|
4
|
-
const supportedHelpers = Container.STANDARD_ACTING_HELPERS
|
|
5
4
|
import recorder from '../recorder.js'
|
|
6
5
|
import event from '../event.js'
|
|
7
6
|
import { scanForErrorMessages } from '../html.js'
|
|
7
|
+
import { captureSnapshot, pickActingHelper } from '../utils/trace.js'
|
|
8
8
|
import { output } from '../index.js'
|
|
9
|
+
import store from '../store.js'
|
|
9
10
|
import { humanizeString, ucfirst } from '../utils.js'
|
|
10
11
|
import { testToFileName } from '../mocha/test.js'
|
|
12
|
+
|
|
11
13
|
const defaultConfig = {
|
|
12
14
|
errorClasses: ['error', 'warning', 'alert', 'danger'],
|
|
13
15
|
browserLogs: ['error'],
|
|
@@ -36,67 +38,66 @@ const defaultConfig = {
|
|
|
36
38
|
*
|
|
37
39
|
*/
|
|
38
40
|
export default function (config = {}) {
|
|
39
|
-
const helpers = Container.helpers()
|
|
40
|
-
let helper
|
|
41
|
-
|
|
42
41
|
config = Object.assign(defaultConfig, config)
|
|
43
42
|
|
|
44
|
-
for (const helperName of supportedHelpers) {
|
|
45
|
-
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
46
|
-
helper = helpers[helperName]
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!helper) return // no helpers for screenshot
|
|
51
|
-
|
|
52
43
|
event.dispatcher.on(event.test.failed, test => {
|
|
44
|
+
const helper = pickActingHelper(Container.helpers())
|
|
45
|
+
if (!helper) return
|
|
46
|
+
|
|
53
47
|
const pageState = {}
|
|
54
48
|
|
|
55
|
-
recorder.add('
|
|
49
|
+
recorder.add('pageInfo capture', async () => {
|
|
56
50
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
51
|
+
const prefix = `${testToFileName(test)}.pageInfo`
|
|
52
|
+
const captured = await captureSnapshot(helper, {
|
|
53
|
+
dir: store.outputDir,
|
|
54
|
+
prefix,
|
|
55
|
+
captureScreenshot: false,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (captured.url) pageState.url = captured.url
|
|
59
|
+
|
|
60
|
+
if (captured.html) {
|
|
61
|
+
const htmlPath = path.join(store.outputDir, captured.html)
|
|
62
|
+
pageState.htmlSnapshot = htmlPath
|
|
63
|
+
const htmlForScan = captured.htmlRaw || (() => {
|
|
64
|
+
try { return fs.readFileSync(htmlPath, 'utf8') } catch { return '' }
|
|
65
|
+
})()
|
|
66
|
+
if (htmlForScan) {
|
|
67
|
+
try {
|
|
68
|
+
const errors = scanForErrorMessages(htmlForScan, config.errorClasses)
|
|
69
|
+
if (errors.length) {
|
|
70
|
+
output.debug('Detected errors in HTML code')
|
|
71
|
+
errors.forEach(error => output.debug(error))
|
|
72
|
+
pageState.htmlErrors = errors
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
74
76
|
}
|
|
75
|
-
} catch (err) {
|
|
76
|
-
// not really needed
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (!logs) return
|
|
78
|
+
if (captured.aria) {
|
|
79
|
+
pageState.ariaSnapshot = path.join(store.outputDir, captured.aria)
|
|
80
|
+
}
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
if (captured.console) {
|
|
83
|
+
const consolePath = path.join(store.outputDir, captured.console)
|
|
84
|
+
pageState.consoleSnapshot = consolePath
|
|
85
|
+
try {
|
|
86
|
+
const logs = JSON.parse(fs.readFileSync(consolePath, 'utf8'))
|
|
87
|
+
pageState.browserErrors = getBrowserErrors(logs, config.browserLogs)
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
}, true)
|
|
91
92
|
|
|
92
93
|
recorder.add('Save page info', () => {
|
|
93
94
|
test.addNote('pageInfo', pageStateToMarkdown(pageState))
|
|
94
95
|
|
|
95
|
-
const pageStateFileName = path.join(
|
|
96
|
+
const pageStateFileName = path.join(store.outputDir, `${testToFileName(test)}.pageInfo.md`)
|
|
96
97
|
fs.writeFileSync(pageStateFileName, pageStateToMarkdown(pageState))
|
|
97
98
|
test.artifacts.pageInfo = pageStateFileName
|
|
98
99
|
return pageState
|
|
99
|
-
})
|
|
100
|
+
}, true)
|
|
100
101
|
})
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -126,15 +127,16 @@ function pageStateToMarkdown(pageState) {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
function getBrowserErrors(logs, type = ['error']) {
|
|
129
|
-
// Playwright
|
|
130
|
-
|
|
130
|
+
// Accepts Playwright ConsoleMessage objects, normalized {type, text}, or strings
|
|
131
|
+
return logs
|
|
131
132
|
.map(log => {
|
|
132
133
|
if (typeof log === 'string') return log
|
|
133
|
-
if (!log
|
|
134
|
-
|
|
134
|
+
if (!log) return null
|
|
135
|
+
const t = typeof log.type === 'function' ? log.type() : log.type
|
|
136
|
+
const text = typeof log.text === 'function' ? log.text() : log.text
|
|
137
|
+
if (!t) return null
|
|
138
|
+
return { type: t, text }
|
|
135
139
|
})
|
|
136
140
|
.filter(l => l && (typeof l === 'string' || type.includes(l.type)))
|
|
137
141
|
.map(l => (typeof l === 'string' ? l : l.text))
|
|
138
|
-
|
|
139
|
-
return errors
|
|
140
142
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import event from '../event.js'
|
|
2
|
+
import pause from '../pause.js'
|
|
3
|
+
import recorder from '../recorder.js'
|
|
4
|
+
import output from '../output.js'
|
|
5
|
+
import {
|
|
6
|
+
parsePluginArgs,
|
|
7
|
+
resolveTrigger,
|
|
8
|
+
matchStepFile,
|
|
9
|
+
matchUrl,
|
|
10
|
+
getBrowserHelper,
|
|
11
|
+
} from '../utils/pluginParser.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pauses test execution interactively. Replaces the legacy `pauseOnFail`
|
|
15
|
+
* plugin. The default `on=fail` matches the old `pauseOnFail` behavior.
|
|
16
|
+
*
|
|
17
|
+
* #### Configuration
|
|
18
|
+
*
|
|
19
|
+
* ```js
|
|
20
|
+
* plugins: {
|
|
21
|
+
* pause: {
|
|
22
|
+
* enabled: false,
|
|
23
|
+
* on: 'fail',
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* #### `on=` modes
|
|
29
|
+
*
|
|
30
|
+
* * **fail** — pause when a step fails (default)
|
|
31
|
+
* * **test** — pause after each test
|
|
32
|
+
* * **step** — pause before the first step (interactive walk-through)
|
|
33
|
+
* * **file** — pause when execution reaches `path=...[;line=...]`
|
|
34
|
+
* * **url** — pause when the browser URL matches `pattern=...`
|
|
35
|
+
*
|
|
36
|
+
* CLI examples:
|
|
37
|
+
*
|
|
38
|
+
* ```
|
|
39
|
+
* npx codeceptjs run -p pause
|
|
40
|
+
* npx codeceptjs run -p pause:on=step
|
|
41
|
+
* npx codeceptjs run -p pause:on=file:path=tests/login_test.js;line=43
|
|
42
|
+
* npx codeceptjs run -p pause:on=url:pattern=/users/*
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export default function (config = {}) {
|
|
46
|
+
const cliArgs = parsePluginArgs(config._args)
|
|
47
|
+
const trigger = resolveTrigger(cliArgs, config, { on: 'fail' }, { name: 'pause' })
|
|
48
|
+
if (!trigger) return
|
|
49
|
+
|
|
50
|
+
switch (trigger.on) {
|
|
51
|
+
case 'fail':
|
|
52
|
+
return initFailMode()
|
|
53
|
+
case 'test':
|
|
54
|
+
return initTestMode()
|
|
55
|
+
case 'step':
|
|
56
|
+
return initStepMode()
|
|
57
|
+
case 'file':
|
|
58
|
+
return initFileMode(trigger.path, trigger.line)
|
|
59
|
+
case 'url':
|
|
60
|
+
return initUrlMode(trigger.pattern)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function initFailMode() {
|
|
65
|
+
let failed = false
|
|
66
|
+
|
|
67
|
+
event.dispatcher.on(event.test.started, () => {
|
|
68
|
+
failed = false
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
event.dispatcher.on(event.step.failed, () => {
|
|
72
|
+
failed = true
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
event.dispatcher.on(event.test.after, () => {
|
|
76
|
+
if (failed) pause()
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function initTestMode() {
|
|
81
|
+
event.dispatcher.on(event.test.after, () => pause())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function initStepMode() {
|
|
85
|
+
let activated = false
|
|
86
|
+
|
|
87
|
+
event.dispatcher.on(event.test.before, () => {
|
|
88
|
+
if (activated) return
|
|
89
|
+
activated = true
|
|
90
|
+
recorder.add('pause:step', () => pause())
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function initFileMode(targetPath, targetLine) {
|
|
95
|
+
let paused = false
|
|
96
|
+
|
|
97
|
+
event.dispatcher.on(event.step.before, step => {
|
|
98
|
+
if (paused) return
|
|
99
|
+
if (!matchStepFile(step, targetPath, targetLine)) return
|
|
100
|
+
paused = true
|
|
101
|
+
recorder.add('pause:file', () => pause())
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function initUrlMode(pattern) {
|
|
106
|
+
const helper = getBrowserHelper()
|
|
107
|
+
|
|
108
|
+
if (!helper) {
|
|
109
|
+
output.error('pause:on=url requires a browser helper (Playwright, WebDriver, Puppeteer, Appium)')
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let paused = false
|
|
114
|
+
|
|
115
|
+
event.dispatcher.on(event.step.after, () => {
|
|
116
|
+
if (paused) return
|
|
117
|
+
|
|
118
|
+
recorder.add('pause:url check', async () => {
|
|
119
|
+
if (paused) return
|
|
120
|
+
try {
|
|
121
|
+
const currentUrl = await helper.grabCurrentUrl()
|
|
122
|
+
if (matchUrl(currentUrl, pattern)) {
|
|
123
|
+
paused = true
|
|
124
|
+
return pause()
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
// page may not be loaded yet
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
}
|