codeceptjs 4.0.0-rc.17 → 4.0.0-rc.19
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/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +733 -196
- 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/aria.js +260 -0
- package/lib/command/dryRun.js +23 -3
- package/lib/command/init.js +247 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/config.js +77 -4
- package/lib/container.js +34 -2
- package/lib/element/WebElement.js +37 -0
- package/lib/globals.js +11 -10
- package/lib/helper/Playwright.js +5 -6
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/locator.js +2 -2
- package/lib/mocha/factory.js +5 -1
- package/lib/mocha/inject.js +1 -1
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +72 -84
- 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 +51 -48
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/screencast.js +287 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -170
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +25 -0
- package/lib/workers.js +1 -15
- package/package.json +12 -10
- 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/plugin/pauseOn.js +0 -167
- package/lib/plugin/stepByStepReport.js +0 -432
- package/lib/plugin/subtitles.js +0 -89
|
@@ -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
|
|
@@ -65,6 +91,8 @@ export default function (config = {}) {
|
|
|
65
91
|
|
|
66
92
|
if (!heal.hasCorrespondingRecipes(step)) return
|
|
67
93
|
|
|
94
|
+
if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
|
|
95
|
+
|
|
68
96
|
recorder.catchWithoutStop(async err => {
|
|
69
97
|
isHealing = true
|
|
70
98
|
if (caughtError === err) throw err // avoid double handling
|
|
@@ -72,6 +100,21 @@ export default function (config = {}) {
|
|
|
72
100
|
|
|
73
101
|
const test = currentTest
|
|
74
102
|
|
|
103
|
+
if (trigger.on === 'url') {
|
|
104
|
+
try {
|
|
105
|
+
const helper = getBrowserHelper()
|
|
106
|
+
const url = helper && helper.grabCurrentUrl ? await helper.grabCurrentUrl() : null
|
|
107
|
+
if (!matchUrl(url, trigger.pattern)) {
|
|
108
|
+
isHealing = false
|
|
109
|
+
throw err
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
if (e === err) throw e
|
|
113
|
+
isHealing = false
|
|
114
|
+
throw err
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
75
118
|
recorder.session.start('heal')
|
|
76
119
|
|
|
77
120
|
debug('Self-healing started', step.toCode())
|
package/lib/plugin/pageInfo.js
CHANGED
|
@@ -1,14 +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
9
|
import store from '../store.js'
|
|
10
10
|
import { humanizeString, ucfirst } from '../utils.js'
|
|
11
11
|
import { testToFileName } from '../mocha/test.js'
|
|
12
|
+
|
|
12
13
|
const defaultConfig = {
|
|
13
14
|
errorClasses: ['error', 'warning', 'alert', 'danger'],
|
|
14
15
|
browserLogs: ['error'],
|
|
@@ -37,57 +38,58 @@ const defaultConfig = {
|
|
|
37
38
|
*
|
|
38
39
|
*/
|
|
39
40
|
export default function (config = {}) {
|
|
40
|
-
const helpers = Container.helpers()
|
|
41
|
-
let helper
|
|
42
|
-
|
|
43
41
|
config = Object.assign(defaultConfig, config)
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
helper = helpers[helperName]
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!helper) return // no helpers for screenshot
|
|
43
|
+
const helper = pickActingHelper(Container.helpers())
|
|
44
|
+
if (!helper) return
|
|
52
45
|
|
|
53
46
|
event.dispatcher.on(event.test.failed, test => {
|
|
54
47
|
const pageState = {}
|
|
55
48
|
|
|
56
|
-
recorder.add('
|
|
57
|
-
try {
|
|
58
|
-
const url = await helper.grabCurrentUrl()
|
|
59
|
-
pageState.url = url
|
|
60
|
-
} catch (err) {
|
|
61
|
-
// not really needed
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
recorder.add('HTML snapshot failed test', async () => {
|
|
49
|
+
recorder.add('pageInfo capture', async () => {
|
|
65
50
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
// Scan raw HTML (pre-cleanHtml) so error classes containing digits
|
|
64
|
+
// or trash-class prefixes aren't stripped before detection.
|
|
65
|
+
const htmlForScan = captured.htmlRaw || (() => {
|
|
66
|
+
try { return fs.readFileSync(htmlPath, 'utf8') } catch { return '' }
|
|
67
|
+
})()
|
|
68
|
+
if (htmlForScan) {
|
|
69
|
+
try {
|
|
70
|
+
const errors = scanForErrorMessages(htmlForScan, config.errorClasses)
|
|
71
|
+
if (errors.length) {
|
|
72
|
+
output.debug('Detected errors in HTML code')
|
|
73
|
+
errors.forEach(error => output.debug(error))
|
|
74
|
+
pageState.htmlErrors = errors
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
75
78
|
}
|
|
76
|
-
} catch (err) {
|
|
77
|
-
// not really needed
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
recorder.add('Browser logs for failed test', async () => {
|
|
82
|
-
try {
|
|
83
|
-
const logs = await helper.grabBrowserLogs()
|
|
84
79
|
|
|
85
|
-
if (
|
|
80
|
+
if (captured.aria) {
|
|
81
|
+
pageState.ariaSnapshot = path.join(store.outputDir, captured.aria)
|
|
82
|
+
}
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
if (captured.console) {
|
|
85
|
+
const consolePath = path.join(store.outputDir, captured.console)
|
|
86
|
+
pageState.consoleSnapshot = consolePath
|
|
87
|
+
try {
|
|
88
|
+
const logs = JSON.parse(fs.readFileSync(consolePath, 'utf8'))
|
|
89
|
+
pageState.browserErrors = getBrowserErrors(logs, config.browserLogs)
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
91
93
|
})
|
|
92
94
|
|
|
93
95
|
recorder.add('Save page info', () => {
|
|
@@ -127,15 +129,16 @@ function pageStateToMarkdown(pageState) {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
function getBrowserErrors(logs, type = ['error']) {
|
|
130
|
-
// Playwright
|
|
131
|
-
|
|
132
|
+
// Accepts Playwright ConsoleMessage objects, normalized {type, text}, or strings
|
|
133
|
+
return logs
|
|
132
134
|
.map(log => {
|
|
133
135
|
if (typeof log === 'string') return log
|
|
134
|
-
if (!log
|
|
135
|
-
|
|
136
|
+
if (!log) return null
|
|
137
|
+
const t = typeof log.type === 'function' ? log.type() : log.type
|
|
138
|
+
const text = typeof log.text === 'function' ? log.text() : log.text
|
|
139
|
+
if (!t) return null
|
|
140
|
+
return { type: t, text }
|
|
136
141
|
})
|
|
137
142
|
.filter(l => l && (typeof l === 'string' || type.includes(l.type)))
|
|
138
143
|
.map(l => (typeof l === 'string' ? l : l.text))
|
|
139
|
-
|
|
140
|
-
return errors
|
|
141
144
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,39 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import output from '../output.js'
|
|
2
|
+
import pause from './pause.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
let warned = false
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Useful for debugging flaky tests on local environment.
|
|
9
|
-
* Add this plugin to config file:
|
|
10
|
-
*
|
|
11
|
-
* ```js
|
|
12
|
-
* plugins: {
|
|
13
|
-
* pauseOnFail: {},
|
|
14
|
-
* }
|
|
15
|
-
* ```
|
|
16
|
-
*
|
|
17
|
-
* Unlike other plugins, `pauseOnFail` is not recommended to be enabled by default.
|
|
18
|
-
* Enable it manually on each run via `-p` option:
|
|
19
|
-
*
|
|
20
|
-
* ```
|
|
21
|
-
* npx codeceptjs run -p pauseOnFail
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
7
|
+
* @deprecated Use the `pause` plugin with `on: 'fail'` (the default).
|
|
24
8
|
*/
|
|
25
|
-
export default function() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
event.dispatcher.on(event.step.failed, () => {
|
|
33
|
-
failed = true
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
event.dispatcher.on(event.test.after, () => {
|
|
37
|
-
if (failed) pause()
|
|
38
|
-
})
|
|
9
|
+
export default function (config = {}) {
|
|
10
|
+
if (!warned) {
|
|
11
|
+
output.error('pauseOnFail is deprecated; use the `pause` plugin (default on=fail).')
|
|
12
|
+
warned = true
|
|
13
|
+
}
|
|
14
|
+
return pause({ ...config, on: 'fail' })
|
|
39
15
|
}
|