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
package/docs/within.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
permalink: /within
|
|
3
|
+
title: Within
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Within
|
|
7
|
+
|
|
8
|
+
`within` scopes all actions inside it to a specific element on the page — useful when working with repeated UI components or narrowing interaction to a specific section.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
within('.js-signup-form', () => {
|
|
12
|
+
I.fillField('user[login]', 'User')
|
|
13
|
+
I.fillField('user[email]', 'user@user.com')
|
|
14
|
+
I.fillField('user[password]', 'user@user.com')
|
|
15
|
+
I.click('button')
|
|
16
|
+
})
|
|
17
|
+
I.see('There were problems creating your account.')
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> ⚠ `within` can cause problems when used incorrectly. If you see unexpected behavior, refactor to use the context parameter on individual actions instead (e.g. `I.click('Login', '.nav')`). Keep `within` for the simplest cases.
|
|
21
|
+
> Since `within` returns a Promise, always `await` it when you need its return value.
|
|
22
|
+
|
|
23
|
+
## IFrames
|
|
24
|
+
|
|
25
|
+
Use a `frame` locator to scope actions inside an iframe:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
within({ frame: '#editor' }, () => {
|
|
29
|
+
I.see('Page')
|
|
30
|
+
I.fillField('Body', 'Hello world')
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Nested iframes _(WebDriver & Puppeteer only)_:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
within({ frame: ['.content', '#editor'] }, () => {
|
|
38
|
+
I.see('Page')
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> ℹ IFrames can also be accessed via `I.switchTo` command.
|
|
43
|
+
|
|
44
|
+
## Returning Values
|
|
45
|
+
|
|
46
|
+
`within` can return a value for use in the scenario:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const val = await within('#sidebar', () => {
|
|
50
|
+
return I.grabTextFrom({ css: 'h1' })
|
|
51
|
+
})
|
|
52
|
+
I.fillField('Description', val)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When running steps inside a `within` block, they will be shown indented in the output.
|
package/lib/aria.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import yaml from 'js-yaml'
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────
|
|
4
|
+
// Roles
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const INTERACTIVE_ROLES = new Set([
|
|
8
|
+
'button',
|
|
9
|
+
'link',
|
|
10
|
+
'textbox',
|
|
11
|
+
'searchbox',
|
|
12
|
+
'checkbox',
|
|
13
|
+
'radio',
|
|
14
|
+
'switch',
|
|
15
|
+
'combobox',
|
|
16
|
+
'listbox',
|
|
17
|
+
'listitem',
|
|
18
|
+
'menu',
|
|
19
|
+
'menuitem',
|
|
20
|
+
'menuitemcheckbox',
|
|
21
|
+
'menuitemradio',
|
|
22
|
+
'option',
|
|
23
|
+
'tab',
|
|
24
|
+
'tabpanel',
|
|
25
|
+
'slider',
|
|
26
|
+
'spinbutton',
|
|
27
|
+
'treeitem',
|
|
28
|
+
'gridcell',
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
// Long groups of same-role siblings get summarised as: first N + "...M omitted..." + last N
|
|
32
|
+
const SIBLING_COLLAPSE_THRESHOLD = 50
|
|
33
|
+
const SIBLING_COLLAPSE_KEEP_EACH_SIDE = 5
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────
|
|
36
|
+
// STEP 1 · Parse: YAML text → AriaNode[]
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function unquote(value) {
|
|
40
|
+
const v = value.trim()
|
|
41
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
42
|
+
return v.slice(1, -1)
|
|
43
|
+
}
|
|
44
|
+
return v
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse one YAML node label like: `button "Save"`, `textbox "Email" [focused]`, `heading "Title" [level=2]`
|
|
48
|
+
function parseLabel(label) {
|
|
49
|
+
if (!label) return null
|
|
50
|
+
const trimmed = label.trim()
|
|
51
|
+
const roleMatch = trimmed.match(/^(\w+)/)
|
|
52
|
+
if (!roleMatch) return null
|
|
53
|
+
const role = roleMatch[1].toLowerCase()
|
|
54
|
+
let rest = trimmed.slice(roleMatch[0].length)
|
|
55
|
+
|
|
56
|
+
let name
|
|
57
|
+
const nameMatch = rest.match(/^\s*"((?:[^"\\]|\\.)*)"/) || rest.match(/^\s*'((?:[^'\\]|\\.)*)'/)
|
|
58
|
+
if (nameMatch) {
|
|
59
|
+
name = nameMatch[1]
|
|
60
|
+
rest = rest.slice(nameMatch[0].length)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const attributes = {}
|
|
64
|
+
const attrMatch = rest.match(/\[([^\]]*)\]/)
|
|
65
|
+
if (attrMatch) {
|
|
66
|
+
for (const tok of attrMatch[1].split(/[\s,]+/).filter(Boolean)) {
|
|
67
|
+
const eq = tok.indexOf('=')
|
|
68
|
+
if (eq === -1) {
|
|
69
|
+
attributes[tok.toLowerCase()] = true
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
attributes[tok.slice(0, eq).trim().toLowerCase()] = unquote(tok.slice(eq + 1))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { role, name, attributes }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function yamlItemToNode(item) {
|
|
80
|
+
if (typeof item === 'string') {
|
|
81
|
+
const label = parseLabel(item)
|
|
82
|
+
if (!label) return null
|
|
83
|
+
const node = { role: label.role, name: label.name, attributes: label.attributes, children: [] }
|
|
84
|
+
return node
|
|
85
|
+
}
|
|
86
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return null
|
|
87
|
+
|
|
88
|
+
const entries = Object.entries(item)
|
|
89
|
+
if (entries.length === 0) return null
|
|
90
|
+
const [key, value] = entries[0]
|
|
91
|
+
const label = parseLabel(key)
|
|
92
|
+
if (!label) return null
|
|
93
|
+
const node = { role: label.role, name: label.name, attributes: label.attributes, children: [] }
|
|
94
|
+
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
node.children = value.map(yamlItemToNode).filter(n => n !== null)
|
|
97
|
+
return node
|
|
98
|
+
}
|
|
99
|
+
if (value !== null && value !== undefined) node.value = String(value)
|
|
100
|
+
return node
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseSnapshot(snapshot) {
|
|
104
|
+
if (!snapshot) return []
|
|
105
|
+
let parsed
|
|
106
|
+
try {
|
|
107
|
+
parsed = yaml.load(snapshot)
|
|
108
|
+
} catch {
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(parsed)) return []
|
|
112
|
+
return parsed.map(yamlItemToNode).filter(n => n !== null)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────
|
|
116
|
+
// STEP 2 · Transform: drop containers that contribute nothing.
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function dropEmpty(nodes) {
|
|
120
|
+
return nodes.flatMap(node => {
|
|
121
|
+
const children = dropEmpty(node.children)
|
|
122
|
+
if (INTERACTIVE_ROLES.has(node.role)) return [{ ...node, children }]
|
|
123
|
+
if (children.length > 0) return [{ ...node, children }]
|
|
124
|
+
return []
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────
|
|
129
|
+
// STEP 3 · Render: AriaNode[] → indented YAML text
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
// One-line representation of a node. Stable attr order so diff comparisons are deterministic.
|
|
133
|
+
function formatNode(node) {
|
|
134
|
+
let line = node.role
|
|
135
|
+
if (node.name && node.name.trim()) line += ` "${node.name.trim()}"`
|
|
136
|
+
const attrParts = []
|
|
137
|
+
for (const k of Object.keys(node.attributes).sort()) {
|
|
138
|
+
const v = node.attributes[k]
|
|
139
|
+
if (v === undefined || v === null || v === '') continue
|
|
140
|
+
if (v === true) attrParts.push(k)
|
|
141
|
+
else attrParts.push(`${k}=${v}`)
|
|
142
|
+
}
|
|
143
|
+
if (attrParts.length > 0) line += ` [${attrParts.join(' ')}]`
|
|
144
|
+
if (node.value !== undefined && node.value !== null) {
|
|
145
|
+
const text = String(node.value).trim()
|
|
146
|
+
if (text) line += `: ${text}`
|
|
147
|
+
}
|
|
148
|
+
return line
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Group consecutive same-role siblings. [a,a,b,a,a,a] → [[a,a],[b],[a,a,a]]
|
|
152
|
+
function groupByConsecutiveRole(nodes) {
|
|
153
|
+
return nodes.reduce((groups, node) => {
|
|
154
|
+
const last = groups[groups.length - 1]
|
|
155
|
+
if (last && last[0].role === node.role) {
|
|
156
|
+
last.push(node)
|
|
157
|
+
return groups
|
|
158
|
+
}
|
|
159
|
+
groups.push([node])
|
|
160
|
+
return groups
|
|
161
|
+
}, [])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Large group of same-role siblings → first N + placeholder line + last N.
|
|
165
|
+
// Returns mix of AriaNode (to render) and pre-rendered placeholder strings.
|
|
166
|
+
function collapseGroup(group, depth) {
|
|
167
|
+
if (group.length <= SIBLING_COLLAPSE_THRESHOLD) return group
|
|
168
|
+
const keep = SIBLING_COLLAPSE_KEEP_EACH_SIDE
|
|
169
|
+
const omitted = group.length - keep * 2
|
|
170
|
+
const placeholder = `${' '.repeat(depth)}- ...${omitted} similar "${group[0].role}" items omitted...`
|
|
171
|
+
return [...group.slice(0, keep), placeholder, ...group.slice(-keep)]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderTree(nodes, depth = 0) {
|
|
175
|
+
const items = groupByConsecutiveRole(nodes).flatMap(group => collapseGroup(group, depth))
|
|
176
|
+
return items
|
|
177
|
+
.map(item => {
|
|
178
|
+
if (typeof item === 'string') return item
|
|
179
|
+
const indent = ' '.repeat(depth)
|
|
180
|
+
const head = `${indent}- ${formatNode(item)}`
|
|
181
|
+
if (item.children.length === 0) return head
|
|
182
|
+
return `${head}:\n${renderTree(item.children, depth + 1)}`
|
|
183
|
+
})
|
|
184
|
+
.join('\n')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─────────────────────────────────────────────────────────────────
|
|
188
|
+
// STEP 4 · Diff: collect interactive summaries → bag diff → text
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
// Walk tree, emit one summary string per meaningful interactive node.
|
|
192
|
+
function collectSummaries(nodes) {
|
|
193
|
+
return nodes.flatMap(node => {
|
|
194
|
+
const fromChildren = collectSummaries(node.children)
|
|
195
|
+
if (!INTERACTIVE_ROLES.has(node.role)) return fromChildren
|
|
196
|
+
const summary = formatNode(node)
|
|
197
|
+
if (summary === node.role) return fromChildren // skip empty unnamed interactive nodes
|
|
198
|
+
return [summary, ...fromChildren]
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function countBy(items) {
|
|
203
|
+
return items.reduce((map, item) => {
|
|
204
|
+
map.set(item, (map.get(item) ?? 0) + 1)
|
|
205
|
+
return map
|
|
206
|
+
}, new Map())
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Bag diff: any summary appearing more in one bag than the other becomes added/removed.
|
|
210
|
+
function diffSummaries(prev, curr) {
|
|
211
|
+
const before = countBy(prev)
|
|
212
|
+
const after = countBy(curr)
|
|
213
|
+
const added = []
|
|
214
|
+
const removed = []
|
|
215
|
+
for (const summary of new Set([...before.keys(), ...after.keys()])) {
|
|
216
|
+
const b = before.get(summary) ?? 0
|
|
217
|
+
const a = after.get(summary) ?? 0
|
|
218
|
+
for (let i = 0; i < a - b; i += 1) added.push(summary)
|
|
219
|
+
for (let i = 0; i < b - a; i += 1) removed.push(summary)
|
|
220
|
+
}
|
|
221
|
+
return { added, removed }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function formatDiff(added, removed) {
|
|
225
|
+
if (added.length === 0 && removed.length === 0) return null
|
|
226
|
+
const lines = ['ariaDiff:']
|
|
227
|
+
if (added.length === 0) {
|
|
228
|
+
lines.push(' added: []')
|
|
229
|
+
} else {
|
|
230
|
+
lines.push(' added:')
|
|
231
|
+
for (const [item, count] of [...countBy(added).entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
232
|
+
const suffix = count > 1 ? ` (x${count})` : ''
|
|
233
|
+
lines.push(` - ${item}${suffix}`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (removed.length === 0) {
|
|
237
|
+
lines.push(' removed: []')
|
|
238
|
+
} else {
|
|
239
|
+
lines.push(` removed: ${removed.length} interactive elements`)
|
|
240
|
+
}
|
|
241
|
+
return lines.join('\n')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────
|
|
245
|
+
// Public API — pipelines composed visibly, top-to-bottom
|
|
246
|
+
// ─────────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
function compactAriaSnapshot(snapshot) {
|
|
249
|
+
if (!snapshot) return ''
|
|
250
|
+
const tree = dropEmpty(parseSnapshot(snapshot))
|
|
251
|
+
return renderTree(tree)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function diffAriaSnapshots(previous, current) {
|
|
255
|
+
const summariesOf = snap => collectSummaries(dropEmpty(parseSnapshot(snap)))
|
|
256
|
+
const { added, removed } = diffSummaries(summariesOf(previous), summariesOf(current))
|
|
257
|
+
return formatDiff(added, removed)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { diffAriaSnapshots, compactAriaSnapshot }
|
package/lib/command/dryRun.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
1
2
|
import { getConfig, getTestRoot } from './utils.js'
|
|
2
3
|
import Config from '../config.js'
|
|
3
4
|
import Codecept from '../codecept.js'
|
|
@@ -8,6 +9,8 @@ import Container from '../container.js'
|
|
|
8
9
|
|
|
9
10
|
export default async function (test, options) {
|
|
10
11
|
if (options.grep) process.env.grep = options.grep
|
|
12
|
+
if (options.ansi === false) chalk.level = 0
|
|
13
|
+
store.dryRun = true
|
|
11
14
|
const configFile = options.config
|
|
12
15
|
let codecept
|
|
13
16
|
|
|
@@ -18,9 +21,15 @@ export default async function (test, options) {
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
if (config.plugins) {
|
|
21
|
-
//
|
|
24
|
+
// Disable plugins that block (interactive) or perform external I/O (AI/network).
|
|
25
|
+
// Leave the rest enabled so they can register support objects (e.g. auth registers
|
|
26
|
+
// `login`); helper calls inside those support fns are already no-op'd by HelperStep
|
|
27
|
+
// when store.dryRun is true.
|
|
28
|
+
const disableInDryRun = new Set(['pause', 'pauseOnFail', 'analyze', 'aiTrace', 'pageInfo', 'heal'])
|
|
22
29
|
for (const plugin in config.plugins) {
|
|
23
|
-
|
|
30
|
+
if (disableInDryRun.has(plugin)) {
|
|
31
|
+
config.plugins[plugin].enabled = false
|
|
32
|
+
}
|
|
24
33
|
}
|
|
25
34
|
}
|
|
26
35
|
|
|
@@ -31,12 +40,12 @@ export default async function (test, options) {
|
|
|
31
40
|
if (options.bootstrap) await codecept.bootstrap()
|
|
32
41
|
|
|
33
42
|
codecept.loadTests()
|
|
34
|
-
store.dryRun = true
|
|
35
43
|
|
|
36
44
|
if (!options.steps && !options.verbose && !options.debug) {
|
|
37
45
|
await printTests(codecept.testFiles)
|
|
38
46
|
return
|
|
39
47
|
}
|
|
48
|
+
if (options.numbers) numberSteps()
|
|
40
49
|
event.dispatcher.on(event.all.result, printFooter)
|
|
41
50
|
await codecept.run(test)
|
|
42
51
|
} catch (err) {
|
|
@@ -45,6 +54,17 @@ export default async function (test, options) {
|
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
function numberSteps() {
|
|
58
|
+
let stepIndex = 0
|
|
59
|
+
event.dispatcher.on(event.test.before, () => {
|
|
60
|
+
stepIndex = 0
|
|
61
|
+
})
|
|
62
|
+
event.dispatcher.prependListener(event.step.before, step => {
|
|
63
|
+
stepIndex++
|
|
64
|
+
step.prefix = `${stepIndex}. ${step.prefix || ''}`
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
async function printTests(files) {
|
|
49
69
|
const { default: figures } = await import('figures')
|
|
50
70
|
const { default: colors } = await import('chalk')
|