codeceptjs 4.0.2-beta.9 → 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 +84 -41
- 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/plugin/auth.js
CHANGED
|
@@ -321,7 +321,7 @@ export default function (config) {
|
|
|
321
321
|
}
|
|
322
322
|
if (config.saveToFile) {
|
|
323
323
|
output.debug(`Saved user session into file for ${name}`)
|
|
324
|
-
fs.writeFileSync(path.join(
|
|
324
|
+
fs.writeFileSync(path.join(store.outputDir, `${name}_session.json`), JSON.stringify(cookies))
|
|
325
325
|
}
|
|
326
326
|
store[`${name}_session`] = cookies
|
|
327
327
|
}
|
|
@@ -350,7 +350,8 @@ export default function (config) {
|
|
|
350
350
|
recorder.session.restore('auto login')
|
|
351
351
|
recorder.session.restore('check login')
|
|
352
352
|
section.end()
|
|
353
|
-
recorder.throw
|
|
353
|
+
// Use regular throw instead of recorder.throw to avoid promise chaining cycle
|
|
354
|
+
throw err
|
|
354
355
|
})
|
|
355
356
|
})
|
|
356
357
|
recorder.add(() => {
|
|
@@ -376,7 +377,7 @@ export default function (config) {
|
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
if (!config.saveToFile) return
|
|
379
|
-
const cookieFile = path.join(
|
|
380
|
+
const cookieFile = path.join(store.outputDir, `${name}_session.json`)
|
|
380
381
|
|
|
381
382
|
if (!fileExists(cookieFile)) {
|
|
382
383
|
return
|
|
@@ -411,7 +412,7 @@ export default function (config) {
|
|
|
411
412
|
|
|
412
413
|
function loadCookiesFromFile(config) {
|
|
413
414
|
for (const name in config.users) {
|
|
414
|
-
const fileName = path.join(
|
|
415
|
+
const fileName = path.join(store.outputDir, `${name}_session.json`)
|
|
415
416
|
if (!fileExists(fileName)) continue
|
|
416
417
|
const data = fs.readFileSync(fileName).toString()
|
|
417
418
|
try {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import output from '../output.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Overrides browser helper config from the command line. Works for all browser helpers
|
|
5
|
+
* (Playwright, Puppeteer, WebDriver, Appium) without touching `codecept.conf`.
|
|
6
|
+
*
|
|
7
|
+
* Enable it via `-p` option with one or more colon-chained args:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* npx codeceptjs run -p browser:show
|
|
11
|
+
* npx codeceptjs run -p browser:hide
|
|
12
|
+
* npx codeceptjs run -p browser:browser=firefox
|
|
13
|
+
* npx codeceptjs run -p browser:windowSize=1024x768:video=false
|
|
14
|
+
* npx codeceptjs run -p browser:hide:browser=webkit:windowSize=800x600
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* #### Args
|
|
18
|
+
*
|
|
19
|
+
* * **show** — force visible browser
|
|
20
|
+
* * **hide** — force headless (also injects `--headless` into WebDriver chrome/firefox capability args)
|
|
21
|
+
* * **`<key>=<value>`** — set `helpers.<eachBrowserHelper>.<key> = <value>`. Three keys
|
|
22
|
+
* get per-helper translation via `setBrowserConfig`:
|
|
23
|
+
* * `browser=<name>` — Puppeteer receives `product`, Playwright receives `browser`
|
|
24
|
+
* * `windowSize=WxH` — also adds `--window-size=W,H` chromium/chrome args
|
|
25
|
+
* * `show=true|false` — toggles `show` on Playwright/Puppeteer; injects/strips
|
|
26
|
+
* `--headless` in WebDriver chrome/firefox capability args
|
|
27
|
+
*
|
|
28
|
+
* Values stay as strings. `true` / `false` are coerced to booleans.
|
|
29
|
+
*
|
|
30
|
+
* Requires `@codeceptjs/configure` to be installed; if missing, the plugin
|
|
31
|
+
* logs a hint and skips the override.
|
|
32
|
+
*/
|
|
33
|
+
export default async function (config = {}) {
|
|
34
|
+
const { _args, enabled, ...rest } = config
|
|
35
|
+
const opts = { ...rest, ...parseArgs(_args || []) }
|
|
36
|
+
if (Object.keys(opts).length === 0) return
|
|
37
|
+
|
|
38
|
+
const configure = await tryImportConfigure()
|
|
39
|
+
if (!configure) return
|
|
40
|
+
|
|
41
|
+
configure.setBrowserConfig(opts)
|
|
42
|
+
output.debug(`browser plugin: applied ${formatOpts(opts)}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function tryImportConfigure() {
|
|
46
|
+
try {
|
|
47
|
+
return await import('@codeceptjs/configure')
|
|
48
|
+
} catch (err) {
|
|
49
|
+
output.error("browser plugin: '@codeceptjs/configure' is not installed; CLI overrides are skipped. Run `npm i @codeceptjs/configure` to enable.")
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(args) {
|
|
55
|
+
return args.filter(Boolean).reduce((acc, arg) => Object.assign(acc, parseArg(arg)), {})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArg(arg) {
|
|
59
|
+
if (arg === 'show') return { show: true }
|
|
60
|
+
if (arg === 'hide') return { show: false }
|
|
61
|
+
if (arg.includes('=')) {
|
|
62
|
+
const [key, ...rest] = arg.split('=')
|
|
63
|
+
return { [key]: parseValue(rest.join('=')) }
|
|
64
|
+
}
|
|
65
|
+
output.error(`browser plugin: unknown arg "${arg}"`)
|
|
66
|
+
return {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseValue(v) {
|
|
70
|
+
if (v === 'true') return true
|
|
71
|
+
if (v === 'false') return false
|
|
72
|
+
return v
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatOpts(opts) {
|
|
76
|
+
return Object.entries(opts).map(([k, v]) => `${k}=${v}`).join(', ')
|
|
77
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import Container from '../container.js'
|
|
2
|
+
|
|
3
|
+
const RESERVED_NAMES = new Set(['I', 'test', 'suite'])
|
|
4
|
+
const SHORTHAND_PROPERTIES = new Set(['page', 'browser', 'browserContext', 'context'])
|
|
5
|
+
|
|
6
|
+
const defaultConfig = {
|
|
7
|
+
inject: {},
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Exposes properties from helper instances as injectable test arguments.
|
|
12
|
+
* Use it to access the underlying Playwright/Puppeteer `page`, the wdio `browser` client,
|
|
13
|
+
* or any other helper internal directly from a Scenario:
|
|
14
|
+
*
|
|
15
|
+
* ```js
|
|
16
|
+
* Scenario('listen for requests', async ({ I, page, browser }) => {
|
|
17
|
+
* page.on('request', r => console.log(r.url()))
|
|
18
|
+
* await page.evaluate(() => 1 + 1)
|
|
19
|
+
* I.amOnPage('/')
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The injected value is a live proxy: every property access reads the *current*
|
|
24
|
+
* helper property, so mid-test reassignments (popups, `switchToNextTab`,
|
|
25
|
+
* `openNewTab`) are reflected automatically. Calls are not wrapped as
|
|
26
|
+
* CodeceptJS steps — `await page.evaluate(...)` runs as native Playwright.
|
|
27
|
+
*
|
|
28
|
+
* #### Configuration
|
|
29
|
+
*
|
|
30
|
+
* `inject` maps an injection name to a `HelperName.propertyName` string. A
|
|
31
|
+
* value with no dot is shorthand for "first configured browser helper that
|
|
32
|
+
* exposes this property" (allowed properties: `page`, `browser`,
|
|
33
|
+
* `browserContext`, `context`).
|
|
34
|
+
*
|
|
35
|
+
* ```js
|
|
36
|
+
* plugins: {
|
|
37
|
+
* expose: {
|
|
38
|
+
* enabled: true,
|
|
39
|
+
* inject: {
|
|
40
|
+
* page: 'Playwright.page',
|
|
41
|
+
* browser: 'Playwright.browser',
|
|
42
|
+
* browserContext: 'Playwright.browserContext',
|
|
43
|
+
* frame: 'Playwright.context', // current frame set by switchTo
|
|
44
|
+
* wdio: 'WebDriver.browser',
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Shorthand:
|
|
51
|
+
*
|
|
52
|
+
* ```js
|
|
53
|
+
* plugins: {
|
|
54
|
+
* expose: {
|
|
55
|
+
* enabled: true,
|
|
56
|
+
* inject: {
|
|
57
|
+
* page: 'page', // resolves to Playwright.page or Puppeteer.page
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* #### Caveats
|
|
64
|
+
*
|
|
65
|
+
* - The injected value is a `Proxy`, not the actual `Page`/`Browser` instance,
|
|
66
|
+
* so `page instanceof Page` is `false`. Use duck typing instead.
|
|
67
|
+
* - Cached method references lose the live binding. Call `page.click(...)`,
|
|
68
|
+
* not `const click = page.click; click(...)`.
|
|
69
|
+
* - In dry-run mode the underlying helper property is `undefined`; accessing
|
|
70
|
+
* any property on the proxy returns `undefined` rather than throwing.
|
|
71
|
+
*/
|
|
72
|
+
export default function (config = {}) {
|
|
73
|
+
config = { ...defaultConfig, ...config }
|
|
74
|
+
|
|
75
|
+
const mappings = parseMappings(config.inject)
|
|
76
|
+
|
|
77
|
+
const support = {}
|
|
78
|
+
for (const [name, { helperName, property }] of Object.entries(mappings)) {
|
|
79
|
+
support[name] = makeLiveProxy(helperName, property)
|
|
80
|
+
}
|
|
81
|
+
Container.append({ support })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseMappings(inject) {
|
|
85
|
+
const out = {}
|
|
86
|
+
for (const [name, value] of Object.entries(inject || {})) {
|
|
87
|
+
if (RESERVED_NAMES.has(name)) {
|
|
88
|
+
throw new Error(`expose plugin: inject name '${name}' is reserved`)
|
|
89
|
+
}
|
|
90
|
+
if (typeof value !== 'string' || !value) {
|
|
91
|
+
throw new Error(`expose plugin: inject value for '${name}' must be a non-empty string`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let helperName
|
|
95
|
+
let property
|
|
96
|
+
|
|
97
|
+
if (value.includes('.')) {
|
|
98
|
+
const dot = value.indexOf('.')
|
|
99
|
+
helperName = value.slice(0, dot)
|
|
100
|
+
property = value.slice(dot + 1)
|
|
101
|
+
if (!helperName || !property) {
|
|
102
|
+
throw new Error(`expose plugin: invalid inject value '${value}' for '${name}' (expected 'HelperName.propertyName')`)
|
|
103
|
+
}
|
|
104
|
+
if (!Container.helpers(helperName)) {
|
|
105
|
+
throw new Error(`expose plugin: helper '${helperName}' is not configured (needed for inject '${name}')`)
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
property = value
|
|
109
|
+
if (!SHORTHAND_PROPERTIES.has(property)) {
|
|
110
|
+
throw new Error(`expose plugin: shorthand '${property}' is not a known helper property for '${name}' (use 'HelperName.${property}' instead)`)
|
|
111
|
+
}
|
|
112
|
+
helperName = Container.STANDARD_ACTING_HELPERS.find(h => Container.helpers(h))
|
|
113
|
+
if (!helperName) {
|
|
114
|
+
throw new Error(`expose plugin: no standard browser helper configured (needed for inject '${name}')`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
out[name] = { helperName, property }
|
|
119
|
+
}
|
|
120
|
+
return out
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function makeLiveProxy(helperName, property) {
|
|
124
|
+
const resolve = () => Container.helpers(helperName)?.[property]
|
|
125
|
+
return new Proxy(function () {}, {
|
|
126
|
+
get(_, prop) {
|
|
127
|
+
const target = resolve()
|
|
128
|
+
if (target == null) return undefined
|
|
129
|
+
const value = target[prop]
|
|
130
|
+
if (typeof value === 'function') return value.bind(target)
|
|
131
|
+
return value
|
|
132
|
+
},
|
|
133
|
+
has(_, prop) {
|
|
134
|
+
const target = resolve()
|
|
135
|
+
return target != null && prop in target
|
|
136
|
+
},
|
|
137
|
+
apply(_, thisArg, args) {
|
|
138
|
+
const target = resolve()
|
|
139
|
+
return target?.apply(thisArg, args)
|
|
140
|
+
},
|
|
141
|
+
set(_, prop, value) {
|
|
142
|
+
const target = resolve()
|
|
143
|
+
if (target != null) target[prop] = value
|
|
144
|
+
return true
|
|
145
|
+
},
|
|
146
|
+
getPrototypeOf() {
|
|
147
|
+
const target = resolve()
|
|
148
|
+
return target != null ? Object.getPrototypeOf(target) : null
|
|
149
|
+
},
|
|
150
|
+
ownKeys() {
|
|
151
|
+
const target = resolve()
|
|
152
|
+
return target != null ? Reflect.ownKeys(target) : []
|
|
153
|
+
},
|
|
154
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
155
|
+
const target = resolve()
|
|
156
|
+
return target != null ? Object.getOwnPropertyDescriptor(target, prop) : undefined
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
}
|
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
|
+
}
|