codeceptjs 4.2.0-beta.1 → 4.2.0-beta.3
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/docs/advanced.md +1 -1
- package/docs/helpers/Obscura.md +9 -8
- package/docs/helpers/Playwright.md +5 -2
- package/lib/command/info.js +9 -2
- package/lib/data/context.js +4 -6
- package/lib/helper/CDPBrowser.js +4 -2
- package/lib/helper/Obscura.js +10 -9
- package/lib/helper/Playwright.js +24 -18
- package/lib/helper/Puppeteer.js +12 -13
- package/lib/helper/WebDriver.js +8 -22
- package/lib/helper/clientscripts/cdpBrowserClient.js +11 -0
- package/lib/helper/errors/MultipleElementsFound.js +60 -3
- package/lib/helper/extras/PlaywrightLocator.js +2 -2
- package/lib/locator.js +7 -2
- package/lib/mocha/asyncWrapper.js +7 -1
- package/lib/step/config.js +1 -0
- package/lib/store.js +6 -0
- package/package.json +2 -2
- package/typings/promiseBasedTypes.d.ts +11 -8
- package/typings/types.d.ts +17 -8
package/docs/advanced.md
CHANGED
|
@@ -79,7 +79,7 @@ Data(function*() {
|
|
|
79
79
|
}).Scenario() // ...
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Objects in a data set are serialized as JSON in the test name.
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
## Debug
|
package/docs/helpers/Obscura.md
CHANGED
|
@@ -28,9 +28,9 @@ process lifecycle, the same way Playwright manages its own browser process.
|
|
|
28
28
|
never spawns or kills anything, no matter what `binaryPath`/`port` are set to.
|
|
29
29
|
* **SELF-LAUNCH** — `endpoint` is unset and a binary can be resolved, in order: `binaryPath` in
|
|
30
30
|
the config, then the `OBSCURA_PATH` environment variable, then `obscura` on `PATH`. The helper
|
|
31
|
-
spawns `obscura serve --port <port> --allow-private-network` (`port` from
|
|
32
|
-
free port picked automatically), waits for it to answer, connects, and kills
|
|
33
|
-
`_finishTest`.
|
|
31
|
+
spawns `obscura serve --port <port> --allow-private-network --allow-file-access` (`port` from
|
|
32
|
+
the config, or a free port picked automatically), waits for it to answer, connects, and kills
|
|
33
|
+
it in `_finishTest`.
|
|
34
34
|
* **COURTESY-ATTACH** — `endpoint` is unset and no binary can be resolved, but something already
|
|
35
35
|
answers `http://127.0.0.1:9222/json/version` (e.g. `obscura serve` started by hand, or by CI
|
|
36
36
|
before this process ever ran). The helper attaches to it and never kills it — it isn't the
|
|
@@ -43,12 +43,13 @@ Download a release binary and put it on your `PATH` (or point `binaryPath`/`OBSC
|
|
|
43
43
|
it directly) and the helper launches and tears it down for you automatically:
|
|
44
44
|
|
|
45
45
|
```sh
|
|
46
|
-
curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.
|
|
46
|
+
curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.2/obscura-x86_64-linux.tar.gz | tar xz
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
`--allow-private-network`
|
|
50
|
-
on `localhost`/private IPs, e.g. a dev server on
|
|
51
|
-
|
|
49
|
+
`--allow-private-network` and `--allow-file-access` are always passed by this helper: the first
|
|
50
|
+
is required to reach apps running on `localhost`/private IPs, e.g. a dev server on
|
|
51
|
+
`127.0.0.1:8000`, the second to let `attachFile` upload local files. Obscura blocks both by
|
|
52
|
+
default.
|
|
52
53
|
|
|
53
54
|
## Config presets
|
|
54
55
|
|
|
@@ -67,7 +68,7 @@ Set them explicitly in your own config to skip probing or to force a mode.
|
|
|
67
68
|
## Limitations
|
|
68
69
|
|
|
69
70
|
* `input` is always `synthetic`, even on rendering builds — see `input` above.
|
|
70
|
-
* No frames
|
|
71
|
+
* No frames or popups.
|
|
71
72
|
* On `-no-render` builds and v0.1.x: no screenshots, no visibility assertions
|
|
72
73
|
(`seeElement`/`dontSeeElement` always throw) — only DOM presence
|
|
73
74
|
(`seeElementInDOM`/`dontSeeElementInDOM`) is meaningful without a layout engine.
|
|
@@ -78,8 +78,9 @@ Type: [object][6]
|
|
|
78
78
|
* `ignoreHTTPSErrors` **[boolean][27]?** Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
79
79
|
* `bypassCSP` **[boolean][27]?** bypass Content Security Policy or CSP
|
|
80
80
|
* `highlightElement` **[boolean][27]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
81
|
+
* `visibleLocator` **[boolean][27]?** append [`visible()`][49] to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to steps that must reach hidden elements: `grab*` methods, `scrollTo`, `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found.
|
|
81
82
|
* `recordHar` **[object][6]?** record HAR and will be saved to `output/har`. See more of [HAR options][3].
|
|
82
|
-
* `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][
|
|
83
|
+
* `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][50].
|
|
83
84
|
* `storageState` **([string][9] | [object][6])?** Playwright storage state (path to JSON file or object)
|
|
84
85
|
passed directly to `browser.newContext`.
|
|
85
86
|
If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`),
|
|
@@ -2967,4 +2968,6 @@ Returns **void** automatically synchronized promise through #recorder
|
|
|
2967
2968
|
|
|
2968
2969
|
[48]: https://playwright.dev/docs/api/class-consolemessage#console-message-type
|
|
2969
2970
|
|
|
2970
|
-
[49]: https://playwright.dev/docs/
|
|
2971
|
+
[49]: https://playwright.dev/docs/api/class-locator#locator-visible
|
|
2972
|
+
|
|
2973
|
+
[50]: https://playwright.dev/docs/locators#locate-by-test-id
|
package/lib/command/info.js
CHANGED
|
@@ -21,9 +21,16 @@ function parsePlaywrightBrowsers(output) {
|
|
|
21
21
|
return versions.join(', ')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// Bun has its own package runner and a Bun-only install has no `npx` on PATH at all, so the
|
|
25
|
+
// runner has to follow the runtime that is actually executing rather than what PATH happens to hold.
|
|
26
|
+
function getPackageRunner() {
|
|
27
|
+
if (process.versions.bun) return 'bunx'
|
|
28
|
+
return 'npx'
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
async function getPlaywrightBrowsers() {
|
|
25
32
|
try {
|
|
26
|
-
const info = execSync(
|
|
33
|
+
const info = execSync(`${getPackageRunner()} playwright install --dry-run`).toString().trim()
|
|
27
34
|
return parsePlaywrightBrowsers(info)
|
|
28
35
|
} catch (err) {
|
|
29
36
|
return 'Playwright not installed'
|
|
@@ -85,7 +92,7 @@ export default async function (path) {
|
|
|
85
92
|
output.print('***************************************')
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
export { parsePlaywrightBrowsers, getRuntimeInfo }
|
|
95
|
+
export { parsePlaywrightBrowsers, getRuntimeInfo, getPackageRunner }
|
|
89
96
|
|
|
90
97
|
export const getMachineInfo = async () => {
|
|
91
98
|
const info = {
|
package/lib/data/context.js
CHANGED
|
@@ -72,13 +72,11 @@ function replaceTitle(title, dataRow) {
|
|
|
72
72
|
return `${title} | ${dataRow.data.getMasked()}`
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (Object.prototype.toString.call(dataRow.data) === Object().toString() && dataRow.data.toString() !== Object().toString()) {
|
|
78
|
-
return `${title} | ${dataRow.data}`
|
|
79
|
-
}
|
|
75
|
+
return `${title} | ${JSON.stringify(dataRow.data, maskSecret)}`
|
|
76
|
+
}
|
|
80
77
|
|
|
81
|
-
|
|
78
|
+
function maskSecret(key, value) {
|
|
79
|
+
return typeof value?.getMasked === 'function' ? value.getMasked() : value
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
function isTableDataRow(row) {
|
package/lib/helper/CDPBrowser.js
CHANGED
|
@@ -1631,6 +1631,9 @@ class CDPBrowser extends Helper {
|
|
|
1631
1631
|
const value = Array.isArray(option) ? option.map(String) : String(option)
|
|
1632
1632
|
const res = await this._run(this._candidates(select, 'field'), 'select', { value }, context)
|
|
1633
1633
|
if (!res.found) throw new ElementNotFound(select, 'Selectable field')
|
|
1634
|
+
if (res.result === '__RADIOGROUP_MULTI__') {
|
|
1635
|
+
throw new Error(`selectOption: a radio group holds one value, but ${value.length} options were passed: ${value.join(', ')}`)
|
|
1636
|
+
}
|
|
1634
1637
|
if (res.result === false) throw new Error(`Option "${Array.isArray(option) ? option.join(',') : option}" not found in ${new Locator(select).toString()}`)
|
|
1635
1638
|
}
|
|
1636
1639
|
|
|
@@ -1810,7 +1813,6 @@ class CDPBrowser extends Helper {
|
|
|
1810
1813
|
*/
|
|
1811
1814
|
async waitInUrl(urlPart, sec = null) {
|
|
1812
1815
|
const timeout = sec || this.options.waitForTimeout
|
|
1813
|
-
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
1814
1816
|
let lastUrl = ''
|
|
1815
1817
|
try {
|
|
1816
1818
|
return await this._poll(
|
|
@@ -1822,7 +1824,7 @@ class CDPBrowser extends Helper {
|
|
|
1822
1824
|
'placeholder',
|
|
1823
1825
|
)
|
|
1824
1826
|
} catch (e) {
|
|
1825
|
-
throw new Error(`expected url to include ${
|
|
1827
|
+
throw new Error(`expected url to include ${urlPart}, but found ${lastUrl}`)
|
|
1826
1828
|
}
|
|
1827
1829
|
}
|
|
1828
1830
|
|
package/lib/helper/Obscura.js
CHANGED
|
@@ -45,9 +45,9 @@ const config = {}
|
|
|
45
45
|
* never spawns or kills anything, no matter what `binaryPath`/`port` are set to.
|
|
46
46
|
* - **SELF-LAUNCH** — `endpoint` is unset and a binary can be resolved, in order: `binaryPath` in
|
|
47
47
|
* the config, then the `OBSCURA_PATH` environment variable, then `obscura` on `PATH`. The helper
|
|
48
|
-
* spawns `obscura serve --port <port> --allow-private-network` (`port` from
|
|
49
|
-
* free port picked automatically), waits for it to answer, connects, and kills
|
|
50
|
-
* `_finishTest`.
|
|
48
|
+
* spawns `obscura serve --port <port> --allow-private-network --allow-file-access` (`port` from
|
|
49
|
+
* the config, or a free port picked automatically), waits for it to answer, connects, and kills
|
|
50
|
+
* it in `_finishTest`.
|
|
51
51
|
* - **COURTESY-ATTACH** — `endpoint` is unset and no binary can be resolved, but something already
|
|
52
52
|
* answers `http://127.0.0.1:9222/json/version` (e.g. `obscura serve` started by hand, or by CI
|
|
53
53
|
* before this process ever ran). The helper attaches to it and never kills it — it isn't the
|
|
@@ -60,12 +60,13 @@ const config = {}
|
|
|
60
60
|
* it directly) and the helper launches and tears it down for you automatically:
|
|
61
61
|
*
|
|
62
62
|
* ```sh
|
|
63
|
-
* curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.
|
|
63
|
+
* curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.2/obscura-x86_64-linux.tar.gz | tar xz
|
|
64
64
|
* ```
|
|
65
65
|
*
|
|
66
|
-
* `--allow-private-network`
|
|
67
|
-
* on `localhost`/private IPs, e.g. a dev server on
|
|
68
|
-
*
|
|
66
|
+
* `--allow-private-network` and `--allow-file-access` are always passed by this helper: the first
|
|
67
|
+
* is required to reach apps running on `localhost`/private IPs, e.g. a dev server on
|
|
68
|
+
* `127.0.0.1:8000`, the second to let `attachFile` upload local files. Obscura blocks both by
|
|
69
|
+
* default.
|
|
69
70
|
*
|
|
70
71
|
* ## Config presets
|
|
71
72
|
*
|
|
@@ -84,7 +85,7 @@ const config = {}
|
|
|
84
85
|
* ## Limitations
|
|
85
86
|
*
|
|
86
87
|
* - `input` is always `synthetic`, even on rendering builds — see `input` above.
|
|
87
|
-
* - No frames
|
|
88
|
+
* - No frames or popups.
|
|
88
89
|
* - On `-no-render` builds and v0.1.x: no screenshots, no visibility assertions
|
|
89
90
|
* (`seeElement`/`dontSeeElement` always throw) — only DOM presence
|
|
90
91
|
* (`seeElementInDOM`/`dontSeeElementInDOM`) is meaningful without a layout engine.
|
|
@@ -179,7 +180,7 @@ class Obscura extends CDPBrowser {
|
|
|
179
180
|
const port = this.options.port || (await this._findFreePort())
|
|
180
181
|
this.options.port = port
|
|
181
182
|
this.serverError = null
|
|
182
|
-
this.serverProcess = spawn(binaryPath, ['serve', '--port', String(port), '--allow-private-network'], { stdio: 'ignore' })
|
|
183
|
+
this.serverProcess = spawn(binaryPath, ['serve', '--port', String(port), '--allow-private-network', '--allow-file-access'], { stdio: 'ignore' })
|
|
183
184
|
this.serverProcess.on('error', err => {
|
|
184
185
|
this.serverError = err
|
|
185
186
|
})
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -50,6 +50,7 @@ let defaultSelectorEnginesInitialized = false
|
|
|
50
50
|
const popupStore = new Popup()
|
|
51
51
|
const consoleLogStore = new Console()
|
|
52
52
|
const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
|
|
53
|
+
const visibilityAgnosticSteps = ['seeElementInDOM', 'dontSeeElementInDOM', 'seeNumberOfElements', 'scrollTo']
|
|
53
54
|
const checkableRoles = ['checkbox', 'radio', 'switch']
|
|
54
55
|
|
|
55
56
|
import { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } from './extras/PlaywrightRestartOpts.js'
|
|
@@ -102,6 +103,7 @@ const pathSeparator = path.sep
|
|
|
102
103
|
* @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
103
104
|
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
|
|
104
105
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
106
|
+
* @prop {boolean} [visibleLocator=false] - append [`visible()`](https://playwright.dev/docs/api/class-locator#locator-visible) to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to steps that must reach hidden elements: `grab*` methods, `scrollTo`, `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found.
|
|
105
107
|
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
106
108
|
* @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
107
109
|
* @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object)
|
|
@@ -399,6 +401,7 @@ class Playwright extends Helper {
|
|
|
399
401
|
storageState: undefined,
|
|
400
402
|
onResponse: null,
|
|
401
403
|
strict: false,
|
|
404
|
+
visibleLocator: false,
|
|
402
405
|
}
|
|
403
406
|
|
|
404
407
|
process.env.testIdAttribute = 'data-testid'
|
|
@@ -555,6 +558,11 @@ class Playwright extends Helper {
|
|
|
555
558
|
}
|
|
556
559
|
}
|
|
557
560
|
|
|
561
|
+
_beforeStep(step) {
|
|
562
|
+
const reachesHidden = step.helperMethod?.startsWith('grab') || visibilityAgnosticSteps.includes(step.helperMethod)
|
|
563
|
+
store.visibleLocator = step.opts?.visibleLocator ?? (this.options.visibleLocator && !reachesHidden)
|
|
564
|
+
}
|
|
565
|
+
|
|
558
566
|
async _before(test) {
|
|
559
567
|
// Skip browser operations in dry-run mode (used by check command)
|
|
560
568
|
if (store.dryRun) {
|
|
@@ -1518,6 +1526,7 @@ class Playwright extends Helper {
|
|
|
1518
1526
|
assertElementExists(el, locator)
|
|
1519
1527
|
}
|
|
1520
1528
|
|
|
1529
|
+
await el.scrollIntoViewIfNeeded()
|
|
1521
1530
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
1522
1531
|
const { x, y } = await clickablePoint(el)
|
|
1523
1532
|
await this.page.mouse.move(x + offsetX, y + offsetY)
|
|
@@ -4197,6 +4206,14 @@ export function buildLocatorString(locator) {
|
|
|
4197
4206
|
return locator.simplify()
|
|
4198
4207
|
}
|
|
4199
4208
|
|
|
4209
|
+
function withVisibleLocator(locator) {
|
|
4210
|
+
if (!store.visibleLocator) return locator
|
|
4211
|
+
if (typeof locator.visible !== 'function') {
|
|
4212
|
+
throw new Error('visibleLocator option requires Playwright 1.63 or newer. Upgrade the playwright package or disable visibleLocator in helper config')
|
|
4213
|
+
}
|
|
4214
|
+
return locator.visible()
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4200
4217
|
/**
|
|
4201
4218
|
* Handles role locator objects by converting them to Playwright's getByRole() API
|
|
4202
4219
|
* Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
|
|
@@ -4212,7 +4229,7 @@ async function handleRoleLocator(context, locator) {
|
|
|
4212
4229
|
if (roleObj.name) options.name = roleObj.name
|
|
4213
4230
|
if (roleObj.exact !== undefined) options.exact = roleObj.exact
|
|
4214
4231
|
|
|
4215
|
-
return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4232
|
+
return withVisibleLocator(context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined)).all()
|
|
4216
4233
|
}
|
|
4217
4234
|
|
|
4218
4235
|
async function findByRole(context, locator) {
|
|
@@ -4220,13 +4237,13 @@ async function findByRole(context, locator) {
|
|
|
4220
4237
|
const options = {}
|
|
4221
4238
|
if (locator.name) options.name = locator.name
|
|
4222
4239
|
if (locator.exact !== undefined) options.exact = locator.exact
|
|
4223
|
-
return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4240
|
+
return withVisibleLocator(context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined)).all()
|
|
4224
4241
|
}
|
|
4225
4242
|
|
|
4226
4243
|
async function findElements(matcher, locator) {
|
|
4227
4244
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4228
4245
|
|
|
4229
|
-
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4246
|
+
if (isPwLocator) return withVisibleLocator(findByPlaywrightLocator.call(this, matcher, locator)).all()
|
|
4230
4247
|
|
|
4231
4248
|
// Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
4232
4249
|
const roleElements = await handleRoleLocator(matcher, locator)
|
|
@@ -4236,11 +4253,11 @@ async function findElements(matcher, locator) {
|
|
|
4236
4253
|
|
|
4237
4254
|
const locatorString = buildLocatorString(locator)
|
|
4238
4255
|
|
|
4239
|
-
return matcher.locator(locatorString).all()
|
|
4256
|
+
return withVisibleLocator(matcher.locator(locatorString)).all()
|
|
4240
4257
|
}
|
|
4241
4258
|
|
|
4242
4259
|
async function findElement(matcher, locator) {
|
|
4243
|
-
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4260
|
+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator).first()
|
|
4244
4261
|
|
|
4245
4262
|
locator = new Locator(locator, 'css')
|
|
4246
4263
|
|
|
@@ -4313,14 +4330,14 @@ async function findClickable(matcher, locator) {
|
|
|
4313
4330
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
4314
4331
|
|
|
4315
4332
|
try {
|
|
4316
|
-
els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
|
|
4333
|
+
els = await withVisibleLocator(matcher.getByRole('button', { name: matchedLocator.value })).all()
|
|
4317
4334
|
if (els.length) return els
|
|
4318
4335
|
} catch (err) {
|
|
4319
4336
|
// getByRole not supported or failed
|
|
4320
4337
|
}
|
|
4321
4338
|
|
|
4322
4339
|
try {
|
|
4323
|
-
els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
|
|
4340
|
+
els = await withVisibleLocator(matcher.getByRole('link', { name: matchedLocator.value })).all()
|
|
4324
4341
|
if (els.length) return els
|
|
4325
4342
|
} catch (err) {
|
|
4326
4343
|
// getByRole not supported or failed
|
|
@@ -4389,17 +4406,6 @@ async function findCheckable(locator, context) {
|
|
|
4389
4406
|
return findElements.call(this, contextEl, matchedLocator)
|
|
4390
4407
|
}
|
|
4391
4408
|
|
|
4392
|
-
for (const exact of [true, false]) {
|
|
4393
|
-
for (const role of checkableRoles) {
|
|
4394
|
-
try {
|
|
4395
|
-
const roleEls = await contextEl.getByRole(role, { name: matchedLocator.value, exact }).all()
|
|
4396
|
-
if (roleEls.length) return roleEls
|
|
4397
|
-
} catch (err) {
|
|
4398
|
-
// getByRole not supported or failed
|
|
4399
|
-
}
|
|
4400
|
-
}
|
|
4401
|
-
}
|
|
4402
|
-
|
|
4403
4409
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
4404
4410
|
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
4405
4411
|
if (els.length) {
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -64,7 +64,6 @@ function wrapError(e) {
|
|
|
64
64
|
let perfTiming
|
|
65
65
|
const popupStore = new Popup()
|
|
66
66
|
const consoleLogStore = new Console()
|
|
67
|
-
const checkableRoles = ['checkbox', 'radio', 'switch']
|
|
68
67
|
|
|
69
68
|
/**
|
|
70
69
|
* ## Configuration
|
|
@@ -846,6 +845,9 @@ class Puppeteer extends Helper {
|
|
|
846
845
|
}
|
|
847
846
|
}
|
|
848
847
|
|
|
848
|
+
if (!(await el.isIntersectingViewport({ threshold: 1 }))) {
|
|
849
|
+
await el.evaluate(el => el.scrollIntoView({ block: 'center', inline: 'center' }))
|
|
850
|
+
}
|
|
849
851
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
850
852
|
const { x, y } = await getClickablePoint(el)
|
|
851
853
|
await this.page.mouse.move(x + offsetX, y + offsetY)
|
|
@@ -3196,19 +3198,8 @@ async function findCheckable(locator, context) {
|
|
|
3196
3198
|
return findElements.call(this, contextEl, matchedLocator)
|
|
3197
3199
|
}
|
|
3198
3200
|
|
|
3199
|
-
// Try ARIA selector for accessible name
|
|
3200
|
-
let els
|
|
3201
|
-
for (const role of checkableRoles) {
|
|
3202
|
-
try {
|
|
3203
|
-
els = await contextEl.$$(`::-p-aria([name="${matchedLocator.value}"][role="${role}"])`)
|
|
3204
|
-
if (els.length) return els
|
|
3205
|
-
} catch (err) {
|
|
3206
|
-
// ARIA selector not supported or failed
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
|
|
3210
3201
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
3211
|
-
els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
3202
|
+
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
3212
3203
|
if (els.length) {
|
|
3213
3204
|
return els
|
|
3214
3205
|
}
|
|
@@ -3217,6 +3208,14 @@ async function findCheckable(locator, context) {
|
|
|
3217
3208
|
return els
|
|
3218
3209
|
}
|
|
3219
3210
|
|
|
3211
|
+
// Try ARIA selector for accessible name
|
|
3212
|
+
try {
|
|
3213
|
+
els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`)
|
|
3214
|
+
if (els.length) return els
|
|
3215
|
+
} catch (err) {
|
|
3216
|
+
// ARIA selector not supported or failed
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3220
3219
|
return findElements.call(this, contextEl, matchedLocator.value)
|
|
3221
3220
|
}
|
|
3222
3221
|
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -3248,14 +3248,6 @@ async function findCheckable(locator, locateFn) {
|
|
|
3248
3248
|
if (locator.isRole()) return locateFn(locator, true)
|
|
3249
3249
|
if (!locator.isFuzzy()) return locateFn(locator, true)
|
|
3250
3250
|
|
|
3251
|
-
// Try ARIA selector for accessible name
|
|
3252
|
-
try {
|
|
3253
|
-
els = await keepCheckable.call(this, await locateFn(`aria/${locator.value}`))
|
|
3254
|
-
if (els.length) return els
|
|
3255
|
-
} catch (e) {
|
|
3256
|
-
// ARIA selector not supported or failed
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
3251
|
const literal = xpathLocator.literal(locator.value)
|
|
3260
3252
|
els = await locateFn(Locator.checkable.byText(literal))
|
|
3261
3253
|
if (els.length) return els
|
|
@@ -3263,23 +3255,17 @@ async function findCheckable(locator, locateFn) {
|
|
|
3263
3255
|
els = await locateFn(Locator.checkable.byName(literal))
|
|
3264
3256
|
if (els.length) return els
|
|
3265
3257
|
|
|
3258
|
+
// Try ARIA selector for accessible name
|
|
3259
|
+
try {
|
|
3260
|
+
els = await locateFn(`aria/${locator.value}`)
|
|
3261
|
+
if (els.length) return els
|
|
3262
|
+
} catch (e) {
|
|
3263
|
+
// ARIA selector not supported or failed
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
3266
|
return await locateFn(locator.value) // by css or xpath
|
|
3267
3267
|
}
|
|
3268
3268
|
|
|
3269
|
-
async function keepCheckable(els) {
|
|
3270
|
-
if (!els || !els.length) return []
|
|
3271
|
-
|
|
3272
|
-
const checkable = await this.browser.execute(function () {
|
|
3273
|
-
return Array.prototype.slice.call(arguments).map(function (el) {
|
|
3274
|
-
if (!el) return false
|
|
3275
|
-
const role = el.getAttribute('role')
|
|
3276
|
-
if (role) return ['checkbox', 'radio', 'switch'].indexOf(role) > -1
|
|
3277
|
-
return el.tagName === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio')
|
|
3278
|
-
})
|
|
3279
|
-
}, ...els)
|
|
3280
|
-
|
|
3281
|
-
return els.filter((el, index) => checkable[index])
|
|
3282
|
-
}
|
|
3283
3269
|
|
|
3284
3270
|
function withStrictLocator(locator) {
|
|
3285
3271
|
locator = new Locator(locator)
|
|
@@ -387,6 +387,17 @@ export default function installCodeceptClient(xpathNeedsPolyfill) {
|
|
|
387
387
|
return true
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
if (resolveRole(el) === 'radiogroup') {
|
|
391
|
+
if (values.length > 1) return '__RADIOGROUP_MULTI__'
|
|
392
|
+
const radios = Array.from(el.querySelectorAll('[role="radio"]'))
|
|
393
|
+
const [wanted] = values
|
|
394
|
+
const named = (radio, matchFn) => roleTextCandidates(radio).some(matchFn)
|
|
395
|
+
const radio = radios.find(r => named(r, t => t === wanted)) || radios.find(r => named(r, t => t.indexOf(wanted) !== -1))
|
|
396
|
+
if (!radio) return false
|
|
397
|
+
radio.click()
|
|
398
|
+
return true
|
|
399
|
+
}
|
|
400
|
+
|
|
390
401
|
// ARIA combobox/listbox widgets: click the trigger (if any) to reveal the
|
|
391
402
|
// listbox, then click each matching [role="option"].
|
|
392
403
|
let container = el
|
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
import Locator from '../../locator.js'
|
|
2
2
|
|
|
3
|
+
export function splitXPath(xpath) {
|
|
4
|
+
if (typeof xpath !== 'string' || xpath.length === 0) return []
|
|
5
|
+
const withoutRoot = xpath.startsWith('//') ? xpath.slice(1) : xpath
|
|
6
|
+
return withoutRoot.split('/').filter(Boolean)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isAncestorXPath(ancestor, descendant) {
|
|
10
|
+
if (!ancestor || !descendant || ancestor === descendant) return false
|
|
11
|
+
const ancestorSegments = splitXPath(ancestor)
|
|
12
|
+
const descendantSegments = splitXPath(descendant)
|
|
13
|
+
if (ancestorSegments.length === 0 || ancestorSegments.length >= descendantSegments.length) return false
|
|
14
|
+
return ancestorSegments.every((segment, index) => segment === descendantSegments[index])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function computeParents(entries) {
|
|
18
|
+
const parents = new Array(entries.length).fill(-1)
|
|
19
|
+
const stack = []
|
|
20
|
+
for (let i = 0; i < entries.length; i++) {
|
|
21
|
+
const xpath = entries[i].xpath
|
|
22
|
+
if (!xpath) continue
|
|
23
|
+
while (stack.length > 0 && !isAncestorXPath(entries[stack[stack.length - 1]].xpath, xpath)) {
|
|
24
|
+
stack.pop()
|
|
25
|
+
}
|
|
26
|
+
parents[i] = stack.length > 0 ? stack[stack.length - 1] : -1
|
|
27
|
+
stack.push(i)
|
|
28
|
+
}
|
|
29
|
+
return parents
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function computeDepths(entries) {
|
|
33
|
+
const parents = computeParents(entries)
|
|
34
|
+
return parents.map((parent, i) => {
|
|
35
|
+
if (!entries[i].xpath) return 0
|
|
36
|
+
let depth = 0
|
|
37
|
+
let current = parent
|
|
38
|
+
while (current !== -1) {
|
|
39
|
+
depth++
|
|
40
|
+
current = parents[current]
|
|
41
|
+
}
|
|
42
|
+
return depth
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatTree(entries, depths, parents) {
|
|
47
|
+
return entries.map((entry, i) => {
|
|
48
|
+
const pad = ' '.repeat(depths[i] || 0)
|
|
49
|
+
if (entry.error) {
|
|
50
|
+
return `${pad} ${entry.index}. [Unable to get element info: ${entry.error}]`
|
|
51
|
+
}
|
|
52
|
+
const parentPos = parents ? parents[i] : -1
|
|
53
|
+
const nesting = parentPos !== undefined && parentPos !== -1 ? ` (inside ${entries[parentPos].index}.)` : ''
|
|
54
|
+
return `${pad} ${entry.index}.${nesting} > ${entry.xpath}\n${pad} ${entry.html}`
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
3
58
|
class MultipleElementsFound extends Error {
|
|
4
59
|
constructor(locator, webElements) {
|
|
5
60
|
const locatorStr = (typeof locator === 'object' && !(locator instanceof Locator))
|
|
@@ -17,7 +72,7 @@ class MultipleElementsFound extends Error {
|
|
|
17
72
|
if (this._detailsFetched) return
|
|
18
73
|
|
|
19
74
|
try {
|
|
20
|
-
const
|
|
75
|
+
const entries = []
|
|
21
76
|
const maxToShow = Math.min(this.count, 10)
|
|
22
77
|
|
|
23
78
|
for (let i = 0; i < maxToShow; i++) {
|
|
@@ -25,12 +80,14 @@ class MultipleElementsFound extends Error {
|
|
|
25
80
|
try {
|
|
26
81
|
const xpath = await webEl.toAbsoluteXPath()
|
|
27
82
|
const html = await webEl.toSimplifiedHTML()
|
|
28
|
-
|
|
83
|
+
entries.push({ index: i + 1, xpath, html })
|
|
29
84
|
} catch (err) {
|
|
30
|
-
|
|
85
|
+
entries.push({ index: i + 1, error: err.message })
|
|
31
86
|
}
|
|
32
87
|
}
|
|
33
88
|
|
|
89
|
+
const items = formatTree(entries, computeDepths(entries), computeParents(entries))
|
|
90
|
+
|
|
34
91
|
if (this.count > 10) {
|
|
35
92
|
items.push(` ... and ${this.count - 10} more`)
|
|
36
93
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
function findByPlaywrightLocator(matcher, locator) {
|
|
2
2
|
const pwLocator = locator.locator || locator
|
|
3
3
|
if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
|
|
4
4
|
return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
|
|
5
5
|
}
|
|
6
6
|
const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
|
|
7
|
-
return matcher.locator(pwValue)
|
|
7
|
+
return matcher.locator(pwValue)
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export { findByPlaywrightLocator }
|
package/lib/locator.js
CHANGED
|
@@ -649,6 +649,9 @@ Locator.field = {
|
|
|
649
649
|
]),
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
+
const checkable = `self::input[@type = 'checkbox' or @type = 'radio'] or @role = 'checkbox' or @role = 'radio' or @role = 'switch'`
|
|
653
|
+
const visibleCheckable = `.//*[${checkable}][not(@aria-hidden = 'true')]`
|
|
654
|
+
|
|
652
655
|
Locator.checkable = {
|
|
653
656
|
/**
|
|
654
657
|
* @param {string} literal
|
|
@@ -656,8 +659,10 @@ Locator.checkable = {
|
|
|
656
659
|
*/
|
|
657
660
|
byText: literal =>
|
|
658
661
|
xpathLocator.combine([
|
|
659
|
-
|
|
660
|
-
`.//label[contains(normalize-space(string(.)), ${literal})]
|
|
662
|
+
`${visibleCheckable}[(@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
|
|
663
|
+
`.//label[contains(normalize-space(string(.)), ${literal})]//*[${checkable}][not(@aria-hidden = 'true')]`,
|
|
664
|
+
`${visibleCheckable}[@aria-labelledby = //*[@id][contains(normalize-space(string(.)), ${literal})]/@id]`,
|
|
665
|
+
`${visibleCheckable}[@aria-label = ${literal}]`,
|
|
661
666
|
]),
|
|
662
667
|
|
|
663
668
|
/**
|
|
@@ -4,7 +4,7 @@ import recorder from '../recorder.js'
|
|
|
4
4
|
import assertThrown from '../assert/throws.js'
|
|
5
5
|
import { ucfirst, isAsyncFunction } from '../utils.js'
|
|
6
6
|
import { getInjectedArguments } from './inject.js'
|
|
7
|
-
import { fireHook } from './hooks.js'
|
|
7
|
+
import { fireHook, BeforeSuiteHook, AfterSuiteHook } from './hooks.js'
|
|
8
8
|
|
|
9
9
|
const injectHook = function (inject, suite) {
|
|
10
10
|
try {
|
|
@@ -232,6 +232,10 @@ export function suiteSetup(suite) {
|
|
|
232
232
|
|
|
233
233
|
// Set up error handler for suite setup
|
|
234
234
|
recorder.errHandler(err => {
|
|
235
|
+
// A helper's `_beforeSuite()` runs through this hook, not through the
|
|
236
|
+
// `injected()` wrapper, so nothing here used to emit `hook.failed` and
|
|
237
|
+
// reporters listening for it never saw the failure. (#5660)
|
|
238
|
+
event.emit(event.hook.failed, new BeforeSuiteHook(suite, err))
|
|
235
239
|
doneFn(err)
|
|
236
240
|
})
|
|
237
241
|
|
|
@@ -254,6 +258,8 @@ export function suiteTeardown(suite) {
|
|
|
254
258
|
|
|
255
259
|
// Set up error handler for suite teardown
|
|
256
260
|
recorder.errHandler(err => {
|
|
261
|
+
// Same for a helper's `_afterSuite()`. (#5660)
|
|
262
|
+
event.emit(event.hook.failed, new AfterSuiteHook(suite, err))
|
|
257
263
|
doneFn(err)
|
|
258
264
|
})
|
|
259
265
|
|
package/lib/step/config.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @property {boolean} [exact] - Enable strict mode for this step. Throws if multiple elements match.
|
|
5
5
|
* @property {boolean} [strictMode] - Alias for exact.
|
|
6
6
|
* @property {boolean} [ignoreCase] - Perform case-insensitive text matching.
|
|
7
|
+
* @property {boolean} [visibleLocator] - Match only visible elements. Overrides the Playwright helper `visibleLocator` config option for this step.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
/**
|
package/lib/store.js
CHANGED
|
@@ -93,6 +93,12 @@ const store = {
|
|
|
93
93
|
/** @type {CodeceptJS.Suite | null} */
|
|
94
94
|
currentSuite: null,
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Locators match only visible elements, resolved per step
|
|
98
|
+
* @type {boolean}
|
|
99
|
+
*/
|
|
100
|
+
visibleLocator: false,
|
|
101
|
+
|
|
96
102
|
/** @type {Map<string, string> | null} */
|
|
97
103
|
tsFileMapping: null,
|
|
98
104
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeceptjs",
|
|
3
|
-
"version": "4.2.0-beta.
|
|
3
|
+
"version": "4.2.0-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Supercharged End 2 End Testing Framework for NodeJS",
|
|
6
6
|
"keywords": [
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
"jsdoc": "^3.6.11",
|
|
178
178
|
"jsdoc-typeof-plugin": "1.0.0",
|
|
179
179
|
"json-server": "0.17.4",
|
|
180
|
-
"playwright": "^1.
|
|
180
|
+
"playwright": "^1.63.0",
|
|
181
181
|
"prettier": "^3.3.2",
|
|
182
182
|
"puppeteer": "24.36.0",
|
|
183
183
|
"qrcode-terminal": "0.12.0",
|
|
@@ -3429,9 +3429,9 @@ declare namespace CodeceptJS {
|
|
|
3429
3429
|
* never spawns or kills anything, no matter what `binaryPath`/`port` are set to.
|
|
3430
3430
|
* - **SELF-LAUNCH** — `endpoint` is unset and a binary can be resolved, in order: `binaryPath` in
|
|
3431
3431
|
* the config, then the `OBSCURA_PATH` environment variable, then `obscura` on `PATH`. The helper
|
|
3432
|
-
* spawns `obscura serve --port <port> --allow-private-network` (`port` from
|
|
3433
|
-
* free port picked automatically), waits for it to answer, connects, and kills
|
|
3434
|
-
* `_finishTest`.
|
|
3432
|
+
* spawns `obscura serve --port <port> --allow-private-network --allow-file-access` (`port` from
|
|
3433
|
+
* the config, or a free port picked automatically), waits for it to answer, connects, and kills
|
|
3434
|
+
* it in `_finishTest`.
|
|
3435
3435
|
* - **COURTESY-ATTACH** — `endpoint` is unset and no binary can be resolved, but something already
|
|
3436
3436
|
* answers `http://127.0.0.1:9222/json/version` (e.g. `obscura serve` started by hand, or by CI
|
|
3437
3437
|
* before this process ever ran). The helper attaches to it and never kills it — it isn't the
|
|
@@ -3444,12 +3444,13 @@ declare namespace CodeceptJS {
|
|
|
3444
3444
|
* it directly) and the helper launches and tears it down for you automatically:
|
|
3445
3445
|
*
|
|
3446
3446
|
* ```sh
|
|
3447
|
-
* curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.
|
|
3447
|
+
* curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.2/obscura-x86_64-linux.tar.gz | tar xz
|
|
3448
3448
|
* ```
|
|
3449
3449
|
*
|
|
3450
|
-
* `--allow-private-network`
|
|
3451
|
-
* on `localhost`/private IPs, e.g. a dev server on
|
|
3452
|
-
*
|
|
3450
|
+
* `--allow-private-network` and `--allow-file-access` are always passed by this helper: the first
|
|
3451
|
+
* is required to reach apps running on `localhost`/private IPs, e.g. a dev server on
|
|
3452
|
+
* `127.0.0.1:8000`, the second to let `attachFile` upload local files. Obscura blocks both by
|
|
3453
|
+
* default.
|
|
3453
3454
|
*
|
|
3454
3455
|
* ## Config presets
|
|
3455
3456
|
*
|
|
@@ -3468,7 +3469,7 @@ declare namespace CodeceptJS {
|
|
|
3468
3469
|
* ## Limitations
|
|
3469
3470
|
*
|
|
3470
3471
|
* - `input` is always `synthetic`, even on rendering builds — see `input` above.
|
|
3471
|
-
* - No frames
|
|
3472
|
+
* - No frames or popups.
|
|
3472
3473
|
* - On `-no-render` builds and v0.1.x: no screenshots, no visibility assertions
|
|
3473
3474
|
* (`seeElement`/`dontSeeElement` always throw) — only DOM presence
|
|
3474
3475
|
* (`seeElementInDOM`/`dontSeeElementInDOM`) is meaningful without a layout engine.
|
|
@@ -3607,6 +3608,7 @@ declare namespace CodeceptJS {
|
|
|
3607
3608
|
* @property [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
3608
3609
|
* @property [bypassCSP] - bypass Content Security Policy or CSP
|
|
3609
3610
|
* @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
3611
|
+
* @property [visibleLocator = false] - append [`visible()`](https://playwright.dev/docs/api/class-locator#locator-visible) to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to steps that must reach hidden elements: `grab*` methods, `scrollTo`, `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found.
|
|
3610
3612
|
* @property [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
3611
3613
|
* @property [testIdAttribute = data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
3612
3614
|
* @property [storageState] - Playwright storage state (path to JSON file or object)
|
|
@@ -3651,6 +3653,7 @@ declare namespace CodeceptJS {
|
|
|
3651
3653
|
ignoreHTTPSErrors?: boolean;
|
|
3652
3654
|
bypassCSP?: boolean;
|
|
3653
3655
|
highlightElement?: boolean;
|
|
3656
|
+
visibleLocator?: boolean;
|
|
3654
3657
|
recordHar?: any;
|
|
3655
3658
|
testIdAttribute?: string;
|
|
3656
3659
|
storageState?: string | any;
|
package/typings/types.d.ts
CHANGED
|
@@ -3462,9 +3462,9 @@ declare namespace CodeceptJS {
|
|
|
3462
3462
|
* never spawns or kills anything, no matter what `binaryPath`/`port` are set to.
|
|
3463
3463
|
* - **SELF-LAUNCH** — `endpoint` is unset and a binary can be resolved, in order: `binaryPath` in
|
|
3464
3464
|
* the config, then the `OBSCURA_PATH` environment variable, then `obscura` on `PATH`. The helper
|
|
3465
|
-
* spawns `obscura serve --port <port> --allow-private-network` (`port` from
|
|
3466
|
-
* free port picked automatically), waits for it to answer, connects, and kills
|
|
3467
|
-
* `_finishTest`.
|
|
3465
|
+
* spawns `obscura serve --port <port> --allow-private-network --allow-file-access` (`port` from
|
|
3466
|
+
* the config, or a free port picked automatically), waits for it to answer, connects, and kills
|
|
3467
|
+
* it in `_finishTest`.
|
|
3468
3468
|
* - **COURTESY-ATTACH** — `endpoint` is unset and no binary can be resolved, but something already
|
|
3469
3469
|
* answers `http://127.0.0.1:9222/json/version` (e.g. `obscura serve` started by hand, or by CI
|
|
3470
3470
|
* before this process ever ran). The helper attaches to it and never kills it — it isn't the
|
|
@@ -3477,12 +3477,13 @@ declare namespace CodeceptJS {
|
|
|
3477
3477
|
* it directly) and the helper launches and tears it down for you automatically:
|
|
3478
3478
|
*
|
|
3479
3479
|
* ```sh
|
|
3480
|
-
* curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.
|
|
3480
|
+
* curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.2/obscura-x86_64-linux.tar.gz | tar xz
|
|
3481
3481
|
* ```
|
|
3482
3482
|
*
|
|
3483
|
-
* `--allow-private-network`
|
|
3484
|
-
* on `localhost`/private IPs, e.g. a dev server on
|
|
3485
|
-
*
|
|
3483
|
+
* `--allow-private-network` and `--allow-file-access` are always passed by this helper: the first
|
|
3484
|
+
* is required to reach apps running on `localhost`/private IPs, e.g. a dev server on
|
|
3485
|
+
* `127.0.0.1:8000`, the second to let `attachFile` upload local files. Obscura blocks both by
|
|
3486
|
+
* default.
|
|
3486
3487
|
*
|
|
3487
3488
|
* ## Config presets
|
|
3488
3489
|
*
|
|
@@ -3501,7 +3502,7 @@ declare namespace CodeceptJS {
|
|
|
3501
3502
|
* ## Limitations
|
|
3502
3503
|
*
|
|
3503
3504
|
* - `input` is always `synthetic`, even on rendering builds — see `input` above.
|
|
3504
|
-
* - No frames
|
|
3505
|
+
* - No frames or popups.
|
|
3505
3506
|
* - On `-no-render` builds and v0.1.x: no screenshots, no visibility assertions
|
|
3506
3507
|
* (`seeElement`/`dontSeeElement` always throw) — only DOM presence
|
|
3507
3508
|
* (`seeElementInDOM`/`dontSeeElementInDOM`) is meaningful without a layout engine.
|
|
@@ -3641,6 +3642,7 @@ declare namespace CodeceptJS {
|
|
|
3641
3642
|
* @property [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
3642
3643
|
* @property [bypassCSP] - bypass Content Security Policy or CSP
|
|
3643
3644
|
* @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
3645
|
+
* @property [visibleLocator = false] - append [`visible()`](https://playwright.dev/docs/api/class-locator#locator-visible) to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to steps that must reach hidden elements: `grab*` methods, `scrollTo`, `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found.
|
|
3644
3646
|
* @property [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
3645
3647
|
* @property [testIdAttribute = data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
3646
3648
|
* @property [storageState] - Playwright storage state (path to JSON file or object)
|
|
@@ -3685,6 +3687,7 @@ declare namespace CodeceptJS {
|
|
|
3685
3687
|
ignoreHTTPSErrors?: boolean;
|
|
3686
3688
|
bypassCSP?: boolean;
|
|
3687
3689
|
highlightElement?: boolean;
|
|
3690
|
+
visibleLocator?: boolean;
|
|
3688
3691
|
recordHar?: any;
|
|
3689
3692
|
testIdAttribute?: string;
|
|
3690
3693
|
storageState?: string | any;
|
|
@@ -11054,12 +11057,14 @@ declare namespace CodeceptJS {
|
|
|
11054
11057
|
* @property [exact] - Enable strict mode for this step. Throws if multiple elements match.
|
|
11055
11058
|
* @property [strictMode] - Alias for exact.
|
|
11056
11059
|
* @property [ignoreCase] - Perform case-insensitive text matching.
|
|
11060
|
+
* @property [visibleLocator] - Match only visible elements. Overrides the Playwright helper `visibleLocator` config option for this step.
|
|
11057
11061
|
*/
|
|
11058
11062
|
type StepOptions = {
|
|
11059
11063
|
elementIndex?: number | 'first' | 'last';
|
|
11060
11064
|
exact?: boolean;
|
|
11061
11065
|
strictMode?: boolean;
|
|
11062
11066
|
ignoreCase?: boolean;
|
|
11067
|
+
visibleLocator?: boolean;
|
|
11063
11068
|
};
|
|
11064
11069
|
/**
|
|
11065
11070
|
* StepConfig is a configuration object for a step.
|
|
@@ -11169,6 +11174,10 @@ declare namespace CodeceptJS {
|
|
|
11169
11174
|
var currentTest: CodeceptJS.Test | null;
|
|
11170
11175
|
var currentStep: CodeceptJS.Step | null;
|
|
11171
11176
|
var currentSuite: CodeceptJS.Suite | null;
|
|
11177
|
+
/**
|
|
11178
|
+
* Locators match only visible elements, resolved per step
|
|
11179
|
+
*/
|
|
11180
|
+
var visibleLocator: boolean;
|
|
11172
11181
|
var tsFileMapping: Map<string, string> | null;
|
|
11173
11182
|
/**
|
|
11174
11183
|
* Initialize required store fields.
|