From 5a666b86c23166786a22bbb3f022c649d59a7e64 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Wed, 23 Sep 2026 17:05:21 +0300 Subject: [PATCH 1/2] docs: include inherited CDPBrowser methods in Obscura and Kitesurf pages Helper docs are built with `documentation --shallow`, so subclasses only listed their own methods. Generalize the Appium/WebDriver merge into `inheritedHelperDocs` and apply it to Obscura and Kitesurf (parent CDPBrowser), including parent config properties. Build all docs/build files before generating markdown so a parent is never stale, and run the merged output through the same post-processing (fixes leftover "(optional, default ...)" in Appium.md). CDPBrowser is abstract: stop generating its page and drop links to it. Co-Authored-By: Claude Opus 5.5 (1M context) --- Bunoshfile.js | 78 +- docs/alternative-browsers.md | 16 +- docs/helpers/Appium.md | 2233 ++++++++++++++++++++++++---------- docs/helpers/CDPBrowser.md | 2201 --------------------------------- docs/helpers/Kitesurf.md | 2134 +++++++++++++++++++++++++++++++- docs/helpers/Obscura.md | 2159 +++++++++++++++++++++++++++++++- lib/helper/Obscura.js | 3 +- runok.cjs | 76 +- 8 files changed, 5944 insertions(+), 2956 deletions(-) delete mode 100644 docs/helpers/CDPBrowser.md diff --git a/Bunoshfile.js b/Bunoshfile.js index 4fa00cc66..94cf919e8 100644 --- a/Bunoshfile.js +++ b/Bunoshfile.js @@ -249,10 +249,9 @@ export async function docsHelpers() { const sharedPlaceholders = sharedPartials.map(file => `{{ ${path.basename(file, '.mustache')} }}`) const sharedTemplates = sharedPartials.map(file => fs.readFileSync(`docs/shared/${file}`).toString()).map(template => `\n\n\n${template}`) - for (const file of files) { - const name = path.basename(file, '.js') - if (ignoreList.indexOf(name) >= 0) continue - say(`Writing documentation for ${name}`) + const helperFiles = files.filter(file => ignoreList.indexOf(path.basename(file, '.js')) < 0) + + for (const file of helperFiles) { copyFile(`lib/helper/${file}`, `docs/build/${file}`) replaceInFile(`docs/build/${file}`, cfg => { for (const i in placeholders) { @@ -286,8 +285,18 @@ export async function docsHelpers() { cfg.replace(/^export\s*\{\s*([^}]+)\s*\}/gm, 'module.exports = { $1 }') cfg.replace(/^export\s+(class|function|const|let|var)\s+([^\s=]+)/gm, '$1 $2') }) + } + + for (const file of helperFiles) { + const name = path.basename(file, '.js') + if (abstractHelpers.includes(name)) continue + say(`Writing documentation for ${name}`) - await shell`npx documentation build docs/build/${file} -o docs/helpers/${name}.md ${documentjsCliArgs}` + if (inheritedHelperDocs[name]) { + await docsInheritedHelper(name, inheritedHelperDocs[name]) + } else { + await shell`npx documentation build docs/build/${file} -o docs/helpers/${name}.md ${documentjsCliArgs}` + } replaceInFile(helperMarkDownFile(name), cfg => { cfg.replace(/\(optional, default.*?\)/gm, '') cfg.replace(/\\*/gm, '') @@ -309,10 +318,6 @@ export async function docsHelpers() { cfg.replace(regex, '[1]') }) - if (name === 'Appium') { - await docsAppium() - } - await writeToFile(helperMarkDownFile(name), line => { line`--- permalink: /helpers/${name} @@ -392,28 +397,43 @@ export async function wiki() { }) } -/** - * Generate docs for Appium by merging in public WebDriver methods. - */ -export async function docsAppium() { - const documentation = await import('documentation') - const onlyWeb = [/Title/, /Popup/, /Cookie/, /Url/, /^press/, /^refreshPage/, /^resizeWindow/, /Script$/, /cursor/, /Css/, /Tab$/, /^wait/] - const webdriverDoc = await documentation.build(['docs/build/WebDriver.js'], { - shallow: true, - order: 'asc', - }) - const doc = await documentation.build(['docs/build/Appium.js'], { - shallow: true, - order: 'asc', - }) +const inheritedHelperDocs = { + Appium: { + parent: 'WebDriver', + exclude: [/Title/, /Popup/, /Cookie/, /Url/, /^press/, /^refreshPage/, /^resizeWindow/, /Script$/, /cursor/, /Css/, /Tab$/, /^wait/], + }, + Obscura: { parent: 'CDPBrowser' }, + Kitesurf: { parent: 'CDPBrowser', excludeConfig: ['endpoint', 'headers'] }, +} + +const abstractHelpers = ['CDPBrowser'] - for (const method of webdriverDoc[0].members.instance) { - if (onlyWeb.filter(f => method.name.match(f)).length) continue - if (doc[0].members.instance.filter(m => m.name === method.name).length) continue - doc[0].members.instance.push(method) +async function docsInheritedHelper(name, { parent, exclude = [], excludeConfig = [] }) { + const documentation = await import('documentation') + const buildOptions = { shallow: true, sortOrder: ['alpha'] } + const parentDoc = await documentation.build([`docs/build/${parent}.js`], buildOptions) + const doc = await documentation.build([`docs/build/${name}.js`], buildOptions) + const members = doc[0].members.instance + + for (const method of parentDoc[0].members.instance) { + if (exclude.some(f => method.name.match(f))) continue + if (members.some(m => m.name === method.name)) continue + members.push(method) } - const output = await documentation.formats.md(doc) - fs.writeFileSync('docs/helpers/Appium.md', output) + members.sort((a, b) => a.name.localeCompare(b.name)) + + const config = doc.find(c => c.name === 'config') + const parentConfig = parentDoc.find(c => c.name === 'config') + if (config && parentConfig) { + for (const prop of parentConfig.properties) { + if (excludeConfig.includes(prop.name)) continue + if (config.properties.some(p => p.name === prop.name)) continue + config.properties.push(prop) + } + } + + const output = await documentation.formats.md(doc, { markdownToc: false }) + fs.writeFileSync(helperMarkDownFile(name), output) } /** diff --git a/docs/alternative-browsers.md b/docs/alternative-browsers.md index 978128acc..f3ea7dd8e 100644 --- a/docs/alternative-browsers.md +++ b/docs/alternative-browsers.md @@ -6,12 +6,12 @@ title: Alternative Browser Engines # Alternative Browser Engines ::: warning Experimental -The `CDPBrowser`, `Obscura`, and `Kitesurf` helpers are experimental in CodeceptJS 4.2. Pin browser versions in CI and retain Playwright or WebDriver coverage for compatibility-critical tests. +The `Obscura` and `Kitesurf` helpers are experimental in CodeceptJS 4.2. Pin browser versions in CI and retain Playwright or WebDriver coverage for compatibility-critical tests. ::: Playwright and Puppeteer drive full Chromium — the most accurate way to test what users see. But a new class of lightweight, agent-era browsers has appeared, and CodeceptJS can drive them -through the `CDPBrowser` helper family: +through dedicated helpers: - **[Obscura](https://github.com/h4ckf0r0day/obscura)** — an open-source Rust browser with a real V8 engine. From v0.2.0, the default release build also renders — real layout, computed styles, @@ -96,15 +96,6 @@ CodeceptJS 4.2 is tested in CI with Obscura 0.2.2. Obscura 0.2.x is recommended; }, } -Any other CDP endpoint works through the base helper: - - helpers: { - CDPBrowser: { - url: 'http://localhost:3000', - endpoint: 'http://127.0.0.1:9222', - }, - } - ### Obscura's three connection modes Obscura manages its own `obscura serve` process, the same way Playwright manages its own browser @@ -154,5 +145,4 @@ process — there is nothing to start by hand in the common case: | Where it runs | local/grid | local | Cloudflare only | | License / cost | open source | Apache-2.0 | proprietary, free beta | -See helper reference pages: [CDPBrowser](/helpers/CDPBrowser), [Obscura](/helpers/Obscura), -[Kitesurf](/helpers/Kitesurf). +See helper reference pages: [Obscura](/helpers/Obscura), [Kitesurf](/helpers/Kitesurf). diff --git a/docs/helpers/Appium.md b/docs/helpers/Appium.md index 4e10dbda3..c552028a3 100644 --- a/docs/helpers/Appium.md +++ b/docs/helpers/Appium.md @@ -38,7 +38,7 @@ This helper should be configured in codecept.conf.ts or codecept.conf.js * `port`: (default: '4723') Appium port * `platform`: (Android or IOS), which mobile OS to use; alias to desiredCapabilities.platformName * `restart`: restart browser or app between tests (default: true), if set to false cookies will be cleaned but browser window will be kept and for apps nothing will be changed. -* `desiredCapabilities`: \[], Appium capabilities, see below +* `desiredCapabilities`: [], Appium capabilities, see below * `platformName` - Which mobile OS platform to use * `appPackage` - Java package of the Android app you want to run * `appActivity` - Activity name for the Android activity you want to launch from your package. @@ -159,724 +159,754 @@ let browser = this.helpers['Appium'].browser * `config` -### runOnIOS +### _isShadowLocator -Execute code only on iOS +Check if locator is type of "Shadow" -```js -I.runOnIOS(() => { - I.click('//UIAApplication[1]/UIAWindow[1]/UIAButton[1]'); - I.see('Hi, IOS', '~welcome'); -}); -``` +#### Parameters -Additional filter can be applied by checking for capabilities. -For instance, this code will be executed only on iPhone 5s: +* `locator` **[object][5]** -```js -I.runOnIOS({deviceName: 'iPhone 5s'},() => { - // ... -}); -``` +### _locate -Also capabilities can be checked by a function. +Get elements by different locator types, including strict locator. +Should be used in custom helpers: ```js -I.runOnAndroid((caps) => { - // caps is current config of desiredCapabiliites - return caps.platformVersion >= 6 -},() => { - // ... -}); +this.helpers['WebDriver']._locate({name: 'password'}).then //... ``` #### Parameters -* `caps` **any** -* `fn` **any** +* `locator` **([string][6] | [object][5])** element located by CSS|XPath|strict locator. +* `smartWait` -### runOnAndroid +### _locateByRole -Execute code only on Android +Locate elements by ARIA role using WebdriverIO accessibility selectors -```js -I.runOnAndroid(() => { - I.click('io.selendroid.testapp:id/buttonTest'); -}); -``` +#### Parameters -Additional filter can be applied by checking for capabilities. -For instance, this code will be executed only on Android 6.0: +* `locator` **[object][5]** role locator object { role: string, text?: string, exact?: boolean } -```js -I.runOnAndroid({platformVersion: '6.0'},() => { - // ... -}); -``` +### _locateCheckable -Also capabilities can be checked by a function. -In this case, code will be executed only on Android >= 6. +Find a checkbox by providing human-readable text: ```js -I.runOnAndroid((caps) => { - // caps is current config of desiredCapabiliites - return caps.platformVersion >= 6 -},() => { - // ... -}); +this.helpers['WebDriver']._locateCheckable('I agree with terms and conditions').then // ... ``` #### Parameters -* `caps` **any** -* `fn` **any** +* `locator` **([string][6] | [object][5])** element located by CSS|XPath|strict locator. -### runInWeb +### _locateClickable -Execute code only in Web mode. +Find a clickable element by providing human-readable text: ```js -I.runInWeb(() => { - I.waitForElement('#data'); - I.seeInCurrentUrl('/data'); -}); +const els = await this.helpers.WebDriver._locateClickable('Next page'); +const els = await this.helpers.WebDriver._locateClickable('Next page', '.pages'); ``` -### checkIfAppIsInstalled +#### Parameters -Returns app installation status. +* `locator` **([string][6] | [object][5])** element located by CSS|XPath|strict locator. +* `context` + +### _locateFields + +Find field elements by providing human-readable text: ```js -I.checkIfAppIsInstalled("com.example.android.apis"); +this.helpers['WebDriver']._locateFields('Your email').then // ... ``` #### Parameters -* `bundleId` **[string][5]** String ID of bundled app +* `locator` **([string][6] | [object][5])** element located by CSS|XPath|strict locator. -Returns **[Promise][6]<[boolean][7]>** Appium: support only Android +### _locateShadow -### seeAppIsInstalled +Locate Element within the Shadow Dom -Check if an app is installed. +#### Parameters -```js -I.seeAppIsInstalled("com.example.android.apis"); -``` +* `locator` **[object][5]** -#### Parameters +### _smartWait -* `bundleId` **[string][5]** String ID of bundled app +Smart Wait to locate an element -Returns **[Promise][6]\** Appium: support only Android +#### Parameters -### seeAppIsNotInstalled +* `locator` **[object][5]** -Check if an app is not installed. +### amOnPage + +Opens a web page in a browser. Requires relative or absolute url. +If url starts with `/`, opens a web page of a site defined in `url` config parameter. ```js -I.seeAppIsNotInstalled("com.example.android.apis"); +I.amOnPage('/'); // opens main page of website +I.amOnPage('https://github.com'); // opens github +I.amOnPage('/login'); // opens a login page ``` #### Parameters -* `bundleId` **[string][5]** String ID of bundled app +* `url` **[string][6]** url path or global url. -Returns **[Promise][6]\** Appium: support only Android +Returns **void** automatically synchronized promise through #recorder -### installApp +### appendField -Install an app on device. +Appends text to a input field or textarea. +Field is located by name, label, CSS or XPath + +The third parameter is an optional context (CSS or XPath locator) to narrow the search. ```js -I.installApp('/path/to/file.apk'); +I.appendField('#myTextField', 'appended'); +// typing secret +I.appendField('password', secret('123456')); +// within a context +I.appendField('name', 'John', '.form-container'); ``` #### Parameters -* `path` **[string][5]** path to apk file +* `field` **([string][6] | [object][5])** located by label|name|CSS|XPath|strict locator +* `value` **[string][6]** text value to append. +* `context` **([string][6]? | [object][5])** (optional, `null` by default) element located by CSS | XPath | strict locator. -Returns **[Promise][6]\** Appium: support only Android +Returns **void** automatically synchronized promise through #recorder -### removeApp +### attachFile -Remove an app from the device. +Appium: not tested + +Attaches a file to element located by label, name, CSS or XPath +Path to file is relative current codecept directory (where codecept.conf.ts or codecept.conf.js is located). +File will be uploaded to remote system (if tests are running remotely). + +The third parameter is an optional context (CSS or XPath locator) to narrow the search. ```js -I.removeApp('appName', 'com.example.android.apis'); +I.attachFile('Avatar', 'data/avatar.jpg'); +I.attachFile('form input[name=avatar]', 'data/avatar.jpg'); +// within a context +I.attachFile('Avatar', 'data/avatar.jpg', '.form-container'); ``` -Appium: support only Android +If the locator points to a non-file-input element (e.g., a dropzone area), +the file will be dropped onto that element using drag-and-drop events. + +```js +I.attachFile('#dropzone', 'data/avatar.jpg'); +``` #### Parameters -* `appId` **[string][5]** -* `bundleId` **[string][5]?** ID of bundle +* `locator` **([string][6] | [object][5])** field located by label|name|CSS|XPath|strict locator. +* `pathToFile` **[string][6]** local file path relative to codecept.conf.ts or codecept.conf.js config file. +* `context` **([string][6]? | [object][5])** (optional, `null` by default) element located by CSS | XPath | strict locator. -### resetApp +Returns **void** automatically synchronized promise through #recorder -Reset the currently running app for current session. +### blur -```js -I.resetApp(); -``` +Remove focus from a text input, button, etc. +Calls [blur][7] on the element. -### seeCurrentActivityIs +Examples: -Check current activity on an Android device. +```js +I.blur('.text-area') +``` ```js -I.seeCurrentActivityIs(".HomeScreenActivity") +//element `#product-tile` is focused +I.see('#add-to-cart-btn'); +I.blur('#product-tile') +I.dontSee('#add-to-cart-btn'); ``` #### Parameters -* `currentActivity` **[string][5]** +* `locator` **([string][6] | [object][5])** field located by label|name|CSS|XPath|strict locator. +* `options` **any?** Playwright only: [Additional options][8] for available options object as 2nd argument. -Returns **[Promise][6]\** Appium: support only Android +Returns **void** automatically synchronized promise through #recorder -### seeDeviceIsLocked +### checkIfAppIsInstalled -Check whether the device is locked. +Returns app installation status. ```js -I.seeDeviceIsLocked(); +I.checkIfAppIsInstalled("com.example.android.apis"); ``` -Returns **[Promise][6]\** Appium: support only Android - -### seeDeviceIsUnlocked +#### Parameters -Check whether the device is not locked. +* `bundleId` **[string][6]** String ID of bundled app -```js -I.seeDeviceIsUnlocked(); -``` +Returns **[Promise][9]<[boolean][10]>** Appium: support only Android -Returns **[Promise][6]\** Appium: support only Android +### checkOption -### seeOrientationIs +Selects a checkbox or radio button. +Element is located by label or name or CSS or XPath. -Check the device orientation +The second parameter is an optional context (CSS or XPath locator) to narrow the search. ```js -I.seeOrientationIs('PORTRAIT'); -I.seeOrientationIs('LANDSCAPE') +I.checkOption('#agree'); +I.checkOption('I Agree to Terms and Conditions'); +I.checkOption('agree', '//form'); ``` #### Parameters -* `orientation` **(`"LANDSCAPE"` | `"PORTRAIT"`)** LANDSCAPE or PORTRAITAppium: support Android and iOS +* `field` **([string][6] | [object][5])** checkbox located by label | name | CSS | XPath | strict locator. +* `context` **([string][6]? | [object][5])** (optional, `null` by default) element located by CSS | XPath | strict locator. -Returns **[Promise][6]\** +Returns **void** automatically synchronized promise through #recorder -### setOrientation +### clearClipboard -Set a device orientation. Will fail, if app will not set orientation +Clears the system clipboard. ```js -I.setOrientation('PORTRAIT'); -I.setOrientation('LANDSCAPE') +I.clearClipboard(); +I.seeClipboardEquals(''); ``` -#### Parameters +Returns **void** automatically synchronized promise through #recorderAppium: support both Android and iOS -* `orientation` **(`"LANDSCAPE"` | `"PORTRAIT"`)** LANDSCAPE or PORTRAITAppium: support Android and iOS +### clearField -### grabAllContexts +Clears a `