diff --git a/.eslintrc.js b/.eslintrc.js index 8b8c9405f..b38236895 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -43,6 +43,21 @@ module.exports = { 'no-loop-func': 0, 'arrow-parens': 0, 'default-param-last': 0, + semi: 0, + 'operator-linebreak': 0, + 'nonblock-statement-body-position': 0, + curly: 0, + 'implicit-arrow-linebreak': 0, + indent: 0, + 'object-curly-newline': 0, + 'semi-style': 0, + 'function-paren-newline': 0, + 'prefer-template': 0, + 'newline-per-chained-call': 0, + 'prefer-arrow-callback': 0, + 'no-bitwise': 0, + 'prefer-const': 0, + 'no-extra-semi': 0, }, ignorePatterns: ['test/data/output', 'lib/css2xpath/*'], }; diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 35aaaf72b..4dfcda903 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,7 +17,7 @@ jobs: - name: Check out the repo with latest code uses: actions/checkout@v4 - name: Push latest to Docker Hub - uses: docker/build-push-action@v4 # Info: https://github.com/docker/build-push-action/tree/releases/v1#tags + uses: docker/build-push-action@v6 # Info: https://github.com/docker/build-push-action/tree/releases/v1#tags with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} @@ -36,7 +36,7 @@ jobs: ref: ${{ env.TAG }} - name: Push current tag to Docker Hub - uses: docker/build-push-action@v1 # Info: https://github.com/docker/build-push-action/tree/releases/v1#tags + uses: docker/build-push-action@v6 # Info: https://github.com/docker/build-push-action/tree/releases/v1#tags with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} diff --git a/.husky/pre-commit b/.husky/pre-commit index 1eda87251..635d81b70 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npm run lint && npm run dtslint +npm run prettier && npm run lint && npm run dtslint diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml deleted file mode 100644 index 7b7bafd6b..000000000 --- a/.semaphore/semaphore.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: v1.0 -name: Initial Pipeline -agent: - machine: - type: e1-standard-2 - os_image: ubuntu1804 -blocks: - - name: Appium tests - task: - secrets: - - name: saucelabs - prologue: - commands: - - checkout - - npm i - - cache restore - jobs: - - name: Quick tests - commands: - - 'npm run test:appium-quick' - - name: Other Tests - commands: - - 'npm run test:appium-other' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d1e748b2..7767540bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## 3.6.5 + +❤️ Thanks all to those who contributed to make this release! ❤️ + +🛩️ *Features* +* feat(helper): playwright > wait for disabled (#4412) - by @kobenguyent +``` +it('should wait for input text field to be disabled', () => + I.amOnPage('/form/wait_disabled').then(() => I.waitForDisabled('#text', 1))) + + it('should wait for input text field to be enabled by xpath', () => + I.amOnPage('/form/wait_disabled').then(() => I.waitForDisabled("//*[@name = 'test']", 1))) + + it('should wait for a button to be disabled', () => + I.amOnPage('/form/wait_disabled').then(() => I.waitForDisabled('#text', 1))) + +Waits for element to become disabled (by default waits for 1sec). +Element can be located by CSS or XPath. + +@param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator. +@param {number} [sec=1] (optional) time in seconds to wait, 1 by default. +@returns {void} automatically synchronized promise through #recorder +``` + +🐛 *Bug Fixes* +* fix(AI): AI is not triggered (#4422) - by @kobenguyent +* fix(plugin): stepByStep > report doesn't sync properly (#4413) - by @kobenguyent +* fix: Locator > Unsupported pseudo selector 'has' (#4448) - by @anils92 + +📖 *Documentation* +* docs: setup azure open ai using bearer token (#4434) - by @kobenguyent + ## 3.6.4 ❤️ Thanks all to those who contributed to make this release! ❤️ @@ -19,7 +51,7 @@ REST: { › [CURL Request] curl --location --request POST https://httpbin.org/post -H ... ``` -* feat(AI): Generate PageObject, added types, shell improvement (#4319) - by @davert +* feat(AI): Generate PageObject, added types, shell improvement (#4319) - by @DavertMik * added `askForPageObject` method to generate PageObjects on the fly * improved AI types * interactive shell improved to restore history @@ -30,7 +62,7 @@ REST: { * fix(heal): wrong priority (#4394) - by @kobenguyent 📖 *Documentation* -* AI docs improvements +* AI docs improvements by @DavertMik ## 3.6.3 diff --git a/README.md b/README.md index b483e3c80..4f5406735 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ Build Status: Appium Helper: [![Appium V2 Tests - Android](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appiumV2_Android.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appiumV2_Android.yml) -[![Appium V2 Tests - iOS](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appiumV2_iOS.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appiumV2_iOS.yml) Web Helper: [![Playwright Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml) diff --git a/bin/codecept.js b/bin/codecept.js index 9477d809c..9a0ff7db0 100755 --- a/bin/codecept.js +++ b/bin/codecept.js @@ -1,8 +1,8 @@ #!/usr/bin/env node -const program = require('commander'); -const Codecept = require('../lib/codecept'); -const { print, error } = require('../lib/output'); -const { printError } = require('../lib/command/utils'); +const program = require('commander') +const Codecept = require('../lib/codecept') +const { print, error } = require('../lib/output') +const { printError } = require('../lib/command/utils') const commandFlags = { ai: { @@ -29,104 +29,120 @@ const commandFlags = { flag: '--steps', description: 'show step-by-step execution', }, -}; - -const errorHandler = (fn) => async (...args) => { - try { - await fn(...args); - } catch (e) { - printError(e); - process.exitCode = 1; +} + +const errorHandler = + (fn) => + async (...args) => { + try { + await fn(...args) + } catch (e) { + printError(e) + process.exitCode = 1 + } } -}; if (process.versions.node && process.versions.node.split('.') && process.versions.node.split('.')[0] < 12) { - error('NodeJS >= 12 is required to run.'); - print(); - print('Please upgrade your NodeJS engine'); - print(`Current NodeJS version: ${process.version}`); - process.exit(1); + error('NodeJS >= 12 is required to run.') + print() + print('Please upgrade your NodeJS engine') + print(`Current NodeJS version: ${process.version}`) + process.exit(1) } -program.usage(' [options]'); -program.version(Codecept.version()); +program.usage(' [options]') +program.version(Codecept.version()) -program.command('init [path]') +program + .command('init [path]') .description('Creates dummy config in current dir or [path]') - .action(errorHandler(require('../lib/command/init'))); + .action(errorHandler(require('../lib/command/init'))) -program.command('migrate [path]') +program + .command('migrate [path]') .description('Migrate json config to js config in current dir or [path]') - .action(errorHandler(require('../lib/command/configMigrate'))); + .action(errorHandler(require('../lib/command/configMigrate'))) -program.command('shell [path]') +program + .command('shell [path]') .alias('sh') .description('Interactive shell') .option(commandFlags.verbose.flag, commandFlags.verbose.description) .option(commandFlags.profile.flag, commandFlags.profile.description) .option(commandFlags.ai.flag, commandFlags.ai.description) .option(commandFlags.config.flag, commandFlags.config.description) - .action(errorHandler(require('../lib/command/interactive'))); + .action(errorHandler(require('../lib/command/interactive'))) -program.command('list [path]') +program + .command('list [path]') .alias('l') .description('List all actions for I.') - .action(errorHandler(require('../lib/command/list'))); + .action(errorHandler(require('../lib/command/list'))) -program.command('def [path]') +program + .command('def [path]') .description('Generates TypeScript definitions for all I actions.') .option(commandFlags.config.flag, commandFlags.config.description) .option('-o, --output [folder]', 'target folder to paste definitions') - .action(errorHandler(require('../lib/command/definitions'))); + .action(errorHandler(require('../lib/command/definitions'))) -program.command('gherkin:init [path]') +program + .command('gherkin:init [path]') .alias('bdd:init') .description('Prepare CodeceptJS to run feature files.') .option(commandFlags.config.flag, commandFlags.config.description) - .action(errorHandler(require('../lib/command/gherkin/init'))); + .action(errorHandler(require('../lib/command/gherkin/init'))) -program.command('gherkin:steps [path]') +program + .command('gherkin:steps [path]') .alias('bdd:steps') .description('Prints all defined gherkin steps.') .option(commandFlags.config.flag, commandFlags.config.description) - .action(errorHandler(require('../lib/command/gherkin/steps'))); + .action(errorHandler(require('../lib/command/gherkin/steps'))) -program.command('gherkin:snippets [path]') +program + .command('gherkin:snippets [path]') .alias('bdd:snippets') .description('Generate step definitions from steps.') .option('--dry-run', "don't save snippets to file") .option(commandFlags.config.flag, commandFlags.config.description) .option('--feature [file]', 'feature files(s) to scan') .option('--path [file]', 'file in which to place the new snippets') - .action(errorHandler(require('../lib/command/gherkin/snippets'))); + .action(errorHandler(require('../lib/command/gherkin/snippets'))) -program.command('generate:test [path]') +program + .command('generate:test [path]') .alias('gt') .description('Generates an empty test') - .action(errorHandler(require('../lib/command/generate').test)); + .action(errorHandler(require('../lib/command/generate').test)) -program.command('generate:pageobject [path]') +program + .command('generate:pageobject [path]') .alias('gpo') .description('Generates an empty page object') - .action(errorHandler(require('../lib/command/generate').pageObject)); + .action(errorHandler(require('../lib/command/generate').pageObject)) -program.command('generate:object [path]') +program + .command('generate:object [path]') .alias('go') .option('--type, -t [kind]', 'type of object to be created') .description('Generates an empty support object (page/step/fragment)') - .action(errorHandler(require('../lib/command/generate').pageObject)); + .action(errorHandler(require('../lib/command/generate').pageObject)) -program.command('generate:helper [path]') +program + .command('generate:helper [path]') .alias('gh') .description('Generates a new helper') - .action(errorHandler(require('../lib/command/generate').helper)); + .action(errorHandler(require('../lib/command/generate').helper)) -program.command('generate:heal [path]') +program + .command('generate:heal [path]') .alias('gr') .description('Generates basic heal recipes') - .action(errorHandler(require('../lib/command/generate').heal)); + .action(errorHandler(require('../lib/command/generate').heal)) -program.command('run [test]') +program + .command('run [test]') .description('Executes tests') // codecept-only options @@ -162,9 +178,10 @@ program.command('run [test]') .option('--recursive', 'include sub directories') .option('--trace', 'trace function calls') .option('--child ', 'option for child processes') - .action(errorHandler(require('../lib/command/run'))); + .action(errorHandler(require('../lib/command/run'))) -program.command('run-workers [selectedRuns...]') +program + .command('run-workers [selectedRuns...]') .description('Executes tests in workers') .option(commandFlags.config.flag, commandFlags.config.description) .option('-g, --grep ', 'only run tests matching ') @@ -180,9 +197,10 @@ program.command('run-workers [selectedRuns...]') .option('-p, --plugins ', 'enable plugins, comma-separated') .option('-O, --reporter-options ', 'reporter-specific options') .option('-R, --reporter ', 'specify the reporter to use') - .action(errorHandler(require('../lib/command/run-workers'))); + .action(errorHandler(require('../lib/command/run-workers'))) -program.command('run-multiple [suites...]') +program + .command('run-multiple [suites...]') .description('Executes tests multiple') .option(commandFlags.config.flag, commandFlags.config.description) .option(commandFlags.profile.flag, commandFlags.profile.description) @@ -205,14 +223,16 @@ program.command('run-multiple [suites...]') // mocha options .option('--colors', 'force enabling of colors') - .action(errorHandler(require('../lib/command/run-multiple'))); + .action(errorHandler(require('../lib/command/run-multiple'))) -program.command('info [path]') +program + .command('info [path]') .description('Print debugging information concerning the local environment') .option('-c, --config', 'your config file path') - .action(errorHandler(require('../lib/command/info'))); + .action(errorHandler(require('../lib/command/info'))) -program.command('dry-run [test]') +program + .command('dry-run [test]') .description('Prints step-by-step scenario for a test without actually running it') .option('-p, --plugins ', 'enable plugins, comma-separated') .option('--bootstrap', 'enable bootstrap & teardown scripts for dry-run') @@ -226,9 +246,10 @@ program.command('dry-run [test]') .option(commandFlags.steps.flag, commandFlags.steps.description) .option(commandFlags.verbose.flag, commandFlags.verbose.description) .option(commandFlags.debug.flag, commandFlags.debug.description) - .action(errorHandler(require('../lib/command/dryRun'))); + .action(errorHandler(require('../lib/command/dryRun'))) -program.command('run-rerun [test]') +program + .command('run-rerun [test]') .description('Executes tests in more than one test suite run') // codecept-only options @@ -263,15 +284,15 @@ program.command('run-rerun [test]') .option('--trace', 'trace function calls') .option('--child ', 'option for child processes') - .action(require('../lib/command/run-rerun')); + .action(require('../lib/command/run-rerun')) program.on('command:*', (cmd) => { - console.log(`\nUnknown command ${cmd}\n`); - program.outputHelp(); -}); + console.log(`\nUnknown command ${cmd}\n`) + program.outputHelp() +}) if (process.argv.length <= 2) { - program.outputHelp(); + program.outputHelp() } else { - program.parse(process.argv); + program.parse(process.argv) } diff --git a/docs/ai.md b/docs/ai.md index a06c3b243..5d99712d1 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -154,6 +154,8 @@ ai: { #### Azure OpenAI +When your setup using Azure API key + Prerequisite: * Install `@azure/openai` package @@ -175,7 +177,88 @@ ai: { } ``` +When your setup using `bearer token` + +Prerequisite: + +* Install `@azure/openai`, `@azure/identity` packages +* obtain `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET` + +```js +ai: { + request: async (messages) => { + try { + const { OpenAIClient} = require("@azure/openai"); + const { DefaultAzureCredential } = require("@azure/identity"); + + const endpoint = process.env.API_ENDPOINT; + const deploymentId = process.env.DEPLOYMENT_ID; + + const client = new OpenAIClient(endpoint, new DefaultAzureCredential()); + const result = await client.getCompletions(deploymentId, { + prompt: messages, + model: 'gpt-3.5-turbo' // your preferred model + }); + + return result.choices[0]?.text; + } catch (error) { + console.error("Error calling API:", error); + throw error; + } + } +} +``` + +Or you could try with direct API request + +```js +ai: { + request: async (messages) => { + try { + const endpoint = process.env.API_ENDPOINT; + const deploymentId = process.env.DEPLOYMENT_ID; + const result = await makeApiRequest(endpoint, deploymentId, messages) + + return result.choices[0]?.message.content + } catch (error) { + console.error("Error calling API:", error); + throw error; + } + } +} +... + +async function getAccessToken() { + const credential = new DefaultAzureCredential(); + const scope = "https://cognitiveservices.azure.com/.default"; + + try { + const accessToken = await credential.getToken(scope); + return `Bearer ${accessToken.token}`; + } catch (err) { + console.error("Failed to get access token:", err); + } +} + +async function makeApiRequest(endpoint, deploymentId, messages) { + const token = await getAccessToken(); + const url = `${endpoint}/openai/deployments/${deploymentId}/chat/completions?api-version=2024-06-01`; + + const data = { messages }; + + try { + const response = await axios.post(url, data, { + headers: { + 'Authorization': `${token}` + } + }); + return response.data + } catch (err) { + console.error("API request failed:", err.response); + } +} +``` ### Writing Tests with AI Copilot @@ -490,4 +573,4 @@ or if you run it in shell mode: ``` DEBUG="codeceptjs:ai" npx codeceptjs shell --ai -``` \ No newline at end of file +``` diff --git a/docs/changelog.md b/docs/changelog.md index a2ecb9c23..7ef0f19da 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,40 @@ layout: Section # Releases +## 3.6.4 + +❤️ Thanks all to those who contributed to make this release! ❤️ + +🛩️ *Features* +* feat(rest): print curl ([#4396](https://github.com/codeceptjs/CodeceptJS/issues/4396)) - by **[kobenguyent](https://github.com/kobenguyent)** + +``` +Config: + +... +REST: { + ... + printCurl: true, + ... +} +... + +› [CURL Request] curl --location --request POST https://httpbin.org/post -H ... +``` + +* feat(AI): Generate PageObject, added types, shell improvement ([#4319](https://github.com/codeceptjs/CodeceptJS/issues/4319)) - by **[DavertMik](https://github.com/DavertMik)** + * added `askForPageObject` method to generate PageObjects on the fly + * improved AI types + * interactive shell improved to restore history + +![Screenshot from 2024-06-17 02-47-37](https://github.com/codeceptjs/CodeceptJS/assets/220264/12acd2c7-18d1-4105-a24b-84070ec4d393) + +🐛 *Bug Fixes* +* fix(heal): wrong priority ([#4394](https://github.com/codeceptjs/CodeceptJS/issues/4394)) - by **[kobenguyent](https://github.com/kobenguyent)** + +📖 *Documentation* +* AI docs improvements by **[DavertMik](https://github.com/DavertMik)** + ## 3.6.3 ❤️ Thanks all to those who contributed to make this release! ❤️ diff --git a/docs/helpers/AI.md b/docs/helpers/AI.md index 2ee6c6582..a3b6082ea 100644 --- a/docs/helpers/AI.md +++ b/docs/helpers/AI.md @@ -14,13 +14,13 @@ title: AI AI Helper for CodeceptJS. This helper class provides integration with the AI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts. -This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available. +This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDriver to ensure the HTML context is available. Use it only in development mode. It is recommended to run it only inside pause() mode. ## Configuration -This helper should be configured in codecept.json or codecept.conf.js +This helper should be configured in codecept.conf.{js|ts} - `chunkSize`: - The maximum number of characters to send to the AI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4. diff --git a/docs/helpers/Playwright.md b/docs/helpers/Playwright.md index c75b5cc7f..75b41c06f 100644 --- a/docs/helpers/Playwright.md +++ b/docs/helpers/Playwright.md @@ -2444,6 +2444,18 @@ I.waitForDetached('#popup'); Returns **void** automatically synchronized promise through #recorder +### waitForDisabled + +Waits for element to become disabled (by default waits for 1sec). +Element can be located by CSS or XPath. + +#### Parameters + +- `locator` **([string][9] | [object][6])** element located by CSS|XPath|strict locator. +- `sec` **[number][20]** (optional) time in seconds to wait, 1 by default. + +Returns **void** automatically synchronized promise through #recorder + ### waitForElement Waits for element to be present on page (by default waits for 1sec). diff --git a/docs/webapi/waitForDisabled.mustache b/docs/webapi/waitForDisabled.mustache new file mode 100644 index 000000000..c2822db30 --- /dev/null +++ b/docs/webapi/waitForDisabled.mustache @@ -0,0 +1,6 @@ +Waits for element to become disabled (by default waits for 1sec). +Element can be located by CSS or XPath. + +@param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator. +@param {number} [sec=1] (optional) time in seconds to wait, 1 by default. +@returns {void} automatically synchronized promise through #recorder diff --git a/lib/assert/empty.js b/lib/assert/empty.js index 2b1fd67fe..859bcbfa7 100644 --- a/lib/assert/empty.js +++ b/lib/assert/empty.js @@ -1,43 +1,43 @@ -const Assertion = require('../assert'); -const AssertionFailedError = require('./error'); -const { template } = require('../utils'); -const output = require('../output'); +const Assertion = require('../assert') +const AssertionFailedError = require('./error') +const { template } = require('../utils') +const output = require('../output') class EmptinessAssertion extends Assertion { constructor(params) { super((value) => { if (Array.isArray(value)) { - return value.length === 0; + return value.length === 0 } - return !value; - }, params); - this.params.type = 'to be empty'; + return !value + }, params) + this.params.type = 'to be empty' } getException() { if (Array.isArray(this.params.value)) { - this.params.value = `[${this.params.value.join(', ')}]`; + this.params.value = `[${this.params.value.join(', ')}]` } - const err = new AssertionFailedError(this.params, "{{customMessage}}expected {{subject}} '{{value}}' {{type}}"); + const err = new AssertionFailedError(this.params, "{{customMessage}}expected {{subject}} '{{value}}' {{type}}") err.cliMessage = () => { const msg = err.template .replace('{{value}}', output.colors.bold('{{value}}')) - .replace('{{subject}}', output.colors.bold('{{subject}}')); - return template(msg, this.params); - }; - return err; + .replace('{{subject}}', output.colors.bold('{{subject}}')) + return template(msg, this.params) + } + return err } addAssertParams() { - this.params.value = this.params.actual = arguments[0]; - this.params.expected = []; - this.params.customMessage = arguments[1] ? `${arguments[1]}\n\n` : ''; + this.params.value = this.params.actual = arguments[0] + this.params.expected = [] + this.params.customMessage = arguments[1] ? `${arguments[1]}\n\n` : '' } } module.exports = { Assertion: EmptinessAssertion, - empty: subject => new EmptinessAssertion({ subject }), -}; + empty: (subject) => new EmptinessAssertion({ subject }), +} diff --git a/lib/assert/equal.js b/lib/assert/equal.js index 17db64e5c..f8ef0a9d9 100644 --- a/lib/assert/equal.js +++ b/lib/assert/equal.js @@ -1,55 +1,57 @@ -const Assertion = require('../assert'); -const AssertionFailedError = require('./error'); -const { template } = require('../utils'); -const output = require('../output'); +const Assertion = require('../assert') +const AssertionFailedError = require('./error') +const { template } = require('../utils') +const output = require('../output') class EqualityAssertion extends Assertion { constructor(params) { const comparator = function (a, b) { if (b.length === 0) { - b = ''; + b = '' } - return a === b; - }; - super(comparator, params); - this.params.type = 'to equal'; + return a === b + } + super(comparator, params) + this.params.type = 'to equal' } getException() { - const params = this.params; - params.jar = template(params.jar, params); - const err = new AssertionFailedError(params, '{{customMessage}}expected {{jar}} "{{expected}}" {{type}} "{{actual}}"'); - err.showDiff = false; + const params = this.params + params.jar = template(params.jar, params) + const err = new AssertionFailedError( + params, + '{{customMessage}}expected {{jar}} "{{expected}}" {{type}} "{{actual}}"', + ) + err.showDiff = false if (typeof err.cliMessage === 'function') { - err.message = err.cliMessage(); + err.message = err.cliMessage() } err.cliMessage = () => { - const msg = err.template - .replace('{{jar}}', output.colors.bold('{{jar}}')); - return template(msg, this.params); - }; - return err; + const msg = err.template.replace('{{jar}}', output.colors.bold('{{jar}}')) + return template(msg, this.params) + } + return err } addAssertParams() { - this.params.expected = arguments[0]; - this.params.actual = arguments[1]; - this.params.customMessage = arguments[2] ? `${arguments[2]}\n\n` : ''; + this.params.expected = arguments[0] + this.params.actual = arguments[1] + this.params.customMessage = arguments[2] ? `${arguments[2]}\n\n` : '' } } module.exports = { Assertion: EqualityAssertion, - equals: jar => new EqualityAssertion({ jar }), + equals: (jar) => new EqualityAssertion({ jar }), urlEquals: (baseUrl) => { - const assert = new EqualityAssertion({ jar: 'url of current page' }); + const assert = new EqualityAssertion({ jar: 'url of current page' }) assert.comparator = function (expected, actual) { if (expected.indexOf('http') !== 0) { - actual = actual.slice(actual.indexOf(baseUrl) + baseUrl.length); + actual = actual.slice(actual.indexOf(baseUrl) + baseUrl.length) } - return actual === expected; - }; - return assert; + return actual === expected + } + return assert }, - fileEquals: file => new EqualityAssertion({ file, jar: 'contents of {{file}}' }), -}; + fileEquals: (file) => new EqualityAssertion({ file, jar: 'contents of {{file}}' }), +} diff --git a/lib/assert/error.js b/lib/assert/error.js index 072d82372..ed72233ec 100644 --- a/lib/assert/error.js +++ b/lib/assert/error.js @@ -1,4 +1,4 @@ -const subs = require('../utils').template; +const subs = require('../utils').template /** * Assertion errors, can provide a detailed error messages. @@ -6,27 +6,27 @@ const subs = require('../utils').template; * inspect() and cliMessage() added to display errors with params. */ function AssertionFailedError(params, template) { - this.params = params; - this.template = template; + this.params = params + this.template = template // this.message = "AssertionFailedError"; // this.showDiff = true; // @todo cut assert things nicer - this.showDiff = true; + this.showDiff = true - this.actual = this.params.actual; - this.expected = this.params.expected; + this.actual = this.params.actual + this.expected = this.params.expected this.inspect = () => { - const params = this.params || {}; - const msg = params.customMessage || ''; - return msg + subs(this.template, params); - }; + const params = this.params || {} + const msg = params.customMessage || '' + return msg + subs(this.template, params) + } - this.cliMessage = () => this.inspect(); + this.cliMessage = () => this.inspect() } -AssertionFailedError.prototype = Object.create(Error.prototype); -AssertionFailedError.constructor = AssertionFailedError; +AssertionFailedError.prototype = Object.create(Error.prototype) +AssertionFailedError.constructor = AssertionFailedError -module.exports = AssertionFailedError; +module.exports = AssertionFailedError diff --git a/lib/assert/include.js b/lib/assert/include.js index 6019bcc59..3899f4ca0 100644 --- a/lib/assert/include.js +++ b/lib/assert/include.js @@ -1,78 +1,78 @@ -const Assertion = require('../assert'); -const AssertionFailedError = require('./error'); -const { template } = require('../utils'); -const output = require('../output'); +const Assertion = require('../assert') +const AssertionFailedError = require('./error') +const { template } = require('../utils') +const output = require('../output') -const MAX_LINES = 10; +const MAX_LINES = 10 class InclusionAssertion extends Assertion { constructor(params) { - params.jar = params.jar || 'string'; + params.jar = params.jar || 'string' const comparator = function (needle, haystack) { if (Array.isArray(haystack)) { - return haystack.filter(part => part.indexOf(needle) >= 0).length > 0; + return haystack.filter((part) => part.indexOf(needle) >= 0).length > 0 } - return haystack.indexOf(needle) >= 0; - }; - super(comparator, params); - this.params.type = 'to include'; + return haystack.indexOf(needle) >= 0 + } + super(comparator, params) + this.params.type = 'to include' } getException() { - const params = this.params; - params.jar = template(params.jar, params); - const err = new AssertionFailedError(params, '{{customMessage}}expected {{jar}} {{type}} "{{needle}}"'); - err.expected = params.needle; - err.actual = params.haystack; + const params = this.params + params.jar = template(params.jar, params) + const err = new AssertionFailedError(params, '{{customMessage}}expected {{jar}} {{type}} "{{needle}}"') + err.expected = params.needle + err.actual = params.haystack if (Array.isArray(this.params.haystack)) { - this.params.haystack = this.params.haystack.join('\n___(next element)___\n'); + this.params.haystack = this.params.haystack.join('\n___(next element)___\n') } err.cliMessage = function () { const msg = this.template .replace('{{jar}}', output.colors.bold('{{jar}}')) - .replace('{{needle}}', output.colors.bold('{{needle}}')); - return template(msg, this.params); - }; - return err; + .replace('{{needle}}', output.colors.bold('{{needle}}')) + return template(msg, this.params) + } + return err } getFailedAssertion() { - const err = this.getException(); - const lines = this.params.haystack.split('\n'); + const err = this.getException() + const lines = this.params.haystack.split('\n') if (lines.length > MAX_LINES) { - const more = lines.length - MAX_LINES; - err.actual = `${lines.slice(0, MAX_LINES).join('\n')}\n--( ${more} lines more )---`; + const more = lines.length - MAX_LINES + err.actual = `${lines.slice(0, MAX_LINES).join('\n')}\n--( ${more} lines more )---` } - return err; + return err } getFailedNegation() { - this.params.type = 'not to include'; - const err = this.getException(); - const pattern = new RegExp(`^.*?\n?^.*?\n?^.*?${escapeRegExp(this.params.needle)}.*?$\n?.*$\n?.*$`, 'm'); - const matched = this.params.haystack.match(pattern); - if (!matched) return err; - err.actual = matched[0].replace(this.params.needle, output.colors.bold(this.params.needle)); - err.actual = `------\n${err.actual}\n------`; - return err; + this.params.type = 'not to include' + const err = this.getException() + const pattern = new RegExp(`^.*?\n?^.*?\n?^.*?${escapeRegExp(this.params.needle)}.*?$\n?.*$\n?.*$`, 'm') + const matched = this.params.haystack.match(pattern) + if (!matched) return err + err.actual = matched[0].replace(this.params.needle, output.colors.bold(this.params.needle)) + err.actual = `------\n${err.actual}\n------` + return err } addAssertParams() { - this.params.needle = arguments[0]; - this.params.haystack = arguments[1]; - this.params.customMessage = arguments[2] ? `${arguments[2]}\n\n` : ''; + this.params.needle = arguments[0] + this.params.haystack = arguments[1] + this.params.customMessage = arguments[2] ? `${arguments[2]}\n\n` : '' } } module.exports = { Assertion: InclusionAssertion, includes: (needleType) => { - needleType = needleType || 'string'; - return new InclusionAssertion({ jar: needleType }); + needleType = needleType || 'string' + return new InclusionAssertion({ jar: needleType }) }, - fileIncludes: file => new InclusionAssertion({ file, jar: 'file {{file}}' }), -}; + fileIncludes: (file) => new InclusionAssertion({ file, jar: 'file {{file}}' }), +} function escapeRegExp(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') } diff --git a/lib/assert/throws.js b/lib/assert/throws.js index c5b97f007..c0e92276c 100644 --- a/lib/assert/throws.js +++ b/lib/assert/throws.js @@ -1,20 +1,22 @@ function errorThrown(actual, expected) { - if (!expected) return null; - if (!actual) throw new Error(`Expected ${expected} error to be thrown`); - const msg = actual.inspect ? actual.inspect() : actual.toString(); + if (!expected) return null + if (!actual) throw new Error(`Expected ${expected} error to be thrown`) + const msg = actual.inspect ? actual.inspect() : actual.toString() if (expected instanceof RegExp) { - if (msg.match(expected)) return null; - throw new Error(`Expected error to be thrown with message matching ${expected} while '${msg}' caught`); + if (msg.match(expected)) return null + throw new Error(`Expected error to be thrown with message matching ${expected} while '${msg}' caught`) } if (typeof expected === 'string') { - if (msg === expected) return null; - throw new Error(`Expected error to be thrown with message ${expected} while '${msg}' caught`); + if (msg === expected) return null + throw new Error(`Expected error to be thrown with message ${expected} while '${msg}' caught`) } if (typeof expected === 'object') { - if (actual.constructor.name !== expected.constructor.name) throw new Error(`Expected ${expected} error to be thrown but ${actual} was caught`); - if (expected.message && expected.message !== msg) throw new Error(`Expected error to be thrown with message ${expected.message} while '${msg}' caught`); + if (actual.constructor.name !== expected.constructor.name) + throw new Error(`Expected ${expected} error to be thrown but ${actual} was caught`) + if (expected.message && expected.message !== msg) + throw new Error(`Expected error to be thrown with message ${expected.message} while '${msg}' caught`) } - return null; + return null } -module.exports = errorThrown; +module.exports = errorThrown diff --git a/lib/assert/truth.js b/lib/assert/truth.js index 0d72ba491..51dbe2def 100644 --- a/lib/assert/truth.js +++ b/lib/assert/truth.js @@ -1,37 +1,36 @@ -const Assertion = require('../assert'); -const AssertionFailedError = require('./error'); -const { template } = require('../utils'); -const output = require('../output'); +const Assertion = require('../assert') +const AssertionFailedError = require('./error') +const { template } = require('../utils') +const output = require('../output') class TruthAssertion extends Assertion { constructor(params) { super((value) => { if (Array.isArray(value)) { - return value.filter(val => !!val).length > 0; + return value.filter((val) => !!val).length > 0 } - return !!value; - }, params); - this.params.type = this.params.type || 'to be true'; + return !!value + }, params) + this.params.type = this.params.type || 'to be true' } getException() { - const err = new AssertionFailedError(this.params, '{{customMessage}}expected {{subject}} {{type}}'); + const err = new AssertionFailedError(this.params, '{{customMessage}}expected {{subject}} {{type}}') err.cliMessage = () => { - const msg = err.template - .replace('{{subject}}', output.colors.bold('{{subject}}')); - return template(msg, this.params); - }; - return err; + const msg = err.template.replace('{{subject}}', output.colors.bold('{{subject}}')) + return template(msg, this.params) + } + return err } addAssertParams() { - this.params.value = this.params.actual = arguments[0]; - this.params.expected = true; - this.params.customMessage = arguments[1] ? `${arguments[1]}\n\n` : ''; + this.params.value = this.params.actual = arguments[0] + this.params.expected = true + this.params.customMessage = arguments[1] ? `${arguments[1]}\n\n` : '' } } module.exports = { Assertion: TruthAssertion, truth: (subject, type) => new TruthAssertion({ subject, type }), -}; +} diff --git a/lib/command/configMigrate.js b/lib/command/configMigrate.js index b06815359..0101c9cc2 100644 --- a/lib/command/configMigrate.js +++ b/lib/command/configMigrate.js @@ -1,71 +1,76 @@ -const colors = require('chalk'); -const fs = require('fs'); -const inquirer = require('inquirer'); -const mkdirp = require('mkdirp'); -const path = require('path'); -const util = require('util'); +const colors = require('chalk') +const fs = require('fs') +const inquirer = require('inquirer') +const mkdirp = require('mkdirp') +const path = require('path') +const util = require('util') -const { print, success, error } = require('../output'); -const { fileExists } = require('../utils'); -const { getTestRoot } = require('./utils'); +const { print, success, error } = require('../output') +const { fileExists } = require('../utils') +const { getTestRoot } = require('./utils') module.exports = function (initPath) { - const testsPath = getTestRoot(initPath); + const testsPath = getTestRoot(initPath) - print(); - print(` Welcome to ${colors.magenta.bold('CodeceptJS')} configuration migration tool`); - print(` It will help you switch from ${colors.cyan.bold('.json')} to ${colors.magenta.bold('.js')} config format at ease`); - print(); + print() + print(` Welcome to ${colors.magenta.bold('CodeceptJS')} configuration migration tool`) + print( + ` It will help you switch from ${colors.cyan.bold('.json')} to ${colors.magenta.bold('.js')} config format at ease`, + ) + print() if (!path) { - print('No config file is specified.'); - print(`Test root is assumed to be ${colors.yellow.bold(testsPath)}`); - print('----------------------------------'); + print('No config file is specified.') + print(`Test root is assumed to be ${colors.yellow.bold(testsPath)}`) + print('----------------------------------') } else { - print(`Migrating ${colors.magenta.bold('.js')} config to ${colors.bold(testsPath)}`); + print(`Migrating ${colors.magenta.bold('.js')} config to ${colors.bold(testsPath)}`) } if (!fileExists(testsPath)) { - print(`Directory ${testsPath} does not exist, creating...`); - mkdirp.sync(testsPath); + print(`Directory ${testsPath} does not exist, creating...`) + mkdirp.sync(testsPath) } - const configFile = path.join(testsPath, 'codecept.conf.js'); + const configFile = path.join(testsPath, 'codecept.conf.js') if (fileExists(configFile)) { - error(`Config is already created at ${configFile}`); - return; + error(`Config is already created at ${configFile}`) + return } - inquirer.prompt([{ - name: 'configFile', - type: 'confirm', - message: `Would you like to switch from ${colors.cyan.bold('.json')} to ${colors.magenta.bold('.js')} config format?`, - default: true, - }, - { - name: 'delete', - type: 'confirm', - message: `Would you like to delete ${colors.cyan.bold('.json')} config format afterwards?`, - default: true, - }, - ]).then((result) => { - if (result.configFile) { - const jsonConfigFile = path.join(testsPath, 'codecept.js'); - const config = JSON.parse(fs.readFileSync(jsonConfigFile, 'utf8')); - config.name = testsPath.split(path.sep).pop(); + inquirer + .prompt([ + { + name: 'configFile', + type: 'confirm', + message: `Would you like to switch from ${colors.cyan.bold('.json')} to ${colors.magenta.bold('.js')} config format?`, + default: true, + }, + { + name: 'delete', + type: 'confirm', + message: `Would you like to delete ${colors.cyan.bold('.json')} config format afterwards?`, + default: true, + }, + ]) + .then((result) => { + if (result.configFile) { + const jsonConfigFile = path.join(testsPath, 'codecept.js') + const config = JSON.parse(fs.readFileSync(jsonConfigFile, 'utf8')) + config.name = testsPath.split(path.sep).pop() - const finish = () => { - fs.writeFileSync(configFile, `exports.config = ${util.inspect(config, false, 4, false)}`, 'utf-8'); - success(`Config is successfully migrated at ${configFile}`); + const finish = () => { + fs.writeFileSync(configFile, `exports.config = ${util.inspect(config, false, 4, false)}`, 'utf-8') + success(`Config is successfully migrated at ${configFile}`) - if (result.delete) { - if (fileExists(jsonConfigFile)) { - fs.unlinkSync(jsonConfigFile); - success('JSON config file is deleted!'); + if (result.delete) { + if (fileExists(jsonConfigFile)) { + fs.unlinkSync(jsonConfigFile) + success('JSON config file is deleted!') + } } } - }; - finish(); - } - }); -}; + finish() + } + }) +} diff --git a/lib/command/definitions.js b/lib/command/definitions.js index 3751fe5fa..cfa4ae459 100644 --- a/lib/command/definitions.js +++ b/lib/command/definitions.js @@ -1,11 +1,11 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require('fs') +const path = require('path') -const { getConfig, getTestRoot } = require('./utils'); -const Codecept = require('../codecept'); -const container = require('../container'); -const output = require('../output'); -const actingHelpers = [...require('../plugin/standardActingHelpers'), 'REST']; +const { getConfig, getTestRoot } = require('./utils') +const Codecept = require('../codecept') +const container = require('../container') +const output = require('../output') +const actingHelpers = [...require('../plugin/standardActingHelpers'), 'REST'] /** * Prepare data and generate content of definitions file @@ -29,32 +29,27 @@ const getDefinitionsFileContent = ({ importPaths, translations, }) => { - const getHelperListFragment = ({ - hasCustomHelper, - hasCustomStepsFile, - }) => { + const getHelperListFragment = ({ hasCustomHelper, hasCustomStepsFile }) => { if (hasCustomHelper && hasCustomStepsFile) { - return `${['ReturnType', 'WithTranslation'].join(', ')}`; + return `${['ReturnType', 'WithTranslation'].join(', ')}` } if (hasCustomStepsFile) { - return 'ReturnType'; + return 'ReturnType' } - return 'WithTranslation'; - }; + return 'WithTranslation' + } const helpersListFragment = getHelperListFragment({ hasCustomHelper, hasCustomStepsFile, - }); + }) - const importPathsFragment = importPaths.join('\n'); - const supportObjectsTypeFragment = convertMapToType(supportObject); - const methodsTypeFragment = helperNames.length > 0 - ? `interface Methods extends ${helperNames.join(', ')} {}` - : ''; - const translatedActionsFragment = JSON.stringify(translations.vocabulary.actions, null, 2); + const importPathsFragment = importPaths.join('\n') + const supportObjectsTypeFragment = convertMapToType(supportObject) + const methodsTypeFragment = helperNames.length > 0 ? `interface Methods extends ${helperNames.join(', ')} {}` : '' + const translatedActionsFragment = JSON.stringify(translations.vocabulary.actions, null, 2) return generateDefinitionsContent({ helpersListFragment, @@ -62,8 +57,8 @@ const getDefinitionsFileContent = ({ supportObjectsTypeFragment, methodsTypeFragment, translatedActionsFragment, - }); -}; + }) +} /** * Generate content for definitions file from fragments @@ -96,83 +91,83 @@ declare namespace CodeceptJS { interface Actions ${translatedActionsFragment} } } -`; -}; +` +} /** @type {Array} */ -const helperNames = []; +const helperNames = [] /** @type {Array} */ -const customHelpers = []; +const customHelpers = [] module.exports = function (genPath, options) { - const configFile = options.config || genPath; + const configFile = options.config || genPath /** @type {string} */ - const testsPath = getTestRoot(configFile); - const config = getConfig(configFile); - if (!config) return; + const testsPath = getTestRoot(configFile) + const config = getConfig(configFile) + if (!config) return /** @type {Object} */ - const helperPaths = {}; + const helperPaths = {} /** @type {Object} */ - const supportPaths = {}; + const supportPaths = {} /** @type {boolean} */ - let hasCustomStepsFile = false; + let hasCustomStepsFile = false /** @type {boolean} */ - let hasCustomHelper = false; + let hasCustomHelper = false /** @type {string} */ - const targetFolderPath = options.output && getTestRoot(options.output) || testsPath; + const targetFolderPath = (options.output && getTestRoot(options.output)) || testsPath - const codecept = new Codecept(config, {}); - codecept.init(testsPath); + const codecept = new Codecept(config, {}) + codecept.init(testsPath) - const helpers = container.helpers(); - const translations = container.translation(); + const helpers = container.helpers() + const translations = container.translation() for (const name in helpers) { - const require = codecept.config.helpers[name].require; + const require = codecept.config.helpers[name].require if (require) { - helperPaths[name] = require; - helperNames.push(name); + helperPaths[name] = require + helperNames.push(name) } else { - const fullBasedPromised = codecept.config.fullPromiseBased; - helperNames.push(fullBasedPromised === true ? `${name}Ts` : name); + const fullBasedPromised = codecept.config.fullPromiseBased + helperNames.push(fullBasedPromised === true ? `${name}Ts` : name) } if (!actingHelpers.includes(name)) { - customHelpers.push(name); + customHelpers.push(name) } } - let autoLogin; + let autoLogin if (config.plugins.autoLogin) { - autoLogin = config.plugins.autoLogin.inject; + autoLogin = config.plugins.autoLogin.inject } - const supportObject = new Map(); - supportObject.set('I', 'I'); - supportObject.set('current', 'any'); + const supportObject = new Map() + supportObject.set('I', 'I') + supportObject.set('current', 'any') if (translations.loaded) { - supportObject.set(translations.I, translations.I); + supportObject.set(translations.I, translations.I) } if (autoLogin) { - supportObject.set(autoLogin, 'any'); + supportObject.set(autoLogin, 'any') } if (customHelpers.length > 0) { - hasCustomHelper = true; + hasCustomHelper = true } for (const name in codecept.config.include) { - const includePath = codecept.config.include[name]; + const includePath = codecept.config.include[name] if (name === 'I' || name === translations.I) { - hasCustomStepsFile = true; - supportPaths.steps_file = includePath; - continue; + hasCustomStepsFile = true + supportPaths.steps_file = includePath + continue } - supportPaths[name] = includePath; - supportObject.set(name, name); + supportPaths[name] = includePath + supportObject.set(name, name) } let definitionsFileContent = getDefinitionsFileContent({ @@ -182,31 +177,34 @@ module.exports = function (genPath, options) { translations, hasCustomStepsFile, hasCustomHelper, - }); + }) // add aliases for translations if (translations.loaded) { - const namespaceTranslationAliases = []; - namespaceTranslationAliases.push(`interface ${translations.vocabulary.I} extends WithTranslation {}`); + const namespaceTranslationAliases = [] + namespaceTranslationAliases.push(`interface ${translations.vocabulary.I} extends WithTranslation {}`) - namespaceTranslationAliases.push(' namespace Translation {'); - definitionsFileContent = definitionsFileContent.replace('namespace Translation {', namespaceTranslationAliases.join('\n')); + namespaceTranslationAliases.push(' namespace Translation {') + definitionsFileContent = definitionsFileContent.replace( + 'namespace Translation {', + namespaceTranslationAliases.join('\n'), + ) - const translationAliases = []; + const translationAliases = [] if (translations.vocabulary.contexts) { - Object.keys(translations.vocabulary.contexts).forEach(k => { - translationAliases.push(`declare const ${translations.vocabulary.contexts[k]}: typeof ${k};`); - }); + Object.keys(translations.vocabulary.contexts).forEach((k) => { + translationAliases.push(`declare const ${translations.vocabulary.contexts[k]}: typeof ${k};`) + }) } - definitionsFileContent += `\n${translationAliases.join('\n')}`; + definitionsFileContent += `\n${translationAliases.join('\n')}` } - fs.writeFileSync(path.join(targetFolderPath, 'steps.d.ts'), definitionsFileContent); - output.print('TypeScript Definitions provide autocompletion in Visual Studio Code and other IDEs'); - output.print('Definitions were generated in steps.d.ts'); -}; + fs.writeFileSync(path.join(targetFolderPath, 'steps.d.ts'), definitionsFileContent) + output.print('TypeScript Definitions provide autocompletion in Visual Studio Code and other IDEs') + output.print('Definitions were generated in steps.d.ts') +} /** * Returns the relative path from the to the targeted folder. @@ -215,13 +213,13 @@ module.exports = function (genPath, options) { * @param {string} testsPath */ function getPath(originalPath, targetFolderPath, testsPath) { - const parsedPath = path.parse(originalPath); + const parsedPath = path.parse(originalPath) // Remove typescript extension if exists. - if (parsedPath.base.endsWith('.d.ts')) parsedPath.base = parsedPath.base.substring(0, parsedPath.base.length - 5); - else if (parsedPath.ext === '.ts') parsedPath.base = parsedPath.name; + if (parsedPath.base.endsWith('.d.ts')) parsedPath.base = parsedPath.base.substring(0, parsedPath.base.length - 5) + else if (parsedPath.ext === '.ts') parsedPath.base = parsedPath.name - if (!parsedPath.dir.startsWith('.')) return path.posix.join(parsedPath.dir, parsedPath.base); + if (!parsedPath.dir.startsWith('.')) return path.posix.join(parsedPath.dir, parsedPath.base) const relativePath = path.posix.relative( targetFolderPath.split(path.sep).join(path.posix.sep), path.posix.join( @@ -229,9 +227,9 @@ function getPath(originalPath, targetFolderPath, testsPath) { parsedPath.dir.split(path.sep).join(path.posix.sep), parsedPath.base.split(path.sep).join(path.posix.sep), ), - ); + ) - return relativePath.startsWith('.') ? relativePath : `./${relativePath}`; + return relativePath.startsWith('.') ? relativePath : `./${relativePath}` } /** @@ -245,19 +243,19 @@ function getPath(originalPath, targetFolderPath, testsPath) { * @returns {Array} */ function getImportString(testsPath, targetFolderPath, pathsToType, pathsToValue) { - const importStrings = []; + const importStrings = [] for (const name in pathsToType) { - const relativePath = getPath(pathsToType[name], targetFolderPath, testsPath); - importStrings.push(`type ${name} = typeof import('${relativePath}');`); + const relativePath = getPath(pathsToType[name], targetFolderPath, testsPath) + importStrings.push(`type ${name} = typeof import('${relativePath}');`) } for (const name in pathsToValue) { - const relativePath = getPath(pathsToValue[name], targetFolderPath, testsPath); - importStrings.push(`type ${name} = import('${relativePath}');`); + const relativePath = getPath(pathsToValue[name], targetFolderPath, testsPath) + importStrings.push(`type ${name} = import('${relativePath}');`) } - return importStrings; + return importStrings } /** @@ -266,5 +264,7 @@ function getImportString(testsPath, targetFolderPath, pathsToType, pathsToValue) * @returns {string} */ function convertMapToType(map) { - return `{ ${Array.from(map).map(([key, value]) => `${key}: ${value}`).join(', ')} }`; + return `{ ${Array.from(map) + .map(([key, value]) => `${key}: ${value}`) + .join(', ')} }` } diff --git a/lib/command/dryRun.js b/lib/command/dryRun.js index f6fe2a490..0de23df4f 100644 --- a/lib/command/dryRun.js +++ b/lib/command/dryRun.js @@ -1,120 +1,122 @@ -const { getConfig, getTestRoot } = require('./utils'); -const Config = require('../config'); -const Codecept = require('../codecept'); -const output = require('../output'); -const event = require('../event'); -const store = require('../store'); -const Container = require('../container'); +const { getConfig, getTestRoot } = require('./utils') +const Config = require('../config') +const Codecept = require('../codecept') +const output = require('../output') +const event = require('../event') +const store = require('../store') +const Container = require('../container') module.exports = async function (test, options) { - if (options.grep) process.env.grep = options.grep.toLowerCase(); - const configFile = options.config; - let codecept; + if (options.grep) process.env.grep = options.grep.toLowerCase() + const configFile = options.config + let codecept - const testRoot = getTestRoot(configFile); - let config = getConfig(configFile); + const testRoot = getTestRoot(configFile) + let config = getConfig(configFile) if (options.override) { - config = Config.append(JSON.parse(options.override)); + config = Config.append(JSON.parse(options.override)) } if (config.plugins) { // disable all plugins by default, they can be enabled with -p option for (const plugin in config.plugins) { // if `-p all` is passed, then enabling all plugins, otherwise plugins could be enabled by `-p customLocator,commentStep,tryTo` - config.plugins[plugin].enabled = options.plugins === 'all'; + config.plugins[plugin].enabled = options.plugins === 'all' } } try { - codecept = new Codecept(config, options); - codecept.init(testRoot); + codecept = new Codecept(config, options) + codecept.init(testRoot) - if (options.bootstrap) await codecept.bootstrap(); + if (options.bootstrap) await codecept.bootstrap() - codecept.loadTests(); - store.dryRun = true; + codecept.loadTests() + store.dryRun = true if (!options.steps && !options.verbose && !options.debug) { - printTests(codecept.testFiles); - return; + printTests(codecept.testFiles) + return } - event.dispatcher.on(event.all.result, printFooter); - codecept.run(test); + event.dispatcher.on(event.all.result, printFooter) + codecept.run(test) } catch (err) { - console.error(err); - process.exit(1); + console.error(err) + process.exit(1) } -}; +} function printTests(files) { - const figures = require('figures'); - const colors = require('chalk'); + const figures = require('figures') + const colors = require('chalk') - output.print(output.styles.debug(`Tests from ${global.codecept_dir}:`)); - output.print(); + output.print(output.styles.debug(`Tests from ${global.codecept_dir}:`)) + output.print() - const mocha = Container.mocha(); - mocha.files = files; - mocha.loadFiles(); + const mocha = Container.mocha() + mocha.files = files + mocha.loadFiles() - let numOfTests = 0; - let numOfSuites = 0; - let outputString = ''; - const filterBy = process.env.grep ? process.env.grep.toLowerCase() : undefined; + let numOfTests = 0 + let numOfSuites = 0 + let outputString = '' + const filterBy = process.env.grep ? process.env.grep.toLowerCase() : undefined if (filterBy) { for (const suite of mocha.suite.suites) { - const currentSuite = suite.title; + const currentSuite = suite.title if (suite.title.toLowerCase().includes(filterBy)) { - outputString += `${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')} -- ${mocha.suite.suites.length} tests\n`; - numOfSuites++; + outputString += `${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')} -- ${mocha.suite.suites.length} tests\n` + numOfSuites++ } for (test of suite.tests) { if (test.title.toLowerCase().includes(filterBy)) { - numOfTests++; - outputString += `${colors.white.bold(test.parent.title)} -- ${output.styles.log(test.parent.file || '')} -- ${mocha.suite.suites.length} tests\n`; - outputString += ` ${output.styles.scenario(figures.checkboxOff)} ${test.title}\n`; + numOfTests++ + outputString += `${colors.white.bold(test.parent.title)} -- ${output.styles.log(test.parent.file || '')} -- ${mocha.suite.suites.length} tests\n` + outputString += ` ${output.styles.scenario(figures.checkboxOff)} ${test.title}\n` } } } - numOfSuites = countSuites(outputString); + numOfSuites = countSuites(outputString) } else { for (const suite of mocha.suite.suites) { - output.print(`${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')} -- ${mocha.suite.suites.length} tests`); - numOfSuites++; + output.print( + `${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')} -- ${mocha.suite.suites.length} tests`, + ) + numOfSuites++ for (test of suite.tests) { - numOfTests++; - output.print(` ${output.styles.scenario(figures.checkboxOff)} ${test.title}`); + numOfTests++ + output.print(` ${output.styles.scenario(figures.checkboxOff)} ${test.title}`) } } } - output.print(removeDuplicates(outputString)); - output.print(''); - output.success(` Total: ${numOfSuites} suites | ${numOfTests} tests `); - printFooter(); - process.exit(0); + output.print(removeDuplicates(outputString)) + output.print('') + output.success(` Total: ${numOfSuites} suites | ${numOfTests} tests `) + printFooter() + process.exit(0) } function printFooter() { - output.print(); - output.print('--- DRY MODE: No tests were executed ---'); + output.print() + output.print('--- DRY MODE: No tests were executed ---') } function removeDuplicates(inputString) { - const array = inputString.split('\n'); - const uniqueLines = [...new Set(array)]; - const resultString = uniqueLines.join('\n'); + const array = inputString.split('\n') + const uniqueLines = [...new Set(array)] + const resultString = uniqueLines.join('\n') - return resultString; + return resultString } function countSuites(inputString) { - const array = inputString.split('\n'); + const array = inputString.split('\n') - const uniqueLines = [...new Set(array)]; - const res = uniqueLines.filter(item => item.includes('-- ')); - return res.length; + const uniqueLines = [...new Set(array)] + const res = uniqueLines.filter((item) => item.includes('-- ')) + return res.length } diff --git a/lib/command/generate.js b/lib/command/generate.js index 8d4011731..914b11bca 100644 --- a/lib/command/generate.js +++ b/lib/command/generate.js @@ -1,80 +1,80 @@ -const colors = require('chalk'); -const fs = require('fs'); -const inquirer = require('inquirer'); -const mkdirp = require('mkdirp'); -const path = require('path'); -const { - fileExists, ucfirst, lcfirst, beautify, -} = require('../utils'); -const output = require('../output'); -const generateDefinitions = require('./definitions'); -const { - getConfig, getTestRoot, safeFileWrite, readConfig, -} = require('./utils'); - -let extension = 'js'; +const colors = require('chalk') +const fs = require('fs') +const inquirer = require('inquirer') +const mkdirp = require('mkdirp') +const path = require('path') +const { fileExists, ucfirst, lcfirst, beautify } = require('../utils') +const output = require('../output') +const generateDefinitions = require('./definitions') +const { getConfig, getTestRoot, safeFileWrite, readConfig } = require('./utils') + +let extension = 'js' const testTemplate = `Feature('{{feature}}'); Scenario('test something', async ({ {{actor}} }) => { }); -`; +` // generates empty test module.exports.test = function (genPath) { - const testsPath = getTestRoot(genPath); - global.codecept_dir = testsPath; - const config = getConfig(testsPath); - if (!config) return; - - output.print('Creating a new test...'); - output.print('----------------------'); - - const defaultExt = config.tests.match(/([^\*/]*?)$/)[1] || `_test.${extension}`; - - return inquirer.prompt([ - { - type: 'input', - name: 'feature', - message: 'Feature which is being tested (ex: account, login, etc)', - validate: (val) => !!val, - }, - { - type: 'input', - message: 'Filename of a test', - name: 'filename', - default(answers) { - return (answers.feature).replace(' ', '_') + defaultExt; + const testsPath = getTestRoot(genPath) + global.codecept_dir = testsPath + const config = getConfig(testsPath) + if (!config) return + + output.print('Creating a new test...') + output.print('----------------------') + + const defaultExt = config.tests.match(/([^\*/]*?)$/)[1] || `_test.${extension}` + + return inquirer + .prompt([ + { + type: 'input', + name: 'feature', + message: 'Feature which is being tested (ex: account, login, etc)', + validate: (val) => !!val, }, - }, - ]).then((result) => { - const testFilePath = path.dirname(path.join(testsPath, config.tests)).replace(/\*\*$/, ''); - let testFile = path.join(testFilePath, result.filename); - const ext = path.extname(testFile); - if (!ext) testFile += defaultExt; - const dir = path.dirname(testFile); - if (!fileExists(dir)) mkdirp.sync(dir); - let testContent = testTemplate.replace('{{feature}}', result.feature); - - const container = require('../container'); - container.create(config, {}); - // translate scenario test - if (container.translation().loaded) { - const vocabulary = container.translation().vocabulary; - testContent = testContent.replace('{{actor}}', container.translation().I); - if (vocabulary.contexts.Feature) testContent = testContent.replace('Feature', vocabulary.contexts.Feature); - if (vocabulary.contexts.Scenario) testContent = testContent.replace('Scenario', vocabulary.contexts.Scenario); - output.print(`Test was created in ${colors.bold(config.translation)} localization. See: https://codecept.io/translation/`); - } else { - testContent = testContent.replace('{{actor}}', 'I'); - } - if (!config.fullPromiseBased) testContent = testContent.replace('async', ''); + { + type: 'input', + message: 'Filename of a test', + name: 'filename', + default(answers) { + return answers.feature.replace(' ', '_') + defaultExt + }, + }, + ]) + .then((result) => { + const testFilePath = path.dirname(path.join(testsPath, config.tests)).replace(/\*\*$/, '') + let testFile = path.join(testFilePath, result.filename) + const ext = path.extname(testFile) + if (!ext) testFile += defaultExt + const dir = path.dirname(testFile) + if (!fileExists(dir)) mkdirp.sync(dir) + let testContent = testTemplate.replace('{{feature}}', result.feature) + + const container = require('../container') + container.create(config, {}) + // translate scenario test + if (container.translation().loaded) { + const vocabulary = container.translation().vocabulary + testContent = testContent.replace('{{actor}}', container.translation().I) + if (vocabulary.contexts.Feature) testContent = testContent.replace('Feature', vocabulary.contexts.Feature) + if (vocabulary.contexts.Scenario) testContent = testContent.replace('Scenario', vocabulary.contexts.Scenario) + output.print( + `Test was created in ${colors.bold(config.translation)} localization. See: https://codecept.io/translation/`, + ) + } else { + testContent = testContent.replace('{{actor}}', 'I') + } + if (!config.fullPromiseBased) testContent = testContent.replace('async', '') - if (!safeFileWrite(testFile, testContent)) return; - output.success(`\nTest for ${result.filename} was created in ${testFile}`); - }); -}; + if (!safeFileWrite(testFile, testContent)) return + output.success(`\nTest for ${result.filename} was created in ${testFile}`) + }) +} const pageObjectTemplate = `const { I } = inject(); @@ -82,7 +82,7 @@ module.exports = { // insert your locators and methods here } -`; +` const poModuleTemplateTS = `const { I } = inject(); @@ -90,7 +90,7 @@ export = { // insert your locators and methods here } -`; +` const poClassTemplate = `const { I } = inject(); @@ -105,96 +105,101 @@ class {{name}} { // For inheritance module.exports = new {{name}}(); export = {{name}}; -`; +` module.exports.pageObject = function (genPath, opts) { - const testsPath = getTestRoot(genPath); - const config = getConfig(testsPath); - const kind = opts.T || 'page'; - if (!config) return; + const testsPath = getTestRoot(genPath) + const config = getConfig(testsPath) + const kind = opts.T || 'page' + if (!config) return - let configFile = path.join(testsPath, `codecept.conf.${extension}`); + let configFile = path.join(testsPath, `codecept.conf.${extension}`) if (!fileExists(configFile)) { - extension = 'ts'; - configFile = path.join(testsPath, `codecept.conf.${extension}`); + extension = 'ts' + configFile = path.join(testsPath, `codecept.conf.${extension}`) } - output.print(`Creating a new ${kind} object`); - output.print('--------------------------'); - - return inquirer.prompt([ - { - type: 'input', - name: 'name', - message: `Name of a ${kind} object`, - validate: (val) => !!val, - }, - { - type: 'input', - name: 'filename', - message: 'Where should it be stored', - default: answers => `./${kind}s/${answers.name}.${extension}`, - }, - { - type: 'list', - name: 'objectType', - message: 'What is your preferred object type', - choices: ['module', 'class'], - default: 'module', - }, - ]).then((result) => { - const pageObjectFile = path.join(testsPath, result.filename); - const dir = path.dirname(pageObjectFile); - if (!fileExists(dir)) fs.mkdirSync(dir); - - let actor = 'actor'; - - if (config.include.I) { - let actorPath = config.include.I; - if (actorPath.charAt(0) === '.') { // relative path - actorPath = path.relative(dir, path.dirname(path.join(testsPath, actorPath))) + actorPath.substring(1); // get an upper level + output.print(`Creating a new ${kind} object`) + output.print('--------------------------') + + return inquirer + .prompt([ + { + type: 'input', + name: 'name', + message: `Name of a ${kind} object`, + validate: (val) => !!val, + }, + { + type: 'input', + name: 'filename', + message: 'Where should it be stored', + default: (answers) => `./${kind}s/${answers.name}.${extension}`, + }, + { + type: 'list', + name: 'objectType', + message: 'What is your preferred object type', + choices: ['module', 'class'], + default: 'module', + }, + ]) + .then((result) => { + const pageObjectFile = path.join(testsPath, result.filename) + const dir = path.dirname(pageObjectFile) + if (!fileExists(dir)) fs.mkdirSync(dir) + + let actor = 'actor' + + if (config.include.I) { + let actorPath = config.include.I + if (actorPath.charAt(0) === '.') { + // relative path + actorPath = path.relative(dir, path.dirname(path.join(testsPath, actorPath))) + actorPath.substring(1) // get an upper level + } + actor = `require('${actorPath}')` } - actor = `require('${actorPath}')`; - } - const name = lcfirst(result.name) + ucfirst(kind); - if (result.objectType === 'module' && extension === 'ts') { - if (!safeFileWrite(pageObjectFile, poModuleTemplateTS.replace('{{actor}}', actor))) return; - } else if (result.objectType === 'module' && extension === 'js') { - if (!safeFileWrite(pageObjectFile, pageObjectTemplate.replace('{{actor}}', actor))) return; - } else if (result.objectType === 'class') { - const content = poClassTemplate.replace(/{{actor}}/g, actor).replace(/{{name}}/g, name); - if (!safeFileWrite(pageObjectFile, content)) return; - } + const name = lcfirst(result.name) + ucfirst(kind) + if (result.objectType === 'module' && extension === 'ts') { + if (!safeFileWrite(pageObjectFile, poModuleTemplateTS.replace('{{actor}}', actor))) return + } else if (result.objectType === 'module' && extension === 'js') { + if (!safeFileWrite(pageObjectFile, pageObjectTemplate.replace('{{actor}}', actor))) return + } else if (result.objectType === 'class') { + const content = poClassTemplate.replace(/{{actor}}/g, actor).replace(/{{name}}/g, name) + if (!safeFileWrite(pageObjectFile, content)) return + } - let data = readConfig(configFile); - config.include[name] = result.filename; + let data = readConfig(configFile) + config.include[name] = result.filename - if (!data) throw Error('Config file is empty'); - const currentInclude = `${data.match(/include:[\s\S][^\}]*/i)[0]}\n ${name}:${JSON.stringify(config.include[name])}`; + if (!data) throw Error('Config file is empty') + const currentInclude = `${data.match(/include:[\s\S][^\}]*/i)[0]}\n ${name}:${JSON.stringify(config.include[name])}` - data = data.replace(/include:[\s\S][^\}]*/i, `${currentInclude},`); + data = data.replace(/include:[\s\S][^\}]*/i, `${currentInclude},`) - fs.writeFileSync(configFile, beautify(data), 'utf-8'); + fs.writeFileSync(configFile, beautify(data), 'utf-8') - output.success(`${ucfirst(kind)} object for ${result.name} was created in ${pageObjectFile}`); - output.print(`Your config file (${colors.cyan('include')} section) has included the new created PO: + output.success(`${ucfirst(kind)} object for ${result.name} was created in ${pageObjectFile}`) + output.print(`Your config file (${colors.cyan('include')} section) has included the new created PO: include: { ... ${name}: '${result.filename}', - },`); + },`) - output.print(`Use ${output.colors.bold(colors.cyan(name))} as parameter in test scenarios to access this object:`); - output.print(`\nScenario('my new test', ({ I, ${name} })) { /** ... */ }\n`); + output.print(`Use ${output.colors.bold(colors.cyan(name))} as parameter in test scenarios to access this object:`) + output.print(`\nScenario('my new test', ({ I, ${name} })) { /** ... */ }\n`) - try { - generateDefinitions(testsPath, {}); - } catch (_err) { - output.print(`Run ${colors.green('npx codeceptjs def')} to update your types to get auto-completion for object.`); - } - }); -}; + try { + generateDefinitions(testsPath, {}) + } catch (_err) { + output.print( + `Run ${colors.green('npx codeceptjs def')} to update your types to get auto-completion for object.`, + ) + } + }) +} const helperTemplate = `const Helper = require('@codeceptjs/helper'); @@ -222,59 +227,64 @@ class {{name}} extends Helper { } module.exports = {{name}}; -`; +` module.exports.helper = function (genPath) { - const testsPath = getTestRoot(genPath); - - output.print('Creating a new helper'); - output.print('--------------------------'); - - return inquirer.prompt([{ - type: 'input', - name: 'name', - message: 'Name of a Helper', - validate: (val) => !!val, - }, { - type: 'input', - name: 'filename', - message: 'Where should it be stored', - default: answers => `./${answers.name.toLowerCase()}_helper.${extension}`, - }]).then((result) => { - const name = ucfirst(result.name); - const helperFile = path.join(testsPath, result.filename); - const dir = path.dirname(helperFile); - if (!fileExists(dir)) fs.mkdirSync(dir); - - if (!safeFileWrite(helperFile, helperTemplate.replace(/{{name}}/g, name))) return; - output.success(`Helper for ${name} was created in ${helperFile}`); - output.print(`Update your config file (add to ${colors.cyan('helpers')} section): + const testsPath = getTestRoot(genPath) + + output.print('Creating a new helper') + output.print('--------------------------') + + return inquirer + .prompt([ + { + type: 'input', + name: 'name', + message: 'Name of a Helper', + validate: (val) => !!val, + }, + { + type: 'input', + name: 'filename', + message: 'Where should it be stored', + default: (answers) => `./${answers.name.toLowerCase()}_helper.${extension}`, + }, + ]) + .then((result) => { + const name = ucfirst(result.name) + const helperFile = path.join(testsPath, result.filename) + const dir = path.dirname(helperFile) + if (!fileExists(dir)) fs.mkdirSync(dir) + + if (!safeFileWrite(helperFile, helperTemplate.replace(/{{name}}/g, name))) return + output.success(`Helper for ${name} was created in ${helperFile}`) + output.print(`Update your config file (add to ${colors.cyan('helpers')} section): helpers: { ${name}: { require: '${result.filename}', }, }, - `); - }); -}; + `) + }) +} -const healTemplate = fs.readFileSync(path.join(__dirname, '../template/heal.js'), 'utf8').toString(); +const healTemplate = fs.readFileSync(path.join(__dirname, '../template/heal.js'), 'utf8').toString() module.exports.heal = function (genPath) { - const testsPath = getTestRoot(genPath); + const testsPath = getTestRoot(genPath) - let configFile = path.join(testsPath, `codecept.conf.${extension}`); + let configFile = path.join(testsPath, `codecept.conf.${extension}`) if (!fileExists(configFile)) { - configFile = path.join(testsPath, `codecept.conf.${extension}`); - if (fileExists(configFile)) extension = 'ts'; + configFile = path.join(testsPath, `codecept.conf.${extension}`) + if (fileExists(configFile)) extension = 'ts' } - output.print('Creating basic heal recipes'); - output.print(`Add your own custom recipes to ./heal.${extension} file`); - output.print('Require this file in the config file and enable heal plugin:'); - output.print('--------------------------'); + output.print('Creating basic heal recipes') + output.print(`Add your own custom recipes to ./heal.${extension} file`) + output.print('Require this file in the config file and enable heal plugin:') + output.print('--------------------------') output.print(` require('./heal') @@ -286,9 +296,9 @@ exports.config = { } } } - `); + `) - const healFile = path.join(testsPath, `heal.${extension}`); - if (!safeFileWrite(healFile, healTemplate)) return; - output.success(`Heal recipes were created in ${healFile}`); -}; + const healFile = path.join(testsPath, `heal.${extension}`) + if (!safeFileWrite(healFile, healTemplate)) return + output.success(`Heal recipes were created in ${healFile}`) +} diff --git a/lib/command/info.js b/lib/command/info.js index 933a469a6..55f9856c3 100644 --- a/lib/command/info.js +++ b/lib/command/info.js @@ -1,42 +1,44 @@ -const envinfo = require('envinfo'); +const envinfo = require('envinfo') -const { getConfig, getTestRoot } = require('./utils'); -const Codecept = require('../codecept'); -const output = require('../output'); +const { getConfig, getTestRoot } = require('./utils') +const Codecept = require('../codecept') +const output = require('../output') module.exports = async function (path) { - const testsPath = getTestRoot(path); - const config = getConfig(testsPath); - const codecept = new Codecept(config, {}); - codecept.init(testsPath); + const testsPath = getTestRoot(path) + const config = getConfig(testsPath) + const codecept = new Codecept(config, {}) + codecept.init(testsPath) - output.print('\n Environment information:-\n'); - const info = {}; - info.codeceptVersion = Codecept.version(); - info.nodeInfo = await envinfo.helpers.getNodeInfo(); - info.osInfo = await envinfo.helpers.getOSInfo(); - info.cpuInfo = await envinfo.helpers.getCPUInfo(); - info.chromeInfo = await envinfo.helpers.getChromeInfo(); - info.edgeInfo = await envinfo.helpers.getEdgeInfo(); - info.firefoxInfo = await envinfo.helpers.getFirefoxInfo(); - info.safariInfo = await envinfo.helpers.getSafariInfo(); - const { helpers, plugins } = config; - info.helpers = helpers || "You don't use any helpers"; - info.plugins = plugins || "You don't have any enabled plugins"; + output.print('\n Environment information:-\n') + const info = {} + info.codeceptVersion = Codecept.version() + info.nodeInfo = await envinfo.helpers.getNodeInfo() + info.osInfo = await envinfo.helpers.getOSInfo() + info.cpuInfo = await envinfo.helpers.getCPUInfo() + info.chromeInfo = await envinfo.helpers.getChromeInfo() + info.edgeInfo = await envinfo.helpers.getEdgeInfo() + info.firefoxInfo = await envinfo.helpers.getFirefoxInfo() + info.safariInfo = await envinfo.helpers.getSafariInfo() + const { helpers, plugins } = config + info.helpers = helpers || "You don't use any helpers" + info.plugins = plugins || "You don't have any enabled plugins" for (const [key, value] of Object.entries(info)) { if (Array.isArray(value)) { - output.print(`${key}: ${value[1]}`); + output.print(`${key}: ${value[1]}`) } else { - output.print(`${key}: ${JSON.stringify(value, null, ' ')}`); + output.print(`${key}: ${JSON.stringify(value, null, ' ')}`) } } - output.print('***************************************'); - output.print('If you have questions ask them in our Slack: http://bit.ly/chat-codeceptjs'); - output.print('Or ask them on our discussion board: https://codecept.discourse.group/'); - output.print('Please copy environment info when you report issues on GitHub: https://github.com/Codeception/CodeceptJS/issues'); - output.print('***************************************'); -}; + output.print('***************************************') + output.print('If you have questions ask them in our Slack: http://bit.ly/chat-codeceptjs') + output.print('Or ask them on our discussion board: https://codecept.discourse.group/') + output.print( + 'Please copy environment info when you report issues on GitHub: https://github.com/Codeception/CodeceptJS/issues', + ) + output.print('***************************************') +} module.exports.getMachineInfo = async () => { const info = { @@ -47,17 +49,17 @@ module.exports.getMachineInfo = async () => { edgeInfo: await envinfo.helpers.getEdgeInfo(), firefoxInfo: await envinfo.helpers.getFirefoxInfo(), safariInfo: await envinfo.helpers.getSafariInfo(), - }; + } - output.print('***************************************'); + output.print('***************************************') for (const [key, value] of Object.entries(info)) { if (Array.isArray(value)) { - output.print(`${key}: ${value[1]}`); + output.print(`${key}: ${value[1]}`) } else { - output.print(`${key}: ${JSON.stringify(value, null, ' ')}`); + output.print(`${key}: ${JSON.stringify(value, null, ' ')}`) } } - output.print('If you need more detailed info, just run this: npx codeceptjs info'); - output.print('***************************************'); - return info; -}; + output.print('If you need more detailed info, just run this: npx codeceptjs info') + output.print('***************************************') + return info +} diff --git a/lib/command/init.js b/lib/command/init.js index 0e1321646..dc7027a77 100644 --- a/lib/command/init.js +++ b/lib/command/init.js @@ -1,37 +1,37 @@ -const colors = require('chalk'); -const fs = require('fs'); -const inquirer = require('inquirer'); -const mkdirp = require('mkdirp'); -const path = require('path'); -const { inspect } = require('util'); -const spawn = require('cross-spawn'); - -const { print, success, error } = require('../output'); -const { fileExists, beautify, installedLocally } = require('../utils'); -const { getTestRoot } = require('./utils'); -const generateDefinitions = require('./definitions'); -const { test: generateTest } = require('./generate'); -const isLocal = require('../utils').installedLocally(); +const colors = require('chalk') +const fs = require('fs') +const inquirer = require('inquirer') +const mkdirp = require('mkdirp') +const path = require('path') +const { inspect } = require('util') +const spawn = require('cross-spawn') + +const { print, success, error } = require('../output') +const { fileExists, beautify, installedLocally } = require('../utils') +const { getTestRoot } = require('./utils') +const generateDefinitions = require('./definitions') +const { test: generateTest } = require('./generate') +const isLocal = require('../utils').installedLocally() const defaultConfig = { tests: './*_test.js', output: '', helpers: {}, include: {}, -}; +} -const helpers = ['Playwright', 'WebDriver', 'Puppeteer', 'REST', 'GraphQL', 'Appium', 'TestCafe']; -const translations = Object.keys(require('../../translations')); +const helpers = ['Playwright', 'WebDriver', 'Puppeteer', 'REST', 'GraphQL', 'Appium', 'TestCafe'] +const translations = Object.keys(require('../../translations')) -const noTranslation = 'English (no localization)'; -translations.unshift(noTranslation); +const noTranslation = 'English (no localization)' +translations.unshift(noTranslation) -const packages = []; -let isTypeScript = false; -let extension = 'js'; +const packages = [] +let isTypeScript = false +let extension = 'js' -const requireCodeceptConfigure = "const { setHeadlessWhen, setCommonPlugins } = require('@codeceptjs/configure');"; -const importCodeceptConfigure = "import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';"; +const requireCodeceptConfigure = "const { setHeadlessWhen, setCommonPlugins } = require('@codeceptjs/configure');" +const importCodeceptConfigure = "import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';" const configHeader = ` // turn on headless mode when running with HEADLESS=true environment variable @@ -41,7 +41,7 @@ setHeadlessWhen(process.env.HEADLESS); // enable all common plugins https://github.com/codeceptjs/configure#setcommonplugins setCommonPlugins(); -`; +` const defaultActor = `// in this file you can append custom step methods to 'I' object @@ -53,7 +53,7 @@ module.exports = function() { }); } -`; +` const defaultActorTs = `// in this file you can append custom step methods to 'I' object @@ -65,354 +65,357 @@ export = function() { }); } -`; +` module.exports = function (initPath) { - const testsPath = getTestRoot(initPath); - - print(); - print(` Welcome to ${colors.magenta.bold('CodeceptJS')} initialization tool`); - print(' It will prepare and configure a test environment for you'); - print(); - print(' Useful links:'); - print(); - print(' 👉 How to start testing ASAP: https://codecept.io/quickstart/#init'); - print(' 👉 How to select helper: https://codecept.io/basics/#architecture'); - print(' 👉 TypeScript setup: https://codecept.io/typescript/#getting-started'); - print(); + const testsPath = getTestRoot(initPath) + + print() + print(` Welcome to ${colors.magenta.bold('CodeceptJS')} initialization tool`) + print(' It will prepare and configure a test environment for you') + print() + print(' Useful links:') + print() + print(' 👉 How to start testing ASAP: https://codecept.io/quickstart/#init') + print(' 👉 How to select helper: https://codecept.io/basics/#architecture') + print(' 👉 TypeScript setup: https://codecept.io/typescript/#getting-started') + print() if (!path) { - print('No test root specified.'); - print(`Test root is assumed to be ${colors.yellow.bold(testsPath)}`); - print('----------------------------------'); + print('No test root specified.') + print(`Test root is assumed to be ${colors.yellow.bold(testsPath)}`) + print('----------------------------------') } else { - print(`Installing to ${colors.bold(testsPath)}`); + print(`Installing to ${colors.bold(testsPath)}`) } if (!fileExists(testsPath)) { - print(`Directory ${testsPath} does not exist, creating...`); - mkdirp.sync(testsPath); + print(`Directory ${testsPath} does not exist, creating...`) + mkdirp.sync(testsPath) } - const configFile = path.join(testsPath, 'codecept.conf.js'); + const configFile = path.join(testsPath, 'codecept.conf.js') if (fileExists(configFile)) { - error(`Config is already created at ${configFile}`); - return; + error(`Config is already created at ${configFile}`) + return } - const typeScriptconfigFile = path.join(testsPath, 'codecept.conf.ts'); + const typeScriptconfigFile = path.join(testsPath, 'codecept.conf.ts') if (fileExists(typeScriptconfigFile)) { - error(`Config is already created at ${typeScriptconfigFile}`); - return; + error(`Config is already created at ${typeScriptconfigFile}`) + return } - inquirer.prompt([ - { - name: 'typescript', - type: 'confirm', - default: false, - message: 'Do you plan to write tests in TypeScript?', - }, - { - name: 'tests', - type: 'input', - default: (answers) => `./*_test.${answers.typescript ? 'ts' : 'js'}`, - message: 'Where are your tests located?', - }, - { - name: 'helper', - type: 'list', - choices: helpers, - default: 'Playwright', - message: 'What helpers do you want to use?', - }, - { - name: 'jsonResponse', - type: 'confirm', - default: true, - message: 'Do you want to use JSONResponse helper for assertions on JSON responses? http://bit.ly/3ASVPy9', - when: (answers) => ['GraphQL', 'REST'].includes(answers.helper) === true, - }, - { - name: 'output', - default: './output', - message: 'Where should logs, screenshots, and reports to be stored?', - }, - { - name: 'translation', - type: 'list', - message: 'Do you want to enable localization for tests? http://bit.ly/3GNUBbh', - choices: translations, - }, - ]).then((result) => { - if (result.typescript === true) { - isTypeScript = true; - extension = isTypeScript === true ? 'ts' : 'js'; - packages.push('typescript'); - packages.push('ts-node'); - packages.push('@types/node'); - } - - const config = defaultConfig; - config.name = testsPath.split(path.sep).pop(); - config.output = result.output; + inquirer + .prompt([ + { + name: 'typescript', + type: 'confirm', + default: false, + message: 'Do you plan to write tests in TypeScript?', + }, + { + name: 'tests', + type: 'input', + default: (answers) => `./*_test.${answers.typescript ? 'ts' : 'js'}`, + message: 'Where are your tests located?', + }, + { + name: 'helper', + type: 'list', + choices: helpers, + default: 'Playwright', + message: 'What helpers do you want to use?', + }, + { + name: 'jsonResponse', + type: 'confirm', + default: true, + message: 'Do you want to use JSONResponse helper for assertions on JSON responses? http://bit.ly/3ASVPy9', + when: (answers) => ['GraphQL', 'REST'].includes(answers.helper) === true, + }, + { + name: 'output', + default: './output', + message: 'Where should logs, screenshots, and reports to be stored?', + }, + { + name: 'translation', + type: 'list', + message: 'Do you want to enable localization for tests? http://bit.ly/3GNUBbh', + choices: translations, + }, + ]) + .then((result) => { + if (result.typescript === true) { + isTypeScript = true + extension = isTypeScript === true ? 'ts' : 'js' + packages.push('typescript') + packages.push('ts-node') + packages.push('@types/node') + } - config.tests = result.tests; - if (isTypeScript) { - config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`; - } + const config = defaultConfig + config.name = testsPath.split(path.sep).pop() + config.output = result.output - // create a directory tests if it is included in tests path - const matchResults = config.tests.match(/[^*.]+/); - if (matchResults) { - mkdirp.sync(path.join(testsPath, matchResults[0])); - } - - if (result.translation !== noTranslation) config.translation = result.translation; + config.tests = result.tests + if (isTypeScript) { + config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}` + } - const helperName = result.helper; - config.helpers[helperName] = {}; + // create a directory tests if it is included in tests path + const matchResults = config.tests.match(/[^*.]+/) + if (matchResults) { + mkdirp.sync(path.join(testsPath, matchResults[0])) + } - if (result.jsonResponse === true) { - config.helpers.JSONResponse = {}; - } + if (result.translation !== noTranslation) config.translation = result.translation - let helperConfigs = []; + const helperName = result.helper + config.helpers[helperName] = {} - try { - const Helper = require(`../helper/${helperName}`); - if (Helper._checkRequirements) { - packages.concat(Helper._checkRequirements()); + if (result.jsonResponse === true) { + config.helpers.JSONResponse = {} } - if (!Helper._config()) return; - helperConfigs = helperConfigs.concat(Helper._config().map((config) => { - config.message = `[${helperName}] ${config.message}`; - config.name = `${helperName}_${config.name}`; - config.type = config.type || 'input'; - return config; - })); - } catch (err) { - error(err); - } + let helperConfigs = [] - const finish = async () => { - // create steps file by default - // no extra step file for typescript (as it doesn't match TS conventions) - const stepFile = `./steps_file.${extension}`; - fs.writeFileSync(path.join(testsPath, stepFile), extension === 'ts' ? defaultActorTs : defaultActor); + try { + const Helper = require(`../helper/${helperName}`) + if (Helper._checkRequirements) { + packages.concat(Helper._checkRequirements()) + } - if (isTypeScript) { - config.include = _actorTranslation('./steps_file', config.translation); - } else { - config.include = _actorTranslation(stepFile, config.translation); + if (!Helper._config()) return + helperConfigs = helperConfigs.concat( + Helper._config().map((config) => { + config.message = `[${helperName}] ${config.message}` + config.name = `${helperName}_${config.name}` + config.type = config.type || 'input' + return config + }), + ) + } catch (err) { + error(err) } - print(`Steps file created at ${stepFile}`); + const finish = async () => { + // create steps file by default + // no extra step file for typescript (as it doesn't match TS conventions) + const stepFile = `./steps_file.${extension}` + fs.writeFileSync(path.join(testsPath, stepFile), extension === 'ts' ? defaultActorTs : defaultActor) - let configSource; - const hasConfigure = isLocal && !initPath; - - if (isTypeScript) { - configSource = beautify(`export const config : CodeceptJS.MainConfig = ${inspect(config, false, 4, false)}`); + if (isTypeScript) { + config.include = _actorTranslation('./steps_file', config.translation) + } else { + config.include = _actorTranslation(stepFile, config.translation) + } - if (hasConfigure) configSource = importCodeceptConfigure + configHeader + configSource; + print(`Steps file created at ${stepFile}`) - fs.writeFileSync(typeScriptconfigFile, configSource, 'utf-8'); - print(`Config created at ${typeScriptconfigFile}`); - } else { - configSource = beautify(`/** @type {CodeceptJS.MainConfig} */\nexports.config = ${inspect(config, false, 4, false)}`); + let configSource + const hasConfigure = isLocal && !initPath - if (hasConfigure) configSource = requireCodeceptConfigure + configHeader + configSource; + if (isTypeScript) { + configSource = beautify(`export const config : CodeceptJS.MainConfig = ${inspect(config, false, 4, false)}`) - fs.writeFileSync(configFile, configSource, 'utf-8'); - print(`Config created at ${configFile}`); - } + if (hasConfigure) configSource = importCodeceptConfigure + configHeader + configSource - if (config.output) { - if (!fileExists(config.output)) { - mkdirp.sync(path.join(testsPath, config.output)); - print(`Directory for temporary output files created at '${config.output}'`); + fs.writeFileSync(typeScriptconfigFile, configSource, 'utf-8') + print(`Config created at ${typeScriptconfigFile}`) } else { - print(`Directory for temporary output files is already created at '${config.output}'`); + configSource = beautify( + `/** @type {CodeceptJS.MainConfig} */\nexports.config = ${inspect(config, false, 4, false)}`, + ) + + if (hasConfigure) configSource = requireCodeceptConfigure + configHeader + configSource + + fs.writeFileSync(configFile, configSource, 'utf-8') + print(`Config created at ${configFile}`) } - } - const jsconfig = { - compilerOptions: { - allowJs: true, - }, - }; - - const tsconfig = { - 'ts-node': { - files: true, - }, - compilerOptions: { - target: 'es2018', - lib: ['es2018', 'DOM'], - esModuleInterop: true, - module: 'commonjs', - strictNullChecks: false, - types: ['codeceptjs', 'node'], - declaration: true, - skipLibCheck: true, - }, - exclude: ['node_modules'], - }; + if (config.output) { + if (!fileExists(config.output)) { + mkdirp.sync(path.join(testsPath, config.output)) + print(`Directory for temporary output files created at '${config.output}'`) + } else { + print(`Directory for temporary output files is already created at '${config.output}'`) + } + } - if (isTypeScript) { - const tsconfigJson = beautify(JSON.stringify(tsconfig)); - const tsconfigFile = path.join(testsPath, 'tsconfig.json'); - if (fileExists(tsconfigFile)) { - print(`tsconfig.json already exists at ${tsconfigFile}`); - } else { - fs.writeFileSync(tsconfigFile, tsconfigJson); + const jsconfig = { + compilerOptions: { + allowJs: true, + }, } - } else { - const jsconfigJson = beautify(JSON.stringify(jsconfig)); - const jsconfigFile = path.join(testsPath, 'jsconfig.json'); - if (fileExists(jsconfigFile)) { - print(`jsconfig.json already exists at ${jsconfigFile}`); + + const tsconfig = { + 'ts-node': { + files: true, + }, + compilerOptions: { + target: 'es2018', + lib: ['es2018', 'DOM'], + esModuleInterop: true, + module: 'commonjs', + strictNullChecks: false, + types: ['codeceptjs', 'node'], + declaration: true, + skipLibCheck: true, + }, + exclude: ['node_modules'], + } + + if (isTypeScript) { + const tsconfigJson = beautify(JSON.stringify(tsconfig)) + const tsconfigFile = path.join(testsPath, 'tsconfig.json') + if (fileExists(tsconfigFile)) { + print(`tsconfig.json already exists at ${tsconfigFile}`) + } else { + fs.writeFileSync(tsconfigFile, tsconfigJson) + } } else { - fs.writeFileSync(jsconfigFile, jsconfigJson); - print(`Intellisense enabled in ${jsconfigFile}`); + const jsconfigJson = beautify(JSON.stringify(jsconfig)) + const jsconfigFile = path.join(testsPath, 'jsconfig.json') + if (fileExists(jsconfigFile)) { + print(`jsconfig.json already exists at ${jsconfigFile}`) + } else { + fs.writeFileSync(jsconfigFile, jsconfigJson) + print(`Intellisense enabled in ${jsconfigFile}`) + } } - } - const generateDefinitionsManually = colors.bold(`To get auto-completion support, please generate type definitions: ${colors.green('npx codeceptjs def')}`); + const generateDefinitionsManually = colors.bold( + `To get auto-completion support, please generate type definitions: ${colors.green('npx codeceptjs def')}`, + ) + + if (packages) { + try { + install(packages) + } catch (err) { + print(colors.bold.red(err.toString())) + print() + print(colors.bold.red('Please install next packages manually:')) + print(`npm i ${packages.join(' ')} --save-dev`) + print() + print('Things to do after missing packages installed:') + print('☑', generateDefinitionsManually) + print('☑ Create first test:', colors.green('npx codeceptjs gt')) + print(colors.bold.magenta('Find more information at https://codecept.io')) + return + } + } - if (packages) { try { - install(packages); + generateDefinitions(testsPath, {}) } catch (err) { - print(colors.bold.red(err.toString())); - print(); - print(colors.bold.red('Please install next packages manually:')); - print(`npm i ${packages.join(' ')} --save-dev`); - print(); - print('Things to do after missing packages installed:'); - print('☑', generateDefinitionsManually); - print('☑ Create first test:', colors.green('npx codeceptjs gt')); - print(colors.bold.magenta('Find more information at https://codecept.io')); - return; + print(colors.bold.red("Couldn't generate type definitions")) + print(colors.red(err.toString())) + print('Skipping type definitions...') + print(generateDefinitionsManually) } - } - try { - generateDefinitions(testsPath, {}); - } catch (err) { - print(colors.bold.red('Couldn\'t generate type definitions')); - print(colors.red(err.toString())); - print('Skipping type definitions...'); - print(generateDefinitionsManually); + print('') + success(' Almost ready... Next step:') + + const generatedTest = generateTest(testsPath) + if (!generatedTest) return + generatedTest.then(() => { + print('\n--') + print(colors.bold.green('CodeceptJS Installed! Enjoy supercharged testing! 🤩')) + print(colors.bold.magenta('Find more information at https://codecept.io')) + print() + }) } - print(''); - success(' Almost ready... Next step:'); - - const generatedTest = generateTest(testsPath); - if (!generatedTest) return; - generatedTest.then(() => { - print('\n--'); - print(colors.bold.green('CodeceptJS Installed! Enjoy supercharged testing! 🤩')); - print(colors.bold.magenta('Find more information at https://codecept.io')); - print(); - }); - }; - - print('Configure helpers...'); - inquirer.prompt(helperConfigs).then(async (helperResult) => { - if (helperResult.Playwright_browser === 'electron') { - delete helperResult.Playwright_url; - delete helperResult.Playwright_show; - - helperResult.Playwright_electron = { - executablePath: '// require("electron") or require("electron-forge")', - args: ['path/to/your/main.js'], - }; - } + print('Configure helpers...') + inquirer.prompt(helperConfigs).then(async (helperResult) => { + if (helperResult.Playwright_browser === 'electron') { + delete helperResult.Playwright_url + delete helperResult.Playwright_show - Object.keys(helperResult).forEach((key) => { - const parts = key.split('_'); - const helperName = parts[0]; - const configName = parts[1]; - if (!configName) return; - config.helpers[helperName][configName] = helperResult[key]; - }); - - print(''); - await finish(); - }); - }); -}; + helperResult.Playwright_electron = { + executablePath: '// require("electron") or require("electron-forge")', + args: ['path/to/your/main.js'], + } + } + + Object.keys(helperResult).forEach((key) => { + const parts = key.split('_') + const helperName = parts[0] + const configName = parts[1] + if (!configName) return + config.helpers[helperName][configName] = helperResult[key] + }) + + print('') + await finish() + }) + }) +} function install(dependencies) { - let command; - let args; + let command + let args if (!fs.existsSync(path.join(process.cwd(), 'package.json'))) { - dependencies.push('codeceptjs'); - throw new Error("Error: 'package.json' file not found. Generate it with 'npm init -y' command."); + dependencies.push('codeceptjs') + throw new Error("Error: 'package.json' file not found. Generate it with 'npm init -y' command.") } if (!installedLocally()) { - console.log('CodeceptJS should be installed locally'); - dependencies.push('codeceptjs'); + console.log('CodeceptJS should be installed locally') + dependencies.push('codeceptjs') } - console.log('Installing packages: ', colors.green(dependencies.join(', '))); + console.log('Installing packages: ', colors.green(dependencies.join(', '))) if (fileExists('yarn.lock')) { - command = 'yarnpkg'; - args = ['add', '-D', '--exact']; - [].push.apply(args, dependencies); + command = 'yarnpkg' + args = ['add', '-D', '--exact'] + ;[].push.apply(args, dependencies) - args.push('--cwd'); - args.push(process.cwd()); + args.push('--cwd') + args.push(process.cwd()) } else { - command = 'npm'; - args = [ - 'install', - '--save-dev', - '--loglevel', - 'error', - ].concat(dependencies); + command = 'npm' + args = ['install', '--save-dev', '--loglevel', 'error'].concat(dependencies) } if (process.env._INIT_DRY_RUN_INSTALL) { - args.push('--dry-run'); + args.push('--dry-run') } - const { status } = spawn.sync(command, args, { stdio: 'inherit' }); + const { status } = spawn.sync(command, args, { stdio: 'inherit' }) if (status !== 0) { - throw new Error(`${command} ${args.join(' ')} failed`); + throw new Error(`${command} ${args.join(' ')} failed`) } - return true; + return true } function _actorTranslation(stepFile, translationSelected) { - let actor; + let actor for (const translationAvailable of translations) { if (actor) { - break; + break } if (translationSelected === translationAvailable) { - const nameOfActor = require('../../translations')[translationAvailable].I; + const nameOfActor = require('../../translations')[translationAvailable].I actor = { [nameOfActor]: stepFile, - }; + } } } if (!actor) { actor = { I: stepFile, - }; + } } - return actor; + return actor } diff --git a/lib/command/interactive.js b/lib/command/interactive.js index e3a33536a..7f92cbec8 100644 --- a/lib/command/interactive.js +++ b/lib/command/interactive.js @@ -1,55 +1,55 @@ -const { getConfig, getTestRoot } = require('./utils'); -const recorder = require('../recorder'); -const Codecept = require('../codecept'); -const Container = require('../container'); -const event = require('../event'); -const output = require('../output'); -const webHelpers = require('../plugin/standardActingHelpers'); +const { getConfig, getTestRoot } = require('./utils') +const recorder = require('../recorder') +const Codecept = require('../codecept') +const Container = require('../container') +const event = require('../event') +const output = require('../output') +const webHelpers = require('../plugin/standardActingHelpers') module.exports = async function (path, options) { // Backward compatibility for --profile - process.profile = options.profile; - process.env.profile = options.profile; - const configFile = options.config; + process.profile = options.profile + process.env.profile = options.profile + const configFile = options.config - const config = getConfig(configFile); - const testsPath = getTestRoot(configFile); + const config = getConfig(configFile) + const testsPath = getTestRoot(configFile) - const codecept = new Codecept(config, options); - codecept.init(testsPath); + const codecept = new Codecept(config, options) + codecept.init(testsPath) try { - await codecept.bootstrap(); + await codecept.bootstrap() - if (options.verbose) output.level(3); + if (options.verbose) output.level(3) - output.print('Starting interactive shell for current suite...'); - recorder.start(); + output.print('Starting interactive shell for current suite...') + recorder.start() event.emit(event.suite.before, { fullTitle: () => 'Interactive Shell', tests: [], - }); + }) event.emit(event.test.before, { title: '', artifacts: {}, - }); + }) - const enabledHelpers = Container.helpers(); + const enabledHelpers = Container.helpers() for (const helperName of Object.keys(enabledHelpers)) { if (webHelpers.includes(helperName)) { - const I = enabledHelpers[helperName]; - recorder.add(() => I.amOnPage('/')); - recorder.catchWithoutStop(e => output.print(`Error while loading home page: ${e.message}}`)); - break; + const I = enabledHelpers[helperName] + recorder.add(() => I.amOnPage('/')) + recorder.catchWithoutStop((e) => output.print(`Error while loading home page: ${e.message}}`)) + break } } - require('../pause')(); + require('../pause')() // recorder.catchWithoutStop((err) => console.log(err.stack)); - recorder.add(() => event.emit(event.test.after, {})); - recorder.add(() => event.emit(event.suite.after, {})); - recorder.add(() => event.emit(event.all.result, {})); - recorder.add(() => codecept.teardown()); + recorder.add(() => event.emit(event.test.after, {})) + recorder.add(() => event.emit(event.suite.after, {})) + recorder.add(() => event.emit(event.all.result, {})) + recorder.add(() => codecept.teardown()) } catch (err) { - output.error(`Error while running bootstrap file :${err}`); + output.error(`Error while running bootstrap file :${err}`) } -}; +} diff --git a/lib/command/list.js b/lib/command/list.js index 3a14e5942..47a1047c9 100644 --- a/lib/command/list.js +++ b/lib/command/list.js @@ -1,36 +1,36 @@ -const { getConfig, getTestRoot } = require('./utils'); -const Codecept = require('../codecept'); -const container = require('../container'); -const { getParamsToString } = require('../parser'); -const { methodsOfObject } = require('../utils'); -const output = require('../output'); +const { getConfig, getTestRoot } = require('./utils') +const Codecept = require('../codecept') +const container = require('../container') +const { getParamsToString } = require('../parser') +const { methodsOfObject } = require('../utils') +const output = require('../output') module.exports = function (path) { - const testsPath = getTestRoot(path); - const config = getConfig(testsPath); - const codecept = new Codecept(config, {}); - codecept.init(testsPath); + const testsPath = getTestRoot(path) + const config = getConfig(testsPath) + const codecept = new Codecept(config, {}) + codecept.init(testsPath) - output.print('List of test actions: -- '); - const helpers = container.helpers(); - const supportI = container.support('I'); - const actions = []; + output.print('List of test actions: -- ') + const helpers = container.helpers() + const supportI = container.support('I') + const actions = [] for (const name in helpers) { - const helper = helpers[name]; + const helper = helpers[name] methodsOfObject(helper).forEach((action) => { - const params = getParamsToString(helper[action]); - actions[action] = 1; - output.print(` ${output.colors.grey(name)} I.${output.colors.bold(action)}(${params})`); - }); + const params = getParamsToString(helper[action]) + actions[action] = 1 + output.print(` ${output.colors.grey(name)} I.${output.colors.bold(action)}(${params})`) + }) } for (const name in supportI) { if (actions[name]) { - continue; + continue } - const actor = supportI[name]; - const params = getParamsToString(actor); - output.print(` I.${output.colors.bold(name)}(${params})`); + const actor = supportI[name] + const params = getParamsToString(actor) + output.print(` I.${output.colors.bold(name)}(${params})`) } - output.print('PS: Actions are retrieved from enabled helpers. '); - output.print('Implement custom actions in your helper classes.'); -}; + output.print('PS: Actions are retrieved from enabled helpers. ') + output.print('Implement custom actions in your helper classes.') +} diff --git a/lib/command/run-multiple.js b/lib/command/run-multiple.js index 228cbb36c..0600b4372 100644 --- a/lib/command/run-multiple.js +++ b/lib/command/run-multiple.js @@ -1,186 +1,206 @@ -const { fork } = require('child_process'); -const path = require('path'); -const crypto = require('crypto'); - -const runHook = require('../hooks'); -const event = require('../event'); -const collection = require('./run-multiple/collection'); -const { clearString, replaceValueDeep } = require('../utils'); -const { - getConfig, getTestRoot, fail, -} = require('./utils'); - -const runner = path.join(__dirname, '/../../bin/codecept'); -let config; -const childOpts = {}; -const copyOptions = ['override', 'steps', 'reporter', 'verbose', 'config', 'reporter-options', 'grep', 'fgrep', 'invert', 'debug', 'plugins', 'colors']; -let overrides = {}; +const { fork } = require('child_process') +const path = require('path') +const crypto = require('crypto') + +const runHook = require('../hooks') +const event = require('../event') +const collection = require('./run-multiple/collection') +const { clearString, replaceValueDeep } = require('../utils') +const { getConfig, getTestRoot, fail } = require('./utils') + +const runner = path.join(__dirname, '/../../bin/codecept') +let config +const childOpts = {} +const copyOptions = [ + 'override', + 'steps', + 'reporter', + 'verbose', + 'config', + 'reporter-options', + 'grep', + 'fgrep', + 'invert', + 'debug', + 'plugins', + 'colors', +] +let overrides = {} // codeceptjs run-multiple smoke:chrome regression:firefox - will launch smoke run in chrome and regression in firefox // codeceptjs run-multiple smoke:chrome regression - will launch smoke run in chrome and regression in firefox and chrome // codeceptjs run-multiple --all - will launch all runs // codeceptjs run-multiple smoke regression' -let runId = 1; -let subprocessCount = 0; -let totalSubprocessCount = 0; -let processesDone; +let runId = 1 +let subprocessCount = 0 +let totalSubprocessCount = 0 +let processesDone module.exports = async function (selectedRuns, options) { // registering options globally to use in config if (options.profile) { - process.env.profile = options.profile; + process.env.profile = options.profile } - const configFile = options.config; + const configFile = options.config - const testRoot = getTestRoot(configFile); - global.codecept_dir = testRoot; + const testRoot = getTestRoot(configFile) + global.codecept_dir = testRoot // copy opts to run Object.keys(options) - .filter(key => copyOptions.indexOf(key) > -1) + .filter((key) => copyOptions.indexOf(key) > -1) .forEach((key) => { - childOpts[key] = options[key]; - }); + childOpts[key] = options[key] + }) try { - overrides = JSON.parse(childOpts.override); - delete childOpts.override; + overrides = JSON.parse(childOpts.override) + delete childOpts.override } catch (e) { - overrides = {}; + overrides = {} } config = { ...getConfig(configFile), ...overrides, - }; + } if (!config.multiple) { - fail('Multiple runs not configured, add "multiple": { /../ } section to config'); + fail('Multiple runs not configured, add "multiple": { /../ } section to config') } - selectedRuns = options.all ? Object.keys(config.multiple) : selectedRuns; + selectedRuns = options.all ? Object.keys(config.multiple) : selectedRuns if (!selectedRuns.length) { - fail('No runs provided. Use --all option to run all configured runs'); + fail('No runs provided. Use --all option to run all configured runs') } - await runHook(config.bootstrapAll, 'bootstrapAll'); + await runHook(config.bootstrapAll, 'bootstrapAll') - event.emit(event.multiple.before, null); - if (options.config) { // update paths to config path + event.emit(event.multiple.before, null) + if (options.config) { + // update paths to config path if (config.tests) { - config.tests = path.resolve(testRoot, config.tests); + config.tests = path.resolve(testRoot, config.tests) } if (config.gherkin && config.gherkin.features) { - config.gherkin.features = path.resolve(testRoot, config.gherkin.features); + config.gherkin.features = path.resolve(testRoot, config.gherkin.features) } } if (options.features) { - config.tests = ''; + config.tests = '' } if (options.tests && config.gherkin) { - config.gherkin.features = ''; + config.gherkin.features = '' } const childProcessesPromise = new Promise((resolve) => { - processesDone = resolve; - }); + processesDone = resolve + }) - const runsToExecute = []; + const runsToExecute = [] collection.createRuns(selectedRuns, config).forEach((run) => { - const runName = run.getOriginalName() || run.getName(); - const runConfig = run.getConfig(); - runsToExecute.push(executeRun(runName, runConfig)); - }); + const runName = run.getOriginalName() || run.getName() + const runConfig = run.getConfig() + runsToExecute.push(executeRun(runName, runConfig)) + }) if (!runsToExecute.length) { - fail('Nothing scheduled for execution'); + fail('Nothing scheduled for execution') } // Execute all forks - totalSubprocessCount = runsToExecute.length; - runsToExecute.forEach(runToExecute => runToExecute.call(this)); + totalSubprocessCount = runsToExecute.length + runsToExecute.forEach((runToExecute) => runToExecute.call(this)) return childProcessesPromise.then(async () => { // fire hook - await runHook(config.teardownAll, 'teardownAll'); - event.emit(event.multiple.after, null); - }); -}; + await runHook(config.teardownAll, 'teardownAll') + event.emit(event.multiple.after, null) + }) +} function executeRun(runName, runConfig) { // clone config - let overriddenConfig = { ...config }; + let overriddenConfig = { ...config } // get configuration - const browserConfig = runConfig.browser; - const browserName = browserConfig.browser; + const browserConfig = runConfig.browser + const browserName = browserConfig.browser for (const key in browserConfig) { - overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key]); + overriddenConfig.helpers = replaceValueDeep(overriddenConfig.helpers, key, browserConfig[key]) } - let outputDir = `${runName}_`; + let outputDir = `${runName}_` if (browserConfig.outputName) { - outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName; + outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName } else { - const hash = crypto.createHash('sha256'); - hash.update(JSON.stringify(browserConfig)); - outputDir += hash.digest('hex'); + const hash = crypto.createHash('sha256') + hash.update(JSON.stringify(browserConfig)) + outputDir += hash.digest('hex') } - outputDir += `_${runId}`; + outputDir += `_${runId}` - outputDir = clearString(outputDir); + outputDir = clearString(outputDir) // tweaking default output directories and for mochawesome - overriddenConfig = replaceValueDeep(overriddenConfig, 'output', path.join(config.output, outputDir)); - overriddenConfig = replaceValueDeep(overriddenConfig, 'reportDir', path.join(config.output, outputDir)); - overriddenConfig = replaceValueDeep(overriddenConfig, 'mochaFile', path.join(config.output, outputDir, `${browserName}_report.xml`)); + overriddenConfig = replaceValueDeep(overriddenConfig, 'output', path.join(config.output, outputDir)) + overriddenConfig = replaceValueDeep(overriddenConfig, 'reportDir', path.join(config.output, outputDir)) + overriddenConfig = replaceValueDeep( + overriddenConfig, + 'mochaFile', + path.join(config.output, outputDir, `${browserName}_report.xml`), + ) // override tests configuration if (overriddenConfig.tests) { - overriddenConfig.tests = runConfig.tests; + overriddenConfig.tests = runConfig.tests } if (overriddenConfig.gherkin && runConfig.gherkin && runConfig.gherkin.features) { - overriddenConfig.gherkin.features = runConfig.gherkin.features; + overriddenConfig.gherkin.features = runConfig.gherkin.features } // override grep param and collect all params - const params = ['run', - '--child', `${runId++}.${runName}:${browserName}`, - '--override', JSON.stringify(overriddenConfig), - ]; + const params = [ + 'run', + '--child', + `${runId++}.${runName}:${browserName}`, + '--override', + JSON.stringify(overriddenConfig), + ] Object.keys(childOpts).forEach((key) => { - params.push(`--${key}`); - if (childOpts[key] !== true) params.push(childOpts[key]); - }); + params.push(`--${key}`) + if (childOpts[key] !== true) params.push(childOpts[key]) + }) if (runConfig.grep) { - params.push('--grep'); - params.push(runConfig.grep); + params.push('--grep') + params.push(runConfig.grep) } const onProcessEnd = (errorCode) => { if (errorCode !== 0) { - process.exitCode = errorCode; + process.exitCode = errorCode } - subprocessCount += 1; + subprocessCount += 1 if (subprocessCount === totalSubprocessCount) { - processesDone(); + processesDone() } - return errorCode; - }; + return errorCode + } // Return function of fork for later execution - return () => fork(runner, params, { stdio: [0, 1, 2, 'ipc'] }) - .on('exit', (code) => { - return onProcessEnd(code); - }) - .on('error', () => { - return onProcessEnd(1); - }); + return () => + fork(runner, params, { stdio: [0, 1, 2, 'ipc'] }) + .on('exit', (code) => { + return onProcessEnd(code) + }) + .on('error', () => { + return onProcessEnd(1) + }) } diff --git a/lib/command/run-rerun.js b/lib/command/run-rerun.js index a104bbaa6..62f17bdae 100644 --- a/lib/command/run-rerun.js +++ b/lib/command/run-rerun.js @@ -1,38 +1,38 @@ -const { getConfig, getTestRoot } = require('./utils'); -const { printError, createOutputDir } = require('./utils'); -const Config = require('../config'); -const Codecept = require('../rerun'); +const { getConfig, getTestRoot } = require('./utils') +const { printError, createOutputDir } = require('./utils') +const Config = require('../config') +const Codecept = require('../rerun') module.exports = async function (test, options) { // registering options globally to use in config // Backward compatibility for --profile - process.profile = options.profile; - process.env.profile = options.profile; - const configFile = options.config; + process.profile = options.profile + process.env.profile = options.profile + const configFile = options.config - let config = getConfig(configFile); + let config = getConfig(configFile) if (options.override) { - config = Config.append(JSON.parse(options.override)); + config = Config.append(JSON.parse(options.override)) } - const testRoot = getTestRoot(configFile); - createOutputDir(config, testRoot); + const testRoot = getTestRoot(configFile) + createOutputDir(config, testRoot) function processError(err) { - printError(err); - process.exit(1); + printError(err) + process.exit(1) } - const codecept = new Codecept(config, options); + const codecept = new Codecept(config, options) try { - codecept.init(testRoot); + codecept.init(testRoot) - await codecept.bootstrap(); - codecept.loadTests(test); - await codecept.run(); + await codecept.bootstrap() + codecept.loadTests(test) + await codecept.run() } catch (err) { - printError(err); - process.exitCode = 1; + printError(err) + process.exitCode = 1 } finally { - await codecept.teardown(); + await codecept.teardown() } -}; +} diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js index 4b968b971..caae6f6cd 100644 --- a/lib/command/run-workers.js +++ b/lib/command/run-workers.js @@ -1,89 +1,89 @@ // For Node version >=10.5.0, have to use experimental flag -const { tryOrDefault } = require('../utils'); -const output = require('../output'); -const store = require('../store'); -const event = require('../event'); -const Workers = require('../workers'); +const { tryOrDefault } = require('../utils') +const output = require('../output') +const store = require('../store') +const event = require('../event') +const Workers = require('../workers') module.exports = async function (workerCount, selectedRuns, options) { - process.env.profile = options.profile; - - const suiteArr = []; - const passedTestArr = []; - const failedTestArr = []; - const skippedTestArr = []; - const stepArr = []; - - const { config: testConfig, override = '' } = options; - const overrideConfigs = tryOrDefault(() => JSON.parse(override), {}); - const by = options.suites ? 'suite' : 'test'; - delete options.parent; + process.env.profile = options.profile + + const suiteArr = [] + const passedTestArr = [] + const failedTestArr = [] + const skippedTestArr = [] + const stepArr = [] + + const { config: testConfig, override = '' } = options + const overrideConfigs = tryOrDefault(() => JSON.parse(override), {}) + const by = options.suites ? 'suite' : 'test' + delete options.parent const config = { by, testConfig, options, selectedRuns, - }; + } - const numberOfWorkers = parseInt(workerCount, 10); + const numberOfWorkers = parseInt(workerCount, 10) - output.print(`CodeceptJS v${require('../codecept').version()} ${output.standWithUkraine()}`); - output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`); - output.print(); + output.print(`CodeceptJS v${require('../codecept').version()} ${output.standWithUkraine()}`) + output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`) + output.print() - const workers = new Workers(numberOfWorkers, config); - workers.overrideConfig(overrideConfigs); + const workers = new Workers(numberOfWorkers, config) + workers.overrideConfig(overrideConfigs) workers.on(event.suite.before, (suite) => { - suiteArr.push(suite); - }); + suiteArr.push(suite) + }) workers.on(event.step.passed, (step) => { - stepArr.push(step); - }); + stepArr.push(step) + }) workers.on(event.step.failed, (step) => { - stepArr.push(step); - }); + stepArr.push(step) + }) workers.on(event.test.failed, (test) => { - failedTestArr.push(test); - output.test.failed(test); - }); + failedTestArr.push(test) + output.test.failed(test) + }) workers.on(event.test.passed, (test) => { - passedTestArr.push(test); - output.test.passed(test); - }); + passedTestArr.push(test) + output.test.passed(test) + }) workers.on(event.test.skipped, (test) => { - skippedTestArr.push(test); - output.test.skipped(test); - }); + skippedTestArr.push(test) + output.test.skipped(test) + }) workers.on(event.all.result, () => { // expose test stats after all workers finished their execution function addStepsToTest(test, stepArr) { - stepArr.test.steps.forEach(step => { + stepArr.test.steps.forEach((step) => { if (test.steps.length === 0) { - test.steps.push(step); + test.steps.push(step) } - }); + }) } - stepArr.forEach(step => { - passedTestArr.forEach(test => { + stepArr.forEach((step) => { + passedTestArr.forEach((test) => { if (step.test.title === test.title) { - addStepsToTest(test, step); + addStepsToTest(test, step) } - }); + }) - failedTestArr.forEach(test => { + failedTestArr.forEach((test) => { if (step.test.title === test.title) { - addStepsToTest(test, step); + addStepsToTest(test, step) } - }); - }); + }) + }) event.dispatcher.emit(event.workers.result, { suites: suiteArr, @@ -92,24 +92,24 @@ module.exports = async function (workerCount, selectedRuns, options) { failed: failedTestArr, skipped: skippedTestArr, }, - }); - workers.printResults(); - }); + }) + workers.printResults() + }) try { - if (options.verbose || options.debug) store.debugMode = true; + if (options.verbose || options.debug) store.debugMode = true if (options.verbose) { - global.debugMode = true; - const { getMachineInfo } = require('./info'); - await getMachineInfo(); + global.debugMode = true + const { getMachineInfo } = require('./info') + await getMachineInfo() } - await workers.bootstrapAll(); - await workers.run(); + await workers.bootstrapAll() + await workers.run() } catch (err) { - output.error(err); - process.exit(1); + output.error(err) + process.exit(1) } finally { - await workers.teardownAll(); + await workers.teardownAll() } -}; +} diff --git a/lib/command/run.js b/lib/command/run.js index ec3248e24..e76257404 100644 --- a/lib/command/run.js +++ b/lib/command/run.js @@ -1,48 +1,46 @@ -const { - getConfig, printError, getTestRoot, createOutputDir, -} = require('./utils'); -const Config = require('../config'); -const store = require('../store'); -const Codecept = require('../codecept'); +const { getConfig, printError, getTestRoot, createOutputDir } = require('./utils') +const Config = require('../config') +const store = require('../store') +const Codecept = require('../codecept') module.exports = async function (test, options) { // registering options globally to use in config // Backward compatibility for --profile // TODO: remove in CodeceptJS 4 - process.profile = options.profile; + process.profile = options.profile if (options.profile) { - process.env.profile = options.profile; + process.env.profile = options.profile } - if (options.verbose || options.debug) store.debugMode = true; + if (options.verbose || options.debug) store.debugMode = true - const configFile = options.config; + const configFile = options.config - let config = getConfig(configFile); + let config = getConfig(configFile) if (options.override) { - config = Config.append(JSON.parse(options.override)); + config = Config.append(JSON.parse(options.override)) } - const testRoot = getTestRoot(configFile); - createOutputDir(config, testRoot); + const testRoot = getTestRoot(configFile) + createOutputDir(config, testRoot) - const codecept = new Codecept(config, options); + const codecept = new Codecept(config, options) try { - codecept.init(testRoot); - await codecept.bootstrap(); - codecept.loadTests(test); + codecept.init(testRoot) + await codecept.bootstrap() + codecept.loadTests(test) if (options.verbose) { - global.debugMode = true; - const { getMachineInfo } = require('./info'); - await getMachineInfo(); + global.debugMode = true + const { getMachineInfo } = require('./info') + await getMachineInfo() } - await codecept.run(); + await codecept.run() } catch (err) { - printError(err); - process.exitCode = 1; + printError(err) + process.exitCode = 1 } finally { - await codecept.teardown(); + await codecept.teardown() } -}; +} diff --git a/lib/command/utils.js b/lib/command/utils.js index 60ceee8a1..cb5620b79 100644 --- a/lib/command/utils.js +++ b/lib/command/utils.js @@ -1,117 +1,118 @@ -const fs = require('fs'); -const path = require('path'); -const util = require('util'); -const mkdirp = require('mkdirp'); +const fs = require('fs') +const path = require('path') +const util = require('util') +const mkdirp = require('mkdirp') -const output = require('../output'); -const { fileExists, beautify } = require('../utils'); +const output = require('../output') +const { fileExists, beautify } = require('../utils') // alias to deep merge -module.exports.deepMerge = require('../utils').deepMerge; +module.exports.deepMerge = require('../utils').deepMerge module.exports.getConfig = function (configFile) { try { - return require('../config').load(configFile); + return require('../config').load(configFile) } catch (err) { - fail(err.stack); + fail(err.stack) } -}; +} module.exports.readConfig = function (configFile) { try { - const data = fs.readFileSync(configFile, 'utf8'); - return data; + const data = fs.readFileSync(configFile, 'utf8') + return data } catch (err) { - output.error(err); + output.error(err) } -}; +} function getTestRoot(currentPath) { - if (!currentPath) currentPath = '.'; - if (!path.isAbsolute(currentPath)) currentPath = path.join(process.cwd(), currentPath); - currentPath = fs.lstatSync(currentPath).isDirectory() || !path.extname(currentPath) ? currentPath : path.dirname(currentPath); - return currentPath; + if (!currentPath) currentPath = '.' + if (!path.isAbsolute(currentPath)) currentPath = path.join(process.cwd(), currentPath) + currentPath = + fs.lstatSync(currentPath).isDirectory() || !path.extname(currentPath) ? currentPath : path.dirname(currentPath) + return currentPath } -module.exports.getTestRoot = getTestRoot; +module.exports.getTestRoot = getTestRoot function fail(msg) { - output.error(msg); - process.exit(1); + output.error(msg) + process.exit(1) } -module.exports.fail = fail; +module.exports.fail = fail function updateConfig(testsPath, config, extension) { - const configFile = path.join(testsPath, `codecept.conf.${extension}`); + const configFile = path.join(testsPath, `codecept.conf.${extension}`) if (!fileExists(configFile)) { - const msg = `codecept.conf.${extension} config can\'t be updated automatically`; - console.log(); - console.log(`${output.colors.bold.red(msg)}`); - console.log(`${output.colors.bold.red('Please update it manually:')}`); - console.log(); - console.log(config); - console.log(); - return; + const msg = `codecept.conf.${extension} config can\'t be updated automatically` + console.log() + console.log(`${output.colors.bold.red(msg)}`) + console.log(`${output.colors.bold.red('Please update it manually:')}`) + console.log() + console.log(config) + console.log() + return } - console.log(`${output.colors.yellow('Updating configuration file...')}`); - return fs.writeFileSync(configFile, beautify(`exports.config = ${util.inspect(config, false, 4, false)}`), 'utf-8'); + console.log(`${output.colors.yellow('Updating configuration file...')}`) + return fs.writeFileSync(configFile, beautify(`exports.config = ${util.inspect(config, false, 4, false)}`), 'utf-8') } -module.exports.updateConfig = updateConfig; +module.exports.updateConfig = updateConfig function safeFileWrite(file, contents) { if (fileExists(file)) { - output.error(`File ${file} already exist, skipping...`); - return false; + output.error(`File ${file} already exist, skipping...`) + return false } - fs.writeFileSync(file, contents); - return true; + fs.writeFileSync(file, contents) + return true } -module.exports.safeFileWrite = safeFileWrite; +module.exports.safeFileWrite = safeFileWrite module.exports.captureStream = (stream) => { - let oldStream; - let buffer = ''; + let oldStream + let buffer = '' return { startCapture() { - buffer = ''; - oldStream = stream.write.bind(stream); - stream.write = chunk => (buffer += chunk); + buffer = '' + oldStream = stream.write.bind(stream) + stream.write = (chunk) => (buffer += chunk) }, stopCapture() { - if (oldStream !== undefined) stream.write = oldStream; + if (oldStream !== undefined) stream.write = oldStream }, getData: () => buffer, - }; -}; + } +} module.exports.printError = (err) => { - output.print(''); - output.error(err.message); - output.print(''); - output.print(output.colors.grey(err.stack.replace(err.message, ''))); -}; + output.print('') + output.error(err.message) + output.print('') + output.print(output.colors.grey(err.stack.replace(err.message, ''))) +} module.exports.createOutputDir = (config, testRoot) => { - let outputDir; - if (path.isAbsolute(config.output)) outputDir = config.output; - else outputDir = path.join(testRoot, config.output); + let outputDir + if (path.isAbsolute(config.output)) outputDir = config.output + else outputDir = path.join(testRoot, config.output) if (!fileExists(outputDir)) { - output.print(`creating output directory: ${outputDir}`); - mkdirp.sync(outputDir); + output.print(`creating output directory: ${outputDir}`) + mkdirp.sync(outputDir) } -}; +} module.exports.findConfigFile = (testsPath) => { - const extensions = ['js', 'ts']; + const extensions = ['js', 'ts'] for (const ext of extensions) { - const configFile = path.join(testsPath, `codecept.conf.${ext}`); + const configFile = path.join(testsPath, `codecept.conf.${ext}`) if (fileExists(configFile)) { - return configFile; + return configFile } } - return null; -}; + return null +} diff --git a/lib/data/context.js b/lib/data/context.js index 4d6d21bdf..82f3897f8 100644 --- a/lib/data/context.js +++ b/lib/data/context.js @@ -1,129 +1,129 @@ -const { isGenerator } = require('../utils'); -const DataTable = require('./table'); -const DataScenarioConfig = require('./dataScenarioConfig'); -const Secret = require('../secret'); +const { isGenerator } = require('../utils') +const DataTable = require('./table') +const DataScenarioConfig = require('./dataScenarioConfig') +const Secret = require('../secret') module.exports = function (context) { context.Data = function (dataTable) { - const data = detectDataType(dataTable); + const data = detectDataType(dataTable) return { Scenario(title, opts, fn) { - const scenarios = []; + const scenarios = [] if (typeof opts === 'function' && !fn) { - fn = opts; - opts = {}; + fn = opts + opts = {} } - opts.data = data.map(dataRow => dataRow.data); + opts.data = data.map((dataRow) => dataRow.data) data.forEach((dataRow) => { - const dataTitle = replaceTitle(title, dataRow); + const dataTitle = replaceTitle(title, dataRow) if (dataRow.skip) { - context.xScenario(dataTitle); + context.xScenario(dataTitle) } else { - scenarios.push(context.Scenario(dataTitle, opts, fn) - .inject({ current: dataRow.data })); + scenarios.push(context.Scenario(dataTitle, opts, fn).inject({ current: dataRow.data })) } - }); - maskSecretInTitle(scenarios); - return new DataScenarioConfig(scenarios); + }) + maskSecretInTitle(scenarios) + return new DataScenarioConfig(scenarios) }, only: { Scenario(title, opts, fn) { - const scenarios = []; + const scenarios = [] if (typeof opts === 'function' && !fn) { - fn = opts; - opts = {}; + fn = opts + opts = {} } - opts.data = data.map(dataRow => dataRow.data); + opts.data = data.map((dataRow) => dataRow.data) data.forEach((dataRow) => { - const dataTitle = replaceTitle(title, dataRow); + const dataTitle = replaceTitle(title, dataRow) if (dataRow.skip) { - context.xScenario(dataTitle); + context.xScenario(dataTitle) } else { - scenarios.push(context.Scenario.only(dataTitle, opts, fn) - .inject({ current: dataRow.data })); + scenarios.push(context.Scenario.only(dataTitle, opts, fn).inject({ current: dataRow.data })) } - }); - maskSecretInTitle(scenarios); - return new DataScenarioConfig(scenarios); + }) + maskSecretInTitle(scenarios) + return new DataScenarioConfig(scenarios) }, }, - }; - }; + } + } context.xData = function (dataTable) { - const data = detectDataType(dataTable); + const data = detectDataType(dataTable) return { Scenario: (title) => { data.forEach((dataRow) => { - const dataTitle = replaceTitle(title, dataRow); - context.xScenario(dataTitle); - }); - return new DataScenarioConfig([]); + const dataTitle = replaceTitle(title, dataRow) + context.xScenario(dataTitle) + }) + return new DataScenarioConfig([]) }, - }; - }; -}; + } + } +} function replaceTitle(title, dataRow) { if (typeof dataRow.data !== 'object') { - return `${title} | {${JSON.stringify(dataRow.data)}}`; + return `${title} | {${JSON.stringify(dataRow.data)}}` } // if `dataRow` is object and has own `toString()` method, // it should be printed - if (Object.prototype.toString.call(dataRow.data) === (Object()).toString() - && dataRow.data.toString() !== (Object()).toString()) { - return `${title} | ${dataRow.data}`; + if ( + Object.prototype.toString.call(dataRow.data) === Object().toString() && + dataRow.data.toString() !== Object().toString() + ) { + return `${title} | ${dataRow.data}` } - return `${title} | ${JSON.stringify(dataRow.data)}`; + return `${title} | ${JSON.stringify(dataRow.data)}` } function isTableDataRow(row) { - const has = Object.prototype.hasOwnProperty; - return has.call(row, 'data') && has.call(row, 'skip'); + const has = Object.prototype.hasOwnProperty + return has.call(row, 'data') && has.call(row, 'skip') } function detectDataType(dataTable) { if (dataTable instanceof DataTable) { - return dataTable.rows; + return dataTable.rows } if (isGenerator(dataTable)) { - const data = []; + const data = [] for (const dataRow of dataTable()) { data.push({ data: dataRow, - }); + }) } - return data; + return data } if (typeof dataTable === 'function') { - return dataTable(); + return dataTable() } if (Array.isArray(dataTable)) { return dataTable.map((item) => { if (isTableDataRow(item)) { - return item; + return item } return { data: item, skip: false, - }; - }); + } + }) } - throw new Error('Invalid data type. Data accepts either: DataTable || generator || Array || function'); + throw new Error('Invalid data type. Data accepts either: DataTable || generator || Array || function') } function maskSecretInTitle(scenarios) { - scenarios.forEach(scenario => { - const res = []; + scenarios.forEach((scenario) => { + const res = [] - scenario.test.title.split(',').forEach(item => { - res.push(item.replace(/{"_secret":"(.*)"}/, '"*****"')); - }); + scenario.test.title.split(',').forEach((item) => { + res.push(item.replace(/{"_secret":"(.*)"}/, '"*****"')) + }) - scenario.test.title = res.join(','); - }); + scenario.test.title = res.join(',') + }) } diff --git a/lib/data/dataScenarioConfig.js b/lib/data/dataScenarioConfig.js index 9cb9173f7..9d6977819 100644 --- a/lib/data/dataScenarioConfig.js +++ b/lib/data/dataScenarioConfig.js @@ -1,84 +1,84 @@ class DataScenarioConfig { constructor(scenarios) { - this.scenarios = scenarios; + this.scenarios = scenarios } /** - * Declares that test throws error. - * Can pass an Error object or regex matching expected message. - * - * @param {*} err - */ + * Declares that test throws error. + * Can pass an Error object or regex matching expected message. + * + * @param {*} err + */ throws(err) { - this.scenarios.forEach(scenario => scenario.throws(err)); - return this; + this.scenarios.forEach((scenario) => scenario.throws(err)) + return this } /** - * Declares that test should fail. - * If test passes - throws an error. - * Can pass an Error object or regex matching expected message. - * - */ + * Declares that test should fail. + * If test passes - throws an error. + * Can pass an Error object or regex matching expected message. + * + */ fails() { - this.scenarios.forEach(scenario => scenario.fails()); - return this; + this.scenarios.forEach((scenario) => scenario.fails()) + return this } /** - * Retry this test for x times - * - * @param {*} retries - */ + * Retry this test for x times + * + * @param {*} retries + */ retry(retries) { - this.scenarios.forEach(scenario => scenario.retry(retries)); - return this; + this.scenarios.forEach((scenario) => scenario.retry(retries)) + return this } /** - * Set timeout for this test - * @param {*} timeout - */ + * Set timeout for this test + * @param {*} timeout + */ timeout(timeout) { - this.scenarios.forEach(scenario => scenario.timeout(timeout)); - return this; + this.scenarios.forEach((scenario) => scenario.timeout(timeout)) + return this } /** - * Configures a helper. - * Helper name can be omitted and values will be applied to first helper. - */ + * Configures a helper. + * Helper name can be omitted and values will be applied to first helper. + */ config(helper, obj) { - this.scenarios.forEach(scenario => scenario.config(helper, obj)); - return this; + this.scenarios.forEach((scenario) => scenario.config(helper, obj)) + return this } /** - * Append a tag name to scenario title - * @param {*} tagName - */ + * Append a tag name to scenario title + * @param {*} tagName + */ tag(tagName) { - this.scenarios.forEach(scenario => scenario.tag(tagName)); - return this; + this.scenarios.forEach((scenario) => scenario.tag(tagName)) + return this } /** - * Pass in additional objects to inject into test - * @param {*} obj - */ + * Pass in additional objects to inject into test + * @param {*} obj + */ inject(obj) { - this.scenarios.forEach(scenario => scenario.inject(obj)); - return this; + this.scenarios.forEach((scenario) => scenario.inject(obj)) + return this } /** - * Dynamically injects dependencies, see https://codecept.io/pageobjects/#dynamic-injection - * @param {*} dependencies - */ + * Dynamically injects dependencies, see https://codecept.io/pageobjects/#dynamic-injection + * @param {*} dependencies + */ injectDependencies(dependencies) { - this.scenarios.forEach(scenario => scenario.injectDependencies(dependencies)); - return this; + this.scenarios.forEach((scenario) => scenario.injectDependencies(dependencies)) + return this } } -module.exports = DataScenarioConfig; +module.exports = DataScenarioConfig diff --git a/lib/data/dataTableArgument.js b/lib/data/dataTableArgument.js index b32953483..7d1a72d32 100644 --- a/lib/data/dataTableArgument.js +++ b/lib/data/dataTableArgument.js @@ -7,60 +7,60 @@ class DataTableArgument { constructor(gherkinDataTable) { this.rawData = gherkinDataTable.rows.map((row) => { return row.cells.map((cell) => { - return cell.value; - }); - }); + return cell.value + }) + }) } /** Returns the table as a 2-D array - * @returns {string[][]} - */ + * @returns {string[][]} + */ raw() { - return this.rawData.slice(0); + return this.rawData.slice(0) } /** Returns the table as a 2-D array, without the first row - * @returns {string[][]} - */ + * @returns {string[][]} + */ rows() { - const copy = this.raw(); - copy.shift(); - return copy; + const copy = this.raw() + copy.shift() + return copy } /** Returns an array of objects where each row is converted to an object (column header is the key) - * @returns {any[]} - */ + * @returns {any[]} + */ hashes() { - const copy = this.raw(); - const header = copy.shift(); + const copy = this.raw() + const header = copy.shift() return copy.map((row) => { - const r = {}; - row.forEach((cell, index) => r[header[index]] = cell); - return r; - }); + const r = {} + row.forEach((cell, index) => (r[header[index]] = cell)) + return r + }) } /** Returns an object where each row corresponds to an entry * (first column is the key, second column is the value) - * @returns {Record} - */ + * @returns {Record} + */ rowsHash() { - const rows = this.raw(); - const everyRowHasTwoColumns = rows.every((row) => row.length === 2); + const rows = this.raw() + const everyRowHasTwoColumns = rows.every((row) => row.length === 2) if (!everyRowHasTwoColumns) { - throw new Error('rowsHash can only be called on a data table where all rows have exactly two columns'); + throw new Error('rowsHash can only be called on a data table where all rows have exactly two columns') } /** @type {Record} */ - const result = {}; - rows.forEach((x) => (result[x[0]] = x[1])); - return result; + const result = {} + rows.forEach((x) => (result[x[0]] = x[1])) + return result } /** Transposed the data */ transpose() { - this.rawData = this.rawData[0].map((x, i) => this.rawData.map((y) => y[i])); + this.rawData = this.rawData[0].map((x, i) => this.rawData.map((y) => y[i])) } } -module.exports = DataTableArgument; +module.exports = DataTableArgument diff --git a/lib/data/table.js b/lib/data/table.js index 20981a65a..841e444dc 100644 --- a/lib/data/table.js +++ b/lib/data/table.js @@ -4,40 +4,46 @@ class DataTable { /** @param {Array<*>} array */ constructor(array) { - this.array = array; - this.rows = new Array(0); + this.array = array + this.rows = new Array(0) } /** @param {Array<*>} array */ add(array) { - if (array.length !== this.array.length) throw new Error(`There is too many elements in given data array. Please provide data in this format: ${this.array}`); - const tempObj = {}; - let arrayCounter = 0; + if (array.length !== this.array.length) + throw new Error( + `There is too many elements in given data array. Please provide data in this format: ${this.array}`, + ) + const tempObj = {} + let arrayCounter = 0 this.array.forEach((elem) => { - tempObj[elem] = array[arrayCounter]; - tempObj.toString = () => JSON.stringify(tempObj); - arrayCounter++; - }); - this.rows.push({ skip: false, data: tempObj }); + tempObj[elem] = array[arrayCounter] + tempObj.toString = () => JSON.stringify(tempObj) + arrayCounter++ + }) + this.rows.push({ skip: false, data: tempObj }) } /** @param {Array<*>} array */ xadd(array) { - if (array.length !== this.array.length) throw new Error(`There is too many elements in given data array. Please provide data in this format: ${this.array}`); - const tempObj = {}; - let arrayCounter = 0; + if (array.length !== this.array.length) + throw new Error( + `There is too many elements in given data array. Please provide data in this format: ${this.array}`, + ) + const tempObj = {} + let arrayCounter = 0 this.array.forEach((elem) => { - tempObj[elem] = array[arrayCounter]; - tempObj.toString = () => JSON.stringify(tempObj); - arrayCounter++; - }); - this.rows.push({ skip: true, data: tempObj }); + tempObj[elem] = array[arrayCounter] + tempObj.toString = () => JSON.stringify(tempObj) + arrayCounter++ + }) + this.rows.push({ skip: true, data: tempObj }) } /** @param {Function} func */ filter(func) { - return this.rows.filter(row => func(row.data)); + return this.rows.filter((row) => func(row.data)) } } -module.exports = DataTable; +module.exports = DataTable diff --git a/lib/helper/AI.js b/lib/helper/AI.js index b00d33e67..d59b15531 100644 --- a/lib/helper/AI.js +++ b/lib/helper/AI.js @@ -1,47 +1,52 @@ -const Helper = require('@codeceptjs/helper'); -const ora = require('ora-classic'); -const fs = require('fs'); -const path = require('path'); -const ai = require('../ai'); -const standardActingHelpers = require('../plugin/standardActingHelpers'); -const Container = require('../container'); -const { splitByChunks, minifyHtml } = require('../html'); -const { beautify } = require('../utils'); -const output = require('../output'); -const { registerVariable } = require('../pause'); +const Helper = require('@codeceptjs/helper') +const ora = require('ora-classic') +const fs = require('fs') +const path = require('path') +const ai = require('../ai') +const standardActingHelpers = require('../plugin/standardActingHelpers') +const Container = require('../container') +const { splitByChunks, minifyHtml } = require('../html') +const { beautify } = require('../utils') +const output = require('../output') +const { registerVariable } = require('../pause') + +const gtpRole = { + user: 'user', +} /** * AI Helper for CodeceptJS. * * This helper class provides integration with the AI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts. - * This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available. + * This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDriver to ensure the HTML context is available. * * Use it only in development mode. It is recommended to run it only inside pause() mode. * * ## Configuration * - * This helper should be configured in codecept.json or codecept.conf.js + * This helper should be configured in codecept.conf.{js|ts} * * * `chunkSize`: (optional, default: 80000) - The maximum number of characters to send to the AI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4. */ class AI extends Helper { constructor(config) { - super(config); - this.aiAssistant = ai; + super(config) + this.aiAssistant = ai this.options = { chunkSize: 80000, - }; - this.options = { ...this.options, ...config }; + } + this.options = { ...this.options, ...config } + this.aiAssistant.enable(this.config) } _beforeSuite() { - const helpers = Container.helpers(); + const helpers = Container.helpers() for (const helperName of standardActingHelpers) { if (Object.keys(helpers).indexOf(helperName) > -1) { - this.helper = helpers[helperName]; - break; + this.helper = helpers[helperName] + break } } } @@ -58,30 +63,34 @@ class AI extends Helper { * @returns {Promise} - A Promise that resolves to the generated responses from the GPT model, joined by newlines. */ async askGptOnPage(prompt) { - const html = await this.helper.grabSource(); + const html = await this.helper.grabSource() - const htmlChunks = splitByChunks(html, this.options.chunkSize); + const htmlChunks = splitByChunks(html, this.options.chunkSize) - if (htmlChunks.length > 1) this.debug(`Splitting HTML into ${htmlChunks.length} chunks`); + if (htmlChunks.length > 1) this.debug(`Splitting HTML into ${htmlChunks.length} chunks`) - const responses = []; + const responses = [] for (const chunk of htmlChunks) { const messages = [ - { role: 'user', content: prompt }, - { role: 'user', content: `Within this HTML: ${minifyHtml(chunk)}` }, - ]; + { role: gtpRole.user, content: prompt }, + { role: gtpRole.user, content: `Within this HTML: ${minifyHtml(chunk)}` }, + ] - if (htmlChunks.length > 1) messages.push({ role: 'user', content: 'If action is not possible on this page, do not propose anything, I will send another HTML fragment' }); + if (htmlChunks.length > 1) + messages.push({ + role: 'user', + content: 'If action is not possible on this page, do not propose anything, I will send another HTML fragment', + }) - const response = await this._processAIRequest(messages); + const response = await this._processAIRequest(messages) - output.print(response); + output.print(response) - responses.push(response); + responses.push(response) } - return responses.join('\n\n'); + return responses.join('\n\n') } /** @@ -97,18 +106,18 @@ class AI extends Helper { * @returns {Promise} - A Promise that resolves to the generated response from the GPT model. */ async askGptOnPageFragment(prompt, locator) { - const html = await this.helper.grabHTMLFrom(locator); + const html = await this.helper.grabHTMLFrom(locator) const messages = [ - { role: 'user', content: prompt }, - { role: 'user', content: `Within this HTML: ${minifyHtml(html)}` }, - ]; + { role: gtpRole.user, content: prompt }, + { role: gtpRole.user, content: `Within this HTML: ${minifyHtml(html)}` }, + ] - const response = await this._processAIRequest(messages); + const response = await this._processAIRequest(messages) - output.print(response); + output.print(response) - return response; + return response } /** @@ -117,15 +126,13 @@ class AI extends Helper { * @returns {Promise} - A Promise that resolves to the generated response from the GPT model. */ async askGptGeneralPrompt(prompt) { - const messages = [ - { role: 'user', content: prompt }, - ]; + const messages = [{ role: gtpRole.user, content: prompt }] - const response = await this._processAIRequest(messages); + const response = await this._processAIRequest(messages) - output.print(response); + output.print(response) - return response; + return response } /** @@ -154,48 +161,53 @@ class AI extends Helper { * @returns {Promise} A promise that resolves to the requested page object. */ async askForPageObject(pageName, extraPrompt = null, locator = null) { - const html = locator ? await this.helper.grabHTMLFrom(locator) : await this.helper.grabSource(); + const spinner = ora(' Processing AI request...').start() - const spinner = ora(' Processing AI request...').start(); - await this.aiAssistant.setHtmlContext(html); - const response = await this.aiAssistant.generatePageObject(extraPrompt, locator); - spinner.stop(); + try { + const html = locator ? await this.helper.grabHTMLFrom(locator) : await this.helper.grabSource() + await this.aiAssistant.setHtmlContext(html) + const response = await this.aiAssistant.generatePageObject(extraPrompt, locator) + spinner.stop() + + if (!response[0]) { + output.error('No response from AI') + return '' + } - if (!response[0]) { - output.error('No response from AI'); - return ''; - } + const code = beautify(response[0]) - const code = beautify(response[0]); + output.print('----- Generated PageObject ----') + output.print(code) + output.print('-------------------------------') - output.print('----- Generated PageObject ----'); - output.print(code); - output.print('-------------------------------'); + const fileName = path.join(output_dir, `${pageName}Page-${Date.now()}.js`) - const fileName = path.join(output_dir, `${pageName}Page-${Date.now()}.js`); + output.print(output.styles.bold(`Page object for ${pageName} is saved to ${output.styles.bold(fileName)}`)) + fs.writeFileSync(fileName, code) - output.print(output.styles.bold(`Page object for ${pageName} is saved to ${output.styles.bold(fileName)}`)); - fs.writeFileSync(fileName, code); + try { + registerVariable('page', require(fileName)) + output.success('Page object registered for this session as `page` variable') + output.print('Use `=>page.methodName()` in shell to run methods of page object') + output.print('Use `click(page.locatorName)` to check locators of page object') + } catch (err) { + output.error('Error while registering page object') + output.error(err.message) + } - try { - registerVariable('page', require(fileName)); - output.success('Page object registered for this session as `page` variable'); - output.print('Use `=>page.methodName()` in shell to run methods of page object'); - output.print('Use `click(page.locatorName)` to check locators of page object'); - } catch (err) { - output.error('Error while registering page object'); - output.error(err.message); + return code + } catch (e) { + spinner.stop() + throw Error(`Something went wrong! ${e.message}`) } - - return code; } async _processAIRequest(messages) { - const spinner = ora(' Processing AI request...').start(); - const response = await this.aiAssistant.createCompletion(messages); - spinner.stop(); - return response; + const spinner = ora(' Processing AI request...').start() + const response = await this.aiAssistant.createCompletion(messages) + spinner.stop() + return response } } -module.exports = AI; +module.exports = AI diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js index e8f9c5dd6..ece565636 100644 --- a/lib/helper/ApiDataFactory.js +++ b/lib/helper/ApiDataFactory.js @@ -1,7 +1,7 @@ -const path = require('path'); +const path = require('path') -const Helper = require('@codeceptjs/helper'); -const REST = require('./REST'); +const Helper = require('@codeceptjs/helper') +const REST = require('./REST') /** * Helper for managing remote data using REST API. @@ -184,67 +184,67 @@ const REST = require('./REST'); */ class ApiDataFactory extends Helper { constructor(config) { - super(config); + super(config) const defaultConfig = { cleanup: true, REST: {}, factories: {}, returnId: false, - }; - this.config = Object.assign(defaultConfig, this.config); + } + this.config = Object.assign(defaultConfig, this.config) - if (this.config.headers) this.config.REST.defaultHeaders = this.config.headers; - if (this.config.onRequest) this.config.REST.onRequest = this.config.onRequest; - this.restHelper = new REST(Object.assign(this.config.REST, { endpoint: this.config.endpoint })); - this.factories = this.config.factories; + if (this.config.headers) this.config.REST.defaultHeaders = this.config.headers + if (this.config.onRequest) this.config.REST.onRequest = this.config.onRequest + this.restHelper = new REST(Object.assign(this.config.REST, { endpoint: this.config.endpoint })) + this.factories = this.config.factories for (const factory in this.factories) { - const factoryConfig = this.factories[factory]; + const factoryConfig = this.factories[factory] if (!factoryConfig.uri && !factoryConfig.create) { throw new Error(`Uri for factory "${factory}" is not defined. Please set "uri" parameter: "factories": { "${factory}": { "uri": ... - `); + `) } - if (!factoryConfig.create) factoryConfig.create = { post: factoryConfig.uri }; - if (!factoryConfig.delete) factoryConfig.delete = { delete: `${factoryConfig.uri}/{id}` }; + if (!factoryConfig.create) factoryConfig.create = { post: factoryConfig.uri } + if (!factoryConfig.delete) factoryConfig.delete = { delete: `${factoryConfig.uri}/{id}` } - this.factories[factory] = factoryConfig; + this.factories[factory] = factoryConfig } - this.created = {}; - Object.keys(this.factories).forEach(f => this.created[f] = []); + this.created = {} + Object.keys(this.factories).forEach((f) => (this.created[f] = [])) } static _checkRequirements() { try { - require('axios'); - require('rosie'); + require('axios') + require('rosie') } catch (e) { - return ['axios', 'rosie']; + return ['axios', 'rosie'] } } _after() { if (!this.config.cleanup || this.config.cleanup === false) { - return Promise.resolve(); + return Promise.resolve() } - const promises = []; + const promises = [] // clean up all created items for (const factoryName in this.created) { - const createdItems = this.created[factoryName]; - if (!createdItems.length) continue; - this.debug(`Deleting ${createdItems.length} ${factoryName}(s)`); + const createdItems = this.created[factoryName] + if (!createdItems.length) continue + this.debug(`Deleting ${createdItems.length} ${factoryName}(s)`) for (const id in createdItems) { - promises.push(this._requestDelete(factoryName, createdItems[id])); + promises.push(this._requestDelete(factoryName, createdItems[id])) } } - return Promise.all(promises); + return Promise.all(promises) } /** @@ -266,9 +266,9 @@ class ApiDataFactory extends Helper { * @returns {Promise<*>} */ have(factory, params, options) { - const item = this._createItem(factory, params, options); - this.debug(`Creating ${factory} ${JSON.stringify(item)}`); - return this._requestCreate(factory, item); + const item = this._createItem(factory, params, options) + this.debug(`Creating ${factory} ${JSON.stringify(item)}`) + return this._requestCreate(factory, item) } /** @@ -291,27 +291,27 @@ class ApiDataFactory extends Helper { * @param {*} [options] */ haveMultiple(factory, times, params, options) { - const promises = []; + const promises = [] for (let i = 0; i < times; i++) { - promises.push(this.have(factory, params, options)); + promises.push(this.have(factory, params, options)) } - return Promise.all(promises); + return Promise.all(promises) } _createItem(model, data, options) { if (!this.factories[model]) { - throw new Error(`Factory ${model} is not defined in config`); + throw new Error(`Factory ${model} is not defined in config`) } - let modulePath = this.factories[model].factory; + let modulePath = this.factories[model].factory try { try { - require.resolve(modulePath); + require.resolve(modulePath) } catch (e) { - modulePath = path.join(global.codecept_dir, modulePath); + modulePath = path.join(global.codecept_dir, modulePath) } // check if the new syntax `export default new Factory()` is used and loads the builder, otherwise loads the module that used old syntax `module.exports = new Factory()`. - const builder = require(modulePath).default || require(modulePath); - return builder.build(data, options); + const builder = require(modulePath).default || require(modulePath) + return builder.build(data, options) } catch (err) { throw new Error(`Couldn't load factory file from ${modulePath}, check that @@ -322,17 +322,17 @@ class ApiDataFactory extends Helper { points to valid factory file. Factory file should export an object with build method. -Current file error: ${err.message}`); +Current file error: ${err.message}`) } } _fetchId(body, factory) { if (this.config.factories[factory].fetchId) { - return this.config.factories[factory].fetchId(body); + return this.config.factories[factory].fetchId(body) } - if (body.id) return body.id; - if (body[factory] && body[factory].id) return body[factory].id; - return null; + if (body.id) return body.id + if (body[factory] && body[factory].id) return body[factory].id + return null } /** @@ -343,27 +343,27 @@ Current file error: ${err.message}`); * @param {*} data */ _requestCreate(factory, data) { - let request = createRequestFromFunction(this.factories[factory].create, data); + let request = createRequestFromFunction(this.factories[factory].create, data) if (!request) { - const method = Object.keys(this.factories[factory].create)[0]; - const url = this.factories[factory].create[method]; + const method = Object.keys(this.factories[factory].create)[0] + const url = this.factories[factory].create[method] request = { method, url, data, - }; + } } - request.baseURL = this.config.endpoint; + request.baseURL = this.config.endpoint return this.restHelper._executeRequest(request).then((resp) => { - const id = this._fetchId(resp.data, factory); - this.created[factory].push(id); - this.debugSection('Created', `Id: ${id}`); - if (this.config.returnId) return id; - return resp.data; - }); + const id = this._fetchId(resp.data, factory) + this.created[factory].push(id) + this.debugSection('Created', `Id: ${id}`) + if (this.config.returnId) return id + return resp.data + }) } /** @@ -374,37 +374,40 @@ Current file error: ${err.message}`); * @param {*} id */ _requestDelete(factory, id) { - if (!this.factories[factory].delete) return; - let request = createRequestFromFunction(this.factories[factory].delete, id); + if (!this.factories[factory].delete) return + let request = createRequestFromFunction(this.factories[factory].delete, id) if (!request) { - const method = Object.keys(this.factories[factory].delete)[0]; + const method = Object.keys(this.factories[factory].delete)[0] - const url = this.factories[factory].delete[method].replace('{id}', id); + const url = this.factories[factory].delete[method].replace('{id}', id) request = { method, url, - }; + } } - request.baseURL = this.config.endpoint; + request.baseURL = this.config.endpoint if (request.url.match(/^undefined/)) { - return this.debugSection('Please configure the delete request in your ApiDataFactory helper', 'delete: () => ({ method: \'DELETE\', url: \'/api/users\' })'); + return this.debugSection( + 'Please configure the delete request in your ApiDataFactory helper', + "delete: () => ({ method: 'DELETE', url: '/api/users' })", + ) } return this.restHelper._executeRequest(request).then(() => { - const idx = this.created[factory].indexOf(id); - this.debugSection('Deleted Id', `Id: ${id}`); - this.created[factory].splice(idx, 1); - }); + const idx = this.created[factory].indexOf(id) + this.debugSection('Deleted Id', `Id: ${id}`) + this.created[factory].splice(idx, 1) + }) } } -module.exports = ApiDataFactory; +module.exports = ApiDataFactory function createRequestFromFunction(param, data) { - if (typeof param !== 'function') return; - return param(data); + if (typeof param !== 'function') return + return param(data) } diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js index b6f2953c4..58b17335b 100644 --- a/lib/helper/Appium.js +++ b/lib/helper/Appium.js @@ -1,26 +1,26 @@ -let webdriverio; +let webdriverio -const fs = require('fs'); -const axios = require('axios').default; -const { v4: uuidv4 } = require('uuid'); +const fs = require('fs') +const axios = require('axios').default +const { v4: uuidv4 } = require('uuid') -const Webdriver = require('./WebDriver'); -const AssertionFailedError = require('../assert/error'); -const { truth } = require('../assert/truth'); -const recorder = require('../recorder'); -const Locator = require('../locator'); -const ConnectionRefused = require('./errors/ConnectionRefused'); +const Webdriver = require('./WebDriver') +const AssertionFailedError = require('../assert/error') +const { truth } = require('../assert/truth') +const recorder = require('../recorder') +const Locator = require('../locator') +const ConnectionRefused = require('./errors/ConnectionRefused') -const mobileRoot = '//*'; -const webRoot = 'body'; +const mobileRoot = '//*' +const webRoot = 'body' const supportedPlatform = { android: 'Android', iOS: 'iOS', -}; +} const vendorPrefix = { appium: 'appium', -}; +} /** * Appium helper extends [Webdriver](http://codecept.io/helpers/WebDriver/) helper. @@ -175,19 +175,21 @@ class Appium extends Webdriver { // @ts-ignore constructor(config) { - super(config); + super(config) - this.isRunning = false; + this.isRunning = false if (config.appiumV2 === true) { - this.appiumV2 = true; + this.appiumV2 = true } - this.axios = axios.create(); + this.axios = axios.create() - webdriverio = require('webdriverio'); + webdriverio = require('webdriverio') if (!config.appiumV2) { - console.log('The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Please migrating to Appium 2.x by adding appiumV2: true to your config.'); - console.log('More info: https://bit.ly/appium-v2-migration'); - console.log('This Appium 1.x support will be removed in next major release.'); + console.log( + 'The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Please migrating to Appium 2.x by adding appiumV2: true to your config.', + ) + console.log('More info: https://bit.ly/appium-v2-migration') + console.log('This Appium 1.x support will be removed in next major release.') } } @@ -204,7 +206,7 @@ class Appium extends Webdriver { } } } - `); + `) } // set defaults @@ -225,166 +227,174 @@ class Appium extends Webdriver { timeouts: { script: 0, // ms }, - }; + } // override defaults with config - config = Object.assign(defaults, config); + config = Object.assign(defaults, config) - config.baseUrl = config.url || config.baseUrl; + config.baseUrl = config.url || config.baseUrl if (config.desiredCapabilities && Object.keys(config.desiredCapabilities).length) { - config.capabilities = this.appiumV2 === true ? this._convertAppiumV2Caps(config.desiredCapabilities) : config.desiredCapabilities; + config.capabilities = + this.appiumV2 === true ? this._convertAppiumV2Caps(config.desiredCapabilities) : config.desiredCapabilities } if (this.appiumV2) { - config.capabilities[`${vendorPrefix.appium}:deviceName`] = config[`${vendorPrefix.appium}:device`] || config.capabilities[`${vendorPrefix.appium}:deviceName`]; - config.capabilities[`${vendorPrefix.appium}:browserName`] = config[`${vendorPrefix.appium}:browser`] || config.capabilities[`${vendorPrefix.appium}:browserName`]; - config.capabilities[`${vendorPrefix.appium}:app`] = config[`${vendorPrefix.appium}:app`] || config.capabilities[`${vendorPrefix.appium}:app`]; - config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] = config[`${vendorPrefix.appium}:tunnelIdentifier`] || config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`]; // Adding the code to connect to sauce labs via sauce tunnel + config.capabilities[`${vendorPrefix.appium}:deviceName`] = + config[`${vendorPrefix.appium}:device`] || config.capabilities[`${vendorPrefix.appium}:deviceName`] + config.capabilities[`${vendorPrefix.appium}:browserName`] = + config[`${vendorPrefix.appium}:browser`] || config.capabilities[`${vendorPrefix.appium}:browserName`] + config.capabilities[`${vendorPrefix.appium}:app`] = + config[`${vendorPrefix.appium}:app`] || config.capabilities[`${vendorPrefix.appium}:app`] + config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] = + config[`${vendorPrefix.appium}:tunnelIdentifier`] || + config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] // Adding the code to connect to sauce labs via sauce tunnel } else { - config.capabilities.deviceName = config.device || config.capabilities.deviceName; - config.capabilities.browserName = config.browser || config.capabilities.browserName; - config.capabilities.app = config.app || config.capabilities.app; - config.capabilities.tunnelIdentifier = config.tunnelIdentifier || config.capabilities.tunnelIdentifier; // Adding the code to connect to sauce labs via sauce tunnel + config.capabilities.deviceName = config.device || config.capabilities.deviceName + config.capabilities.browserName = config.browser || config.capabilities.browserName + config.capabilities.app = config.app || config.capabilities.app + config.capabilities.tunnelIdentifier = config.tunnelIdentifier || config.capabilities.tunnelIdentifier // Adding the code to connect to sauce labs via sauce tunnel } - config.capabilities.platformName = config.platform || config.capabilities.platformName; - config.waitForTimeoutInSeconds = config.waitForTimeout / 1000; // convert to seconds + config.capabilities.platformName = config.platform || config.capabilities.platformName + config.waitForTimeoutInSeconds = config.waitForTimeout / 1000 // convert to seconds // [CodeceptJS compatible] transform host to hostname - config.hostname = config.host || config.hostname; + config.hostname = config.host || config.hostname if (!config.app && config.capabilities.browserName) { - this.isWeb = true; - this.root = webRoot; + this.isWeb = true + this.root = webRoot } else { - this.isWeb = false; - this.root = mobileRoot; + this.isWeb = false + this.root = mobileRoot } - this.platform = null; + this.platform = null if (config.capabilities[`${vendorPrefix.appium}:platformName`]) { - this.platform = config.capabilities[`${vendorPrefix.appium}:platformName`].toLowerCase(); + this.platform = config.capabilities[`${vendorPrefix.appium}:platformName`].toLowerCase() } if (config.capabilities.platformName) { - this.platform = config.capabilities.platformName.toLowerCase(); + this.platform = config.capabilities.platformName.toLowerCase() } - return config; + return config } _convertAppiumV2Caps(capabilities) { - const _convertedCaps = {}; + const _convertedCaps = {} for (const [key, value] of Object.entries(capabilities)) { if (!key.startsWith(vendorPrefix.appium)) { if (key !== 'platformName' && key !== 'bstack:options') { - _convertedCaps[`${vendorPrefix.appium}:${key}`] = value; + _convertedCaps[`${vendorPrefix.appium}:${key}`] = value } else { - _convertedCaps[`${key}`] = value; + _convertedCaps[`${key}`] = value } } else { - _convertedCaps[`${key}`] = value; + _convertedCaps[`${key}`] = value } } - return _convertedCaps; + return _convertedCaps } static _config() { - return [{ - name: 'app', - message: 'Application package. Path to file or url', - default: 'http://localhost', - }, { - name: 'platform', - message: 'Mobile Platform', - type: 'list', - choices: ['iOS', supportedPlatform.android], - default: supportedPlatform.android, - }, { - name: 'device', - message: 'Device to run tests on', - default: 'emulator', - }]; + return [ + { + name: 'app', + message: 'Application package. Path to file or url', + default: 'http://localhost', + }, + { + name: 'platform', + message: 'Mobile Platform', + type: 'list', + choices: ['iOS', supportedPlatform.android], + default: supportedPlatform.android, + }, + { + name: 'device', + message: 'Device to run tests on', + default: 'emulator', + }, + ] } async _startBrowser() { if (this.appiumV2 === true) { - this.options.capabilities = this._convertAppiumV2Caps(this.options.capabilities); - this.options.desiredCapabilities = this._convertAppiumV2Caps(this.options.desiredCapabilities); + this.options.capabilities = this._convertAppiumV2Caps(this.options.capabilities) + this.options.desiredCapabilities = this._convertAppiumV2Caps(this.options.desiredCapabilities) } try { if (this.options.multiremote) { - this.browser = await webdriverio.multiremote(this.options.multiremote); + this.browser = await webdriverio.multiremote(this.options.multiremote) } else { - this.browser = await webdriverio.remote(this.options); + this.browser = await webdriverio.remote(this.options) } } catch (err) { if (err.toString().indexOf('ECONNREFUSED')) { - throw new ConnectionRefused(err); + throw new ConnectionRefused(err) } - throw err; + throw err } - this.$$ = this.browser.$$.bind(this.browser); + this.$$ = this.browser.$$.bind(this.browser) - this.isRunning = true; + this.isRunning = true if (this.options.timeouts && this.isWeb) { - await this.defineTimeout(this.options.timeouts); + await this.defineTimeout(this.options.timeouts) } if (this.options.windowSize === 'maximize' && !this.platform) { - const res = await this.browser.execute('return [screen.width, screen.height]'); + const res = await this.browser.execute('return [screen.width, screen.height]') return this.browser.windowHandleSize({ width: res.value[0], height: res.value[1], - }); + }) } if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && !this.platform) { - const dimensions = this.options.windowSize.split('x'); + const dimensions = this.options.windowSize.split('x') await this.browser.windowHandleSize({ width: dimensions[0], height: dimensions[1], - }); + }) } } async _after() { - if (!this.isRunning) return; + if (!this.isRunning) return if (this.options.restart) { - this.isRunning = false; - return this.browser.deleteSession(); + this.isRunning = false + return this.browser.deleteSession() } if (this.isWeb && !this.platform) { - return super._after(); + return super._after() } } async _withinBegin(context) { if (this.isWeb) { - return super._withinBegin(context); + return super._withinBegin(context) } if (context === 'webview') { - return this.switchToWeb(); + return this.switchToWeb() } if (typeof context === 'object') { - if (context.web) return this.switchToWeb(context.web); - if (context.webview) return this.switchToWeb(context.webview); + if (context.web) return this.switchToWeb(context.web) + if (context.webview) return this.switchToWeb(context.webview) } - return this.switchToContext(context); + return this.switchToContext(context) } _withinEnd() { if (this.isWeb) { - return super._withinEnd(); + return super._withinEnd() } - return this.switchToNative(); + return this.switchToNative() } _buildAppiumEndpoint() { - const { - protocol, port, hostname, path, - } = this.browser.options; + const { protocol, port, hostname, path } = this.browser.options // Build path to Appium REST API endpoint - return `${protocol}://${hostname}:${port}${path}`; + return `${protocol}://${hostname}:${port}${path}` } /** @@ -422,11 +432,11 @@ class Appium extends Webdriver { * @param {*} fn */ async runOnIOS(caps, fn) { - if (this.platform !== 'ios') return; - recorder.session.start('iOS-only actions'); - await this._runWithCaps(caps, fn); - await recorder.add('restore from iOS session', () => recorder.session.restore()); - return recorder.promise(); + if (this.platform !== 'ios') return + recorder.session.start('iOS-only actions') + await this._runWithCaps(caps, fn) + await recorder.add('restore from iOS session', () => recorder.session.restore()) + return recorder.promise() } /** @@ -464,11 +474,11 @@ class Appium extends Webdriver { * @param {*} fn */ async runOnAndroid(caps, fn) { - if (this.platform !== 'android') return; - recorder.session.start('Android-only actions'); - await this._runWithCaps(caps, fn); - await recorder.add('restore from Android session', () => recorder.session.restore()); - return recorder.promise(); + if (this.platform !== 'android') return + recorder.session.start('Android-only actions') + await this._runWithCaps(caps, fn) + await recorder.add('restore from Android session', () => recorder.session.restore()) + return recorder.promise() } /** @@ -485,11 +495,11 @@ class Appium extends Webdriver { */ /* eslint-disable */ async runInWeb(fn) { - if (!this.isWeb) return; - recorder.session.start('Web-only actions'); + if (!this.isWeb) return + recorder.session.start('Web-only actions') - recorder.add('restore from Web session', () => recorder.session.restore(), true); - return recorder.promise(); + recorder.add('restore from Web session', () => recorder.session.restore(), true) + return recorder.promise() } /* eslint-enable */ @@ -498,21 +508,21 @@ class Appium extends Webdriver { for (const key in caps) { // skip if capabilities do not match if (this.config.desiredCapabilities[key] !== caps[key]) { - return; + return } } } if (typeof caps === 'function') { if (!fn) { - fn = caps; + fn = caps } else { // skip if capabilities are checked inside a function - const enabled = caps(this.config.desiredCapabilities); - if (!enabled) return; + const enabled = caps(this.config.desiredCapabilities) + if (!enabled) return } } - fn(); + fn() } /** @@ -528,9 +538,9 @@ class Appium extends Webdriver { * Appium: support only Android */ async checkIfAppIsInstalled(bundleId) { - onlyForApps.call(this, supportedPlatform.android); + onlyForApps.call(this, supportedPlatform.android) - return this.browser.isAppInstalled(bundleId); + return this.browser.isAppInstalled(bundleId) } /** @@ -546,9 +556,9 @@ class Appium extends Webdriver { * Appium: support only Android */ async seeAppIsInstalled(bundleId) { - onlyForApps.call(this, supportedPlatform.android); - const res = await this.browser.isAppInstalled(bundleId); - return truth(`app ${bundleId}`, 'to be installed').assert(res); + onlyForApps.call(this, supportedPlatform.android) + const res = await this.browser.isAppInstalled(bundleId) + return truth(`app ${bundleId}`, 'to be installed').assert(res) } /** @@ -564,9 +574,9 @@ class Appium extends Webdriver { * Appium: support only Android */ async seeAppIsNotInstalled(bundleId) { - onlyForApps.call(this, supportedPlatform.android); - const res = await this.browser.isAppInstalled(bundleId); - return truth(`app ${bundleId}`, 'not to be installed').negate(res); + onlyForApps.call(this, supportedPlatform.android) + const res = await this.browser.isAppInstalled(bundleId) + return truth(`app ${bundleId}`, 'not to be installed').negate(res) } /** @@ -581,8 +591,8 @@ class Appium extends Webdriver { * Appium: support only Android */ async installApp(path) { - onlyForApps.call(this, supportedPlatform.android); - return this.browser.installApp(path); + onlyForApps.call(this, supportedPlatform.android) + return this.browser.installApp(path) } /** @@ -598,13 +608,13 @@ class Appium extends Webdriver { * @param {string} [bundleId] ID of bundle */ async removeApp(appId, bundleId) { - onlyForApps.call(this, supportedPlatform.android); + onlyForApps.call(this, supportedPlatform.android) return this.axios({ method: 'post', url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/device/remove_app`, data: { appId, bundleId }, - }); + }) } /** @@ -616,11 +626,11 @@ class Appium extends Webdriver { * */ async resetApp() { - onlyForApps.call(this); + onlyForApps.call(this) return this.axios({ method: 'post', url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/app/reset`, - }); + }) } /** @@ -635,9 +645,9 @@ class Appium extends Webdriver { * Appium: support only Android */ async seeCurrentActivityIs(currentActivity) { - onlyForApps.call(this, supportedPlatform.android); - const res = await this.browser.getCurrentActivity(); - return truth('current activity', `to be ${currentActivity}`).assert(res === currentActivity); + onlyForApps.call(this, supportedPlatform.android) + const res = await this.browser.getCurrentActivity() + return truth('current activity', `to be ${currentActivity}`).assert(res === currentActivity) } /** @@ -652,9 +662,9 @@ class Appium extends Webdriver { * Appium: support only Android */ async seeDeviceIsLocked() { - onlyForApps.call(this, supportedPlatform.android); - const res = await this.browser.isLocked(); - return truth('device', 'to be locked').assert(res); + onlyForApps.call(this, supportedPlatform.android) + const res = await this.browser.isLocked() + return truth('device', 'to be locked').assert(res) } /** @@ -669,9 +679,9 @@ class Appium extends Webdriver { * Appium: support only Android */ async seeDeviceIsUnlocked() { - onlyForApps.call(this, supportedPlatform.android); - const res = await this.browser.isLocked(); - return truth('device', 'to be locked').negate(res); + onlyForApps.call(this, supportedPlatform.android) + const res = await this.browser.isLocked() + return truth('device', 'to be locked').negate(res) } /** @@ -689,15 +699,15 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async seeOrientationIs(orientation) { - onlyForApps.call(this); + onlyForApps.call(this) const res = await this.axios({ method: 'get', url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`, - }); + }) - const currentOrientation = res.data.value; - return truth('orientation', `to be ${orientation}`).assert(currentOrientation === orientation); + const currentOrientation = res.data.value + return truth('orientation', `to be ${orientation}`).assert(currentOrientation === orientation) } /** @@ -713,13 +723,13 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async setOrientation(orientation) { - onlyForApps.call(this); + onlyForApps.call(this) return this.axios({ method: 'post', url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`, data: { orientation }, - }); + }) } /** @@ -734,8 +744,8 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async grabAllContexts() { - onlyForApps.call(this); - return this.browser.getContexts(); + onlyForApps.call(this) + return this.browser.getContexts() } /** @@ -750,8 +760,8 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async grabContext() { - onlyForApps.call(this); - return this.browser.getContext(); + onlyForApps.call(this) + return this.browser.getContext() } /** @@ -766,8 +776,8 @@ class Appium extends Webdriver { * Appium: support only Android */ async grabCurrentActivity() { - onlyForApps.call(this, supportedPlatform.android); - return this.browser.getCurrentActivity(); + onlyForApps.call(this, supportedPlatform.android) + return this.browser.getCurrentActivity() } /** @@ -784,14 +794,14 @@ class Appium extends Webdriver { * Appium: support only Android */ async grabNetworkConnection() { - onlyForApps.call(this, supportedPlatform.android); - const res = await this.browser.getNetworkConnection(); + onlyForApps.call(this, supportedPlatform.android) + const res = await this.browser.getNetworkConnection() return { value: res, inAirplaneMode: res.inAirplaneMode, hasWifi: res.hasWifi, hasData: res.hasData, - }; + } } /** @@ -806,10 +816,10 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async grabOrientation() { - onlyForApps.call(this); - const res = await this.browser.orientation(); - this.debugSection('Orientation', res); - return res; + onlyForApps.call(this) + const res = await this.browser.orientation() + this.debugSection('Orientation', res) + return res } /** @@ -824,10 +834,10 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async grabSettings() { - onlyForApps.call(this); - const res = await this.browser.getSettings(); - this.debugSection('Settings', JSON.stringify(res)); - return res; + onlyForApps.call(this) + const res = await this.browser.getSettings() + this.debugSection('Settings', JSON.stringify(res)) + return res } /** @@ -836,7 +846,7 @@ class Appium extends Webdriver { * @param {*} context the context to switch to */ async switchToContext(context) { - return this.browser.switchContext(context); + return this.browser.switchContext(context) } /** @@ -856,17 +866,17 @@ class Appium extends Webdriver { * @param {string} [context] */ async switchToWeb(context) { - this.isWeb = true; - this.defaultContext = 'body'; + this.isWeb = true + this.defaultContext = 'body' - if (context) return this.switchToContext(context); - const contexts = await this.grabAllContexts(); - this.debugSection('Contexts', contexts.toString()); + if (context) return this.switchToContext(context) + const contexts = await this.grabAllContexts() + this.debugSection('Contexts', contexts.toString()) for (const idx in contexts) { - if (contexts[idx].match(/^WEBVIEW/)) return this.switchToContext(contexts[idx]); + if (contexts[idx].match(/^WEBVIEW/)) return this.switchToContext(contexts[idx]) } - throw new Error('No WEBVIEW could be guessed, please specify one in params'); + throw new Error('No WEBVIEW could be guessed, please specify one in params') } /** @@ -883,11 +893,11 @@ class Appium extends Webdriver { * @return {Promise} */ async switchToNative(context = null) { - this.isWeb = false; - this.defaultContext = '//*'; + this.isWeb = false + this.defaultContext = '//*' - if (context) return this.switchToContext(context); - return this.switchToContext('NATIVE_APP'); + if (context) return this.switchToContext(context) + return this.switchToContext('NATIVE_APP') } /** @@ -904,8 +914,8 @@ class Appium extends Webdriver { * @return {Promise} */ async startActivity(appPackage, appActivity) { - onlyForApps.call(this, supportedPlatform.android); - return this.browser.startActivity(appPackage, appActivity); + onlyForApps.call(this, supportedPlatform.android) + return this.browser.startActivity(appPackage, appActivity) } /** @@ -930,8 +940,8 @@ class Appium extends Webdriver { * @return {Promise} */ async setNetworkConnection(value) { - onlyForApps.call(this, supportedPlatform.android); - return this.browser.setNetworkConnection(value); + onlyForApps.call(this, supportedPlatform.android) + return this.browser.setNetworkConnection(value) } /** @@ -946,8 +956,8 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async setSettings(settings) { - onlyForApps.call(this); - return this.browser.settings(settings); + onlyForApps.call(this) + return this.browser.settings(settings) } /** @@ -968,9 +978,9 @@ class Appium extends Webdriver { * @param {string} [key] Optional key */ async hideDeviceKeyboard(strategy, key) { - onlyForApps.call(this); - strategy = strategy || 'tapOutside'; - return this.browser.hideKeyboard(strategy, key); + onlyForApps.call(this) + strategy = strategy || 'tapOutside' + return this.browser.hideKeyboard(strategy, key) } /** @@ -987,8 +997,8 @@ class Appium extends Webdriver { * Appium: support only Android */ async sendDeviceKeyEvent(keyValue) { - onlyForApps.call(this, supportedPlatform.android); - return this.browser.pressKeyCode(keyValue); + onlyForApps.call(this, supportedPlatform.android) + return this.browser.pressKeyCode(keyValue) } /** @@ -1003,8 +1013,8 @@ class Appium extends Webdriver { * Appium: support only Android */ async openNotifications() { - onlyForApps.call(this, supportedPlatform.android); - return this.browser.openNotifications(); + onlyForApps.call(this, supportedPlatform.android) + return this.browser.openNotifications() } /** @@ -1023,13 +1033,13 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async makeTouchAction(locator, action) { - onlyForApps.call(this); - const element = await this.browser.$(parseLocator.call(this, locator)); + onlyForApps.call(this) + const element = await this.browser.$(parseLocator.call(this, locator)) return this.browser.touchAction({ action, element, - }); + }) } /** @@ -1046,7 +1056,7 @@ class Appium extends Webdriver { * @param {*} locator */ async tap(locator) { - return this.makeTouchAction(locator, 'tap'); + return this.makeTouchAction(locator, 'tap') } /** @@ -1069,10 +1079,13 @@ class Appium extends Webdriver { */ /* eslint-disable */ async swipe(locator, xoffset, yoffset, speed = 1000) { - onlyForApps.call(this); - const res = await this.browser.$(parseLocator.call(this, locator)); + onlyForApps.call(this) + const res = await this.browser.$(parseLocator.call(this, locator)) // if (!res.length) throw new ElementNotFound(locator, 'was not found in UI'); - return this.performSwipe(await res.getLocation(), { x: (await res.getLocation()).x + xoffset, y: (await res.getLocation()).y + yoffset }); + return this.performSwipe(await res.getLocation(), { + x: (await res.getLocation()).x + xoffset, + y: (await res.getLocation()).y + yoffset, + }) } /* eslint-enable */ @@ -1089,42 +1102,44 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async performSwipe(from, to) { - await this.browser.performActions([{ - id: uuidv4(), - type: 'pointer', - parameters: { - pointerType: 'touch', - }, - actions: [ - { - duration: 0, - x: from.x, - y: from.y, - type: 'pointerMove', - origin: 'viewport', - }, - { - button: 1, - type: 'pointerDown', - }, - { - duration: 200, - type: 'pause', - }, - { - duration: 600, - x: to.x, - y: to.y, - type: 'pointerMove', - origin: 'viewport', - }, - { - button: 1, - type: 'pointerUp', + await this.browser.performActions([ + { + id: uuidv4(), + type: 'pointer', + parameters: { + pointerType: 'touch', }, - ], - }]); - await this.browser.pause(1000); + actions: [ + { + duration: 0, + x: from.x, + y: from.y, + type: 'pointerMove', + origin: 'viewport', + }, + { + button: 1, + type: 'pointerDown', + }, + { + duration: 200, + type: 'pause', + }, + { + duration: 600, + x: to.x, + y: to.y, + type: 'pointerMove', + origin: 'viewport', + }, + { + button: 1, + type: 'pointerUp', + }, + ], + }, + ]) + await this.browser.pause(1000) } /** @@ -1145,14 +1160,14 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async swipeDown(locator, yoffset = 1000, speed) { - onlyForApps.call(this); + onlyForApps.call(this) if (!speed) { - speed = yoffset; - yoffset = 100; + speed = yoffset + yoffset = 100 } - return this.swipe(parseLocator.call(this, locator), 0, yoffset, speed); + return this.swipe(parseLocator.call(this, locator), 0, yoffset, speed) } /** @@ -1174,13 +1189,13 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async swipeLeft(locator, xoffset = 1000, speed) { - onlyForApps.call(this); + onlyForApps.call(this) if (!speed) { - speed = xoffset; - xoffset = 100; + speed = xoffset + xoffset = 100 } - return this.swipe(parseLocator.call(this, locator), -xoffset, 0, speed); + return this.swipe(parseLocator.call(this, locator), -xoffset, 0, speed) } /** @@ -1201,13 +1216,13 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async swipeRight(locator, xoffset = 1000, speed) { - onlyForApps.call(this); + onlyForApps.call(this) if (!speed) { - speed = xoffset; - xoffset = 100; + speed = xoffset + xoffset = 100 } - return this.swipe(parseLocator.call(this, locator), xoffset, 0, speed); + return this.swipe(parseLocator.call(this, locator), xoffset, 0, speed) } /** @@ -1228,14 +1243,14 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async swipeUp(locator, yoffset = 1000, speed) { - onlyForApps.call(this); + onlyForApps.call(this) if (!speed) { - speed = yoffset; - yoffset = 100; + speed = yoffset + yoffset = 100 } - return this.swipe(parseLocator.call(this, locator), 0, -yoffset, speed); + return this.swipe(parseLocator.call(this, locator), 0, -yoffset, speed) } /** @@ -1262,55 +1277,66 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async swipeTo(searchableLocator, scrollLocator, direction, timeout, offset, speed) { - onlyForApps.call(this); - direction = direction || 'down'; + onlyForApps.call(this) + direction = direction || 'down' switch (direction) { case 'down': - direction = 'swipeDown'; - break; + direction = 'swipeDown' + break case 'up': - direction = 'swipeUp'; - break; + direction = 'swipeUp' + break case 'left': - direction = 'swipeLeft'; - break; + direction = 'swipeLeft' + break case 'right': - direction = 'swipeRight'; - break; + direction = 'swipeRight' + break } - timeout = timeout || this.options.waitForTimeoutInSeconds; - - const errorMsg = `element ("${searchableLocator}") still not visible after ${timeout}seconds`; - const browser = this.browser; - let err = false; - let currentSource; - return browser.waitUntil(() => { - if (err) { - return new Error(`Scroll to the end and element ${searchableLocator} was not found`); - } - return browser.$$(parseLocator.call(this, searchableLocator)) - .then(els => els.length && els[0].isDisplayed()) - .then((res) => { - if (res) { - return true; + timeout = timeout || this.options.waitForTimeoutInSeconds + + const errorMsg = `element ("${searchableLocator}") still not visible after ${timeout}seconds` + const browser = this.browser + let err = false + let currentSource + return browser + .waitUntil( + () => { + if (err) { + return new Error(`Scroll to the end and element ${searchableLocator} was not found`) } - return this[direction](scrollLocator, offset, speed).getSource().then((source) => { - if (source === currentSource) { - err = true; - } else { - currentSource = source; - return false; - } - }); - }); - }, timeout * 1000, errorMsg) + return browser + .$$(parseLocator.call(this, searchableLocator)) + .then((els) => els.length && els[0].isDisplayed()) + .then((res) => { + if (res) { + return true + } + return this[direction](scrollLocator, offset, speed) + .getSource() + .then((source) => { + if (source === currentSource) { + err = true + } else { + currentSource = source + return false + } + }) + }) + }, + timeout * 1000, + errorMsg, + ) .catch((e) => { if (e.message.indexOf('timeout') && e.type !== 'NoSuchElement') { - throw new AssertionFailedError({ customMessage: `Scroll to the end and element ${searchableLocator} was not found` }, ''); + throw new AssertionFailedError( + { customMessage: `Scroll to the end and element ${searchableLocator} was not found` }, + '', + ) } else { - throw e; + throw e } - }); + }) } /** @@ -1342,8 +1368,8 @@ class Appium extends Webdriver { * @param {Array} actions Array of touch actions */ async touchPerform(actions) { - onlyForApps.call(this); - return this.browser.touchPerform(actions); + onlyForApps.call(this) + return this.browser.touchPerform(actions) } /** @@ -1362,13 +1388,15 @@ class Appium extends Webdriver { * Appium: support Android and iOS */ async pullFile(path, dest) { - onlyForApps.call(this); - return this.browser.pullFile(path).then(res => fs.writeFile(dest, Buffer.from(res, 'base64'), (err) => { - if (err) { - return false; - } - return true; - })); + onlyForApps.call(this) + return this.browser.pullFile(path).then((res) => + fs.writeFile(dest, Buffer.from(res, 'base64'), (err) => { + if (err) { + return false + } + return true + }), + ) } /** @@ -1383,8 +1411,8 @@ class Appium extends Webdriver { * Appium: support only iOS */ async shakeDevice() { - onlyForApps.call(this, 'iOS'); - return this.browser.shake(); + onlyForApps.call(this, 'iOS') + return this.browser.shake() } /** @@ -1401,8 +1429,8 @@ class Appium extends Webdriver { * Appium: support only iOS */ async rotate(x, y, duration, radius, rotation, touchCount) { - onlyForApps.call(this, 'iOS'); - return this.browser.rotate(x, y, duration, radius, rotation, touchCount); + onlyForApps.call(this, 'iOS') + return this.browser.rotate(x, y, duration, radius, rotation, touchCount) } /** @@ -1415,8 +1443,8 @@ class Appium extends Webdriver { * Appium: support only iOS */ async setImmediateValue(id, value) { - onlyForApps.call(this, 'iOS'); - return this.browser.setImmediateValue(id, value); + onlyForApps.call(this, 'iOS') + return this.browser.setImmediateValue(id, value) } /** @@ -1434,9 +1462,9 @@ class Appium extends Webdriver { * TODO: not tested */ async simulateTouchId(match) { - onlyForApps.call(this, 'iOS'); - match = match || true; - return this.browser.touchId(match); + onlyForApps.call(this, 'iOS') + match = match || true + return this.browser.touchId(match) } /** @@ -1451,8 +1479,8 @@ class Appium extends Webdriver { * Appium: support both Android and iOS */ async closeApp() { - onlyForApps.call(this); - return this.browser.closeApp(); + onlyForApps.call(this) + return this.browser.closeApp() } /** @@ -1460,8 +1488,8 @@ class Appium extends Webdriver { * */ async appendField(field, value) { - if (this.isWeb) return super.appendField(field, value); - return super.appendField(parseLocator.call(this, field), value); + if (this.isWeb) return super.appendField(field, value) + return super.appendField(parseLocator.call(this, field), value) } /** @@ -1469,8 +1497,8 @@ class Appium extends Webdriver { * */ async checkOption(field) { - if (this.isWeb) return super.checkOption(field); - return super.checkOption(parseLocator.call(this, field)); + if (this.isWeb) return super.checkOption(field) + return super.checkOption(parseLocator.call(this, field)) } /** @@ -1478,8 +1506,8 @@ class Appium extends Webdriver { * */ async click(locator, context) { - if (this.isWeb) return super.click(locator, context); - return super.click(parseLocator.call(this, locator), parseLocator.call(this, context)); + if (this.isWeb) return super.click(locator, context) + return super.click(parseLocator.call(this, locator), parseLocator.call(this, context)) } /** @@ -1487,16 +1515,16 @@ class Appium extends Webdriver { * */ async dontSeeCheckboxIsChecked(field) { - if (this.isWeb) return super.dontSeeCheckboxIsChecked(field); - return super.dontSeeCheckboxIsChecked(parseLocator.call(this, field)); + if (this.isWeb) return super.dontSeeCheckboxIsChecked(field) + return super.dontSeeCheckboxIsChecked(parseLocator.call(this, field)) } /** * {{> dontSeeElement }} */ async dontSeeElement(locator) { - if (this.isWeb) return super.dontSeeElement(locator); - return super.dontSeeElement(parseLocator.call(this, locator)); + if (this.isWeb) return super.dontSeeElement(locator) + return super.dontSeeElement(parseLocator.call(this, locator)) } /** @@ -1504,17 +1532,17 @@ class Appium extends Webdriver { * */ async dontSeeInField(field, value) { - const _value = (typeof value === 'boolean') ? value : value.toString(); - if (this.isWeb) return super.dontSeeInField(field, _value); - return super.dontSeeInField(parseLocator.call(this, field), _value); + const _value = typeof value === 'boolean' ? value : value.toString() + if (this.isWeb) return super.dontSeeInField(field, _value) + return super.dontSeeInField(parseLocator.call(this, field), _value) } /** * {{> dontSee }} */ async dontSee(text, context = null) { - if (this.isWeb) return super.dontSee(text, context); - return super.dontSee(text, parseLocator.call(this, context)); + if (this.isWeb) return super.dontSee(text, context) + return super.dontSee(text, parseLocator.call(this, context)) } /** @@ -1522,9 +1550,9 @@ class Appium extends Webdriver { * */ async fillField(field, value) { - value = value.toString(); - if (this.isWeb) return super.fillField(field, value); - return super.fillField(parseLocator.call(this, field), value); + value = value.toString() + if (this.isWeb) return super.fillField(field, value) + return super.fillField(parseLocator.call(this, field), value) } /** @@ -1532,8 +1560,8 @@ class Appium extends Webdriver { * */ async grabTextFromAll(locator) { - if (this.isWeb) return super.grabTextFromAll(locator); - return super.grabTextFromAll(parseLocator.call(this, locator)); + if (this.isWeb) return super.grabTextFromAll(locator) + return super.grabTextFromAll(parseLocator.call(this, locator)) } /** @@ -1541,16 +1569,16 @@ class Appium extends Webdriver { * */ async grabTextFrom(locator) { - if (this.isWeb) return super.grabTextFrom(locator); - return super.grabTextFrom(parseLocator.call(this, locator)); + if (this.isWeb) return super.grabTextFrom(locator) + return super.grabTextFrom(parseLocator.call(this, locator)) } /** * {{> grabNumberOfVisibleElements }} */ async grabNumberOfVisibleElements(locator) { - if (this.isWeb) return super.grabNumberOfVisibleElements(locator); - return super.grabNumberOfVisibleElements(parseLocator.call(this, locator)); + if (this.isWeb) return super.grabNumberOfVisibleElements(locator) + return super.grabNumberOfVisibleElements(parseLocator.call(this, locator)) } /** @@ -1559,8 +1587,8 @@ class Appium extends Webdriver { * {{> grabAttributeFrom }} */ async grabAttributeFrom(locator, attr) { - if (this.isWeb) return super.grabAttributeFrom(locator, attr); - return super.grabAttributeFrom(parseLocator.call(this, locator), attr); + if (this.isWeb) return super.grabAttributeFrom(locator, attr) + return super.grabAttributeFrom(parseLocator.call(this, locator), attr) } /** @@ -1568,8 +1596,8 @@ class Appium extends Webdriver { * {{> grabAttributeFromAll }} */ async grabAttributeFromAll(locator, attr) { - if (this.isWeb) return super.grabAttributeFromAll(locator, attr); - return super.grabAttributeFromAll(parseLocator.call(this, locator), attr); + if (this.isWeb) return super.grabAttributeFromAll(locator, attr) + return super.grabAttributeFromAll(parseLocator.call(this, locator), attr) } /** @@ -1577,8 +1605,8 @@ class Appium extends Webdriver { * */ async grabValueFromAll(locator) { - if (this.isWeb) return super.grabValueFromAll(locator); - return super.grabValueFromAll(parseLocator.call(this, locator)); + if (this.isWeb) return super.grabValueFromAll(locator) + return super.grabValueFromAll(parseLocator.call(this, locator)) } /** @@ -1586,8 +1614,8 @@ class Appium extends Webdriver { * */ async grabValueFrom(locator) { - if (this.isWeb) return super.grabValueFrom(locator); - return super.grabValueFrom(parseLocator.call(this, locator)); + if (this.isWeb) return super.grabValueFrom(locator) + return super.grabValueFrom(parseLocator.call(this, locator)) } /** @@ -1602,7 +1630,7 @@ class Appium extends Webdriver { * @return {Promise} */ async saveScreenshot(fileName) { - return super.saveScreenshot(fileName, false); + return super.saveScreenshot(fileName, false) } /** @@ -1611,7 +1639,7 @@ class Appium extends Webdriver { * Supported only for web testing */ async scrollIntoView(locator, scrollIntoViewOptions) { - if (this.isWeb) return super.scrollIntoView(locator, scrollIntoViewOptions); + if (this.isWeb) return super.scrollIntoView(locator, scrollIntoViewOptions) } /** @@ -1619,8 +1647,8 @@ class Appium extends Webdriver { * */ async seeCheckboxIsChecked(field) { - if (this.isWeb) return super.seeCheckboxIsChecked(field); - return super.seeCheckboxIsChecked(parseLocator.call(this, field)); + if (this.isWeb) return super.seeCheckboxIsChecked(field) + return super.seeCheckboxIsChecked(parseLocator.call(this, field)) } /** @@ -1628,8 +1656,8 @@ class Appium extends Webdriver { * */ async seeElement(locator) { - if (this.isWeb) return super.seeElement(locator); - return super.seeElement(parseLocator.call(this, locator)); + if (this.isWeb) return super.seeElement(locator) + return super.seeElement(parseLocator.call(this, locator)) } /** @@ -1637,9 +1665,9 @@ class Appium extends Webdriver { * */ async seeInField(field, value) { - const _value = (typeof value === 'boolean') ? value : value.toString(); - if (this.isWeb) return super.seeInField(field, _value); - return super.seeInField(parseLocator.call(this, field), _value); + const _value = typeof value === 'boolean' ? value : value.toString() + if (this.isWeb) return super.seeInField(field, _value) + return super.seeInField(parseLocator.call(this, field), _value) } /** @@ -1647,8 +1675,8 @@ class Appium extends Webdriver { * */ async see(text, context) { - if (this.isWeb) return super.see(text, context); - return super.see(text, parseLocator.call(this, context)); + if (this.isWeb) return super.see(text, context) + return super.see(text, parseLocator.call(this, context)) } /** @@ -1657,8 +1685,8 @@ class Appium extends Webdriver { * Supported only for web testing */ async selectOption(select, option) { - if (this.isWeb) return super.selectOption(select, option); - throw new Error('Should be used only in Web context. In native context use \'click\' method instead'); + if (this.isWeb) return super.selectOption(select, option) + throw new Error("Should be used only in Web context. In native context use 'click' method instead") } /** @@ -1666,8 +1694,8 @@ class Appium extends Webdriver { * */ async waitForElement(locator, sec = null) { - if (this.isWeb) return super.waitForElement(locator, sec); - return super.waitForElement(parseLocator.call(this, locator), sec); + if (this.isWeb) return super.waitForElement(locator, sec) + return super.waitForElement(parseLocator.call(this, locator), sec) } /** @@ -1675,8 +1703,8 @@ class Appium extends Webdriver { * */ async waitForVisible(locator, sec = null) { - if (this.isWeb) return super.waitForVisible(locator, sec); - return super.waitForVisible(parseLocator.call(this, locator), sec); + if (this.isWeb) return super.waitForVisible(locator, sec) + return super.waitForVisible(parseLocator.call(this, locator), sec) } /** @@ -1684,8 +1712,8 @@ class Appium extends Webdriver { * */ async waitForInvisible(locator, sec = null) { - if (this.isWeb) return super.waitForInvisible(locator, sec); - return super.waitForInvisible(parseLocator.call(this, locator), sec); + if (this.isWeb) return super.waitForInvisible(locator, sec) + return super.waitForInvisible(parseLocator.call(this, locator), sec) } /** @@ -1693,74 +1721,76 @@ class Appium extends Webdriver { * */ async waitForText(text, sec = null, context = null) { - if (this.isWeb) return super.waitForText(text, sec, context); - return super.waitForText(text, sec, parseLocator.call(this, context)); + if (this.isWeb) return super.waitForText(text, sec, context) + return super.waitForText(text, sec, parseLocator.call(this, context)) } } function parseLocator(locator) { - if (!locator) return null; + if (!locator) return null if (typeof locator === 'object') { if (locator.web && this.isWeb) { - return parseLocator.call(this, locator.web); + return parseLocator.call(this, locator.web) } if (locator.android && this.platform === 'android') { if (typeof locator.android === 'string') { - return parseLocator.call(this, locator.android); + return parseLocator.call(this, locator.android) } // The locator is an Android DataMatcher or ViewMatcher locator so return as is - return locator.android; + return locator.android } if (locator.ios && this.platform === 'ios') { - return parseLocator.call(this, locator.ios); + return parseLocator.call(this, locator.ios) } } if (typeof locator === 'string') { - if (locator[0] === '~') return locator; - if (locator.substr(0, 2) === '//') return locator; + if (locator[0] === '~') return locator + if (locator.substr(0, 2) === '//') return locator if (locator[0] === '#' && !this.isWeb) { // hook before webdriverio supports native # locators - return parseLocator.call(this, { id: locator.slice(1) }); + return parseLocator.call(this, { id: locator.slice(1) }) } if (this.platform === 'android' && !this.isWeb) { - const isNativeLocator = /^\-?android=?/.exec(locator); - return isNativeLocator - ? locator - : `android=new UiSelector().text("${locator}")`; + const isNativeLocator = /^\-?android=?/.exec(locator) + return isNativeLocator ? locator : `android=new UiSelector().text("${locator}")` } } - locator = new Locator(locator, 'xpath'); - if (locator.type === 'css' && !this.isWeb) throw new Error('Unable to use css locators in apps. Locator strategies for this request: xpath, id, class name or accessibility id'); - if (locator.type === 'name' && !this.isWeb) throw new Error("Can't locate element by name in Native context. Use either ID, class name or accessibility id"); - if (locator.type === 'id' && !this.isWeb && this.platform === 'android') return `//*[@resource-id='${locator.value}']`; - return locator.simplify(); + locator = new Locator(locator, 'xpath') + if (locator.type === 'css' && !this.isWeb) + throw new Error( + 'Unable to use css locators in apps. Locator strategies for this request: xpath, id, class name or accessibility id', + ) + if (locator.type === 'name' && !this.isWeb) + throw new Error("Can't locate element by name in Native context. Use either ID, class name or accessibility id") + if (locator.type === 'id' && !this.isWeb && this.platform === 'android') return `//*[@resource-id='${locator.value}']` + return locator.simplify() } // in the end of a file function onlyForApps(expectedPlatform) { - const stack = new Error().stack || ''; - const re = /Appium.(\w+)/g; - const caller = stack.split('\n')[2].trim(); - const m = re.exec(caller); + const stack = new Error().stack || '' + const re = /Appium.(\w+)/g + const caller = stack.split('\n')[2].trim() + const m = re.exec(caller) if (!m) { - throw new Error(`Invalid caller ${caller}`); + throw new Error(`Invalid caller ${caller}`) } - const callerName = m[1] || m[2]; + const callerName = m[1] || m[2] if (!expectedPlatform) { if (!this.platform) { - throw new Error(`${callerName} method can be used only with apps`); + throw new Error(`${callerName} method can be used only with apps`) } } else if (this.platform !== expectedPlatform.toLowerCase()) { - throw new Error(`${callerName} method can be used only with ${expectedPlatform} apps`); + throw new Error(`${callerName} method can be used only with ${expectedPlatform} apps`) } } -module.exports = Appium; +module.exports = Appium diff --git a/lib/helper/ExpectHelper.js b/lib/helper/ExpectHelper.js index c6a9dd27d..8d891e0ac 100644 --- a/lib/helper/ExpectHelper.js +++ b/lib/helper/ExpectHelper.js @@ -1,15 +1,15 @@ -const output = require('../output'); +const output = require('../output') -let expect; +let expect -import('chai').then(chai => { - expect = chai.expect; - chai.use(require('chai-string')); +import('chai').then((chai) => { + expect = chai.expect + chai.use(require('chai-string')) // @ts-ignore - chai.use(require('chai-exclude')); - chai.use(require('chai-match-pattern')); - chai.use(require('chai-json-schema')); -}); + chai.use(require('chai-exclude')) + chai.use(require('chai-match-pattern')) + chai.use(require('chai-json-schema')) +}) /** * This helper allows performing assertions based on Chai. @@ -32,27 +32,27 @@ import('chai').then(chai => { */ class ExpectHelper { /** - * - * @param {*} actualValue - * @param {*} expectedValue - * @param {*} [customErrorMsg] - */ + * + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ expectEqual(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`); - return expect(actualValue, customErrorMsg).to.equal(expectedValue); + output.step(`I expect "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`) + return expect(actualValue, customErrorMsg).to.equal(expectedValue) } /** - * - * @param {*} actualValue - * @param {*} expectedValue - * @param {*} [customErrorMsg] - */ + * + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ expectNotEqual(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to not equal "${JSON.stringify(expectedValue)}"`); - return expect(actualValue, customErrorMsg).not.to.equal(expectedValue); + output.step(`I expect "${JSON.stringify(actualValue)}" to not equal "${JSON.stringify(expectedValue)}"`) + return expect(actualValue, customErrorMsg).not.to.equal(expectedValue) } /** @@ -64,362 +64,328 @@ class ExpectHelper { */ expectDeepEqual(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to deep equal "${JSON.stringify(expectedValue)}"`); - return expect(actualValue, customErrorMsg).to.deep.equal(expectedValue); + output.step(`I expect "${JSON.stringify(actualValue)}" to deep equal "${JSON.stringify(expectedValue)}"`) + return expect(actualValue, customErrorMsg).to.deep.equal(expectedValue) } /** - * - * @param {*} actualValue - * @param {*} expectedValue - * @param {*} [customErrorMsg] - */ + * + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ expectNotDeepEqual(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to not deep equal "${JSON.stringify(expectedValue)}"`); - return expect(actualValue, customErrorMsg).to.not.deep.equal(expectedValue); + output.step(`I expect "${JSON.stringify(actualValue)}" to not deep equal "${JSON.stringify(expectedValue)}"`) + return expect(actualValue, customErrorMsg).to.not.deep.equal(expectedValue) } /** - * - * @param {*} actualValue - * @param {*} expectedValueToContain - * @param {*} [customErrorMsg] - */ + * + * @param {*} actualValue + * @param {*} expectedValueToContain + * @param {*} [customErrorMsg] + */ expectContain(actualValue, expectedValueToContain, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to contain "${JSON.stringify(expectedValueToContain)}"`); - return expect(actualValue, customErrorMsg).to.contain( - expectedValueToContain, - ); + output.step(`I expect "${JSON.stringify(actualValue)}" to contain "${JSON.stringify(expectedValueToContain)}"`) + return expect(actualValue, customErrorMsg).to.contain(expectedValueToContain) } /** - * - * @param {*} actualValue - * @param {*} expectedValueToNotContain - * @param {*} [customErrorMsg] - */ - expectNotContain( - actualValue, - expectedValueToNotContain, - customErrorMsg = '', - ) { + * + * @param {*} actualValue + * @param {*} expectedValueToNotContain + * @param {*} [customErrorMsg] + */ + expectNotContain(actualValue, expectedValueToNotContain, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to not contain "${JSON.stringify(expectedValueToNotContain)}"`); - return expect(actualValue, customErrorMsg).not.to.contain( - expectedValueToNotContain, - ); + output.step( + `I expect "${JSON.stringify(actualValue)}" to not contain "${JSON.stringify(expectedValueToNotContain)}"`, + ) + return expect(actualValue, customErrorMsg).not.to.contain(expectedValueToNotContain) } /** - * - * @param {*} actualValue - * @param {*} expectedValueToStartWith - * @param {*} [customErrorMsg] - */ + * + * @param {*} actualValue + * @param {*} expectedValueToStartWith + * @param {*} [customErrorMsg] + */ expectStartsWith(actualValue, expectedValueToStartWith, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to start with "${JSON.stringify(expectedValueToStartWith)}"`); - return expect(actualValue, customErrorMsg).to.startsWith( - expectedValueToStartWith, - ); + output.step(`I expect "${JSON.stringify(actualValue)}" to start with "${JSON.stringify(expectedValueToStartWith)}"`) + return expect(actualValue, customErrorMsg).to.startsWith(expectedValueToStartWith) } /** - * - * @param {*} actualValue - * @param {*} expectedValueToNotStartWith - * @param {*} [customErrorMsg] - */ - expectNotStartsWith( - actualValue, - expectedValueToNotStartWith, - customErrorMsg = '', - ) { + * + * @param {*} actualValue + * @param {*} expectedValueToNotStartWith + * @param {*} [customErrorMsg] + */ + expectNotStartsWith(actualValue, expectedValueToNotStartWith, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to not start with "${JSON.stringify(expectedValueToNotStartWith)}"`); - return expect(actualValue, customErrorMsg).not.to.startsWith( - expectedValueToNotStartWith, - ); + output.step( + `I expect "${JSON.stringify(actualValue)}" to not start with "${JSON.stringify(expectedValueToNotStartWith)}"`, + ) + return expect(actualValue, customErrorMsg).not.to.startsWith(expectedValueToNotStartWith) } /** - * @param {*} actualValue - * @param {*} expectedValueToEndWith - * @param {*} [customErrorMsg] - */ + * @param {*} actualValue + * @param {*} expectedValueToEndWith + * @param {*} [customErrorMsg] + */ expectEndsWith(actualValue, expectedValueToEndWith, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to end with "${JSON.stringify(expectedValueToEndWith)}"`); - return expect(actualValue, customErrorMsg).to.endsWith( - expectedValueToEndWith, - ); + output.step(`I expect "${JSON.stringify(actualValue)}" to end with "${JSON.stringify(expectedValueToEndWith)}"`) + return expect(actualValue, customErrorMsg).to.endsWith(expectedValueToEndWith) } /** - * @param {*} actualValue - * @param {*} expectedValueToNotEndWith - * @param {*} [customErrorMsg] - */ - expectNotEndsWith( - actualValue, - expectedValueToNotEndWith, - customErrorMsg = '', - ) { + * @param {*} actualValue + * @param {*} expectedValueToNotEndWith + * @param {*} [customErrorMsg] + */ + expectNotEndsWith(actualValue, expectedValueToNotEndWith, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to not end with "${JSON.stringify(expectedValueToNotEndWith)}"`); - return expect(actualValue, customErrorMsg).not.to.endsWith( - expectedValueToNotEndWith, - ); + output.step( + `I expect "${JSON.stringify(actualValue)}" to not end with "${JSON.stringify(expectedValueToNotEndWith)}"`, + ) + return expect(actualValue, customErrorMsg).not.to.endsWith(expectedValueToNotEndWith) } /** - * @param {*} targetData - * @param {*} jsonSchema - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} jsonSchema + * @param {*} [customErrorMsg] + */ expectJsonSchema(targetData, jsonSchema, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to match this JSON schema "${JSON.stringify(jsonSchema)}"`); + output.step(`I expect "${JSON.stringify(targetData)}" to match this JSON schema "${JSON.stringify(jsonSchema)}"`) - return expect(targetData, customErrorMsg).to.be.jsonSchema(jsonSchema); + return expect(targetData, customErrorMsg).to.be.jsonSchema(jsonSchema) } /** - * @param {*} targetData - * @param {*} jsonSchema - * @param {*} [customErrorMsg] - * @param {*} [ajvOptions] Pass AJV options - */ - expectJsonSchemaUsingAJV( - targetData, - jsonSchema, - customErrorMsg = '', - ajvOptions = { allErrors: true }, - ) { + * @param {*} targetData + * @param {*} jsonSchema + * @param {*} [customErrorMsg] + * @param {*} [ajvOptions] Pass AJV options + */ + expectJsonSchemaUsingAJV(targetData, jsonSchema, customErrorMsg = '', ajvOptions = { allErrors: true }) { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to match this JSON schema using AJV "${JSON.stringify(jsonSchema)}"`); - chai.use(require('chai-json-schema-ajv').create(ajvOptions)); - return expect(targetData, customErrorMsg).to.be.jsonSchema(jsonSchema); + output.step( + `I expect "${JSON.stringify(targetData)}" to match this JSON schema using AJV "${JSON.stringify(jsonSchema)}"`, + ) + chai.use(require('chai-json-schema-ajv').create(ajvOptions)) + return expect(targetData, customErrorMsg).to.be.jsonSchema(jsonSchema) } /** - * @param {*} targetData - * @param {*} propertyName - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} propertyName + * @param {*} [customErrorMsg] + */ expectHasProperty(targetData, propertyName, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have property: "${JSON.stringify(propertyName)}"`); - return expect(targetData, customErrorMsg).to.have.property(propertyName); + output.step(`I expect "${JSON.stringify(targetData)}" to have property: "${JSON.stringify(propertyName)}"`) + return expect(targetData, customErrorMsg).to.have.property(propertyName) } /** - * @param {*} targetData - * @param {*} propertyName - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} propertyName + * @param {*} [customErrorMsg] + */ expectHasAProperty(targetData, propertyName, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have a property: "${JSON.stringify(propertyName)}"`); - return expect(targetData, customErrorMsg).to.have.a.property(propertyName); + output.step(`I expect "${JSON.stringify(targetData)}" to have a property: "${JSON.stringify(propertyName)}"`) + return expect(targetData, customErrorMsg).to.have.a.property(propertyName) } /** - * @param {*} targetData - * @param {*} type - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} type + * @param {*} [customErrorMsg] + */ expectToBeA(targetData, type, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be a "${JSON.stringify(type)}"`); - return expect(targetData, customErrorMsg).to.be.a(type); + output.step(`I expect "${JSON.stringify(targetData)}" to be a "${JSON.stringify(type)}"`) + return expect(targetData, customErrorMsg).to.be.a(type) } /** - * @param {*} targetData - * @param {*} type - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} type + * @param {*} [customErrorMsg] + */ expectToBeAn(targetData, type, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be an "${JSON.stringify(type)}"`); - return expect(targetData, customErrorMsg).to.be.an(type); + output.step(`I expect "${JSON.stringify(targetData)}" to be an "${JSON.stringify(type)}"`) + return expect(targetData, customErrorMsg).to.be.an(type) } /** - * @param {*} targetData - * @param {*} regex - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} regex + * @param {*} [customErrorMsg] + */ expectMatchRegex(targetData, regex, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to match the regex "${JSON.stringify(regex)}"`); - return expect(targetData, customErrorMsg).to.match(regex); + output.step(`I expect "${JSON.stringify(targetData)}" to match the regex "${JSON.stringify(regex)}"`) + return expect(targetData, customErrorMsg).to.match(regex) } /** - * @param {*} targetData - * @param {*} length - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} length + * @param {*} [customErrorMsg] + */ expectLengthOf(targetData, length, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have length of "${JSON.stringify(length)}"`); - return expect(targetData, customErrorMsg).to.have.lengthOf(length); + output.step(`I expect "${JSON.stringify(targetData)}" to have length of "${JSON.stringify(length)}"`) + return expect(targetData, customErrorMsg).to.have.lengthOf(length) } /** - * @param {*} targetData - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} [customErrorMsg] + */ expectEmpty(targetData, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be empty`); - return expect(targetData, customErrorMsg).to.be.empty; + output.step(`I expect "${JSON.stringify(targetData)}" to be empty`) + return expect(targetData, customErrorMsg).to.be.empty } /** - * @param {*} targetData - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} [customErrorMsg] + */ expectTrue(targetData, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be true`); - return expect(targetData, customErrorMsg).to.be.true; + output.step(`I expect "${JSON.stringify(targetData)}" to be true`) + return expect(targetData, customErrorMsg).to.be.true } /** - * @param {*} targetData - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} [customErrorMsg] + */ expectFalse(targetData, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be false`); - return expect(targetData, customErrorMsg).to.be.false; + output.step(`I expect "${JSON.stringify(targetData)}" to be false`) + return expect(targetData, customErrorMsg).to.be.false } /** - * @param {*} targetData - * @param {*} aboveThan - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} aboveThan + * @param {*} [customErrorMsg] + */ expectAbove(targetData, aboveThan, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be above ${JSON.stringify(aboveThan)}`); - return expect(targetData, customErrorMsg).to.be.above(aboveThan); + output.step(`I expect "${JSON.stringify(targetData)}" to be above ${JSON.stringify(aboveThan)}`) + return expect(targetData, customErrorMsg).to.be.above(aboveThan) } /** - * @param {*} targetData - * @param {*} belowThan - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} belowThan + * @param {*} [customErrorMsg] + */ expectBelow(targetData, belowThan, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be below ${JSON.stringify(belowThan)}`); - return expect(targetData, customErrorMsg).to.be.below(belowThan); + output.step(`I expect "${JSON.stringify(targetData)}" to be below ${JSON.stringify(belowThan)}`) + return expect(targetData, customErrorMsg).to.be.below(belowThan) } /** - * @param {*} targetData - * @param {*} lengthAboveThan - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} lengthAboveThan + * @param {*} [customErrorMsg] + */ expectLengthAboveThan(targetData, lengthAboveThan, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have length of above ${JSON.stringify(lengthAboveThan)}`); - return expect(targetData, customErrorMsg).to.have.lengthOf.above( - lengthAboveThan, - ); + output.step(`I expect "${JSON.stringify(targetData)}" to have length of above ${JSON.stringify(lengthAboveThan)}`) + return expect(targetData, customErrorMsg).to.have.lengthOf.above(lengthAboveThan) } /** - * @param {*} targetData - * @param {*} lengthBelowThan - * @param {*} [customErrorMsg] - */ + * @param {*} targetData + * @param {*} lengthBelowThan + * @param {*} [customErrorMsg] + */ expectLengthBelowThan(targetData, lengthBelowThan, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have length of below ${JSON.stringify(lengthBelowThan)}`); - return expect(targetData, customErrorMsg).to.have.lengthOf.below( - lengthBelowThan, - ); + output.step(`I expect "${JSON.stringify(targetData)}" to have length of below ${JSON.stringify(lengthBelowThan)}`) + return expect(targetData, customErrorMsg).to.have.lengthOf.below(lengthBelowThan) } /** - * @param {*} actualValue - * @param {*} expectedValue - * @param {*} [customErrorMsg] - */ + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ expectEqualIgnoreCase(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect and ingore case "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`); - return expect(actualValue, customErrorMsg).to.equalIgnoreCase( - expectedValue, - ); + output.step(`I expect and ingore case "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`) + return expect(actualValue, customErrorMsg).to.equalIgnoreCase(expectedValue) } /** - * expects members of two arrays are deeply equal - * @param {*} actualValue - * @param {*} expectedValue - * @param {*} [customErrorMsg] - */ + * expects members of two arrays are deeply equal + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ expectDeepMembers(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect members of "${JSON.stringify(actualValue)}" and "${JSON.stringify(expectedValue)}" arrays are deeply equal`); - return expect(actualValue, customErrorMsg).to.have.deep.members( - expectedValue, - ); + output.step( + `I expect members of "${JSON.stringify(actualValue)}" and "${JSON.stringify(expectedValue)}" arrays are deeply equal`, + ) + return expect(actualValue, customErrorMsg).to.have.deep.members(expectedValue) } /** - * expects an array to be a superset of another array - * @param {*} superset - * @param {*} set - * @param {*} [customErrorMsg] - */ + * expects an array to be a superset of another array + * @param {*} superset + * @param {*} set + * @param {*} [customErrorMsg] + */ expectDeepIncludeMembers(superset, set, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(superset)}" array to be a superset of "${JSON.stringify(set)}" array`); - return expect(superset, customErrorMsg).to.deep.include.members( - set, - ); + output.step(`I expect "${JSON.stringify(superset)}" array to be a superset of "${JSON.stringify(set)}" array`) + return expect(superset, customErrorMsg).to.deep.include.members(set) } /** - * expects members of two JSON objects are deeply equal excluding some properties - * @param {*} actualValue - * @param {*} expectedValue - * @param {*} fieldsToExclude - * @param {*} [customErrorMsg] - */ - expectDeepEqualExcluding( - actualValue, - expectedValue, - fieldsToExclude, - customErrorMsg = '', - ) { + * expects members of two JSON objects are deeply equal excluding some properties + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} fieldsToExclude + * @param {*} [customErrorMsg] + */ + expectDeepEqualExcluding(actualValue, expectedValue, fieldsToExclude, customErrorMsg = '') { // @ts-ignore - output.step(`I expect members of "${JSON.stringify(actualValue)}" and "${JSON.stringify(expectedValue)}" JSON objects are deeply equal excluding properties: ${JSON.stringify(fieldsToExclude)}`); - return expect(actualValue, customErrorMsg) - .excludingEvery(fieldsToExclude) - .to.deep.equal(expectedValue); + output.step( + `I expect members of "${JSON.stringify(actualValue)}" and "${JSON.stringify(expectedValue)}" JSON objects are deeply equal excluding properties: ${JSON.stringify(fieldsToExclude)}`, + ) + return expect(actualValue, customErrorMsg).excludingEvery(fieldsToExclude).to.deep.equal(expectedValue) } /** - * expects a JSON object matches a provided pattern - * @param {*} actualValue - * @param {*} expectedPattern - * @param {*} [customErrorMsg] - */ + * expects a JSON object matches a provided pattern + * @param {*} actualValue + * @param {*} expectedPattern + * @param {*} [customErrorMsg] + */ expectMatchesPattern(actualValue, expectedPattern, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to match the ${JSON.stringify(expectedPattern)} pattern`); - return expect(actualValue, customErrorMsg).to.matchPattern(expectedPattern); + output.step(`I expect "${JSON.stringify(actualValue)}" to match the ${JSON.stringify(expectedPattern)} pattern`) + return expect(actualValue, customErrorMsg).to.matchPattern(expectedPattern) } } -module.exports = ExpectHelper; +module.exports = ExpectHelper diff --git a/lib/helper/FileSystem.js b/lib/helper/FileSystem.js index 830936c7d..68d6d645d 100644 --- a/lib/helper/FileSystem.js +++ b/lib/helper/FileSystem.js @@ -1,11 +1,11 @@ -const assert = require('assert'); -const path = require('path'); -const fs = require('fs'); +const assert = require('assert') +const path = require('path') +const fs = require('fs') -const Helper = require('@codeceptjs/helper'); -const { fileExists } = require('../utils'); -const { fileIncludes } = require('../assert/include'); -const { fileEquals } = require('../assert/equal'); +const Helper = require('@codeceptjs/helper') +const { fileExists } = require('../utils') +const { fileIncludes } = require('../assert/include') +const { fileEquals } = require('../assert/equal') /** * Helper for testing filesystem. @@ -32,13 +32,13 @@ const { fileEquals } = require('../assert/equal'); */ class FileSystem extends Helper { constructor() { - super(); - this.dir = global.codecept_dir; - this.file = ''; + super() + this.dir = global.codecept_dir + this.file = '' } _before() { - this.debugSection('Dir', this.dir); + this.debugSection('Dir', this.dir) } /** @@ -47,8 +47,8 @@ class FileSystem extends Helper { * @param {string} openPath */ amInPath(openPath) { - this.dir = path.join(global.codecept_dir, openPath); - this.debugSection('Dir', this.dir); + this.dir = path.join(global.codecept_dir, openPath) + this.debugSection('Dir', this.dir) } /** @@ -57,7 +57,7 @@ class FileSystem extends Helper { * @param {string} text */ writeToFile(name, text) { - fs.writeFileSync(path.join(this.dir, name), text); + fs.writeFileSync(path.join(this.dir, name), text) } /** @@ -65,9 +65,9 @@ class FileSystem extends Helper { * @param {string} name */ seeFile(name) { - this.file = path.join(this.dir, name); - this.debugSection('File', this.file); - assert.ok(fileExists(this.file), `File ${name} not found in ${this.dir}`); + this.file = path.join(this.dir, name) + this.debugSection('File', this.file) + assert.ok(fileExists(this.file), `File ${name} not found in ${this.dir}`) } /** @@ -83,31 +83,31 @@ class FileSystem extends Helper { * @param {number} [sec=1] seconds to wait */ async waitForFile(name, sec = 1) { - if (sec === 0) assert.fail('Use `seeFile` instead of waiting 0 seconds!'); - const waitTimeout = sec * 1000; - this.file = path.join(this.dir, name); - this.debugSection('File', this.file); + if (sec === 0) assert.fail('Use `seeFile` instead of waiting 0 seconds!') + const waitTimeout = sec * 1000 + this.file = path.join(this.dir, name) + this.debugSection('File', this.file) return isFileExists(this.file, waitTimeout).catch(() => { - throw new Error(`file (${name}) still not present in directory ${this.dir} after ${waitTimeout / 1000} sec`); - }); + throw new Error(`file (${name}) still not present in directory ${this.dir} after ${waitTimeout / 1000} sec`) + }) } /** - * Checks that file with a name including given text exists in the current directory. - * - *```js - * I.handleDownloads(); - * I.click('Download as PDF'); - * I.amInPath('output/downloads'); - * I.seeFileNameMatching('.pdf'); - * ``` - * @param {string} text - */ + * Checks that file with a name including given text exists in the current directory. + * + *```js + * I.handleDownloads(); + * I.click('Download as PDF'); + * I.amInPath('output/downloads'); + * I.seeFileNameMatching('.pdf'); + * ``` + * @param {string} text + */ seeFileNameMatching(text) { assert.ok( - this.grabFileNames().some(file => file.includes(text)), + this.grabFileNames().some((file) => file.includes(text)), `File name which contains ${text} not found in ${this.dir}`, - ); + ) } /** @@ -116,8 +116,8 @@ class FileSystem extends Helper { * @param {string} [encoding='utf8'] */ seeInThisFile(text, encoding = 'utf8') { - const content = getFileContents(this.file, encoding); - fileIncludes(this.file).assert(text, content); + const content = getFileContents(this.file, encoding) + fileIncludes(this.file).assert(text, content) } /** @@ -126,8 +126,8 @@ class FileSystem extends Helper { * @param {string} [encoding='utf8'] */ dontSeeInThisFile(text, encoding = 'utf8') { - const content = getFileContents(this.file, encoding); - fileIncludes(this.file).negate(text, content); + const content = getFileContents(this.file, encoding) + fileIncludes(this.file).negate(text, content) } /** @@ -136,8 +136,8 @@ class FileSystem extends Helper { * @param {string} [encoding='utf8'] */ seeFileContentsEqual(text, encoding = 'utf8') { - const content = getFileContents(this.file, encoding); - fileEquals(this.file).assert(text, content); + const content = getFileContents(this.file, encoding) + fileEquals(this.file).assert(text, content) } /** @@ -147,11 +147,11 @@ class FileSystem extends Helper { * @param {string} [encodingReference='utf8'] */ seeFileContentsEqualReferenceFile(pathToReferenceFile, encoding = 'utf8', encodingReference = '') { - const content = getFileContents(this.file, encoding); - assert.ok(fileExists(pathToReferenceFile), `Reference file ${pathToReferenceFile} not found.`); - encodingReference = encodingReference || encoding; - const expectedContent = getFileContents(pathToReferenceFile, encodingReference); - fileEquals(this.file).assert(expectedContent, content); + const content = getFileContents(this.file, encoding) + assert.ok(fileExists(pathToReferenceFile), `Reference file ${pathToReferenceFile} not found.`) + encodingReference = encodingReference || encoding + const expectedContent = getFileContents(pathToReferenceFile, encodingReference) + fileEquals(this.file).assert(expectedContent, content) } /** @@ -160,27 +160,26 @@ class FileSystem extends Helper { * @param {string} [encoding='utf8'] */ dontSeeFileContentsEqual(text, encoding = 'utf8') { - const content = getFileContents(this.file, encoding); - fileEquals(this.file).negate(text, content); + const content = getFileContents(this.file, encoding) + fileEquals(this.file).negate(text, content) } /** - * Returns file names in current directory. - * - * ```js - * I.handleDownloads(); - * I.click('Download Files'); - * I.amInPath('output/downloads'); - * const downloadedFileNames = I.grabFileNames(); - * ``` - */ + * Returns file names in current directory. + * + * ```js + * I.handleDownloads(); + * I.click('Download Files'); + * I.amInPath('output/downloads'); + * const downloadedFileNames = I.grabFileNames(); + * ``` + */ grabFileNames() { - return fs.readdirSync(this.dir) - .filter(item => !fs.lstatSync(path.join(this.dir, item)).isDirectory()); + return fs.readdirSync(this.dir).filter((item) => !fs.lstatSync(path.join(this.dir, item)).isDirectory()) } } -module.exports = FileSystem; +module.exports = FileSystem /** * @param {string} file @@ -189,9 +188,9 @@ module.exports = FileSystem; * @returns {string} */ function getFileContents(file, encoding = 'utf8') { - if (!file) assert.fail('No files were opened, please use seeFile action'); - if (encoding === '') assert.fail('Encoding is an empty string, please set a valid encoding'); - return fs.readFileSync(file, encoding); + if (!file) assert.fail('No files were opened, please use seeFile action') + if (encoding === '') assert.fail('Encoding is an empty string, please set a valid encoding') + return fs.readFileSync(file, encoding) } /** @@ -201,28 +200,28 @@ function getFileContents(file, encoding = 'utf8') { * @returns {Promise} */ function isFileExists(file, timeout) { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { const timer = setTimeout(() => { - watcher.close(); - reject(new Error('File did not exists and was not created during the timeout.')); - }, timeout); + watcher.close() + reject(new Error('File did not exists and was not created during the timeout.')) + }, timeout) - const dir = path.dirname(file); - const basename = path.basename(file); + const dir = path.dirname(file) + const basename = path.basename(file) const watcher = fs.watch(dir, (eventType, filename) => { if (eventType === 'rename' && filename === basename) { - clearTimeout(timer); - watcher.close(); - resolve(); + clearTimeout(timer) + watcher.close() + resolve() } - }); + }) fs.access(file, fs.constants.R_OK, (err) => { if (!err) { - clearTimeout(timer); - watcher.close(); - resolve(); + clearTimeout(timer) + watcher.close() + resolve() } - }); - })); + }) + }) } diff --git a/lib/helper/GraphQL.js b/lib/helper/GraphQL.js index fb87f16a2..4a5f39c29 100644 --- a/lib/helper/GraphQL.js +++ b/lib/helper/GraphQL.js @@ -1,5 +1,5 @@ -const axios = require('axios').default; -const Helper = require('@codeceptjs/helper'); +const axios = require('axios').default +const Helper = require('@codeceptjs/helper') /** * GraphQL helper allows to send additional requests to a GraphQl endpoint during acceptance tests. @@ -38,31 +38,35 @@ const Helper = require('@codeceptjs/helper'); */ class GraphQL extends Helper { constructor(config) { - super(config); - this.axios = axios.create(); - this.headers = {}; + super(config) + this.axios = axios.create() + this.headers = {} this.options = { timeout: 10000, defaultHeaders: {}, endpoint: '', - }; - this.options = Object.assign(this.options, config); - this.headers = { ...this.options.defaultHeaders }; - this.axios.defaults.headers = this.options.defaultHeaders; + } + this.options = Object.assign(this.options, config) + this.headers = { ...this.options.defaultHeaders } + this.axios.defaults.headers = this.options.defaultHeaders } static _checkRequirements() { try { - require('axios'); + require('axios') } catch (e) { - return ['axios']; + return ['axios'] } } static _config() { return [ - { name: 'endpoint', message: 'Endpoint of API you are going to test', default: 'http://localhost:3000/graphql' }, - ]; + { + name: 'endpoint', + message: 'Endpoint of API you are going to test', + default: 'http://localhost:3000/graphql', + }, + ] } /** @@ -71,42 +75,39 @@ class GraphQL extends Helper { * @param {object} request */ async _executeQuery(request) { - this.axios.defaults.timeout = request.timeout || this.options.timeout; + this.axios.defaults.timeout = request.timeout || this.options.timeout if (this.headers && this.headers.auth) { - request.auth = this.headers.auth; + request.auth = this.headers.auth } request.headers = Object.assign(request.headers, { 'Content-Type': 'application/json', - }); + }) - request.headers = { ...this.headers, ...request.headers }; + request.headers = { ...this.headers, ...request.headers } if (this.config.onRequest) { - await this.config.onRequest(request); + await this.config.onRequest(request) } - this.debugSection('Request', JSON.stringify(request)); + this.debugSection('Request', JSON.stringify(request)) - let response; + let response try { - response = await this.axios(request); + response = await this.axios(request) } catch (err) { - if (!err.response) throw err; - this.debugSection( - 'Response', - `Response error. Status code: ${err.response.status}`, - ); - response = err.response; + if (!err.response) throw err + this.debugSection('Response', `Response error. Status code: ${err.response.status}`) + response = err.response } if (this.config.onResponse) { - await this.config.onResponse(response); + await this.config.onResponse(response) } - this.debugSection('Response', JSON.stringify(response.data)); - return response; + this.debugSection('Response', JSON.stringify(response.data)) + return response } /** @@ -122,7 +123,7 @@ class GraphQL extends Helper { method: 'POST', data: operation, headers, - }; + } } /** @@ -148,15 +149,15 @@ class GraphQL extends Helper { */ async sendQuery(query, variables, options = {}, headers = {}) { if (typeof query !== 'string') { - throw new Error(`query expected to be a String, instead received ${typeof query}`); + throw new Error(`query expected to be a String, instead received ${typeof query}`) } const operation = { query, variables, ...options, - }; - const request = this._prepareGraphQLRequest(operation, headers); - return this._executeQuery(request); + } + const request = this._prepareGraphQLRequest(operation, headers) + return this._executeQuery(request) } /** @@ -188,19 +189,19 @@ class GraphQL extends Helper { */ async sendMutation(mutation, variables, options = {}, headers = {}) { if (typeof mutation !== 'string') { - throw new Error(`mutation expected to be a String, instead received ${typeof mutation}`); + throw new Error(`mutation expected to be a String, instead received ${typeof mutation}`) } const operation = { query: mutation, variables, ...options, - }; - const request = this._prepareGraphQLRequest(operation, headers); - return this._executeQuery(request); + } + const request = this._prepareGraphQLRequest(operation, headers) + return this._executeQuery(request) } _setRequestTimeout(newTimeout) { - this.options.timeout = newTimeout; + this.options.timeout = newTimeout } /** @@ -209,7 +210,7 @@ class GraphQL extends Helper { * @param {object} headers headers list */ haveRequestHeaders(headers) { - this.headers = { ...this.headers, ...headers }; + this.headers = { ...this.headers, ...headers } } /** @@ -223,7 +224,7 @@ class GraphQL extends Helper { * @param {string | CodeceptJS.Secret} accessToken Bearer access token */ amBearerAuthenticated(accessToken) { - this.haveRequestHeaders({ Authorization: `Bearer ${accessToken}` }); + this.haveRequestHeaders({ Authorization: `Bearer ${accessToken}` }) } } -module.exports = GraphQL; +module.exports = GraphQL diff --git a/lib/helper/GraphQLDataFactory.js b/lib/helper/GraphQLDataFactory.js index 9a6d802e6..e45f7fe19 100644 --- a/lib/helper/GraphQLDataFactory.js +++ b/lib/helper/GraphQLDataFactory.js @@ -1,7 +1,7 @@ -const path = require('path'); +const path = require('path') -const Helper = require('@codeceptjs/helper'); -const GraphQL = require('./GraphQL'); +const Helper = require('@codeceptjs/helper') +const GraphQL = require('./GraphQL') /** * Helper for managing remote data using GraphQL queries. @@ -151,52 +151,52 @@ const GraphQL = require('./GraphQL'); */ class GraphQLDataFactory extends Helper { constructor(config) { - super(config); + super(config) const defaultConfig = { cleanup: true, GraphQL: {}, factories: {}, - }; - this.config = Object.assign(defaultConfig, this.config); + } + this.config = Object.assign(defaultConfig, this.config) if (this.config.headers) { - this.config.GraphQL.defaultHeaders = this.config.headers; + this.config.GraphQL.defaultHeaders = this.config.headers } if (this.config.onRequest) { - this.config.GraphQL.onRequest = this.config.onRequest; + this.config.GraphQL.onRequest = this.config.onRequest } - this.graphqlHelper = new GraphQL(Object.assign(this.config.GraphQL, { endpoint: this.config.endpoint })); - this.factories = this.config.factories; + this.graphqlHelper = new GraphQL(Object.assign(this.config.GraphQL, { endpoint: this.config.endpoint })) + this.factories = this.config.factories - this.created = {}; - Object.keys(this.factories).forEach(f => (this.created[f] = [])); + this.created = {} + Object.keys(this.factories).forEach((f) => (this.created[f] = [])) } static _checkRequirements() { try { - require('axios'); - require('rosie'); + require('axios') + require('rosie') } catch (e) { - return ['axios', 'rosie']; + return ['axios', 'rosie'] } } _after() { if (!this.config.cleanup) { - return Promise.resolve(); + return Promise.resolve() } - const promises = []; + const promises = [] // clean up all created items for (const mutationName in this.created) { - const createdItems = this.created[mutationName]; - if (!createdItems.length) continue; - this.debug(`Deleting ${createdItems.length} ${mutationName}(s)`); + const createdItems = this.created[mutationName] + if (!createdItems.length) continue + this.debug(`Deleting ${createdItems.length} ${mutationName}(s)`) for (const itemData of createdItems) { - promises.push(this._requestDelete(mutationName, itemData)); + promises.push(this._requestDelete(mutationName, itemData)) } } - return Promise.all(promises); + return Promise.all(promises) } /** @@ -214,9 +214,9 @@ class GraphQLDataFactory extends Helper { * @param {*} params predefined parameters */ mutateData(operation, params) { - const variables = this._createItem(operation, params); - this.debug(`Creating ${operation} ${JSON.stringify(variables)}`); - return this._requestCreate(operation, variables); + const variables = this._createItem(operation, params) + this.debug(`Creating ${operation} ${JSON.stringify(variables)}`) + return this._requestCreate(operation, variables) } /** @@ -235,26 +235,26 @@ class GraphQLDataFactory extends Helper { * @param {*} params */ mutateMultiple(operation, times, params) { - const promises = []; + const promises = [] for (let i = 0; i < times; i++) { - promises.push(this.mutateData(operation, params)); + promises.push(this.mutateData(operation, params)) } - return Promise.all(promises); + return Promise.all(promises) } _createItem(operation, data) { if (!this.factories[operation]) { - throw new Error(`Mutation ${operation} is not defined in config.factories`); + throw new Error(`Mutation ${operation} is not defined in config.factories`) } - let modulePath = this.factories[operation].factory; + let modulePath = this.factories[operation].factory try { try { - require.resolve(modulePath); + require.resolve(modulePath) } catch (e) { - modulePath = path.join(global.codecept_dir, modulePath); + modulePath = path.join(global.codecept_dir, modulePath) } - const builder = require(modulePath); - return builder.build(data); + const builder = require(modulePath) + return builder.build(data) } catch (err) { throw new Error(`Couldn't load factory file from ${modulePath}, check that @@ -265,7 +265,7 @@ class GraphQLDataFactory extends Helper { points to valid factory file. Factory file should export an object with build method. - Current file error: ${err.message}`); + Current file error: ${err.message}`) } } @@ -277,13 +277,13 @@ class GraphQLDataFactory extends Helper { * @param {*} variables to be sent along with the query */ _requestCreate(operation, variables) { - const { query } = this.factories[operation]; + const { query } = this.factories[operation] return this.graphqlHelper.sendMutation(query, variables).then((response) => { - const data = response.data.data[operation]; - this.created[operation].push(data); - this.debugSection('Created', `record: ${data}`); - return data; - }); + const data = response.data.data[operation] + this.created[operation].push(data) + this.debugSection('Created', `record: ${data}`) + return data + }) } /** @@ -294,16 +294,15 @@ class GraphQLDataFactory extends Helper { * @param {*} data of the record to be deleted. */ _requestDelete(operation, data) { - const deleteOperation = this.factories[operation].revert(data); - const { query, variables } = deleteOperation; + const deleteOperation = this.factories[operation].revert(data) + const { query, variables } = deleteOperation - return this.graphqlHelper.sendMutation(query, variables) - .then((response) => { - const idx = this.created[operation].indexOf(data); - this.debugSection('Deleted', `record: ${response.data.data}`); - this.created[operation].splice(idx, 1); - }); + return this.graphqlHelper.sendMutation(query, variables).then((response) => { + const idx = this.created[operation].indexOf(data) + this.debugSection('Deleted', `record: ${response.data.data}`) + this.created[operation].splice(idx, 1) + }) } } -module.exports = GraphQLDataFactory; +module.exports = GraphQLDataFactory diff --git a/lib/helper/JSONResponse.js b/lib/helper/JSONResponse.js index e651a6833..765b37fa9 100644 --- a/lib/helper/JSONResponse.js +++ b/lib/helper/JSONResponse.js @@ -1,13 +1,13 @@ -const Helper = require('@codeceptjs/helper'); +const Helper = require('@codeceptjs/helper') -let expect; +let expect -import('chai').then(chai => { - expect = chai.expect; - chai.use(require('chai-deep-match')); -}); +import('chai').then((chai) => { + expect = chai.expect + chai.use(require('chai-deep-match')) +}) -const joi = require('joi'); +const joi = require('joi') /** * This helper allows performing assertions on JSON responses paired with following helpers: @@ -67,33 +67,35 @@ const joi = require('joi'); */ class JSONResponse extends Helper { constructor(config = {}) { - super(config); + super(config) this.options = { requestHelper: 'REST', - }; - this.options = { ...this.options, ...config }; + } + this.options = { ...this.options, ...config } } _beforeSuite() { - this.response = null; + this.response = null if (!this.helpers[this.options.requestHelper]) { - throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`); + throw new Error( + `Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`, + ) } // connect to REST helper this.helpers[this.options.requestHelper].config.onResponse = (response) => { - this.response = response; - }; + this.response = response + } } _before() { - this.response = null; + this.response = null } static _checkRequirements() { try { - require('joi'); + require('joi') } catch (e) { - return ['joi']; + return ['joi'] } } @@ -107,8 +109,8 @@ class JSONResponse extends Helper { * @param {number} code */ seeResponseCodeIs(code) { - this._checkResponseReady(); - expect(this.response.status).to.eql(code, 'Response code is not the same as expected'); + this._checkResponseReady() + expect(this.response.status).to.eql(code, 'Response code is not the same as expected') } /** @@ -121,35 +123,35 @@ class JSONResponse extends Helper { * @param {number} code */ dontSeeResponseCodeIs(code) { - this._checkResponseReady(); - expect(this.response.status).not.to.eql(code); + this._checkResponseReady() + expect(this.response.status).not.to.eql(code) } /** * Checks that the response code is 4xx */ seeResponseCodeIsClientError() { - this._checkResponseReady(); - expect(this.response.status).to.be.gte(400); - expect(this.response.status).to.be.lt(500); + this._checkResponseReady() + expect(this.response.status).to.be.gte(400) + expect(this.response.status).to.be.lt(500) } /** * Checks that the response code is 3xx */ seeResponseCodeIsRedirection() { - this._checkResponseReady(); - expect(this.response.status).to.be.gte(300); - expect(this.response.status).to.be.lt(400); + this._checkResponseReady() + expect(this.response.status).to.be.gte(300) + expect(this.response.status).to.be.lt(400) } /** * Checks that the response code is 5xx */ seeResponseCodeIsServerError() { - this._checkResponseReady(); - expect(this.response.status).to.be.gte(500); - expect(this.response.status).to.be.lt(600); + this._checkResponseReady() + expect(this.response.status).to.be.gte(500) + expect(this.response.status).to.be.lt(600) } /** @@ -161,9 +163,9 @@ class JSONResponse extends Helper { * ``` */ seeResponseCodeIsSuccessful() { - this._checkResponseReady(); - expect(this.response.status).to.be.gte(200); - expect(this.response.status).to.be.lt(300); + this._checkResponseReady() + expect(this.response.status).to.be.gte(200) + expect(this.response.status).to.be.lt(300) } /** @@ -184,19 +186,19 @@ class JSONResponse extends Helper { * @param {object} json */ seeResponseContainsJson(json = {}) { - this._checkResponseReady(); + this._checkResponseReady() if (Array.isArray(this.response.data)) { - let fails = 0; + let fails = 0 for (const el of this.response.data) { try { - expect(el).to.deep.match(json); + expect(el).to.deep.match(json) } catch (err) { - fails++; + fails++ } } - expect(fails < this.response.data.length, `No elements in array matched ${JSON.stringify(json)}`).to.be.true; + expect(fails < this.response.data.length, `No elements in array matched ${JSON.stringify(json)}`).to.be.true } else { - expect(this.response.data).to.deep.match(json); + expect(this.response.data).to.deep.match(json) } } @@ -218,11 +220,11 @@ class JSONResponse extends Helper { * @param {object} json */ dontSeeResponseContainsJson(json = {}) { - this._checkResponseReady(); + this._checkResponseReady() if (Array.isArray(this.response.data)) { - this.response.data.forEach(data => expect(data).not.to.deep.match(json)); + this.response.data.forEach((data) => expect(data).not.to.deep.match(json)) } else { - expect(this.response.data).not.to.deep.match(json); + expect(this.response.data).not.to.deep.match(json) } } @@ -246,11 +248,11 @@ class JSONResponse extends Helper { * @param {array} keys */ seeResponseContainsKeys(keys = []) { - this._checkResponseReady(); + this._checkResponseReady() if (Array.isArray(this.response.data)) { - this.response.data.forEach(data => expect(data).to.include.keys(keys)); + this.response.data.forEach((data) => expect(data).to.include.keys(keys)) } else { - expect(this.response.data).to.include.keys(keys); + expect(this.response.data).to.include.keys(keys) } } @@ -268,10 +270,10 @@ class JSONResponse extends Helper { * @param {function} fn */ seeResponseValidByCallback(fn) { - this._checkResponseReady(); - fn({ ...this.response, expect }); - const body = fn.toString(); - fn.toString = () => `${body.split('\n')[1]}...`; + this._checkResponseReady() + fn({ ...this.response, expect }) + const body = fn.toString() + fn.toString = () => `${body.split('\n')[1]}...` } /** @@ -285,8 +287,8 @@ class JSONResponse extends Helper { * @param {object} resp */ seeResponseEquals(resp) { - this._checkResponseReady(); - expect(this.response.data).to.deep.equal(resp); + this._checkResponseReady() + expect(this.response.data).to.deep.equal(resp) } /** @@ -317,22 +319,22 @@ class JSONResponse extends Helper { * @param {any} fnOrSchema */ seeResponseMatchesJsonSchema(fnOrSchema) { - this._checkResponseReady(); - let schema = fnOrSchema; + this._checkResponseReady() + let schema = fnOrSchema if (typeof fnOrSchema === 'function') { - schema = fnOrSchema(joi); - const body = fnOrSchema.toString(); - fnOrSchema.toString = () => `${body.split('\n')[1]}...`; + schema = fnOrSchema(joi) + const body = fnOrSchema.toString() + fnOrSchema.toString = () => `${body.split('\n')[1]}...` } - if (!schema) throw new Error('Empty Joi schema provided, see https://joi.dev/ for details'); - if (!joi.isSchema(schema)) throw new Error('Invalid Joi schema provided, see https://joi.dev/ for details'); - schema.toString = () => schema.describe(); - joi.assert(this.response.data, schema); + if (!schema) throw new Error('Empty Joi schema provided, see https://joi.dev/ for details') + if (!joi.isSchema(schema)) throw new Error('Invalid Joi schema provided, see https://joi.dev/ for details') + schema.toString = () => schema.describe() + joi.assert(this.response.data, schema) } _checkResponseReady() { - if (!this.response) throw new Error('Response is not available'); + if (!this.response) throw new Error('Response is not available') } } -module.exports = JSONResponse; +module.exports = JSONResponse diff --git a/lib/helper/Mochawesome.js b/lib/helper/Mochawesome.js index 826b2f232..abc143af4 100644 --- a/lib/helper/Mochawesome.js +++ b/lib/helper/Mochawesome.js @@ -1,71 +1,71 @@ -let addMochawesomeContext; -let currentTest; -let currentSuite; +let addMochawesomeContext +let currentTest +let currentSuite -const Helper = require('@codeceptjs/helper'); -const { clearString } = require('../utils'); +const Helper = require('@codeceptjs/helper') +const { clearString } = require('../utils') class Mochawesome extends Helper { constructor(config) { - super(config); + super(config) // set defaults this.options = { uniqueScreenshotNames: false, disableScreenshots: false, - }; + } - addMochawesomeContext = require('mochawesome/addContext'); - this._createConfig(config); + addMochawesomeContext = require('mochawesome/addContext') + this._createConfig(config) } _createConfig(config) { // override defaults with config - Object.assign(this.options, config); + Object.assign(this.options, config) } _beforeSuite(suite) { - currentSuite = suite; - currentTest = ''; + currentSuite = suite + currentTest = '' } _before() { if (currentSuite && currentSuite.ctx) { - currentTest = { test: currentSuite.ctx.currentTest }; + currentTest = { test: currentSuite.ctx.currentTest } } } _test(test) { - currentTest = { test }; + currentTest = { test } } _failed(test) { - if (this.options.disableScreenshots) return; - let fileName; + if (this.options.disableScreenshots) return + let fileName // Get proper name if we are fail on hook if (test.ctx.test.type === 'hook') { - currentTest = { test: test.ctx.test }; + currentTest = { test: test.ctx.test } // ignore retries if we are in hook - test._retries = -1; - fileName = clearString(`${test.title}_${currentTest.test.title}`); + test._retries = -1 + fileName = clearString(`${test.title}_${currentTest.test.title}`) } else { - currentTest = { test }; - fileName = clearString(test.title); + currentTest = { test } + fileName = clearString(test.title) } if (this.options.uniqueScreenshotNames) { - const uuid = test.uuid || test.ctx.test.uuid; - fileName = `${fileName.substring(0, 10)}_${uuid}`; + const uuid = test.uuid || test.ctx.test.uuid + fileName = `${fileName.substring(0, 10)}_${uuid}` } if (test._retries < 1 || test._retries === test.retryNum) { - fileName = `${fileName}.failed.png`; - return addMochawesomeContext(currentTest, fileName); + fileName = `${fileName}.failed.png` + return addMochawesomeContext(currentTest, fileName) } } addMochawesomeContext(context) { - if (currentTest === '') currentTest = { test: currentSuite.ctx.test }; - return addMochawesomeContext(currentTest, context); + if (currentTest === '') currentTest = { test: currentSuite.ctx.test } + return addMochawesomeContext(currentTest, context) } } -module.exports = Mochawesome; +module.exports = Mochawesome diff --git a/lib/helper/MockServer.js b/lib/helper/MockServer.js index 9cb748808..7278c1021 100644 --- a/lib/helper/MockServer.js +++ b/lib/helper/MockServer.js @@ -1,4 +1,4 @@ -const { mock, settings } = require('pactum'); +const { mock, settings } = require('pactum') /** * ## Configuration @@ -18,7 +18,7 @@ let config = { key: '', cert: '', }, -}; +} /** * MockServer @@ -141,10 +141,10 @@ let config = { */ class MockServer { constructor(passedConfig) { - settings.setLogLevel('SILENT'); - config = { ...passedConfig }; + settings.setLogLevel('SILENT') + config = { ...passedConfig } if (global.debugMode) { - settings.setLogLevel('VERBOSE'); + settings.setLogLevel('VERBOSE') } } @@ -155,10 +155,10 @@ class MockServer { * @returns void */ async startMockServer(port) { - const _config = { ...config }; - if (port) _config.port = port; - await mock.setDefaults(_config); - await mock.start(); + const _config = { ...config } + if (port) _config.port = port + await mock.setDefaults(_config) + await mock.start() } /** @@ -168,7 +168,7 @@ class MockServer { * */ async stopMockServer() { - await mock.stop(); + await mock.stop() } /** @@ -214,8 +214,8 @@ class MockServer { * */ async addInteractionToMockServer(interaction) { - await mock.addInteraction(interaction); + await mock.addInteraction(interaction) } } -module.exports = MockServer; +module.exports = MockServer diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js index 59947bec6..d89fc520a 100644 --- a/lib/helper/Nightmare.js +++ b/lib/helper/Nightmare.js @@ -1,29 +1,24 @@ -const path = require('path'); - -const urlResolve = require('url').resolve; - -const Helper = require('@codeceptjs/helper'); -const { includes: stringIncludes } = require('../assert/include'); -const { urlEquals } = require('../assert/equal'); -const { equals } = require('../assert/equal'); -const { empty } = require('../assert/empty'); -const { truth } = require('../assert/truth'); -const Locator = require('../locator'); -const ElementNotFound = require('./errors/ElementNotFound'); -const { - xpathLocator, - fileExists, - screenshotOutputFolder, - toCamelCase, -} = require('../utils'); +const path = require('path') + +const urlResolve = require('url').resolve + +const Helper = require('@codeceptjs/helper') +const { includes: stringIncludes } = require('../assert/include') +const { urlEquals } = require('../assert/equal') +const { equals } = require('../assert/equal') +const { empty } = require('../assert/empty') +const { truth } = require('../assert/truth') +const Locator = require('../locator') +const ElementNotFound = require('./errors/ElementNotFound') +const { xpathLocator, fileExists, screenshotOutputFolder, toCamelCase } = require('../utils') const specialKeys = { Backspace: '\u0008', Enter: '\u000d', Delete: '\u007f', -}; +} -let withinStatus = false; +let withinStatus = false /** * Nightmare helper wraps [Nightmare](https://github.com/segmentio/nightmare) library to provide @@ -54,12 +49,12 @@ let withinStatus = false; */ class Nightmare extends Helper { constructor(config) { - super(config); + super(config) - this.isRunning = false; + this.isRunning = false // override defaults with config - this._setConfig(config); + this._setConfig(config) } _validateConfig(config) { @@ -75,235 +70,268 @@ class Nightmare extends Helper { keepCookies: false, js_errors: null, enableHAR: false, - }; + } - return Object.assign(defaults, config); + return Object.assign(defaults, config) } static _config() { return [ { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' }, { - name: 'show', message: 'Show browser window', default: true, type: 'confirm', + name: 'show', + message: 'Show browser window', + default: true, + type: 'confirm', }, - ]; + ] } static _checkRequirements() { try { - require('nightmare'); + require('nightmare') } catch (e) { - return ['nightmare']; + return ['nightmare'] } } async _init() { - this.Nightmare = require('nightmare'); + this.Nightmare = require('nightmare') if (this.options.enableHAR) { - require('nightmare-har-plugin').install(this.Nightmare); + require('nightmare-har-plugin').install(this.Nightmare) } this.Nightmare.action('findElements', function (locator, contextEl, done) { if (!done) { - done = contextEl; - contextEl = null; + done = contextEl + contextEl = null } - const by = Object.keys(locator)[0]; - const value = locator[by]; + const by = Object.keys(locator)[0] + const value = locator[by] - this.evaluate_now((by, locator, contextEl) => window.codeceptjs.findAndStoreElements(by, locator, contextEl), done, by, value, contextEl); - }); + this.evaluate_now( + (by, locator, contextEl) => window.codeceptjs.findAndStoreElements(by, locator, contextEl), + done, + by, + value, + contextEl, + ) + }) this.Nightmare.action('findElement', function (locator, contextEl, done) { if (!done) { - done = contextEl; - contextEl = null; + done = contextEl + contextEl = null } - const by = Object.keys(locator)[0]; - const value = locator[by]; + const by = Object.keys(locator)[0] + const value = locator[by] - this.evaluate_now((by, locator, contextEl) => { - const res = window.codeceptjs.findAndStoreElement(by, locator, contextEl); - if (res === null) { - throw new Error(`Element ${(new Locator(locator))} couldn't be located by ${by}`); - } - return res; - }, done, by, value, contextEl); - }); + this.evaluate_now( + (by, locator, contextEl) => { + const res = window.codeceptjs.findAndStoreElement(by, locator, contextEl) + if (res === null) { + throw new Error(`Element ${new Locator(locator)} couldn't be located by ${by}`) + } + return res + }, + done, + by, + value, + contextEl, + ) + }) this.Nightmare.action('asyncScript', function () { - let args = Array.prototype.slice.call(arguments); - const done = args.pop(); - args = args.splice(1, 0, done); - this.evaluate_now.apply(this, args); - }); + let args = Array.prototype.slice.call(arguments) + const done = args.pop() + args = args.splice(1, 0, done) + this.evaluate_now.apply(this, args) + }) this.Nightmare.action('enterText', function (el, text, clean, done) { - const child = this.child; - const typeFn = () => child.call('type', text, done); - - this.evaluate_now((el, clean) => { - const element = window.codeceptjs.fetchElement(el); - if (clean) element.value = ''; - element.focus(); - }, () => { - if (clean) return typeFn(); - child.call('pressKey', 'End', typeFn); // type End before - }, el, clean); - }); - - this.Nightmare.action('pressKey', (ns, options, parent, win, renderer, done) => { - parent.respondTo('pressKey', (ch, done) => { - win.webContents.sendInputEvent({ - type: 'keyDown', - keyCode: ch, - }); - - win.webContents.sendInputEvent({ - type: 'char', - keyCode: ch, - }); - - win.webContents.sendInputEvent({ - type: 'keyUp', - keyCode: ch, - }); - done(); - }); - done(); - }, function (key, done) { - this.child.call('pressKey', key, done); - }); - - this.Nightmare.action('triggerMouseEvent', (ns, options, parent, win, renderer, done) => { - parent.respondTo('triggerMouseEvent', (evt, done) => { - win.webContents.sendInputEvent(evt); - done(); - }); - done(); - }, function (event, done) { - this.child.call('triggerMouseEvent', event, done); - }); + const child = this.child + const typeFn = () => child.call('type', text, done) + + this.evaluate_now( + (el, clean) => { + const element = window.codeceptjs.fetchElement(el) + if (clean) element.value = '' + element.focus() + }, + () => { + if (clean) return typeFn() + child.call('pressKey', 'End', typeFn) // type End before + }, + el, + clean, + ) + }) + + this.Nightmare.action( + 'pressKey', + (ns, options, parent, win, renderer, done) => { + parent.respondTo('pressKey', (ch, done) => { + win.webContents.sendInputEvent({ + type: 'keyDown', + keyCode: ch, + }) + + win.webContents.sendInputEvent({ + type: 'char', + keyCode: ch, + }) + + win.webContents.sendInputEvent({ + type: 'keyUp', + keyCode: ch, + }) + done() + }) + done() + }, + function (key, done) { + this.child.call('pressKey', key, done) + }, + ) + + this.Nightmare.action( + 'triggerMouseEvent', + (ns, options, parent, win, renderer, done) => { + parent.respondTo('triggerMouseEvent', (evt, done) => { + win.webContents.sendInputEvent(evt) + done() + }) + done() + }, + function (event, done) { + this.child.call('triggerMouseEvent', event, done) + }, + ) this.Nightmare.action( 'upload', (ns, options, parent, win, renderer, done) => { parent.respondTo('upload', (selector, pathsToUpload, done) => { - parent.emit('log', 'paths', pathsToUpload); + parent.emit('log', 'paths', pathsToUpload) try { - // attach the debugger - // NOTE: this will fail if devtools is open - win.webContents.debugger.attach('1.1'); + // attach the debugger + // NOTE: this will fail if devtools is open + win.webContents.debugger.attach('1.1') } catch (e) { - parent.emit('log', 'problem attaching', e); - return done(e); + parent.emit('log', 'problem attaching', e) + return done(e) } win.webContents.debugger.sendCommand('DOM.getDocument', {}, (err, domDocument) => { - win.webContents.debugger.sendCommand('DOM.querySelector', { - nodeId: domDocument.root.nodeId, - selector, - }, (err, queryResult) => { - // HACK: chromium errors appear to be unpopulated objects? - if (Object.keys(err) - .length > 0) { - parent.emit('log', 'problem selecting', err); - return done(err); - } - win.webContents.debugger.sendCommand('DOM.setFileInputFiles', { - nodeId: queryResult.nodeId, - files: pathsToUpload, - }, (err) => { - if (Object.keys(err) - .length > 0) { - parent.emit('log', 'problem setting input', err); - return done(err); + win.webContents.debugger.sendCommand( + 'DOM.querySelector', + { + nodeId: domDocument.root.nodeId, + selector, + }, + (err, queryResult) => { + // HACK: chromium errors appear to be unpopulated objects? + if (Object.keys(err).length > 0) { + parent.emit('log', 'problem selecting', err) + return done(err) } - win.webContents.debugger.detach(); - done(null, pathsToUpload); - }); - }); - }); - }); - done(); + win.webContents.debugger.sendCommand( + 'DOM.setFileInputFiles', + { + nodeId: queryResult.nodeId, + files: pathsToUpload, + }, + (err) => { + if (Object.keys(err).length > 0) { + parent.emit('log', 'problem setting input', err) + return done(err) + } + win.webContents.debugger.detach() + done(null, pathsToUpload) + }, + ) + }, + ) + }) + }) + done() }, function (selector, pathsToUpload, done) { if (!Array.isArray(pathsToUpload)) { - pathsToUpload = [pathsToUpload]; + pathsToUpload = [pathsToUpload] } this.child.call('upload', selector, pathsToUpload, (err, stuff) => { - done(err, stuff); - }); + done(err, stuff) + }) }, - ); + ) - return Promise.resolve(); + return Promise.resolve() } async _beforeSuite() { if (!this.options.restart && !this.isRunning) { - this.debugSection('Session', 'Starting singleton browser session'); - return this._startBrowser(); + this.debugSection('Session', 'Starting singleton browser session') + return this._startBrowser() } } async _before() { - if (this.options.restart) return this._startBrowser(); - if (!this.isRunning) return this._startBrowser(); - return this.browser; + if (this.options.restart) return this._startBrowser() + if (!this.isRunning) return this._startBrowser() + return this.browser } async _after() { - if (!this.isRunning) return; + if (!this.isRunning) return if (this.options.restart) { - this.isRunning = false; - return this._stopBrowser(); + this.isRunning = false + return this._stopBrowser() } if (this.options.enableHAR) { - await this.browser.resetHAR(); + await this.browser.resetHAR() } - if (this.options.keepBrowserState) return; + if (this.options.keepBrowserState) return if (this.options.keepCookies) { - await this.browser.cookies.clearAll(); + await this.browser.cookies.clearAll() } - this.debugSection('Session', 'cleaning up'); - return this.executeScript(() => localStorage.clear()); + this.debugSection('Session', 'cleaning up') + return this.executeScript(() => localStorage.clear()) } - _afterSuite() { - } + _afterSuite() {} _finishTest() { if (!this.options.restart && this.isRunning) { - this._stopBrowser(); + this._stopBrowser() } } async _startBrowser() { - this.context = this.options.rootElement; + this.context = this.options.rootElement if (this.options.enableHAR) { - this.browser = this.Nightmare(Object.assign(require('nightmare-har-plugin').getDevtoolsOptions(), this.options)); - await this.browser; - await this.browser.waitForDevtools(); + this.browser = this.Nightmare(Object.assign(require('nightmare-har-plugin').getDevtoolsOptions(), this.options)) + await this.browser + await this.browser.waitForDevtools() } else { - this.browser = this.Nightmare(this.options); - await this.browser; + this.browser = this.Nightmare(this.options) + await this.browser } - await this.browser.goto('about:blank'); // Load a blank page so .saveScreenshot (/evaluate) will work - this.isRunning = true; - this.browser.on('dom-ready', () => this._injectClientScripts()); - this.browser.on('did-start-loading', () => this._injectClientScripts()); - this.browser.on('will-navigate', () => this._injectClientScripts()); + await this.browser.goto('about:blank') // Load a blank page so .saveScreenshot (/evaluate) will work + this.isRunning = true + this.browser.on('dom-ready', () => this._injectClientScripts()) + this.browser.on('did-start-loading', () => this._injectClientScripts()) + this.browser.on('will-navigate', () => this._injectClientScripts()) this.browser.on('console', (type, message) => { - this.debug(`${type}: ${message}`); - }); + this.debug(`${type}: ${message}`) + }) if (this.options.windowSize) { - const size = this.options.windowSize.split('x'); - return this.browser.viewport(parseInt(size[0], 10), parseInt(size[1], 10)); + const size = this.options.windowSize.split('x') + return this.browser.viewport(parseInt(size[0], 10), parseInt(size[1], 10)) } } @@ -316,45 +344,49 @@ class Nightmare extends Helper { * ``` */ async grabHAR() { - return this.browser.getHAR(); + return this.browser.getHAR() } async saveHAR(fileName) { - const outputFile = path.join(global.output_dir, fileName); - this.debug(`HAR is saving to ${outputFile}`); + const outputFile = path.join(global.output_dir, fileName) + this.debug(`HAR is saving to ${outputFile}`) await this.browser.getHAR().then((har) => { - require('fs').writeFileSync(outputFile, JSON.stringify({ log: har })); - }); + require('fs').writeFileSync(outputFile, JSON.stringify({ log: har })) + }) } async resetHAR() { - await this.browser.resetHAR(); + await this.browser.resetHAR() } async _stopBrowser() { return this.browser.end().catch((error) => { - this.debugSection('Error on End', error); - }); + this.debugSection('Error on End', error) + }) } async _withinBegin(locator) { - this.context = locator; - locator = new Locator(locator, 'css'); - withinStatus = true; - return this.browser.evaluate((by, locator) => { - const el = window.codeceptjs.findElement(by, locator); - if (!el) throw new Error(`Element by ${by}: ${locator} not found`); - window.codeceptjs.within = el; - }, locator.type, locator.value); + this.context = locator + locator = new Locator(locator, 'css') + withinStatus = true + return this.browser.evaluate( + (by, locator) => { + const el = window.codeceptjs.findElement(by, locator) + if (!el) throw new Error(`Element by ${by}: ${locator} not found`) + window.codeceptjs.within = el + }, + locator.type, + locator.value, + ) } _withinEnd() { - this.context = this.options.rootElement; - withinStatus = false; + this.context = this.options.rootElement + withinStatus = false return this.browser.evaluate(() => { - window.codeceptjs.within = null; - }); + window.codeceptjs.within = null + }) } /** @@ -377,10 +409,14 @@ class Nightmare extends Helper { * ``` */ _locate(locator) { - locator = new Locator(locator, 'css'); - return this.browser.evaluate((by, locator) => { - return window.codeceptjs.findAndStoreElements(by, locator); - }, locator.type, locator.value); + locator = new Locator(locator, 'css') + return this.browser.evaluate( + (by, locator) => { + return window.codeceptjs.findAndStoreElements(by, locator) + }, + locator.type, + locator.value, + ) } /** @@ -392,7 +428,7 @@ class Nightmare extends Helper { * ``` */ haveHeader(header, value) { - return this.browser.header(header, value); + return this.browser.header(header, value) } /** @@ -401,223 +437,237 @@ class Nightmare extends Helper { * */ async amOnPage(url, headers = null) { - if (!(/^\w+\:\/\//.test(url))) { - url = urlResolve(this.options.url, url); + if (!/^\w+\:\/\//.test(url)) { + url = urlResolve(this.options.url, url) } - const currentUrl = await this.browser.url(); + const currentUrl = await this.browser.url() if (url === currentUrl) { // navigating to the same url will cause an error in nightmare, so don't do it - return; + return } return this.browser.goto(url, headers).then((res) => { - this.debugSection('URL', res.url); - this.debugSection('Code', res.code); - this.debugSection('Headers', JSON.stringify(res.headers)); - }); + this.debugSection('URL', res.url) + this.debugSection('Code', res.code) + this.debugSection('Headers', JSON.stringify(res.headers)) + }) } /** * {{> seeInTitle }} */ async seeInTitle(text) { - const title = await this.browser.title(); - stringIncludes('web page title').assert(text, title); + const title = await this.browser.title() + stringIncludes('web page title').assert(text, title) } /** * {{> dontSeeInTitle }} */ async dontSeeInTitle(text) { - const title = await this.browser.title(); - stringIncludes('web page title').negate(text, title); + const title = await this.browser.title() + stringIncludes('web page title').negate(text, title) } /** * {{> grabTitle }} */ async grabTitle() { - return this.browser.title(); + return this.browser.title() } /** * {{> grabCurrentUrl }} */ async grabCurrentUrl() { - return this.browser.url(); + return this.browser.url() } /** * {{> seeInCurrentUrl }} */ async seeInCurrentUrl(url) { - const currentUrl = await this.browser.url(); - stringIncludes('url').assert(url, currentUrl); + const currentUrl = await this.browser.url() + stringIncludes('url').assert(url, currentUrl) } /** * {{> dontSeeInCurrentUrl }} */ async dontSeeInCurrentUrl(url) { - const currentUrl = await this.browser.url(); - stringIncludes('url').negate(url, currentUrl); + const currentUrl = await this.browser.url() + stringIncludes('url').negate(url, currentUrl) } /** * {{> seeCurrentUrlEquals }} */ async seeCurrentUrlEquals(url) { - const currentUrl = await this.browser.url(); - urlEquals(this.options.url).assert(url, currentUrl); + const currentUrl = await this.browser.url() + urlEquals(this.options.url).assert(url, currentUrl) } /** * {{> dontSeeCurrentUrlEquals }} */ async dontSeeCurrentUrlEquals(url) { - const currentUrl = await this.browser.url(); - urlEquals(this.options.url).negate(url, currentUrl); + const currentUrl = await this.browser.url() + urlEquals(this.options.url).negate(url, currentUrl) } /** * {{> see }} */ async see(text, context = null) { - return proceedSee.call(this, 'assert', text, context); + return proceedSee.call(this, 'assert', text, context) } /** * {{> dontSee }} */ dontSee(text, context = null) { - return proceedSee.call(this, 'negate', text, context); + return proceedSee.call(this, 'negate', text, context) } /** * {{> seeElement }} */ async seeElement(locator) { - locator = new Locator(locator, 'css'); - const num = await this.browser.evaluate((by, locator) => { - return window.codeceptjs.findElements(by, locator).filter(e => e.offsetWidth > 0 && e.offsetHeight > 0).length; - }, locator.type, locator.value); - equals('number of elements on a page').negate(0, num); + locator = new Locator(locator, 'css') + const num = await this.browser.evaluate( + (by, locator) => { + return window.codeceptjs.findElements(by, locator).filter((e) => e.offsetWidth > 0 && e.offsetHeight > 0).length + }, + locator.type, + locator.value, + ) + equals('number of elements on a page').negate(0, num) } /** * {{> dontSeeElement }} */ async dontSeeElement(locator) { - locator = new Locator(locator, 'css'); - locator = new Locator(locator, 'css'); - const num = await this.browser.evaluate((by, locator) => { - return window.codeceptjs.findElements(by, locator).filter(e => e.offsetWidth > 0 && e.offsetHeight > 0).length; - }, locator.type, locator.value); - equals('number of elements on a page').assert(0, num); + locator = new Locator(locator, 'css') + locator = new Locator(locator, 'css') + const num = await this.browser.evaluate( + (by, locator) => { + return window.codeceptjs.findElements(by, locator).filter((e) => e.offsetWidth > 0 && e.offsetHeight > 0).length + }, + locator.type, + locator.value, + ) + equals('number of elements on a page').assert(0, num) } /** * {{> seeElementInDOM }} */ async seeElementInDOM(locator) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElements(locator.toStrict()); - empty('elements').negate(els.fill('ELEMENT')); + locator = new Locator(locator, 'css') + const els = await this.browser.findElements(locator.toStrict()) + empty('elements').negate(els.fill('ELEMENT')) } /** * {{> dontSeeElementInDOM }} */ async dontSeeElementInDOM(locator) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElements(locator.toStrict()); - empty('elements').assert(els.fill('ELEMENT')); + locator = new Locator(locator, 'css') + const els = await this.browser.findElements(locator.toStrict()) + empty('elements').assert(els.fill('ELEMENT')) } /** * {{> seeInSource }} */ async seeInSource(text) { - const source = await this.browser.evaluate(() => document.documentElement.outerHTML); - stringIncludes('HTML source of a page').assert(text, source); + const source = await this.browser.evaluate(() => document.documentElement.outerHTML) + stringIncludes('HTML source of a page').assert(text, source) } /** * {{> dontSeeInSource }} */ async dontSeeInSource(text) { - const source = await this.browser.evaluate(() => document.documentElement.outerHTML); - stringIncludes('HTML source of a page').negate(text, source); + const source = await this.browser.evaluate(() => document.documentElement.outerHTML) + stringIncludes('HTML source of a page').negate(text, source) } /** * {{> seeNumberOfElements }} */ async seeNumberOfElements(locator, num) { - const elements = await this._locate(locator); - return equals(`expected number of elements (${(new Locator(locator))}) is ${num}, but found ${elements.length}`).assert(elements.length, num); + const elements = await this._locate(locator) + return equals( + `expected number of elements (${new Locator(locator)}) is ${num}, but found ${elements.length}`, + ).assert(elements.length, num) } /** * {{> seeNumberOfVisibleElements }} */ async seeNumberOfVisibleElements(locator, num) { - const res = await this.grabNumberOfVisibleElements(locator); - return equals(`expected number of visible elements (${(new Locator(locator))}) is ${num}, but found ${res}`).assert(res, num); + const res = await this.grabNumberOfVisibleElements(locator) + return equals(`expected number of visible elements (${new Locator(locator)}) is ${num}, but found ${res}`).assert( + res, + num, + ) } /** * {{> grabNumberOfVisibleElements }} */ async grabNumberOfVisibleElements(locator) { - locator = new Locator(locator, 'css'); + locator = new Locator(locator, 'css') - const num = await this.browser.evaluate((by, locator) => { - return window.codeceptjs.findElements(by, locator) - .filter(e => e.offsetWidth > 0 && e.offsetHeight > 0).length; - }, locator.type, locator.value); + const num = await this.browser.evaluate( + (by, locator) => { + return window.codeceptjs.findElements(by, locator).filter((e) => e.offsetWidth > 0 && e.offsetHeight > 0).length + }, + locator.type, + locator.value, + ) - return num; + return num } /** * {{> click }} */ async click(locator, context = null) { - const el = await findClickable.call(this, locator, context); - assertElementExists(el, locator, 'Clickable'); - return this.browser.evaluate(el => window.codeceptjs.clickEl(el), el) - .wait(this.options.waitForAction); + const el = await findClickable.call(this, locator, context) + assertElementExists(el, locator, 'Clickable') + return this.browser.evaluate((el) => window.codeceptjs.clickEl(el), el).wait(this.options.waitForAction) } /** * {{> doubleClick }} */ async doubleClick(locator, context = null) { - const el = await findClickable.call(this, locator, context); - assertElementExists(el, locator, 'Clickable'); - return this.browser.evaluate(el => window.codeceptjs.doubleClickEl(el), el) - .wait(this.options.waitForAction); + const el = await findClickable.call(this, locator, context) + assertElementExists(el, locator, 'Clickable') + return this.browser.evaluate((el) => window.codeceptjs.doubleClickEl(el), el).wait(this.options.waitForAction) } /** * {{> rightClick }} */ async rightClick(locator, context = null) { - const el = await findClickable.call(this, locator, context); - assertElementExists(el, locator, 'Clickable'); - return this.browser.evaluate(el => window.codeceptjs.rightClickEl(el), el) - .wait(this.options.waitForAction); + const el = await findClickable.call(this, locator, context) + assertElementExists(el, locator, 'Clickable') + return this.browser.evaluate((el) => window.codeceptjs.rightClickEl(el), el).wait(this.options.waitForAction) } /** * {{> moveCursorTo }} */ async moveCursorTo(locator, offsetX = 0, offsetY = 0) { - locator = new Locator(locator, 'css'); - const el = await this.browser.findElement(locator.toStrict()); - assertElementExists(el, locator); - return this.browser.evaluate((el, x, y) => window.codeceptjs.hoverEl(el, x, y), el, offsetX, offsetY) - .wait(this.options.waitForAction); // wait for hover event to happen + locator = new Locator(locator, 'css') + const el = await this.browser.findElement(locator.toStrict()) + assertElementExists(el, locator) + return this.browser + .evaluate((el, x, y) => window.codeceptjs.hoverEl(el, x, y), el, offsetX, offsetY) + .wait(this.options.waitForAction) // wait for hover event to happen } /** @@ -626,8 +676,7 @@ class Nightmare extends Helper { * Wrapper for synchronous [evaluate](https://github.com/segmentio/nightmare#evaluatefn-arg1-arg2) */ async executeScript(...args) { - return this.browser.evaluate.apply(this.browser, args) - .catch(err => err); // Nightmare's first argument is error :( + return this.browser.evaluate.apply(this.browser, args).catch((err) => err) // Nightmare's first argument is error :( } /** @@ -637,8 +686,7 @@ class Nightmare extends Helper { * Unlike NightmareJS implementation calling `done` will return its first argument. */ async executeAsyncScript(...args) { - return this.browser.evaluate.apply(this.browser, args) - .catch(err => err); // Nightmare's first argument is error :( + return this.browser.evaluate.apply(this.browser, args).catch((err) => err) // Nightmare's first argument is error :( } /** @@ -646,72 +694,68 @@ class Nightmare extends Helper { */ async resizeWindow(width, height) { if (width === 'maximize') { - throw new Error('Nightmare doesn\'t support resizeWindow to maximum!'); + throw new Error("Nightmare doesn't support resizeWindow to maximum!") } - return this.browser.viewport(width, height).wait(this.options.waitForAction); + return this.browser.viewport(width, height).wait(this.options.waitForAction) } /** * {{> checkOption }} */ async checkOption(field, context = null) { - const els = await findCheckable.call(this, field, context); - assertElementExists(els[0], field, 'Checkbox or radio'); - return this.browser.evaluate(els => window.codeceptjs.checkEl(els[0]), els) - .wait(this.options.waitForAction); + const els = await findCheckable.call(this, field, context) + assertElementExists(els[0], field, 'Checkbox or radio') + return this.browser.evaluate((els) => window.codeceptjs.checkEl(els[0]), els).wait(this.options.waitForAction) } /** * {{> uncheckOption }} */ async uncheckOption(field, context = null) { - const els = await findCheckable.call(this, field, context); - assertElementExists(els[0], field, 'Checkbox or radio'); - return this.browser.evaluate(els => window.codeceptjs.unCheckEl(els[0]), els) - .wait(this.options.waitForAction); + const els = await findCheckable.call(this, field, context) + assertElementExists(els[0], field, 'Checkbox or radio') + return this.browser.evaluate((els) => window.codeceptjs.unCheckEl(els[0]), els).wait(this.options.waitForAction) } /** * {{> fillField }} */ async fillField(field, value) { - const el = await findField.call(this, field); - assertElementExists(el, field, 'Field'); - return this.browser.enterText(el, value.toString(), true) - .wait(this.options.waitForAction); + const el = await findField.call(this, field) + assertElementExists(el, field, 'Field') + return this.browser.enterText(el, value.toString(), true).wait(this.options.waitForAction) } /** * {{> clearField }} */ async clearField(field) { - return this.fillField(field, ''); + return this.fillField(field, '') } /** * {{> appendField }} */ async appendField(field, value) { - const el = await findField.call(this, field); - assertElementExists(el, field, 'Field'); - return this.browser.enterText(el, value.toString(), false) - .wait(this.options.waitForAction); + const el = await findField.call(this, field) + assertElementExists(el, field, 'Field') + return this.browser.enterText(el, value.toString(), false).wait(this.options.waitForAction) } /** * {{> seeInField }} */ async seeInField(field, value) { - const _value = (typeof value === 'boolean') ? value : value.toString(); - return proceedSeeInField.call(this, 'assert', field, _value); + const _value = typeof value === 'boolean' ? value : value.toString() + return proceedSeeInField.call(this, 'assert', field, _value) } /** * {{> dontSeeInField }} */ async dontSeeInField(field, value) { - const _value = (typeof value === 'boolean') ? value : value.toString(); - return proceedSeeInField.call(this, 'negate', field, _value); + const _value = typeof value === 'boolean' ? value : value.toString() + return proceedSeeInField.call(this, 'negate', field, _value) } /** @@ -720,12 +764,12 @@ class Nightmare extends Helper { */ async pressKey(key) { if (Array.isArray(key)) { - key = key.join('+'); // should work with accelerators... + key = key.join('+') // should work with accelerators... } if (Object.keys(specialKeys).indexOf(key) >= 0) { - key = specialKeys[key]; + key = specialKeys[key] } - return this.browser.pressKey(key).wait(this.options.waitForAction); + return this.browser.pressKey(key).wait(this.options.waitForAction) } /** @@ -739,21 +783,21 @@ class Nightmare extends Helper { } */ async triggerMouseEvent(event) { - return this.browser.triggerMouseEvent(event).wait(this.options.waitForAction); + return this.browser.triggerMouseEvent(event).wait(this.options.waitForAction) } /** * {{> seeCheckboxIsChecked }} */ async seeCheckboxIsChecked(field) { - return proceedIsChecked.call(this, 'assert', field); + return proceedIsChecked.call(this, 'assert', field) } /** * {{> dontSeeCheckboxIsChecked }} */ async dontSeeCheckboxIsChecked(field) { - return proceedIsChecked.call(this, 'negate', field); + return proceedIsChecked.call(this, 'negate', field) } /** @@ -762,167 +806,171 @@ class Nightmare extends Helper { * Doesn't work if the Chromium DevTools panel is open (as Chromium allows only one attachment to the debugger at a time. [See more](https://github.com/rosshinkley/nightmare-upload#important-note-about-setting-file-upload-inputs)) */ async attachFile(locator, pathToFile) { - const file = path.join(global.codecept_dir, pathToFile); + const file = path.join(global.codecept_dir, pathToFile) - locator = new Locator(locator, 'css'); + locator = new Locator(locator, 'css') if (!locator.isCSS()) { - throw new Error('Only CSS locator allowed for attachFile in Nightmare helper'); + throw new Error('Only CSS locator allowed for attachFile in Nightmare helper') } if (!fileExists(file)) { - throw new Error(`File at ${file} can not be found on local system`); + throw new Error(`File at ${file} can not be found on local system`) } - return this.browser.upload(locator.value, file); + return this.browser.upload(locator.value, file) } /** * {{> grabTextFromAll }} */ async grabTextFromAll(locator) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElements(locator.toStrict()); - const texts = []; - const getText = el => window.codeceptjs.fetchElement(el).innerText; + locator = new Locator(locator, 'css') + const els = await this.browser.findElements(locator.toStrict()) + const texts = [] + const getText = (el) => window.codeceptjs.fetchElement(el).innerText for (const el of els) { - texts.push(await this.browser.evaluate(getText, el)); + texts.push(await this.browser.evaluate(getText, el)) } - return texts; + return texts } /** * {{> grabTextFrom }} */ async grabTextFrom(locator) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElement(locator.toStrict()); - assertElementExists(els, locator); - const texts = await this.grabTextFromAll(locator); + locator = new Locator(locator, 'css') + const els = await this.browser.findElement(locator.toStrict()) + assertElementExists(els, locator) + const texts = await this.grabTextFromAll(locator) if (texts.length > 1) { - this.debugSection('GrabText', `Using first element out of ${texts.length}`); + this.debugSection('GrabText', `Using first element out of ${texts.length}`) } - return texts[0]; + return texts[0] } /** * {{> grabValueFromAll }} */ async grabValueFromAll(locator) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElements(locator.toStrict()); - const values = []; - const getValues = el => window.codeceptjs.fetchElement(el).value; + locator = new Locator(locator, 'css') + const els = await this.browser.findElements(locator.toStrict()) + const values = [] + const getValues = (el) => window.codeceptjs.fetchElement(el).value for (const el of els) { - values.push(await this.browser.evaluate(getValues, el)); + values.push(await this.browser.evaluate(getValues, el)) } - return values; + return values } /** * {{> grabValueFrom }} */ async grabValueFrom(locator) { - const el = await findField.call(this, locator); - assertElementExists(el, locator, 'Field'); - const values = await this.grabValueFromAll(locator); + const el = await findField.call(this, locator) + assertElementExists(el, locator, 'Field') + const values = await this.grabValueFromAll(locator) if (values.length > 1) { - this.debugSection('GrabValue', `Using first element out of ${values.length}`); + this.debugSection('GrabValue', `Using first element out of ${values.length}`) } - return values[0]; + return values[0] } /** * {{> grabAttributeFromAll }} */ async grabAttributeFromAll(locator, attr) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElements(locator.toStrict()); - const array = []; + locator = new Locator(locator, 'css') + const els = await this.browser.findElements(locator.toStrict()) + const array = [] for (let index = 0; index < els.length; index++) { - const el = els[index]; - array.push(await this.browser.evaluate((el, attr) => window.codeceptjs.fetchElement(el).getAttribute(attr), el, attr)); + const el = els[index] + array.push( + await this.browser.evaluate((el, attr) => window.codeceptjs.fetchElement(el).getAttribute(attr), el, attr), + ) } - return array; + return array } /** * {{> grabAttributeFrom }} */ async grabAttributeFrom(locator, attr) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElement(locator.toStrict()); - assertElementExists(els, locator); + locator = new Locator(locator, 'css') + const els = await this.browser.findElement(locator.toStrict()) + assertElementExists(els, locator) - const attrs = await this.grabAttributeFromAll(locator, attr); + const attrs = await this.grabAttributeFromAll(locator, attr) if (attrs.length > 1) { - this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`); + this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`) } - return attrs[0]; + return attrs[0] } /** * {{> grabHTMLFromAll }} */ async grabHTMLFromAll(locator) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElements(locator.toStrict()); - const array = []; + locator = new Locator(locator, 'css') + const els = await this.browser.findElements(locator.toStrict()) + const array = [] for (let index = 0; index < els.length; index++) { - const el = els[index]; - array.push(await this.browser.evaluate(el => window.codeceptjs.fetchElement(el).innerHTML, el)); + const el = els[index] + array.push(await this.browser.evaluate((el) => window.codeceptjs.fetchElement(el).innerHTML, el)) } - this.debugSection('GrabHTML', array); + this.debugSection('GrabHTML', array) - return array; + return array } /** * {{> grabHTMLFrom }} */ async grabHTMLFrom(locator) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElement(locator.toStrict()); - assertElementExists(els, locator); - const html = await this.grabHTMLFromAll(locator); + locator = new Locator(locator, 'css') + const els = await this.browser.findElement(locator.toStrict()) + assertElementExists(els, locator) + const html = await this.grabHTMLFromAll(locator) if (html.length > 1) { - this.debugSection('GrabHTML', `Using first element out of ${html.length}`); + this.debugSection('GrabHTML', `Using first element out of ${html.length}`) } - return html[0]; + return html[0] } /** * {{> grabCssPropertyFrom }} */ async grabCssPropertyFrom(locator, cssProperty) { - locator = new Locator(locator, 'css'); - const els = await this.browser.findElements(locator.toStrict()); - const array = []; + locator = new Locator(locator, 'css') + const els = await this.browser.findElements(locator.toStrict()) + const array = [] const getCssPropForElement = async (el, prop) => { - return (await this.browser.evaluate((el) => { - return window.getComputedStyle(window.codeceptjs.fetchElement(el)); - }, el))[toCamelCase(prop)]; - }; + return ( + await this.browser.evaluate((el) => { + return window.getComputedStyle(window.codeceptjs.fetchElement(el)) + }, el) + )[toCamelCase(prop)] + } for (const el of els) { - assertElementExists(el, locator); - const cssValue = await getCssPropForElement(el, cssProperty); - array.push(cssValue); + assertElementExists(el, locator) + const cssValue = await getCssPropForElement(el, cssProperty) + array.push(cssValue) } - this.debugSection('HTML', array); + this.debugSection('HTML', array) - return array.length > 1 ? array : array[0]; + return array.length > 1 ? array : array[0] } _injectClientScripts() { - return this.browser.inject('js', path.join(__dirname, 'clientscripts', 'nightmare.js')); + return this.browser.inject('js', path.join(__dirname, 'clientscripts', 'nightmare.js')) } /** @@ -930,41 +978,41 @@ class Nightmare extends Helper { */ async selectOption(select, option) { const fetchAndCheckOption = function (el, locator) { - el = window.codeceptjs.fetchElement(el); - const found = document.evaluate(locator, el, null, 5, null); - let current = null; - const items = []; - while (current = found.iterateNext()) { - items.push(current); + el = window.codeceptjs.fetchElement(el) + const found = document.evaluate(locator, el, null, 5, null) + let current = null + const items = [] + while ((current = found.iterateNext())) { + items.push(current) } for (let i = 0; i < items.length; i++) { - current = items[i]; + current = items[i] if (current instanceof HTMLOptionElement) { - current.selected = true; - if (!el.multiple) el.value = current.value; + current.selected = true + if (!el.multiple) el.value = current.value } - const event = document.createEvent('HTMLEvents'); - event.initEvent('change', true, true); - el.dispatchEvent(event); + const event = document.createEvent('HTMLEvents') + event.initEvent('change', true, true) + el.dispatchEvent(event) } - return !!current; - }; + return !!current + } - const el = await findField.call(this, select); - assertElementExists(el, select, 'Selectable field'); + const el = await findField.call(this, select) + assertElementExists(el, select, 'Selectable field') if (!Array.isArray(option)) { - option = [option]; + option = [option] } for (const key in option) { - const opt = xpathLocator.literal(option[key]); - const checked = await this.browser.evaluate(fetchAndCheckOption, el, Locator.select.byVisibleText(opt)); + const opt = xpathLocator.literal(option[key]) + const checked = await this.browser.evaluate(fetchAndCheckOption, el, Locator.select.byVisibleText(opt)) if (!checked) { - await this.browser.evaluate(fetchAndCheckOption, el, Locator.select.byValue(opt)); + await this.browser.evaluate(fetchAndCheckOption, el, Locator.select.byValue(opt)) } } - return this.browser.wait(this.options.waitForAction); + return this.browser.wait(this.options.waitForAction) } /** @@ -974,7 +1022,7 @@ class Nightmare extends Helper { * [See more](https://github.com/segmentio/nightmare/blob/master/Readme.md#cookiessetcookie) */ async setCookie(cookie) { - return this.browser.cookies.set(cookie); + return this.browser.cookies.set(cookie) } /** @@ -982,16 +1030,16 @@ class Nightmare extends Helper { * */ async seeCookie(name) { - const res = await this.browser.cookies.get(name); - truth(`cookie ${name}`, 'to be set').assert(res); + const res = await this.browser.cookies.get(name) + truth(`cookie ${name}`, 'to be set').assert(res) } /** * {{> dontSeeCookie }} */ async dontSeeCookie(name) { - const res = await this.browser.cookies.get(name); - truth(`cookie ${name}`, 'to be set').negate(res); + const res = await this.browser.cookies.get(name) + truth(`cookie ${name}`, 'to be set').negate(res) } /** @@ -1002,7 +1050,7 @@ class Nightmare extends Helper { * Multiple cookies can be received by passing query object `I.grabCookie({ secure: true});`. If you'd like get all cookies for all urls, use: `.grabCookie({ url: null }).` */ async grabCookie(name) { - return this.browser.cookies.get(name); + return this.browser.cookies.get(name) } /** @@ -1010,34 +1058,34 @@ class Nightmare extends Helper { */ async clearCookie(cookie) { if (!cookie) { - return this.browser.cookies.clearAll(); + return this.browser.cookies.clearAll() } - return this.browser.cookies.clear(cookie); + return this.browser.cookies.clear(cookie) } /** * {{> waitForFunction }} */ async waitForFunction(fn, argsOrSec = null, sec = null) { - let args = []; + let args = [] if (argsOrSec) { if (Array.isArray(argsOrSec)) { - args = argsOrSec; + args = argsOrSec } else if (typeof argsOrSec === 'number') { - sec = argsOrSec; + sec = argsOrSec } } - this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; - return this.browser.wait(fn, ...args); + this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + return this.browser.wait(fn, ...args) } /** * {{> wait }} */ async wait(sec) { - return new Promise(((done) => { - setTimeout(done, sec * 1000); - })); + return new Promise((done) => { + setTimeout(done, sec * 1000) + }) } /** @@ -1045,113 +1093,136 @@ class Nightmare extends Helper { */ async waitForText(text, sec, context = null) { if (!context) { - context = this.context; + context = this.context } - const locator = new Locator(context, 'css'); - this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; - return this.browser.wait((by, locator, text) => { - return window.codeceptjs.findElement(by, locator).innerText.indexOf(text) > -1; - }, locator.type, locator.value, text).catch((err) => { - if (err.message.indexOf('Cannot read property') > -1) { - throw new Error(`element (${JSON.stringify(context)}) is not in DOM. Unable to wait text.`); - } else if (err.message && err.message.indexOf('.wait() timed out after') > -1) { - throw new Error(`there is no element(${JSON.stringify(context)}) with text "${text}" after ${sec} sec`); - } else throw err; - }); + const locator = new Locator(context, 'css') + this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + return this.browser + .wait( + (by, locator, text) => { + return window.codeceptjs.findElement(by, locator).innerText.indexOf(text) > -1 + }, + locator.type, + locator.value, + text, + ) + .catch((err) => { + if (err.message.indexOf('Cannot read property') > -1) { + throw new Error(`element (${JSON.stringify(context)}) is not in DOM. Unable to wait text.`) + } else if (err.message && err.message.indexOf('.wait() timed out after') > -1) { + throw new Error(`there is no element(${JSON.stringify(context)}) with text "${text}" after ${sec} sec`) + } else throw err + }) } /** * {{> waitForVisible }} */ waitForVisible(locator, sec) { - this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; - locator = new Locator(locator, 'css'); - - return this.browser.wait((by, locator) => { - const el = window.codeceptjs.findElement(by, locator); - if (!el) return false; - return el.offsetWidth > 0 && el.offsetHeight > 0; - }, locator.type, locator.value).catch((err) => { - if (err.message && err.message.indexOf('.wait() timed out after') > -1) { - throw new Error(`element (${JSON.stringify(locator)}) still not visible on page after ${sec} sec`); - } else throw err; - }); + this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + locator = new Locator(locator, 'css') + + return this.browser + .wait( + (by, locator) => { + const el = window.codeceptjs.findElement(by, locator) + if (!el) return false + return el.offsetWidth > 0 && el.offsetHeight > 0 + }, + locator.type, + locator.value, + ) + .catch((err) => { + if (err.message && err.message.indexOf('.wait() timed out after') > -1) { + throw new Error(`element (${JSON.stringify(locator)}) still not visible on page after ${sec} sec`) + } else throw err + }) } /** * {{> waitToHide }} */ async waitToHide(locator, sec = null) { - return this.waitForInvisible(locator, sec); + return this.waitForInvisible(locator, sec) } /** * {{> waitForInvisible }} */ waitForInvisible(locator, sec) { - this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; - locator = new Locator(locator, 'css'); - - return this.browser.wait((by, locator) => { - const el = window.codeceptjs.findElement(by, locator); - if (!el) return true; - return !(el.offsetWidth > 0 && el.offsetHeight > 0); - }, locator.type, locator.value).catch((err) => { - if (err.message && err.message.indexOf('.wait() timed out after') > -1) { - throw new Error(`element (${JSON.stringify(locator)}) still visible after ${sec} sec`); - } else throw err; - }); + this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + locator = new Locator(locator, 'css') + + return this.browser + .wait( + (by, locator) => { + const el = window.codeceptjs.findElement(by, locator) + if (!el) return true + return !(el.offsetWidth > 0 && el.offsetHeight > 0) + }, + locator.type, + locator.value, + ) + .catch((err) => { + if (err.message && err.message.indexOf('.wait() timed out after') > -1) { + throw new Error(`element (${JSON.stringify(locator)}) still visible after ${sec} sec`) + } else throw err + }) } /** * {{> waitForElement }} */ async waitForElement(locator, sec) { - this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; - locator = new Locator(locator, 'css'); + this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + locator = new Locator(locator, 'css') - return this.browser.wait((by, locator) => window.codeceptjs.findElement(by, locator) !== null, locator.type, locator.value).catch((err) => { - if (err.message && err.message.indexOf('.wait() timed out after') > -1) { - throw new Error(`element (${JSON.stringify(locator)}) still not present on page after ${sec} sec`); - } else throw err; - }); + return this.browser + .wait((by, locator) => window.codeceptjs.findElement(by, locator) !== null, locator.type, locator.value) + .catch((err) => { + if (err.message && err.message.indexOf('.wait() timed out after') > -1) { + throw new Error(`element (${JSON.stringify(locator)}) still not present on page after ${sec} sec`) + } else throw err + }) } async waitUntilExists(locator, sec) { console.log(`waitUntilExists deprecated: * use 'waitForElement' to wait for element to be attached - * use 'waitForDetached to wait for element to be removed'`); - return this.waitForDetached(locator, sec); + * use 'waitForDetached to wait for element to be removed'`) + return this.waitForDetached(locator, sec) } /** * {{> waitForDetached }} */ async waitForDetached(locator, sec) { - this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; - sec = this.browser.options.waitForTimeout / 1000; - locator = new Locator(locator, 'css'); + this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + sec = this.browser.options.waitForTimeout / 1000 + locator = new Locator(locator, 'css') - return this.browser.wait((by, locator) => window.codeceptjs.findElement(by, locator) === null, locator.type, locator.value).catch((err) => { - if (err.message && err.message.indexOf('.wait() timed out after') > -1) { - throw new Error(`element (${JSON.stringify(locator)}) still on page after ${sec} sec`); - } else throw err; - }); + return this.browser + .wait((by, locator) => window.codeceptjs.findElement(by, locator) === null, locator.type, locator.value) + .catch((err) => { + if (err.message && err.message.indexOf('.wait() timed out after') > -1) { + throw new Error(`element (${JSON.stringify(locator)}) still on page after ${sec} sec`) + } else throw err + }) } /** * {{> refreshPage }} */ async refreshPage() { - return this.browser.refresh(); + return this.browser.refresh() } /** * Reload the page */ refresh() { - console.log('Deprecated in favor of refreshPage'); - return this.browser.refresh(); + console.log('Deprecated in favor of refreshPage') + return this.browser.refresh() } /** @@ -1159,70 +1230,74 @@ class Nightmare extends Helper { * */ async saveElementScreenshot(locator, fileName) { - const outputFile = screenshotOutputFolder(fileName); + const outputFile = screenshotOutputFolder(fileName) - const rect = await this.grabElementBoundingRect(locator); + const rect = await this.grabElementBoundingRect(locator) const button_clip = { x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.floor(rect.width), height: Math.floor(rect.height), - }; + } - this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`); + this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`) // take the screenshot - await this.browser.screenshot(outputFile, button_clip); + await this.browser.screenshot(outputFile, button_clip) } /** * {{> grabElementBoundingRect }} */ async grabElementBoundingRect(locator, prop) { - locator = new Locator(locator, 'css'); + locator = new Locator(locator, 'css') - const rect = await this.browser.evaluate(async (by, locator) => { - // store the button in a variable + const rect = await this.browser.evaluate( + async (by, locator) => { + // store the button in a variable - const build_cluster_btn = await window.codeceptjs.findElement(by, locator); + const build_cluster_btn = await window.codeceptjs.findElement(by, locator) - // use the getClientRects() function on the button to determine - // the size and location - const rect = build_cluster_btn.getBoundingClientRect(); + // use the getClientRects() function on the button to determine + // the size and location + const rect = build_cluster_btn.getBoundingClientRect() - // convert the rectangle to a clip object and return it - return { - x: rect.left, - y: rect.top, - width: rect.width, - height: rect.height, - }; - }, locator.type, locator.value); + // convert the rectangle to a clip object and return it + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + } + }, + locator.type, + locator.value, + ) - if (prop) return rect[prop]; - return rect; + if (prop) return rect[prop] + return rect } /** * {{> saveScreenshot }} */ async saveScreenshot(fileName, fullPage = this.options.fullPageScreenshots) { - const outputFile = screenshotOutputFolder(fileName); + const outputFile = screenshotOutputFolder(fileName) - this.debug(`Screenshot is saving to ${outputFile}`); + this.debug(`Screenshot is saving to ${outputFile}`) if (!fullPage) { - return this.browser.screenshot(outputFile); + return this.browser.screenshot(outputFile) } const { height, width } = await this.browser.evaluate(() => { - return { height: document.body.scrollHeight, width: document.body.scrollWidth }; - }); - await this.browser.viewport(width, height); - return this.browser.screenshot(outputFile); + return { height: document.body.scrollHeight, width: document.body.scrollWidth } + }) + await this.browser.viewport(width, height) + return this.browser.screenshot(outputFile) } async _failed() { - if (withinStatus !== false) await this._withinEnd(); + if (withinStatus !== false) await this._withinEnd() } /** @@ -1230,28 +1305,40 @@ class Nightmare extends Helper { */ async scrollTo(locator, offsetX = 0, offsetY = 0) { if (typeof locator === 'number' && typeof offsetX === 'number') { - offsetY = offsetX; - offsetX = locator; - locator = null; + offsetY = offsetX + offsetX = locator + locator = null } if (locator) { - locator = new Locator(locator, 'css'); - return this.browser.evaluate((by, locator, offsetX, offsetY) => { - const el = window.codeceptjs.findElement(by, locator); - if (!el) throw new Error(`Element not found ${by}: ${locator}`); - const rect = el.getBoundingClientRect(); - window.scrollTo(rect.left + offsetX, rect.top + offsetY); - }, locator.type, locator.value, offsetX, offsetY); + locator = new Locator(locator, 'css') + return this.browser.evaluate( + (by, locator, offsetX, offsetY) => { + const el = window.codeceptjs.findElement(by, locator) + if (!el) throw new Error(`Element not found ${by}: ${locator}`) + const rect = el.getBoundingClientRect() + window.scrollTo(rect.left + offsetX, rect.top + offsetY) + }, + locator.type, + locator.value, + offsetX, + offsetY, + ) } // eslint-disable-next-line prefer-arrow-callback - return this.executeScript(function (x, y) { return window.scrollTo(x, y); }, offsetX, offsetY); + return this.executeScript( + function (x, y) { + return window.scrollTo(x, y) + }, + offsetX, + offsetY, + ) } /** * {{> scrollPageToTop }} */ async scrollPageToTop() { - return this.executeScript(() => window.scrollTo(0, 0)); + return this.executeScript(() => window.scrollTo(0, 0)) } /** @@ -1260,16 +1347,13 @@ class Nightmare extends Helper { async scrollPageToBottom() { /* eslint-disable prefer-arrow-callback, comma-dangle */ return this.executeScript(function () { - const body = document.body; - const html = document.documentElement; - window.scrollTo(0, Math.max( - body.scrollHeight, - body.offsetHeight, - html.clientHeight, - html.scrollHeight, - html.offsetHeight - )); - }); + const body = document.body + const html = document.documentElement + window.scrollTo( + 0, + Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight), + ) + }) /* eslint-enable */ } @@ -1281,135 +1365,143 @@ class Nightmare extends Helper { function getScrollPosition() { return { x: window.pageXOffset, - y: window.pageYOffset - }; + y: window.pageYOffset, + } } /* eslint-enable comma-dangle */ - return this.executeScript(getScrollPosition); + return this.executeScript(getScrollPosition) } } -module.exports = Nightmare; +module.exports = Nightmare async function proceedSee(assertType, text, context) { - let description; - let locator; + let description + let locator if (!context) { if (this.context === this.options.rootElement) { - locator = new Locator(this.context, 'css'); - description = 'web application'; + locator = new Locator(this.context, 'css') + description = 'web application' } else { - description = `current context ${this.context}`; - locator = new Locator({ xpath: './/*' }); + description = `current context ${this.context}` + locator = new Locator({ xpath: './/*' }) } } else { - locator = new Locator(context, 'css'); - description = `element ${locator.toString()}`; - } - - const texts = await this.browser.evaluate((by, locator) => { - return window.codeceptjs.findElements(by, locator).map(el => el.innerText); - }, locator.type, locator.value); - const allText = texts.join(' | '); - return stringIncludes(description)[assertType](text, allText); + locator = new Locator(context, 'css') + description = `element ${locator.toString()}` + } + + const texts = await this.browser.evaluate( + (by, locator) => { + return window.codeceptjs.findElements(by, locator).map((el) => el.innerText) + }, + locator.type, + locator.value, + ) + const allText = texts.join(' | ') + return stringIncludes(description)[assertType](text, allText) } async function proceedSeeInField(assertType, field, value) { - const el = await findField.call(this, field); - assertElementExists(el, field, 'Field'); - const tag = await this.browser.evaluate(el => window.codeceptjs.fetchElement(el).tagName, el); - const fieldVal = await this.browser.evaluate(el => window.codeceptjs.fetchElement(el).value, el); + const el = await findField.call(this, field) + assertElementExists(el, field, 'Field') + const tag = await this.browser.evaluate((el) => window.codeceptjs.fetchElement(el).tagName, el) + const fieldVal = await this.browser.evaluate((el) => window.codeceptjs.fetchElement(el).value, el) if (tag === 'select') { // locate option by values and check them - const text = await this.browser.evaluate((el, val) => { - return el.querySelector(`option[value="${val}"]`).innerText; - }, el, xpathLocator.literal(fieldVal)); - return equals(`select option by ${field}`)[assertType](value, text); + const text = await this.browser.evaluate( + (el, val) => { + return el.querySelector(`option[value="${val}"]`).innerText + }, + el, + xpathLocator.literal(fieldVal), + ) + return equals(`select option by ${field}`)[assertType](value, text) } - return stringIncludes(`field by ${field}`)[assertType](value, fieldVal); + return stringIncludes(`field by ${field}`)[assertType](value, fieldVal) } async function proceedIsChecked(assertType, option) { - const els = await findCheckable.call(this, option); - assertElementExists(els, option, 'Checkable'); + const els = await findCheckable.call(this, option) + assertElementExists(els, option, 'Checkable') const selected = await this.browser.evaluate((els) => { - return els.map(el => window.codeceptjs.fetchElement(el).checked).reduce((prev, cur) => prev || cur); - }, els); - return truth(`checkable ${option}`, 'to be checked')[assertType](selected); + return els.map((el) => window.codeceptjs.fetchElement(el).checked).reduce((prev, cur) => prev || cur) + }, els) + return truth(`checkable ${option}`, 'to be checked')[assertType](selected) } async function findCheckable(locator, context) { - let contextEl = null; + let contextEl = null if (context) { - contextEl = await this.browser.findElement((new Locator(context, 'css')).toStrict()); + contextEl = await this.browser.findElement(new Locator(context, 'css').toStrict()) } - const matchedLocator = new Locator(locator); + const matchedLocator = new Locator(locator) if (!matchedLocator.isFuzzy()) { - return this.browser.findElements(matchedLocator.toStrict(), contextEl); + return this.browser.findElements(matchedLocator.toStrict(), contextEl) } - const literal = xpathLocator.literal(locator); - let els = await this.browser.findElements({ xpath: Locator.checkable.byText(literal) }, contextEl); + const literal = xpathLocator.literal(locator) + let els = await this.browser.findElements({ xpath: Locator.checkable.byText(literal) }, contextEl) if (els.length) { - return els; + return els } - els = await this.browser.findElements({ xpath: Locator.checkable.byName(literal) }, contextEl); + els = await this.browser.findElements({ xpath: Locator.checkable.byName(literal) }, contextEl) if (els.length) { - return els; + return els } - return this.browser.findElements({ css: locator }, contextEl); + return this.browser.findElements({ css: locator }, contextEl) } async function findClickable(locator, context) { - let contextEl = null; + let contextEl = null if (context) { - contextEl = await this.browser.findElement((new Locator(context, 'css')).toStrict()); + contextEl = await this.browser.findElement(new Locator(context, 'css').toStrict()) } - const matchedLocator = new Locator(locator); + const matchedLocator = new Locator(locator) if (!matchedLocator.isFuzzy()) { - return this.browser.findElement(matchedLocator.toStrict(), contextEl); + return this.browser.findElement(matchedLocator.toStrict(), contextEl) } - const literal = xpathLocator.literal(locator); + const literal = xpathLocator.literal(locator) - let els = await this.browser.findElements({ xpath: Locator.clickable.narrow(literal) }, contextEl); + let els = await this.browser.findElements({ xpath: Locator.clickable.narrow(literal) }, contextEl) if (els.length) { - return els[0]; + return els[0] } - els = await this.browser.findElements({ xpath: Locator.clickable.wide(literal) }, contextEl); + els = await this.browser.findElements({ xpath: Locator.clickable.wide(literal) }, contextEl) if (els.length) { - return els[0]; + return els[0] } - return this.browser.findElement({ css: locator }, contextEl); + return this.browser.findElement({ css: locator }, contextEl) } async function findField(locator) { - const matchedLocator = new Locator(locator); + const matchedLocator = new Locator(locator) if (!matchedLocator.isFuzzy()) { - return this.browser.findElements(matchedLocator.toStrict()); + return this.browser.findElements(matchedLocator.toStrict()) } - const literal = xpathLocator.literal(locator); + const literal = xpathLocator.literal(locator) - let els = await this.browser.findElements({ xpath: Locator.field.labelEquals(literal) }); + let els = await this.browser.findElements({ xpath: Locator.field.labelEquals(literal) }) if (els.length) { - return els[0]; + return els[0] } - els = await this.browser.findElements({ xpath: Locator.field.labelContains(literal) }); + els = await this.browser.findElements({ xpath: Locator.field.labelContains(literal) }) if (els.length) { - return els[0]; + return els[0] } - els = await this.browser.findElements({ xpath: Locator.field.byName(literal) }); + els = await this.browser.findElements({ xpath: Locator.field.byName(literal) }) if (els.length) { - return els[0]; + return els[0] } - return this.browser.findElement({ css: locator }); + return this.browser.findElement({ css: locator }) } function assertElementExists(el, locator, prefix, suffix) { - if (el === null) throw new ElementNotFound(locator, prefix, suffix); + if (el === null) throw new ElementNotFound(locator, prefix, suffix) } diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index d03e3af2b..0921fb1ad 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1,17 +1,17 @@ -const path = require('path'); -const fs = require('fs'); - -const Helper = require('@codeceptjs/helper'); -const { v4: uuidv4 } = require('uuid'); -const assert = require('assert'); -const promiseRetry = require('promise-retry'); -const Locator = require('../locator'); -const recorder = require('../recorder'); -const stringIncludes = require('../assert/include').includes; -const { urlEquals } = require('../assert/equal'); -const { equals } = require('../assert/equal'); -const { empty } = require('../assert/empty'); -const { truth } = require('../assert/truth'); +const path = require('path') +const fs = require('fs') + +const Helper = require('@codeceptjs/helper') +const { v4: uuidv4 } = require('uuid') +const assert = require('assert') +const promiseRetry = require('promise-retry') +const Locator = require('../locator') +const recorder = require('../recorder') +const stringIncludes = require('../assert/include').includes +const { urlEquals } = require('../assert/equal') +const { equals } = require('../assert/equal') +const { empty } = require('../assert/empty') +const { truth } = require('../assert/truth') const { xpathLocator, ucfirst, @@ -24,37 +24,44 @@ const { clearString, requireWithFallback, normalizeSpacesInString, -} = require('../utils'); -const { - isColorProperty, - convertColorToRGBA, -} = require('../colorUtils'); -const ElementNotFound = require('./errors/ElementNotFound'); -const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused'); -const Popup = require('./extras/Popup'); -const Console = require('./extras/Console'); -const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator'); - -let playwright; -let perfTiming; -let defaultSelectorEnginesInitialized = false; - -const popupStore = new Popup(); -const consoleLogStore = new Console(); -const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']; +} = require('../utils') +const { isColorProperty, convertColorToRGBA } = require('../colorUtils') +const ElementNotFound = require('./errors/ElementNotFound') +const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused') +const Popup = require('./extras/Popup') +const Console = require('./extras/Console') +const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator') + +let playwright +let perfTiming +let defaultSelectorEnginesInitialized = false + +const popupStore = new Popup() +const consoleLogStore = new Console() +const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron'] const { - setRestartStrategy, restartsSession, restartsContext, restartsBrowser, -} = require('./extras/PlaywrightRestartOpts'); -const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine'); + setRestartStrategy, + restartsSession, + restartsContext, + restartsBrowser, +} = require('./extras/PlaywrightRestartOpts') +const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine') const { - seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError, -} = require('./errors/ElementAssertion'); + seeElementError, + dontSeeElementError, + dontSeeElementInDOMError, + seeElementInDOMError, +} = require('./errors/ElementAssertion') const { - dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics, -} = require('./network/actions'); + dontSeeTraffic, + seeTraffic, + grabRecordedNetworkTraffics, + stopRecordingTraffic, + flushNetworkTraffics, +} = require('./network/actions') -const pathSeparator = path.sep; +const pathSeparator = path.sep /** * ## Configuration @@ -103,7 +110,7 @@ const pathSeparator = path.sep; * @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). * @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). */ -const config = {}; +const config = {} /** * Uses [Playwright](https://github.com/microsoft/playwright) library to run tests inside: @@ -325,34 +332,34 @@ const config = {}; */ class Playwright extends Helper { constructor(config) { - super(config); + super(config) - playwright = requireWithFallback('playwright', 'playwright-core'); + playwright = requireWithFallback('playwright', 'playwright-core') // set defaults - this.isRemoteBrowser = false; - this.isRunning = false; - this.isAuthenticated = false; - this.sessionPages = {}; - this.activeSessionName = ''; - this.isElectron = false; - this.isCDPConnection = false; - this.electronSessions = []; - this.storageState = null; + this.isRemoteBrowser = false + this.isRunning = false + this.isAuthenticated = false + this.sessionPages = {} + this.activeSessionName = '' + this.isElectron = false + this.isCDPConnection = false + this.electronSessions = [] + this.storageState = null // for network stuff - this.requests = []; - this.recording = false; - this.recordedAtLeastOnce = false; + this.requests = [] + this.recording = false + this.recordedAtLeastOnce = false // for websocket messages - this.webSocketMessages = []; - this.recordingWebSocketMessages = false; - this.recordedWebSocketMessagesAtLeastOnce = false; - this.cdpSession = null; + this.webSocketMessages = [] + this.recordingWebSocketMessages = false + this.recordedWebSocketMessagesAtLeastOnce = false + this.cdpSession = null // override defaults with config - this._setConfig(config); + this._setConfig(config) } _validateConfig(config) { @@ -379,61 +386,65 @@ class Playwright extends Helper { use: { actionTimeout: 0 }, ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors, highlightElement: false, - }; + } - process.env.testIdAttribute = 'data-testid'; - config = Object.assign(defaults, config); + process.env.testIdAttribute = 'data-testid' + config = Object.assign(defaults, config) if (availableBrowsers.indexOf(config.browser) < 0) { - throw new Error(`Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`); + throw new Error( + `Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`, + ) } - return config; + return config } _getOptionsForBrowser(config) { if (config[config.browser]) { if (config[config.browser].browserWSEndpoint && config[config.browser].browserWSEndpoint.wsEndpoint) { - config[config.browser].browserWSEndpoint = config[config.browser].browserWSEndpoint.wsEndpoint; + config[config.browser].browserWSEndpoint = config[config.browser].browserWSEndpoint.wsEndpoint } return { ...config[config.browser], wsEndpoint: config[config.browser].browserWSEndpoint, - }; + } } - return {}; + return {} } _setConfig(config) { - this.options = this._validateConfig(config); - setRestartStrategy(this.options); + this.options = this._validateConfig(config) + setRestartStrategy(this.options) this.playwrightOptions = { headless: !this.options.show, ...this._getOptionsForBrowser(config), - }; + } if (this.options.channel && this.options.browser === 'chromium') { - this.playwrightOptions.channel = this.options.channel; + this.playwrightOptions.channel = this.options.channel } if (this.options.video) { // set the video resolution with window size - let size = parseWindowSize(this.options.windowSize); + let size = parseWindowSize(this.options.windowSize) // if the video resolution is passed, set the record resoultion with that resolution if (this.options.recordVideo && this.options.recordVideo.size) { - size = parseWindowSize(this.options.recordVideo.size); + size = parseWindowSize(this.options.recordVideo.size) } - this.options.recordVideo = { size }; + this.options.recordVideo = { size } } if (this.options.recordVideo && !this.options.recordVideo.dir) { - this.options.recordVideo.dir = `${global.output_dir}/videos/`; + this.options.recordVideo.dir = `${global.output_dir}/videos/` } - this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint; - this.isElectron = this.options.browser === 'electron'; - this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined; - this.isCDPConnection = this.playwrightOptions.cdpConnection; - popupStore.defaultAction = this.options.defaultPopupAction; + this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint + this.isElectron = this.options.browser === 'electron' + this.userDataDir = this.playwrightOptions.userDataDir + ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` + : undefined + this.isCDPConnection = this.playwrightOptions.cdpConnection + popupStore.defaultAction = this.options.defaultPopupAction } static _config() { @@ -456,216 +467,222 @@ class Playwright extends Helper { type: 'confirm', when: (answers) => answers.Playwright_browser !== 'electron', }, - ]; + ] } static _checkRequirements() { try { - requireWithFallback('playwright', 'playwright-core'); + requireWithFallback('playwright', 'playwright-core') } catch (e) { - return ['playwright@^1.18']; + return ['playwright@^1.18'] } } async _init() { // register an internal selector engine for reading value property of elements in a selector - if (defaultSelectorEnginesInitialized) return; - defaultSelectorEnginesInitialized = true; + if (defaultSelectorEnginesInitialized) return + defaultSelectorEnginesInitialized = true try { - await playwright.selectors.register('__value', createValueEngine); - await playwright.selectors.register('__disabled', createDisabledEngine); - if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute); + await playwright.selectors.register('__value', createValueEngine) + await playwright.selectors.register('__disabled', createDisabledEngine) + if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute) } catch (e) { - console.warn(e); + console.warn(e) } } _beforeSuite() { if ((restartsSession() || restartsContext()) && !this.options.manualStart && !this.isRunning) { - this.debugSection('Session', 'Starting singleton browser session'); - return this._startBrowser(); + this.debugSection('Session', 'Starting singleton browser session') + return this._startBrowser() } } async _before(test) { - this.currentRunningTest = test; + this.currentRunningTest = test recorder.retry({ retries: process.env.FAILED_STEP_RETRIES || 3, - when: err => { - if (!err || typeof (err.message) !== 'string') { - return false; + when: (err) => { + if (!err || typeof err.message !== 'string') { + return false } // ignore context errors - return err.message.includes('context'); + return err.message.includes('context') }, - }); + }) - if (restartsBrowser() && !this.options.manualStart) await this._startBrowser(); - if (!this.isRunning && !this.options.manualStart) await this._startBrowser(); + if (restartsBrowser() && !this.options.manualStart) await this._startBrowser() + if (!this.isRunning && !this.options.manualStart) await this._startBrowser() - this.isAuthenticated = false; + this.isAuthenticated = false if (this.isElectron) { - this.browserContext = this.browser.context(); + this.browserContext = this.browser.context() } else if (this.playwrightOptions.userDataDir) { - this.browserContext = this.browser; + this.browserContext = this.browser } else { const contextOptions = { ignoreHTTPSErrors: this.options.ignoreHTTPSErrors, acceptDownloads: true, ...this.options.emulate, - }; + } if (this.options.basicAuth) { - contextOptions.httpCredentials = this.options.basicAuth; - this.isAuthenticated = true; + contextOptions.httpCredentials = this.options.basicAuth + this.isAuthenticated = true } - if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP; - if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo; + if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP + if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo if (this.options.recordHar) { - const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har'; - const fileName = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`; - const dir = path.dirname(fileName); - if (!fileExists(dir)) fs.mkdirSync(dir); - this.options.recordHar.path = fileName; - this.currentRunningTest.artifacts.har = fileName; - contextOptions.recordHar = this.options.recordHar; + const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har' + const fileName = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}` + const dir = path.dirname(fileName) + if (!fileExists(dir)) fs.mkdirSync(dir) + this.options.recordHar.path = fileName + this.currentRunningTest.artifacts.har = fileName + contextOptions.recordHar = this.options.recordHar } - if (this.storageState) contextOptions.storageState = this.storageState; - if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent; - if (this.options.locale) contextOptions.locale = this.options.locale; - if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme; - this.contextOptions = contextOptions; + if (this.storageState) contextOptions.storageState = this.storageState + if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent + if (this.options.locale) contextOptions.locale = this.options.locale + if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme + this.contextOptions = contextOptions if (!this.browserContext || !restartsSession()) { - this.browserContext = await this.browser.newContext(this.contextOptions); // Adding the HTTPSError ignore in the context so that we can ignore those errors + this.browserContext = await this.browser.newContext(this.contextOptions) // Adding the HTTPSError ignore in the context so that we can ignore those errors } } - let mainPage; + let mainPage if (this.isElectron) { - mainPage = await this.browser.firstWindow(); + mainPage = await this.browser.firstWindow() } else { try { - const existingPages = await this.browserContext.pages(); - mainPage = existingPages[0] || await this.browserContext.newPage(); + const existingPages = await this.browserContext.pages() + mainPage = existingPages[0] || (await this.browserContext.newPage()) } catch (e) { if (this.playwrightOptions.userDataDir) { - this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions); - this.browserContext = this.browser; - const existingPages = await this.browserContext.pages(); - mainPage = existingPages[0]; + this.browser = await playwright[this.options.browser].launchPersistentContext( + this.userDataDir, + this.playwrightOptions, + ) + this.browserContext = this.browser + const existingPages = await this.browserContext.pages() + mainPage = existingPages[0] } } } - await targetCreatedHandler.call(this, mainPage); + await targetCreatedHandler.call(this, mainPage) - await this._setPage(mainPage); + await this._setPage(mainPage) - if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true }); + if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true }) - return this.browser; + return this.browser } async _after() { - if (!this.isRunning) return; + if (!this.isRunning) return if (this.isElectron) { - this.browser.close(); - this.electronSessions.forEach(session => session.close()); - return; + this.browser.close() + this.electronSessions.forEach((session) => session.close()) + return } if (restartsSession()) { - return refreshContextSession.bind(this)(); + return refreshContextSession.bind(this)() } if (restartsBrowser()) { - this.isRunning = false; - return this._stopBrowser(); + this.isRunning = false + return this._stopBrowser() } // close other sessions try { if ((await this.browser)._type === 'Browser') { - const contexts = await this.browser.contexts(); - const currentContext = contexts[0]; + const contexts = await this.browser.contexts() + const currentContext = contexts[0] if (currentContext && (this.options.keepCookies || this.options.keepBrowserState)) { - this.storageState = await currentContext.storageState(); + this.storageState = await currentContext.storageState() } - await Promise.all(contexts.map(c => c.close())); + await Promise.all(contexts.map((c) => c.close())) } } catch (e) { - console.log(e); + console.log(e) } // await this.closeOtherTabs(); - return this.browser; + return this.browser } _afterSuite() {} async _finishTest() { - if ((restartsSession() || restartsContext()) && this.isRunning) return this._stopBrowser(); + if ((restartsSession() || restartsContext()) && this.isRunning) return this._stopBrowser() } _session() { - const defaultContext = this.browserContext; + const defaultContext = this.browserContext return { start: async (sessionName = '', config) => { - this.debugSection('New Context', config ? JSON.stringify(config) : 'opened'); - this.activeSessionName = sessionName; + this.debugSection('New Context', config ? JSON.stringify(config) : 'opened') + this.activeSessionName = sessionName - let browserContext; - let page; + let browserContext + let page if (this.isElectron) { - const browser = await playwright._electron.launch(this.playwrightOptions); - this.electronSessions.push(browser); - browserContext = browser.context(); - page = await browser.firstWindow(); + const browser = await playwright._electron.launch(this.playwrightOptions) + this.electronSessions.push(browser) + browserContext = browser.context() + page = await browser.firstWindow() } else { try { - browserContext = await this.browser.newContext(Object.assign(this.contextOptions, config)); - page = await browserContext.newPage(); + browserContext = await this.browser.newContext(Object.assign(this.contextOptions, config)) + page = await browserContext.newPage() } catch (e) { if (this.playwrightOptions.userDataDir) { - browserContext = await playwright[this.options.browser].launchPersistentContext(`${this.userDataDir}_${this.activeSessionName}`, this.playwrightOptions); - this.browser = browserContext; - page = await browserContext.pages()[0]; + browserContext = await playwright[this.options.browser].launchPersistentContext( + `${this.userDataDir}_${this.activeSessionName}`, + this.playwrightOptions, + ) + this.browser = browserContext + page = await browserContext.pages()[0] } } } - if (this.options.trace) await browserContext.tracing.start({ screenshots: true, snapshots: true }); - await targetCreatedHandler.call(this, page); - await this._setPage(page); + if (this.options.trace) await browserContext.tracing.start({ screenshots: true, snapshots: true }) + await targetCreatedHandler.call(this, page) + await this._setPage(page) // Create a new page inside context. - return browserContext; + return browserContext }, stop: async () => { // is closed by _after }, loadVars: async (context) => { if (context) { - this.browserContext = context; - const existingPages = await context.pages(); - this.sessionPages[this.activeSessionName] = existingPages[0]; - return this._setPage(this.sessionPages[this.activeSessionName]); + this.browserContext = context + const existingPages = await context.pages() + this.sessionPages[this.activeSessionName] = existingPages[0] + return this._setPage(this.sessionPages[this.activeSessionName]) } }, restoreVars: async (session) => { - this.withinLocator = null; - this.browserContext = defaultContext; + this.withinLocator = null + this.browserContext = defaultContext if (!session) { - this.activeSessionName = ''; + this.activeSessionName = '' } else { - this.activeSessionName = session; + this.activeSessionName = session } - const existingPages = await this.browserContext.pages(); - await this._setPage(existingPages[0]); + const existingPages = await this.browserContext.pages() + await this._setPage(existingPages[0]) - return this._waitForAction(); + return this._waitForAction() }, - }; + } } /** @@ -686,7 +703,7 @@ class Playwright extends Helper { * @param {function} fn async function that executed with Playwright helper as arguments */ usePlaywrightTo(description, fn) { - return this._useTo(...arguments); + return this._useTo(...arguments) } /** @@ -700,7 +717,7 @@ class Playwright extends Helper { * ``` */ amAcceptingPopups() { - popupStore.actionType = 'accept'; + popupStore.actionType = 'accept' } /** @@ -709,7 +726,7 @@ class Playwright extends Helper { * libraries](http://jster.net/category/windows-modals-popups). */ acceptPopup() { - popupStore.assertPopupActionType('accept'); + popupStore.assertPopupActionType('accept') } /** @@ -723,23 +740,23 @@ class Playwright extends Helper { * ``` */ amCancellingPopups() { - popupStore.actionType = 'cancel'; + popupStore.actionType = 'cancel' } /** * Dismisses the active JavaScript popup, as created by window.alert|window.confirm|window.prompt. */ cancelPopup() { - popupStore.assertPopupActionType('cancel'); + popupStore.assertPopupActionType('cancel') } /** * {{> seeInPopup }} */ async seeInPopup(text) { - popupStore.assertPopupVisible(); - const popupText = await popupStore.popup.message(); - stringIncludes('text in popup').assert(text, popupText); + popupStore.assertPopupVisible() + const popupText = await popupStore.popup.message() + stringIncludes('text in popup').assert(text, popupText) } /** @@ -747,21 +764,21 @@ class Playwright extends Helper { * @param {object} page page to set */ async _setPage(page) { - page = await page; - this._addPopupListener(page); - this.page = page; - if (!page) return; - this.browserContext.setDefaultTimeout(0); - page.setDefaultNavigationTimeout(this.options.getPageTimeout); - page.setDefaultTimeout(this.options.timeout); + page = await page + this._addPopupListener(page) + this.page = page + if (!page) return + this.browserContext.setDefaultTimeout(0) + page.setDefaultNavigationTimeout(this.options.getPageTimeout) + page.setDefaultTimeout(this.options.timeout) page.on('crash', async () => { - console.log('ERROR: Page has crashed, closing page!'); - await page.close(); - }); - this.context = await this.page; - this.contextLocator = null; - await page.bringToFront(); + console.log('ERROR: Page has crashed, closing page!') + await page.close() + }) + this.context = await this.page + this.contextLocator = null + await page.bringToFront() } /** @@ -773,33 +790,33 @@ class Playwright extends Helper { */ _addPopupListener(page) { if (!page) { - return; + return } - page.removeAllListeners('dialog'); + page.removeAllListeners('dialog') page.on('dialog', async (dialog) => { - popupStore.popup = dialog; - const action = popupStore.actionType || this.options.defaultPopupAction; - await this._waitForAction(); + popupStore.popup = dialog + const action = popupStore.actionType || this.options.defaultPopupAction + await this._waitForAction() switch (action) { case 'accept': - return dialog.accept(); + return dialog.accept() case 'cancel': - return dialog.dismiss(); + return dialog.dismiss() default: { - throw new Error('Unknown popup action type. Only "accept" or "cancel" are accepted'); + throw new Error('Unknown popup action type. Only "accept" or "cancel" are accepted') } } - }); + }) } /** * Gets page URL including hash. */ async _getPageUrl() { - return this.executeScript(() => window.location.href); + return this.executeScript(() => window.location.href) } /** @@ -812,45 +829,48 @@ class Playwright extends Helper { */ async grabPopupText() { if (popupStore.popup) { - return popupStore.popup.message(); + return popupStore.popup.message() } - return null; + return null } async _startBrowser() { if (this.isElectron) { - this.browser = await playwright._electron.launch(this.playwrightOptions); + this.browser = await playwright._electron.launch(this.playwrightOptions) } else if (this.isRemoteBrowser && this.isCDPConnection) { try { - this.browser = await playwright[this.options.browser].connectOverCDP(this.playwrightOptions); + this.browser = await playwright[this.options.browser].connectOverCDP(this.playwrightOptions) } catch (err) { if (err.toString().indexOf('ECONNREFUSED')) { - throw new RemoteBrowserConnectionRefused(err); + throw new RemoteBrowserConnectionRefused(err) } - throw err; + throw err } } else if (this.isRemoteBrowser) { try { - this.browser = await playwright[this.options.browser].connect(this.playwrightOptions); + this.browser = await playwright[this.options.browser].connect(this.playwrightOptions) } catch (err) { if (err.toString().indexOf('ECONNREFUSED')) { - throw new RemoteBrowserConnectionRefused(err); + throw new RemoteBrowserConnectionRefused(err) } - throw err; + throw err } } else if (this.playwrightOptions.userDataDir) { - this.browser = await playwright[this.options.browser].launchPersistentContext(this.userDataDir, this.playwrightOptions); + this.browser = await playwright[this.options.browser].launchPersistentContext( + this.userDataDir, + this.playwrightOptions, + ) } else { - this.browser = await playwright[this.options.browser].launch(this.playwrightOptions); + this.browser = await playwright[this.options.browser].launch(this.playwrightOptions) } // works only for Chromium this.browser.on('targetchanged', (target) => { - this.debugSection('Url', target.url()); - }); + this.debugSection('Url', target.url()) + }) - this.isRunning = true; - return this.browser; + this.isRunning = true + return this.browser } /** @@ -859,72 +879,72 @@ class Playwright extends Helper { * @param {object} [contextOptions] See https://playwright.dev/docs/api/class-browser#browser-new-context */ async _createContextPage(contextOptions) { - this.browserContext = await this.browser.newContext(contextOptions); - const page = await this.browserContext.newPage(); - targetCreatedHandler.call(this, page); - await this._setPage(page); + this.browserContext = await this.browser.newContext(contextOptions) + const page = await this.browserContext.newPage() + targetCreatedHandler.call(this, page) + await this._setPage(page) } _getType() { - return this.browser._type; + return this.browser._type } async _stopBrowser() { - this.withinLocator = null; - await this._setPage(null); - this.context = null; - this.frame = null; - popupStore.clear(); - if (this.options.recordHar) await this.browserContext.close(); - await this.browser.close(); + this.withinLocator = null + await this._setPage(null) + this.context = null + this.frame = null + popupStore.clear() + if (this.options.recordHar) await this.browserContext.close() + await this.browser.close() } async _evaluateHandeInContext(...args) { - const context = await this._getContext(); - return context.evaluateHandle(...args); + const context = await this._getContext() + return context.evaluateHandle(...args) } async _withinBegin(locator) { if (this.withinLocator) { - throw new Error('Can\'t start within block inside another within block'); + throw new Error("Can't start within block inside another within block") } - const frame = isFrameLocator(locator); + const frame = isFrameLocator(locator) if (frame) { if (Array.isArray(frame)) { - await this.switchTo(null); - return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()); + await this.switchTo(null) + return frame.reduce((p, frameLocator) => p.then(() => this.switchTo(frameLocator)), Promise.resolve()) } - await this.switchTo(frame); - this.withinLocator = new Locator(frame); - return; + await this.switchTo(frame) + this.withinLocator = new Locator(frame) + return } - const el = await this._locateElement(locator); - assertElementExists(el, locator); - this.context = el; - this.contextLocator = locator; + const el = await this._locateElement(locator) + assertElementExists(el, locator) + this.context = el + this.contextLocator = locator - this.withinLocator = new Locator(locator); + this.withinLocator = new Locator(locator) } async _withinEnd() { - this.withinLocator = null; - this.context = await this.page; - this.contextLocator = null; - this.frame = null; + this.withinLocator = null + this.context = await this.page + this.contextLocator = null + this.frame = null } _extractDataFromPerformanceTiming(timing, ...dataNames) { - const navigationStart = timing.navigationStart; + const navigationStart = timing.navigationStart - const extractedData = {}; + const extractedData = {} dataNames.forEach((name) => { - extractedData[name] = timing[name] - navigationStart; - }); + extractedData[name] = timing[name] - navigationStart + }) - return extractedData; + return extractedData } /** @@ -932,22 +952,22 @@ class Playwright extends Helper { */ async amOnPage(url) { if (this.isElectron) { - throw new Error('Cannot open pages inside an Electron container'); + throw new Error('Cannot open pages inside an Electron container') } - if (!(/^\w+\:(\/\/|.+)/.test(url))) { - url = this.options.url + (url.startsWith('/') ? url : `/${url}`); + if (!/^\w+\:(\/\/|.+)/.test(url)) { + url = this.options.url + (url.startsWith('/') ? url : `/${url}`) } - if (this.options.basicAuth && (this.isAuthenticated !== true)) { + if (this.options.basicAuth && this.isAuthenticated !== true) { if (url.includes(this.options.url)) { - await this.browserContext.setHTTPCredentials(this.options.basicAuth); - this.isAuthenticated = true; + await this.browserContext.setHTTPCredentials(this.options.basicAuth) + this.isAuthenticated = true } } - await this.page.goto(url, { waitUntil: this.options.waitForNavigation }); + await this.page.goto(url, { waitUntil: this.options.waitForNavigation }) - const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing))); + const performanceTiming = JSON.parse(await this.page.evaluate(() => JSON.stringify(window.performance.timing))) perfTiming = this._extractDataFromPerformanceTiming( performanceTiming, @@ -955,9 +975,9 @@ class Playwright extends Helper { 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd', - ); + ) - return this._waitForAction(); + return this._waitForAction() } /** @@ -978,11 +998,11 @@ class Playwright extends Helper { */ async resizeWindow(width, height) { if (width === 'maximize') { - throw new Error('Playwright can\'t control windows, so it can\'t maximize it'); + throw new Error("Playwright can't control windows, so it can't maximize it") } - await this.page.setViewportSize({ width, height }); - return this._waitForAction(); + await this.page.setViewportSize({ width, height }) + return this._waitForAction() } /** @@ -998,9 +1018,9 @@ class Playwright extends Helper { */ async setPlaywrightRequestHeaders(customHeaders) { if (!customHeaders) { - throw new Error('Cannot send empty headers.'); + throw new Error('Cannot send empty headers.') } - return this.browserContext.setExtraHTTPHeaders(customHeaders); + return this.browserContext.setExtraHTTPHeaders(customHeaders) } /** @@ -1008,13 +1028,13 @@ class Playwright extends Helper { * */ async moveCursorTo(locator, offsetX = 0, offsetY = 0) { - const el = await this._locateElement(locator); - assertElementExists(el, locator); + const el = await this._locateElement(locator) + assertElementExists(el, locator) // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates - const { x, y } = await clickablePoint(el); - await this.page.mouse.move(x + offsetX, y + offsetY); - return this._waitForAction(); + const { x, y } = await clickablePoint(el) + await this.page.mouse.move(x + offsetX, y + offsetY) + return this._waitForAction() } /** @@ -1022,11 +1042,11 @@ class Playwright extends Helper { * */ async focus(locator, options = {}) { - const el = await this._locateElement(locator); - assertElementExists(el, locator, 'Element to focus'); + const el = await this._locateElement(locator) + assertElementExists(el, locator, 'Element to focus') - await el.focus(options); - return this._waitForAction(); + await el.focus(options) + return this._waitForAction() } /** @@ -1034,11 +1054,11 @@ class Playwright extends Helper { * */ async blur(locator, options = {}) { - const el = await this._locateElement(locator); - assertElementExists(el, locator, 'Element to blur'); + const el = await this._locateElement(locator) + assertElementExists(el, locator, 'Element to blur') - await el.blur(options); - return this._waitForAction(); + await el.blur(options) + return this._waitForAction() } /** * Return the checked status of given element. @@ -1050,14 +1070,14 @@ class Playwright extends Helper { */ async grabCheckedElementStatus(locator, options = {}) { - const supportedTypes = ['checkbox', 'radio']; - const el = await this._locateElement(locator); - const type = await el.getAttribute('type'); + const supportedTypes = ['checkbox', 'radio'] + const el = await this._locateElement(locator) + const type = await el.getAttribute('type') if (supportedTypes.includes(type)) { - return el.isChecked(options); + return el.isChecked(options) } - throw new Error(`Element is not a ${supportedTypes.join(' or ')} input`); + throw new Error(`Element is not a ${supportedTypes.join(' or ')} input`) } /** * Return the disabled status of given element. @@ -1069,8 +1089,8 @@ class Playwright extends Helper { */ async grabDisabledElementStatus(locator, options = {}) { - const el = await this._locateElement(locator); - return el.isDisabled(options); + const el = await this._locateElement(locator) + return el.isDisabled(options) } /** @@ -1087,24 +1107,24 @@ class Playwright extends Helper { * */ async dragAndDrop(srcElement, destElement, options) { - const src = new Locator(srcElement); - const dst = new Locator(destElement); + const src = new Locator(srcElement) + const dst = new Locator(destElement) if (options) { - return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options); + return this.page.dragAndDrop(buildLocatorString(src), buildLocatorString(dst), options) } - const _smallWaitInMs = 600; - await this.page.locator(buildLocatorString(src)).hover(); - await this.page.mouse.down(); - await this.page.waitForTimeout(_smallWaitInMs); + const _smallWaitInMs = 600 + await this.page.locator(buildLocatorString(src)).hover() + await this.page.mouse.down() + await this.page.waitForTimeout(_smallWaitInMs) - const destElBox = await this.page.locator(buildLocatorString(dst)).boundingBox(); + const destElBox = await this.page.locator(buildLocatorString(dst)).boundingBox() - await this.page.mouse.move(destElBox.x + destElBox.width / 2, destElBox.y + destElBox.height / 2); - await this.page.locator(buildLocatorString(dst)).hover({ position: { x: 10, y: 10 } }); - await this.page.waitForTimeout(_smallWaitInMs); - await this.page.mouse.up(); + await this.page.mouse.move(destElBox.x + destElBox.width / 2, destElBox.y + destElBox.height / 2) + await this.page.locator(buildLocatorString(dst)).hover({ position: { x: 10, y: 10 } }) + await this.page.waitForTimeout(_smallWaitInMs) + await this.page.mouse.up() } /** @@ -1122,16 +1142,16 @@ class Playwright extends Helper { * @param {object} [contextOptions] [Options for browser context](https://playwright.dev/docs/api/class-browser#browser-new-context) when starting new browser */ async restartBrowser(contextOptions) { - await this._stopBrowser(); - await this._startBrowser(); - await this._createContextPage(contextOptions); + await this._stopBrowser() + await this._startBrowser() + await this._createContextPage(contextOptions) } /** * {{> refreshPage }} */ async refreshPage() { - return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation }); + return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation }) } /** @@ -1152,13 +1172,13 @@ class Playwright extends Helper { * @returns Promise */ async replayFromHar(harFilePath, opts) { - const file = path.join(global.codecept_dir, harFilePath); + const file = path.join(global.codecept_dir, harFilePath) if (!fileExists(file)) { - throw new Error(`File at ${file} cannot be found on local system`); + throw new Error(`File at ${file} cannot be found on local system`) } - await this.page.routeFromHAR(harFilePath, opts); + await this.page.routeFromHAR(harFilePath, opts) } /** @@ -1166,8 +1186,8 @@ class Playwright extends Helper { */ scrollPageToTop() { return this.executeScript(() => { - window.scrollTo(0, 0); - }); + window.scrollTo(0, 0) + }) } /** @@ -1175,16 +1195,13 @@ class Playwright extends Helper { */ async scrollPageToBottom() { return this.executeScript(() => { - const body = document.body; - const html = document.documentElement; - window.scrollTo(0, Math.max( - body.scrollHeight, - body.offsetHeight, - html.clientHeight, - html.scrollHeight, - html.offsetHeight, - )); - }); + const body = document.body + const html = document.documentElement + window.scrollTo( + 0, + Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight), + ) + }) } /** @@ -1192,29 +1209,32 @@ class Playwright extends Helper { */ async scrollTo(locator, offsetX = 0, offsetY = 0) { if (typeof locator === 'number' && typeof offsetX === 'number') { - offsetY = offsetX; - offsetX = locator; - locator = null; + offsetY = offsetX + offsetX = locator + locator = null } if (locator) { - const el = await this._locateElement(locator); - assertElementExists(el, locator, 'Element'); - await el.scrollIntoViewIfNeeded(); - const elementCoordinates = await clickablePoint(el); - await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY }); + const el = await this._locateElement(locator) + assertElementExists(el, locator, 'Element') + await el.scrollIntoViewIfNeeded() + const elementCoordinates = await clickablePoint(el) + await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { + offsetX: elementCoordinates.x + offsetX, + offsetY: elementCoordinates.y + offsetY, + }) } else { - await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY }); + await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY }) } - return this._waitForAction(); + return this._waitForAction() } /** * {{> seeInTitle }} */ async seeInTitle(text) { - const title = await this.page.title(); - stringIncludes('web page title').assert(text, title); + const title = await this.page.title() + stringIncludes('web page title').assert(text, title) } /** @@ -1225,34 +1245,34 @@ class Playwright extends Helper { function getScrollPosition() { return { x: window.pageXOffset, - y: window.pageYOffset - }; + y: window.pageYOffset, + } } /* eslint-enable comma-dangle */ - return this.executeScript(getScrollPosition); + return this.executeScript(getScrollPosition) } /** * {{> seeTitleEquals }} */ async seeTitleEquals(text) { - const title = await this.page.title(); - return equals('web page title').assert(title, text); + const title = await this.page.title() + return equals('web page title').assert(title, text) } /** * {{> dontSeeInTitle }} */ async dontSeeInTitle(text) { - const title = await this.page.title(); - stringIncludes('web page title').negate(text, title); + const title = await this.page.title() + stringIncludes('web page title').negate(text, title) } /** * {{> grabTitle }} */ async grabTitle() { - return this.page.title(); + return this.page.title() } /** @@ -1264,11 +1284,11 @@ class Playwright extends Helper { * ``` */ async _locate(locator) { - const context = await this.context || await this._getContext(); + const context = (await this.context) || (await this._getContext()) - if (this.frame) return findElements(this.frame, locator); + if (this.frame) return findElements(this.frame, locator) - return findElements(context, locator); + return findElements(context, locator) } /** @@ -1280,8 +1300,8 @@ class Playwright extends Helper { * ``` */ async _locateElement(locator) { - const context = await this.context || await this._getContext(); - return findElement(context, locator); + const context = (await this.context) || (await this._getContext()) + return findElement(context, locator) } /** @@ -1293,10 +1313,10 @@ class Playwright extends Helper { * ``` */ async _locateCheckable(locator, providedContext = null) { - const context = providedContext || await this._getContext(); - const els = await findCheckable.call(this, locator, context); - assertElementExists(els[0], locator, 'Checkbox or radio'); - return els[0]; + const context = providedContext || (await this._getContext()) + const els = await findCheckable.call(this, locator, context) + assertElementExists(els[0], locator, 'Checkbox or radio') + return els[0] } /** @@ -1307,8 +1327,8 @@ class Playwright extends Helper { * ``` */ async _locateClickable(locator) { - const context = await this._getContext(); - return findClickable.call(this, context, locator); + const context = await this._getContext() + return findClickable.call(this, context, locator) } /** @@ -1319,7 +1339,7 @@ class Playwright extends Helper { * ``` */ async _locateFields(locator) { - return findFields.call(this, locator); + return findFields.call(this, locator) } /** @@ -1327,7 +1347,7 @@ class Playwright extends Helper { * */ async grabWebElements(locator) { - return this._locate(locator); + return this._locate(locator) } /** @@ -1335,7 +1355,7 @@ class Playwright extends Helper { * */ async grabWebElement(locator) { - return this._locateElement(locator); + return this._locateElement(locator) } /** @@ -1350,20 +1370,20 @@ class Playwright extends Helper { */ async switchToNextTab(num = 1) { if (this.isElectron) { - throw new Error('Cannot switch tabs inside an Electron container'); + throw new Error('Cannot switch tabs inside an Electron container') } - const pages = await this.browserContext.pages(); + const pages = await this.browserContext.pages() - const index = pages.indexOf(this.page); - this.withinLocator = null; - const page = pages[index + num]; + const index = pages.indexOf(this.page) + this.withinLocator = null + const page = pages[index + num] if (!page) { - throw new Error(`There is no ability to switch to next tab with offset ${num}`); + throw new Error(`There is no ability to switch to next tab with offset ${num}`) } - await targetCreatedHandler.call(this, page); - await this._setPage(page); - return this._waitForAction(); + await targetCreatedHandler.call(this, page) + await this._setPage(page) + return this._waitForAction() } /** @@ -1377,19 +1397,19 @@ class Playwright extends Helper { */ async switchToPreviousTab(num = 1) { if (this.isElectron) { - throw new Error('Cannot switch tabs inside an Electron container'); + throw new Error('Cannot switch tabs inside an Electron container') } - const pages = await this.browserContext.pages(); - const index = pages.indexOf(this.page); - this.withinLocator = null; - const page = pages[index - num]; + const pages = await this.browserContext.pages() + const index = pages.indexOf(this.page) + this.withinLocator = null + const page = pages[index - num] if (!page) { - throw new Error(`There is no ability to switch to previous tab with offset ${num}`); + throw new Error(`There is no ability to switch to previous tab with offset ${num}`) } - await this._setPage(page); - return this._waitForAction(); + await this._setPage(page) + return this._waitForAction() } /** @@ -1401,12 +1421,12 @@ class Playwright extends Helper { */ async closeCurrentTab() { if (this.isElectron) { - throw new Error('Cannot close current tab inside an Electron container'); + throw new Error('Cannot close current tab inside an Electron container') } - const oldPage = this.page; - await this.switchToPreviousTab(); - await oldPage.close(); - return this._waitForAction(); + const oldPage = this.page + await this.switchToPreviousTab() + await oldPage.close() + return this._waitForAction() } /** @@ -1417,13 +1437,13 @@ class Playwright extends Helper { * ``` */ async closeOtherTabs() { - const pages = await this.browserContext.pages(); - const otherPages = pages.filter(page => page !== this.page); + const pages = await this.browserContext.pages() + const otherPages = pages.filter((page) => page !== this.page) if (otherPages.length) { - this.debug(`Closing ${otherPages.length} tabs`); - return Promise.all(otherPages.map(p => p.close())); + this.debug(`Closing ${otherPages.length} tabs`) + return Promise.all(otherPages.map((p) => p.close())) } - return Promise.resolve(); + return Promise.resolve() } /** @@ -1442,20 +1462,20 @@ class Playwright extends Helper { */ async openNewTab(options) { if (this.isElectron) { - throw new Error('Cannot open new tabs inside an Electron container'); + throw new Error('Cannot open new tabs inside an Electron container') } - const page = await this.browserContext.newPage(options); - await targetCreatedHandler.call(this, page); - await this._setPage(page); - return this._waitForAction(); + const page = await this.browserContext.newPage(options) + await targetCreatedHandler.call(this, page) + await this._setPage(page) + return this._waitForAction() } /** * {{> grabNumberOfOpenTabs }} */ async grabNumberOfOpenTabs() { - const pages = await this.browserContext.pages(); - return pages.length; + const pages = await this.browserContext.pages() + return pages.length } /** @@ -1463,12 +1483,12 @@ class Playwright extends Helper { * */ async seeElement(locator) { - let els = await this._locate(locator); - els = await Promise.all(els.map(el => el.isVisible())); + let els = await this._locate(locator) + els = await Promise.all(els.map((el) => el.isVisible())) try { - return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT')); + return empty('visible elements').negate(els.filter((v) => v).fill('ELEMENT')) } catch (e) { - dontSeeElementError(locator); + dontSeeElementError(locator) } } @@ -1477,12 +1497,12 @@ class Playwright extends Helper { * */ async dontSeeElement(locator) { - let els = await this._locate(locator); - els = await Promise.all(els.map(el => el.isVisible())); + let els = await this._locate(locator) + els = await Promise.all(els.map((el) => el.isVisible())) try { - return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT')); + return empty('visible elements').assert(els.filter((v) => v).fill('ELEMENT')) } catch (e) { - seeElementError(locator); + seeElementError(locator) } } @@ -1490,11 +1510,11 @@ class Playwright extends Helper { * {{> seeElementInDOM }} */ async seeElementInDOM(locator) { - const els = await this._locate(locator); + const els = await this._locate(locator) try { - return empty('elements on page').negate(els.filter(v => v).fill('ELEMENT')); + return empty('elements on page').negate(els.filter((v) => v).fill('ELEMENT')) } catch (e) { - dontSeeElementInDOMError(locator); + dontSeeElementInDOMError(locator) } } @@ -1502,11 +1522,11 @@ class Playwright extends Helper { * {{> dontSeeElementInDOM }} */ async dontSeeElementInDOM(locator) { - const els = await this._locate(locator); + const els = await this._locate(locator) try { - return empty('elements on a page').assert(els.filter(v => v).fill('ELEMENT')); + return empty('elements on a page').assert(els.filter((v) => v).fill('ELEMENT')) } catch (e) { - seeElementInDOMError(locator); + seeElementInDOMError(locator) } } @@ -1529,18 +1549,18 @@ class Playwright extends Helper { */ async handleDownloads(fileName) { this.page.waitForEvent('download').then(async (download) => { - const filePath = await download.path(); - fileName = fileName || `downloads/${path.basename(filePath)}`; + const filePath = await download.path() + fileName = fileName || `downloads/${path.basename(filePath)}` - const downloadPath = path.join(global.output_dir, fileName); + const downloadPath = path.join(global.output_dir, fileName) if (!fs.existsSync(path.dirname(downloadPath))) { - fs.mkdirSync(path.dirname(downloadPath), '0777'); + fs.mkdirSync(path.dirname(downloadPath), '0777') } - fs.copyFileSync(filePath, downloadPath); - this.debug('Download completed'); - this.debugSection('Downloaded From', await download.url()); - this.debugSection('Downloaded To', downloadPath); - }); + fs.copyFileSync(filePath, downloadPath) + this.debug('Download completed') + this.debugSection('Downloaded From', await download.url()) + this.debugSection('Downloaded To', downloadPath) + }) } /** @@ -1560,37 +1580,37 @@ class Playwright extends Helper { * */ async click(locator, context = null, options = {}) { - return proceedClick.call(this, locator, context, options); + return proceedClick.call(this, locator, context, options) } /** * Clicks link and waits for navigation (deprecated) */ async clickLink(locator, context = null) { - console.log('clickLink deprecated: Playwright automatically waits for navigation to happen.'); - console.log('Replace I.clickLink with I.click'); - return this.click(locator, context); + console.log('clickLink deprecated: Playwright automatically waits for navigation to happen.') + console.log('Replace I.clickLink with I.click') + return this.click(locator, context) } /** * {{> forceClick }} */ async forceClick(locator, context = null) { - return proceedClick.call(this, locator, context, { force: true }); + return proceedClick.call(this, locator, context, { force: true }) } /** * {{> doubleClick }} */ async doubleClick(locator, context = null) { - return proceedClick.call(this, locator, context, { clickCount: 2 }); + return proceedClick.call(this, locator, context, { clickCount: 2 }) } /** * {{> rightClick }} */ async rightClick(locator, context = null) { - return proceedClick.call(this, locator, context, { button: 'right' }); + return proceedClick.call(this, locator, context, { button: 'right' }) } /** @@ -1609,9 +1629,9 @@ class Playwright extends Helper { * */ async checkOption(field, context = null, options = { force: true }) { - const elm = await this._locateCheckable(field, context); - await elm.check(options); - return this._waitForAction(); + const elm = await this._locateCheckable(field, context) + await elm.check(options) + return this._waitForAction() } /** @@ -1629,41 +1649,41 @@ class Playwright extends Helper { * {{> uncheckOption }} */ async uncheckOption(field, context = null, options = { force: true }) { - const elm = await this._locateCheckable(field, context); - await elm.uncheck(options); - return this._waitForAction(); + const elm = await this._locateCheckable(field, context) + await elm.uncheck(options) + return this._waitForAction() } /** * {{> seeCheckboxIsChecked }} */ async seeCheckboxIsChecked(field) { - return proceedIsChecked.call(this, 'assert', field); + return proceedIsChecked.call(this, 'assert', field) } /** * {{> dontSeeCheckboxIsChecked }} */ async dontSeeCheckboxIsChecked(field) { - return proceedIsChecked.call(this, 'negate', field); + return proceedIsChecked.call(this, 'negate', field) } /** * {{> pressKeyDown }} */ async pressKeyDown(key) { - key = getNormalizedKey.call(this, key); - await this.page.keyboard.down(key); - return this._waitForAction(); + key = getNormalizedKey.call(this, key) + await this.page.keyboard.down(key) + return this._waitForAction() } /** * {{> pressKeyUp }} */ async pressKeyUp(key) { - key = getNormalizedKey.call(this, key); - await this.page.keyboard.up(key); - return this._waitForAction(); + key = getNormalizedKey.call(this, key) + await this.page.keyboard.up(key) + return this._waitForAction() } /** @@ -1673,28 +1693,28 @@ class Playwright extends Helper { * {{> pressKeyWithKeyNormalization }} */ async pressKey(key) { - const modifiers = []; + const modifiers = [] if (Array.isArray(key)) { for (let k of key) { - k = getNormalizedKey.call(this, k); + k = getNormalizedKey.call(this, k) if (isModifierKey(k)) { - modifiers.push(k); + modifiers.push(k) } else { - key = k; - break; + key = k + break } } } else { - key = getNormalizedKey.call(this, key); + key = getNormalizedKey.call(this, key) } for (const modifier of modifiers) { - await this.page.keyboard.down(modifier); + await this.page.keyboard.down(modifier) } - await this.page.keyboard.press(key); + await this.page.keyboard.press(key) for (const modifier of modifiers) { - await this.page.keyboard.up(modifier); + await this.page.keyboard.up(modifier) } - return this._waitForAction(); + return this._waitForAction() } /** @@ -1702,13 +1722,13 @@ class Playwright extends Helper { */ async type(keys, delay = null) { if (!Array.isArray(keys)) { - keys = keys.toString(); - keys = keys.split(''); + keys = keys.toString() + keys = keys.split('') } for (const key of keys) { - await this.page.keyboard.press(key); - if (delay) await this.wait(delay / 1000); + await this.page.keyboard.press(key) + if (delay) await this.wait(delay / 1000) } } @@ -1717,75 +1737,75 @@ class Playwright extends Helper { * */ async fillField(field, value) { - const els = await findFields.call(this, field); - assertElementExists(els, field, 'Field'); - const el = els[0]; + const els = await findFields.call(this, field) + assertElementExists(els, field, 'Field') + const el = els[0] - await el.clear(); + await el.clear() - await highlightActiveElement.call(this, el); + await highlightActiveElement.call(this, el) - await el.type(value.toString(), { delay: this.options.pressKeyDelay }); + await el.type(value.toString(), { delay: this.options.pressKeyDelay }) - return this._waitForAction(); + return this._waitForAction() } /** * Clears the text input element: ``, `