Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/obscura.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ permissions:
env:
CI: true
FORCE_COLOR: 1
OBSCURA_VERSION: v0.2.0
OBSCURA_SHA256: d601f4f542319c3b9fa8dca9f5ccfc134a2ca001648da528db5f03c9e6c2599b
OBSCURA_VERSION: v0.2.2
OBSCURA_SHA256: 9e5d9d081909ea983bc8c94999bb3d411fd6b74a9788504295b7e25f84310505

jobs:
build:
Expand Down
17 changes: 9 additions & 8 deletions docs/helpers/Obscura.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ process lifecycle, the same way Playwright manages its own browser process.
never spawns or kills anything, no matter what `binaryPath`/`port` are set to.
* **SELF-LAUNCH** — `endpoint` is unset and a binary can be resolved, in order: `binaryPath` in
the config, then the `OBSCURA_PATH` environment variable, then `obscura` on `PATH`. The helper
spawns `obscura serve --port <port> --allow-private-network` (`port` from the config, or a
free port picked automatically), waits for it to answer, connects, and kills it in
`_finishTest`.
spawns `obscura serve --port <port> --allow-private-network --allow-file-access` (`port` from
the config, or a free port picked automatically), waits for it to answer, connects, and kills
it in `_finishTest`.
* **COURTESY-ATTACH** — `endpoint` is unset and no binary can be resolved, but something already
answers `http://127.0.0.1:9222/json/version` (e.g. `obscura serve` started by hand, or by CI
before this process ever ran). The helper attaches to it and never kills it — it isn't the
Expand All @@ -43,12 +43,13 @@ Download a release binary and put it on your `PATH` (or point `binaryPath`/`OBSC
it directly) and the helper launches and tears it down for you automatically:

```sh
curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.0/obscura-x86_64-linux.tar.gz | tar xz
curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.2/obscura-x86_64-linux.tar.gz | tar xz
```

`--allow-private-network` is always passed by this helper (it's required to reach apps running
on `localhost`/private IPs, e.g. a dev server on `127.0.0.1:8000` — Obscura blocks
private-network requests by default).
`--allow-private-network` and `--allow-file-access` are always passed by this helper: the first
is required to reach apps running on `localhost`/private IPs, e.g. a dev server on
`127.0.0.1:8000`, the second to let `attachFile` upload local files. Obscura blocks both by
default.

## Config presets

Expand All @@ -67,7 +68,7 @@ Set them explicitly in your own config to skip probing or to force a mode.
## Limitations

* `input` is always `synthetic`, even on rendering builds — see `input` above.
* No frames, popups, or file uploads.
* No frames or popups.
* On `-no-render` builds and v0.1.x: no screenshots, no visibility assertions
(`seeElement`/`dontSeeElement` always throw) — only DOM presence
(`seeElementInDOM`/`dontSeeElementInDOM`) is meaningful without a layout engine.
Expand Down
6 changes: 4 additions & 2 deletions lib/helper/CDPBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,9 @@ class CDPBrowser extends Helper {
const value = Array.isArray(option) ? option.map(String) : String(option)
const res = await this._run(this._candidates(select, 'field'), 'select', { value }, context)
if (!res.found) throw new ElementNotFound(select, 'Selectable field')
if (res.result === '__RADIOGROUP_MULTI__') {
throw new Error(`selectOption: a radio group holds one value, but ${value.length} options were passed: ${value.join(', ')}`)
}
if (res.result === false) throw new Error(`Option "${Array.isArray(option) ? option.join(',') : option}" not found in ${new Locator(select).toString()}`)
}

Expand Down Expand Up @@ -1810,7 +1813,6 @@ class CDPBrowser extends Helper {
*/
async waitInUrl(urlPart, sec = null) {
const timeout = sec || this.options.waitForTimeout
const expectedUrl = resolveUrl(urlPart, this.options.url)
let lastUrl = ''
try {
return await this._poll(
Expand All @@ -1822,7 +1824,7 @@ class CDPBrowser extends Helper {
'placeholder',
)
} catch (e) {
throw new Error(`expected url to include ${expectedUrl}, but found ${lastUrl}`)
throw new Error(`expected url to include ${urlPart}, but found ${lastUrl}`)
}
}

Expand Down
19 changes: 10 additions & 9 deletions lib/helper/Obscura.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ const config = {}
* never spawns or kills anything, no matter what `binaryPath`/`port` are set to.
* - **SELF-LAUNCH** — `endpoint` is unset and a binary can be resolved, in order: `binaryPath` in
* the config, then the `OBSCURA_PATH` environment variable, then `obscura` on `PATH`. The helper
* spawns `obscura serve --port <port> --allow-private-network` (`port` from the config, or a
* free port picked automatically), waits for it to answer, connects, and kills it in
* `_finishTest`.
* spawns `obscura serve --port <port> --allow-private-network --allow-file-access` (`port` from
* the config, or a free port picked automatically), waits for it to answer, connects, and kills
* it in `_finishTest`.
* - **COURTESY-ATTACH** — `endpoint` is unset and no binary can be resolved, but something already
* answers `http://127.0.0.1:9222/json/version` (e.g. `obscura serve` started by hand, or by CI
* before this process ever ran). The helper attaches to it and never kills it — it isn't the
Expand All @@ -60,12 +60,13 @@ const config = {}
* it directly) and the helper launches and tears it down for you automatically:
*
* ```sh
* curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.0/obscura-x86_64-linux.tar.gz | tar xz
* curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.2/obscura-x86_64-linux.tar.gz | tar xz
* ```
*
* `--allow-private-network` is always passed by this helper (it's required to reach apps running
* on `localhost`/private IPs, e.g. a dev server on `127.0.0.1:8000` — Obscura blocks
* private-network requests by default).
* `--allow-private-network` and `--allow-file-access` are always passed by this helper: the first
* is required to reach apps running on `localhost`/private IPs, e.g. a dev server on
* `127.0.0.1:8000`, the second to let `attachFile` upload local files. Obscura blocks both by
* default.
*
* ## Config presets
*
Expand All @@ -84,7 +85,7 @@ const config = {}
* ## Limitations
*
* - `input` is always `synthetic`, even on rendering builds — see `input` above.
* - No frames, popups, or file uploads.
* - No frames or popups.
* - On `-no-render` builds and v0.1.x: no screenshots, no visibility assertions
* (`seeElement`/`dontSeeElement` always throw) — only DOM presence
* (`seeElementInDOM`/`dontSeeElementInDOM`) is meaningful without a layout engine.
Expand Down Expand Up @@ -179,7 +180,7 @@ class Obscura extends CDPBrowser {
const port = this.options.port || (await this._findFreePort())
this.options.port = port
this.serverError = null
this.serverProcess = spawn(binaryPath, ['serve', '--port', String(port), '--allow-private-network'], { stdio: 'ignore' })
this.serverProcess = spawn(binaryPath, ['serve', '--port', String(port), '--allow-private-network', '--allow-file-access'], { stdio: 'ignore' })
this.serverProcess.on('error', err => {
this.serverError = err
})
Expand Down
11 changes: 11 additions & 0 deletions lib/helper/clientscripts/cdpBrowserClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,17 @@ export default function installCodeceptClient(xpathNeedsPolyfill) {
return true
}

if (resolveRole(el) === 'radiogroup') {
if (values.length > 1) return '__RADIOGROUP_MULTI__'
const radios = Array.from(el.querySelectorAll('[role="radio"]'))
const [wanted] = values
const named = (radio, matchFn) => roleTextCandidates(radio).some(matchFn)
const radio = radios.find(r => named(r, t => t === wanted)) || radios.find(r => named(r, t => t.indexOf(wanted) !== -1))
if (!radio) return false
radio.click()
return true
}

// ARIA combobox/listbox widgets: click the trigger (if any) to reveal the
// listbox, then click each matching [role="option"].
let container = el
Expand Down
7 changes: 6 additions & 1 deletion test/helper/webapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,8 @@ export function tests() {
await I.seeCheckboxIsChecked('Airplane mode')
})

it('checks a radio by its label', async () => {
it('checks a radio by its label', async function () {
if (page === 'baseui' && isHelper('Obscura')) this.skip()
await open(page)
await I.dontSeeCheckboxIsChecked('Comfortable')

Expand Down Expand Up @@ -753,6 +754,10 @@ export function tests() {

for (const page of Object.keys(pages)) {
describe(page, () => {
beforeEach(function () {
if (page === 'baseui' && isHelper('Obscura')) this.skip()
})

it('checks the radio matching the option and unchecks its siblings', async () => {
await open(page)
await I.selectOption('Density', 'Compact')
Expand Down
Loading