diff --git a/Bunoshfile.js b/Bunoshfile.js index 4fa00cc66..12a8d5682 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,14 @@ 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') }) + } - await shell`npx documentation build docs/build/${file} -o docs/helpers/${name}.md ${documentjsCliArgs}` + for (const file of helperFiles) { + const name = path.basename(file, '.js') + if (abstractHelpers.includes(name)) continue + say(`Writing documentation for ${name}`) + + await docsHelperMarkdown(name, inheritedHelperDocs[name]) replaceInFile(helperMarkDownFile(name), cfg => { cfg.replace(/\(optional, default.*?\)/gm, '') cfg.replace(/\\*/gm, '') @@ -309,10 +314,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 +393,65 @@ export async function wiki() { }) } -/** - * Generate docs for Appium by merging in public WebDriver methods. - */ -export async function docsAppium() { +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'] + +const helperHooks = [ + '_init', + '_before', + '_after', + '_beforeStep', + '_afterStep', + '_beforeSuite', + '_afterSuite', + '_passed', + '_failed', + '_finishTest', + '_setConfig', + '_validateConfig', + '_test', + '_useTo', +] + +async function docsHelperMarkdown(name, { parent, exclude = [], excludeConfig = [] } = {}) { 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 buildOptions = { shallow: true, sortOrder: ['alpha'] } + const doc = await documentation.build([`docs/build/${name}.js`], buildOptions) + let members = doc[0].members.instance + + if (parent) { + const parentDoc = await documentation.build([`docs/build/${parent}.js`], buildOptions) + 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) + } - 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) + 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) - fs.writeFileSync('docs/helpers/Appium.md', output) + + members = members.filter(m => !helperHooks.includes(m.name)) + members.sort((a, b) => a.name.startsWith('_') - b.name.startsWith('_') || a.name.localeCompare(b.name)) + doc[0].members.instance = members + + 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/ApiDataFactory.md b/docs/helpers/ApiDataFactory.md index d76af53e0..7642fd3d7 100644 --- a/docs/helpers/ApiDataFactory.md +++ b/docs/helpers/ApiDataFactory.md @@ -192,26 +192,6 @@ By default `id` property of response is taken. This behavior can be changed by s * `config` -### _requestCreate - -Executes request to create a record in API. -Can be replaced from a in custom helper. - -#### Parameters - -* `factory` **any** -* `data` **any** - -### _requestDelete - -Executes request to delete a record in API -Can be replaced from a custom helper. - -#### Parameters - -* `factory` **any** -* `id` **any** - ### have Generates a new record using factory and saves API request to store it. @@ -256,6 +236,26 @@ I.haveMultiple('post', 3, { author: 'davert' }, { publish_date: '01.01.1997' }); * `params` **any?** * `options` **any?** +### _requestCreate + +Executes request to create a record in API. +Can be replaced from a in custom helper. + +#### Parameters + +* `factory` **any** +* `data` **any** + +### _requestDelete + +Executes request to delete a record in API +Can be replaced from a custom helper. + +#### Parameters + +* `factory` **any** +* `id` **any** + [1]: https://github.com/rosiejs/rosie [2]: https://www.npmjs.com/package/faker diff --git a/docs/helpers/Appium.md b/docs/helpers/Appium.md index 4e10dbda3..cae96849b 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,89 +159,103 @@ let browser = this.helpers['Appium'].browser * `config` -### runOnIOS +### amOnPage -Execute code only on iOS +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.runOnIOS(() => { - I.click('//UIAApplication[1]/UIAWindow[1]/UIAButton[1]'); - I.see('Hi, IOS', '~welcome'); -}); +I.amOnPage('/'); // opens main page of website +I.amOnPage('https://github.com'); // opens github +I.amOnPage('/login'); // opens a login page ``` -Additional filter can be applied by checking for capabilities. -For instance, this code will be executed only on iPhone 5s: +#### Parameters -```js -I.runOnIOS({deviceName: 'iPhone 5s'},() => { - // ... -}); -``` +* `url` **[string][5]** url path or global url. -Also capabilities can be checked by a function. +Returns **void** automatically synchronized promise through #recorder + +### appendField + +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.runOnAndroid((caps) => { - // caps is current config of desiredCapabiliites - return caps.platformVersion >= 6 -},() => { - // ... -}); +I.appendField('#myTextField', 'appended'); +// typing secret +I.appendField('password', secret('123456')); +// within a context +I.appendField('name', 'John', '.form-container'); ``` #### Parameters -* `caps` **any** -* `fn` **any** +* `field` **([string][5] | [object][6])** located by label|name|CSS|XPath|strict locator +* `value` **[string][5]** text value to append. +* `context` **([string][5]? | [object][6])** (optional, `null` by default) element located by CSS | XPath | strict locator. -### runOnAndroid +Returns **void** automatically synchronized promise through #recorder -Execute code only on Android +### attachFile -```js -I.runOnAndroid(() => { - I.click('io.selendroid.testapp:id/buttonTest'); -}); -``` +Appium: not tested -Additional filter can be applied by checking for capabilities. -For instance, this code will be executed only on Android 6.0: +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.runOnAndroid({platformVersion: '6.0'},() => { - // ... -}); +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'); ``` -Also capabilities can be checked by a function. -In this case, code will be executed only on Android >= 6. +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.runOnAndroid((caps) => { - // caps is current config of desiredCapabiliites - return caps.platformVersion >= 6 -},() => { - // ... -}); +I.attachFile('#dropzone', 'data/avatar.jpg'); ``` #### Parameters -* `caps` **any** -* `fn` **any** +* `locator` **([string][5] | [object][6])** field located by label|name|CSS|XPath|strict locator. +* `pathToFile` **[string][5]** local file path relative to codecept.conf.ts or codecept.conf.js config file. +* `context` **([string][5]? | [object][6])** (optional, `null` by default) element located by CSS | XPath | strict locator. -### runInWeb +Returns **void** automatically synchronized promise through #recorder -Execute code only in Web mode. +### blur + +Remove focus from a text input, button, etc. +Calls [blur][7] on the element. + +Examples: ```js -I.runInWeb(() => { - I.waitForElement('#data'); - I.seeInCurrentUrl('/data'); -}); +I.blur('.text-area') ``` +```js +//element `#product-tile` is focused +I.see('#add-to-cart-btn'); +I.blur('#product-tile') +I.dontSee('#add-to-cart-btn'); +``` + +#### Parameters + +* `locator` **([string][5] | [object][6])** field located by label|name|CSS|XPath|strict locator. +* `options` **any?** Playwright only: [Additional options][8] for available options object as 2nd argument. + +Returns **void** automatically synchronized promise through #recorder + ### checkIfAppIsInstalled Returns app installation status. @@ -254,1159 +268,2048 @@ I.checkIfAppIsInstalled("com.example.android.apis"); * `bundleId` **[string][5]** String ID of bundled app -Returns **[Promise][6]<[boolean][7]>** Appium: support only Android +Returns **[Promise][9]<[boolean][10]>** Appium: support only Android -### seeAppIsInstalled +### checkOption -Check if an app is installed. +Selects a checkbox or radio button. +Element is located by label or name or CSS or XPath. + +The second parameter is an optional context (CSS or XPath locator) to narrow the search. ```js -I.seeAppIsInstalled("com.example.android.apis"); +I.checkOption('#agree'); +I.checkOption('I Agree to Terms and Conditions'); +I.checkOption('agree', '//form'); ``` #### Parameters -* `bundleId` **[string][5]** String ID of bundled app +* `field` **([string][5] | [object][6])** checkbox located by label | name | CSS | XPath | strict locator. +* `context` **([string][5]? | [object][6])** (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 -### seeAppIsNotInstalled +### clearClipboard -Check if an app is not installed. +Clears the system clipboard. ```js -I.seeAppIsNotInstalled("com.example.android.apis"); +I.clearClipboard(); +I.seeClipboardEquals(''); ``` -#### Parameters - -* `bundleId` **[string][5]** String ID of bundled app +Returns **void** automatically synchronized promise through #recorderAppium: support both Android and iOS -Returns **[Promise][6]\** Appium: support only Android +### clearField -### installApp +Clears a `