diff --git a/.github/workflows/obscura.yml b/.github/workflows/obscura.yml index 06c4221ce..7a3d7aac7 100644 --- a/.github/workflows/obscura.yml +++ b/.github/workflows/obscura.yml @@ -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: diff --git a/docs/helpers/Obscura.md b/docs/helpers/Obscura.md index cf0004830..29be715d0 100644 --- a/docs/helpers/Obscura.md +++ b/docs/helpers/Obscura.md @@ -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 --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 --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 @@ -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 @@ -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. diff --git a/lib/helper/CDPBrowser.js b/lib/helper/CDPBrowser.js index 1b21c9a8b..2121f9235 100644 --- a/lib/helper/CDPBrowser.js +++ b/lib/helper/CDPBrowser.js @@ -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()}`) } @@ -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( @@ -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}`) } } diff --git a/lib/helper/Obscura.js b/lib/helper/Obscura.js index 41377565f..4854548f7 100644 --- a/lib/helper/Obscura.js +++ b/lib/helper/Obscura.js @@ -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 --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 --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 @@ -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 * @@ -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. @@ -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 }) diff --git a/lib/helper/clientscripts/cdpBrowserClient.js b/lib/helper/clientscripts/cdpBrowserClient.js index 0dd09dc52..80a31a268 100644 --- a/lib/helper/clientscripts/cdpBrowserClient.js +++ b/lib/helper/clientscripts/cdpBrowserClient.js @@ -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 diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 5d89be690..5d224d57c 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -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') @@ -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')