diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..6b972ddbf --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,55 @@ +{ + "extends": "airbnb-base", + "env": { + "node": true + }, + "parserOptions": { + "ecmaVersion": 2020 + }, + "rules": { + "func-names": 0, + "no-use-before-define": 0, + "no-unused-vars": 0, + "no-underscore-dangle": 0, + "no-undef": 0, + "prefer-destructuring": 0, + "no-param-reassign": 0, + "max-len": 0, + "camelcase": 0, + "no-shadow": 0, + "consistent-return": 0, + "no-console": 0, + "global-require": 0, + "class-methods-use-this": 0, + "no-plusplus": 0, + "no-return-assign": 0, + "prefer-rest-params": 0, + "no-useless-escape": 0, + "no-restricted-syntax": 0, + "no-unused-expressions": 0, + "guard-for-in": 0, + "no-multi-assign": 0, + "require-yield": 0, + "prefer-spread": 0, + "import/no-dynamic-require": 0, + "no-continue": 0, + "no-mixed-operators": 0, + "default-case": 0, + "import/no-extraneous-dependencies": 0, + "no-cond-assign": 0, + "import/no-unresolved": 0, + "no-await-in-loop": 0, + "arrow-body-style": 0, + "no-loop-func": 0, + "arrow-parens": 0, + "default-param-last": 0, + "import/extensions": 0, + "no-return-await": "off", + "import/prefer-default-export": 0, + "import/no-named-as-default-member": 0, + "import/no-named-default": 0, + "import/no-cycle": 0, + "prefer-const": "off", + "no-import-assign": 0 + } +} diff --git a/.mocharc.js b/.mocharc#.js similarity index 65% rename from .mocharc.js rename to .mocharc#.js index 03529fe60..23bc63079 100644 --- a/.mocharc.js +++ b/.mocharc#.js @@ -1,3 +1,3 @@ -module.exports = { +export default { "require": "./test/support/setup.js" -} +}; diff --git a/bin/codecept.js b/bin/codecept.js index 9a0ff7db0..d66ecbd17 100755 --- a/bin/codecept.js +++ b/bin/codecept.js @@ -1,8 +1,26 @@ #!/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') +import { Command } from 'commander'; +import { version } from '../lib/codecept.js'; +import * as outputLib from '../lib/output.js'; +import { printError } from '../lib/command/utils.js'; +import * as init from '../lib/command/init.js'; +import * as configMigrate from '../lib/command/configMigrate.js'; +import * as interactive from '../lib/command/interactive.js'; +import * as definitions from '../lib/command/definitions.js'; +import * as list from '../lib/command/list.js'; +import * as gherkinInit from '../lib/command/gherkin/init.js'; +import * as gherkinSteps from '../lib/command/gherkin/steps.js'; +import * as gherkinSnippets from '../lib/command/gherkin/snippets.js'; +import * as generate from '../lib/command/generate.js'; +import * as run from '../lib/command/run.js'; +import * as runWorkers from '../lib/command/run-workers.js'; +import * as runMultiple from '../lib/command/run-multiple.js'; +import { runRerun } from '../lib/command/run-rerun.js'; +import * as dryRun from '../lib/command/dryRun.js'; +import * as info from '../lib/command/info.js'; +import { heal } from "../lib/command/generate.js"; + +const program = new Command(); const commandFlags = { ai: { @@ -29,120 +47,104 @@ 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.default(...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) + outputLib.output.output.error('NodeJS >= 12 is required to run.'); + outputLib.print(); + outputLib.print('Please upgrade your NodeJS engine'); + outputLib.print(`Current NodeJS version: ${process.version}`); + process.exit(1); } -program.usage(' [options]') -program.version(Codecept.version()) +program.usage(' [options]'); +program.version(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(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(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(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(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(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(gherkinInit)); -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(gherkinSteps)); -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(gherkinSnippets)); -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(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(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(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(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(heal)); -program - .command('run [test]') +program.command('run [test]') .description('Executes tests') // codecept-only options @@ -178,10 +180,9 @@ program .option('--recursive', 'include sub directories') .option('--trace', 'trace function calls') .option('--child ', 'option for child processes') - .action(errorHandler(require('../lib/command/run'))) + .action(errorHandler(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 ') @@ -197,10 +198,9 @@ program .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(runWorkers)); -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) @@ -223,16 +223,14 @@ program // mocha options .option('--colors', 'force enabling of colors') - .action(errorHandler(require('../lib/command/run-multiple'))) + .action(errorHandler(runMultiple)); -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(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') @@ -246,10 +244,9 @@ program .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(dryRun)); -program - .command('run-rerun [test]') +program.command('run-rerun [test]') .description('Executes tests in more than one test suite run') // codecept-only options @@ -284,15 +281,15 @@ program .option('--trace', 'trace function calls') .option('--child ', 'option for child processes') - .action(require('../lib/command/run-rerun')) + .action(runRerun); 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/configuration.md b/docs/configuration.md index 2d07bd58a..5f1640740 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -9,26 +9,26 @@ CodeceptJS configuration is set in `codecept.conf.js` file. After running `codeceptjs init` it should be saved in test root. -| Name | Type | Description | -| :------ | :------ | :------ | -| `bootstrap?` | (() => `Promise`<`void`\>) \| `boolean` \| `string` | [Execute code before](https://codecept.io/bootstrap/) tests are run. Can be either JS module file or async function: ```bootstrap: async () => server.launch(), ``` or ```bootstrap: 'bootstrap.js', ``` | -| `bootstrapAll?` | (() => `Promise`<`void`\>) \| `boolean` \| `string` | [Execute code before launching tests in parallel mode](https://codecept.io/bootstrap/#bootstrapall-teardownall) | -| `gherkin?` | { `features`: `string` \| `string`[] ; `steps`: `string`[] } | Enable [BDD features](https://codecept.io/bdd/#configuration). Sample configuration: ```gherkin: { features: "./features/*.feature", steps: ["./step_definitions/steps.js"] } ``` | -| `gherkin.features` | `string` \| `string`[] | load feature files by pattern. Multiple patterns can be specified as array | -| `gherkin.steps` | `string`[] | load step definitions from JS files | -| `grep?` | `string` | Pattern to filter tests by name. This option is useful if you plan to use multiple configs for different environments. To execute only tests with @firefox tag use ```grep: '@firefox' ``` | -| `helpers?` | {} | Enable and configure helpers: ```helpers: { Playwright: { url: 'https://mysite.com', browser: 'firefox' } } ``` | -| `include?` | `any` | Include page objects to access them via dependency injection ```I: "./custom_steps.js", loginPage: "./pages/Login.js", User: "./pages/User.js", ``` Configured modules can be injected by name in a Scenario: ```Scenario('test', { I, loginPage, User }) ``` | +| Name | Type | Description | +| :------ | :------ |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `bootstrap?` | (() => `Promise`<`void`\>) \| `boolean` \| `string` | [Execute code before](https://codecept.io/bootstrap/) tests are run. Can be either JS module file or async function: ```bootstrap: async () => server.launch(), ``` or ```bootstrap: 'bootstrap.js', ``` | +| `bootstrapAll?` | (() => `Promise`<`void`\>) \| `boolean` \| `string` | [Execute code before launching tests in parallel mode](https://codecept.io/bootstrap/#bootstrapall-teardownall) | +| `gherkin?` | { `features`: `string` \| `string`[] ; `steps`: `string`[] } | Enable [BDD features](https://codecept.io/bdd/#configuration). Sample configuration: ```gherkin: { features: "./features/*.feature", steps: ["./step_definitions/steps.js"] } ``` | +| `gherkin.features` | `string` \| `string`[] | load feature files by pattern. Multiple patterns can be specified as array | +| `gherkin.steps` | `string`[] | load step definitions from JS files | +| `grep?` | `string` | Pattern to filter tests by name. This option is useful if you plan to use multiple configs for different environments. To execute only tests with @firefox tag use ```grep: '@firefox' ``` | +| `helpers?` | {} | Enable and configure helpers: ```helpers: { Playwright: { url: 'https://mysite.com', browser: 'firefox' } } ``` | +| `include?` | `any` | Include page objects to access them via dependency injection ```I: "./custom_steps.js", loginPage: "./pages/Login.js", User: "./pages/User.js", ``` Configured modules can be injected by name in a Scenario: ```Scenario('test', { I, loginPage, User }) ``` | | `mocha?` | `any` | [Mocha test runner options](https://mochajs.org/#configuring-mocha-nodejs), additional [reporters](https://codecept.io/reports/#xml) can be configured here. Example: ```mocha: { "mocha-junit-reporter": { stdout: "./output/console.log", options: { mochaFile: "./output/result.xml", attachments: true //add screenshot for a failed test } } } ``` | -| `noGlobals?` | `boolean` | Disable registering global functions (Before, Scenario, etc). Not recommended | -| `output` | `string` | Where to store failure screenshots, artifacts, etc ```output: './output' ``` | -| `plugins?` | `any` | Enable CodeceptJS plugins. Example: ```plugins: { autoDelay: { enabled: true } } ``` | -| `require?` | `string`[] | [Require additional JS modules](https://codecept.io/configuration/#require) Example: ``` require: ["should"] ``` | -| `teardown?` | (() => `Promise`<`void`\>) \| `boolean` \| `string` | [Execute code after tests](https://codecept.io/bootstrap/) finished. Can be either JS module file or async function: ```teardown: async () => server.stop(), ``` or ```teardown: 'teardown.js', ``` | -| `teardownAll?` | (() => `Promise`<`void`\>) \| `boolean` \| `string` | [Execute JS code after finishing tests in parallel mode](https://codecept.io/bootstrap/#bootstrapall-teardownall) | -| `tests` | `string` | Pattern to locate CodeceptJS tests. Allows to enter glob pattern or an Array of patterns to match tests / test file names. For tests in JavaScript: ```tests: 'tests/**.test.js' ``` For tests in TypeScript: ```tests: 'tests/**.test.ts' ``` | -| `timeout?` | `number` | Set default tests timeout in seconds. Tests will be killed on no response after timeout. ```timeout: 20, ``` | -| `translation?` | `string` | Enable [localized test commands](https://codecept.io/translation/) | +| `noGlobals?` | `boolean` | Disable registering global functions (Before, Scenario, etc). Not recommended | +| `output` | `string` | Where to store failure screenshots, artifacts, etc ```output: './output' ``` | +| `plugins?` | `any` | Enable CodeceptJS plugins. Example: ```plugins: { autoDelay: { enabled: true } } ``` | +| `require?` | `string`[] | [Require additional JS modules](https://codecept.io/configuration/#require) Example: ``` require: ["should"] ``` | +| `teardown?` | (() => `Promise`<`void`\>) \| `boolean` \| `string` | [Execute code after tests](https://codecept.io/bootstrap/) finished. Can be either JS module file or async function: ```teardown: async () => server.stop(), ``` or ```teardown: 'teardown.js', ``` | +| `teardownAll?` | (() => `Promise`<`void`\>) \| `boolean` \| `string` | [Execute JS code after finishing tests in parallel mode](https://codecept.io/bootstrap/#bootstrapall-teardownall) | +| `tests` | `string` | Pattern to locate CodeceptJS tests. Allows to enter glob pattern or an Array of patterns to match tests / test file names. For tests in JavaScript: ```tests: 'tests/**.test.js' ``` For tests in TypeScript: ```tests: 'tests/**.test.ts' ``` | +| `timeout?` | `number` | Set default tests timeout in seconds. Tests will be killed on no response after timeout. ```timeout: 20, ``` | +| `translation?` | `string` | Enable [localized test commands](https://codecept.io/translation/) | ## Require diff --git a/docs/data.md b/docs/data.md index 91d8f4a22..ac4832ac7 100644 --- a/docs/data.md +++ b/docs/data.md @@ -92,8 +92,7 @@ I.sendPostRequest('/update-status', {}, { http_x_requested_with: 'xmlhttprequest ## GraphQL [GraphQL helper](https://codecept.io/helpers/GraphQL/) allows sending GraphQL queries and mutations to application, over Http. - -This tool allows you to create shortcuts and manage your data pragmatically via a GraphQL endpoint. However, it does not include tools for testing the endpoint, so it should be used in conjunction with WebDriver helpers for browser testing. +This is a tool to make shortcuts and create your data pragmatically via GraphQL endpoint. However, it doesn't provide tools for testing the endpoint, so it should be paired with WebDriver helper for browser testing. Enable GraphQL helper in the config. It is recommended to set `endpoint`, the URL to which the requests go to. If you need some authorization you can optionally set default headers too. diff --git a/docs/helpers/FileSystem.md b/docs/helpers/FileSystem.md index 0af9843db..dca11076a 100644 --- a/docs/helpers/FileSystem.md +++ b/docs/helpers/FileSystem.md @@ -33,6 +33,10 @@ helpers: { ## Methods +### Parameters + +- `config` + ### amInPath Enters a directory In local filesystem. diff --git a/docs/hooks.md b/docs/hooks.md index 2f55ef864..3c383ed7d 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -243,7 +243,7 @@ const output = require('codeceptjs').output; output.print('This is basic information'); output.debug('This is debug information'); -output.log('This is verbose logging information'); +output.output.log('This is verbose logging information'); ``` ### Container diff --git a/docs/internal-api.md b/docs/internal-api.md index aa0846036..eefa4cb46 100644 --- a/docs/internal-api.md +++ b/docs/internal-api.md @@ -191,7 +191,7 @@ const output = require('codeceptjs').output; output.print('This is basic information'); output.debug('This is debug information'); -output.log('This is verbose logging information'); +output.output.log('This is verbose logging information'); ``` #### Test Object diff --git a/docs/plugins.md b/docs/plugins.md index e5b635474..c4d4f4cc6 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -551,7 +551,7 @@ Provides `eachElement` global function to iterate over found elements to perform `eachElement` takes following args: -- `purpose` - the goal of an action. A comment text that will be displayed in output. +- `purpose` - the goal of an action. A comment text that will be displayed in output.output. - `locator` - a CSS/XPath locator to match elements - `fn(element, index)` - **asynchronous** function which will be executed for each matched element. diff --git a/lib/actor.js b/lib/actor.js index a53a03fe4..2e7c7e25b 100644 --- a/lib/actor.js +++ b/lib/actor.js @@ -1,17 +1,16 @@ -const Step = require('./step'); -const { MetaStep } = require('./step'); -const container = require('./container'); -const { methodsOfObject } = require('./utils'); -const recorder = require('./recorder'); -const event = require('./event'); -const store = require('./store'); -const output = require('./output'); +import { Step, MetaStep } from './step.js'; +import container from './container.js'; +import { methodsOfObject } from './utils.js'; +import recorder from './recorder.js'; +import * as event from './event.js'; +import { store } from './store.js'; +import * as output from './output.js'; /** * @interface * @alias ActorStatic */ -class Actor { +export class Actor { /** * Print the comment on log. Also, adding a step in the `Test.steps` object * @param {string} msg @@ -41,7 +40,7 @@ class Actor { if (!store.timeouts) return this; event.dispatcher.prependOnceListener(event.step.before, (step) => { - output.log(`Timeout to ${step}: ${timeout}s`); + output.output.log(`Timeout to ${step}: ${timeout}s`); step.setTimeout(timeout * 1000, Step.TIMEOUT_ORDER.codeLimitTime); }); @@ -69,7 +68,8 @@ class Actor { * Wraps helper methods into promises. * @ignore */ -module.exports = function (obj = {}) { +// eslint-disable-next-line import/prefer-default-export +export function actor(obj = {}) { if (!store.actor) { store.actor = new Actor(); } @@ -117,9 +117,9 @@ module.exports = function (obj = {}) { }); return actor; -}; +} -function recordStep(step, args) { +export function recordStep(step, args) { step.status = 'queued'; step.setArguments(args); diff --git a/lib/ai.js b/lib/ai.js index 86dffcd3b..ef4bf8248 100644 --- a/lib/ai.js +++ b/lib/ai.js @@ -1,7 +1,9 @@ -const debug = require('debug')('codeceptjs:ai'); -const output = require('./output'); -const event = require('./event'); -const { removeNonInteractiveElements, minifyHtml, splitByChunks } = require('./html'); +import debug from 'debug'; +import * as event from './event.js'; +import * as output from './output.js'; +import { removeNonInteractiveElements, minifyHtml, splitByChunks } from './html.js'; + +debug('codeceptjs:ai'); const defaultHtmlConfig = { maxLength: 50000, @@ -122,8 +124,8 @@ class AiAssistant { if (this.config.request) return; const noRequestErrorMessage = ` - No request function is set for AI assistant. - Please implement your own request function and set it in the config. + No request function is set for AI assistant. + Please implement your own request function and set it in the config. [!] AI request was decoupled from CodeceptJS. To connect to OpenAI or other AI service, please implement your own request function and set it in the config. @@ -187,10 +189,11 @@ class AiAssistant { } catch (err) { debug(err.response); output.print(''); - output.error(`AI service error: ${err.message}`); - if (err?.response?.data?.error?.code) output.error(err?.response?.data?.error?.code); - if (err?.response?.data?.error?.message) output.error(err?.response?.data?.error?.message); + output.output.error(`AI service error: ${err.message}`); + if (err?.response?.data?.error?.code) output.output.error(err?.response?.data?.error?.code); + if (err?.response?.data?.error?.message) output.output.error(err?.response?.data?.error?.message); this.stopWhenReachingTokensLimit(); + return ''; } } @@ -295,4 +298,4 @@ function parseCodeBlocks(response) { return modifiedSnippets.filter(snippet => !!snippet); } -module.exports = new AiAssistant(); +export default new AiAssistant(); diff --git a/lib/assert.js b/lib/assert.js index 7e2cbc46d..08cbe7d63 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -1,4 +1,4 @@ -const AssertionFailedError = require('./assert/error'); +import AssertionFailedError from './assert/error.js'; /** * Abstract assertion class introduced for more verbose and customizable messages. @@ -20,7 +20,7 @@ const AssertionFailedError = require('./assert/error'); * to get more customizable exception messages. * */ -class Assertion { +export default class Assertion { constructor(comparator, params) { this.comparator = comparator; this.params = params || {}; @@ -68,5 +68,3 @@ class Assertion { return this.getException(); } } - -module.exports = Assertion; diff --git a/lib/assert/empty.js b/lib/assert/empty.js index 859bcbfa7..f6c864562 100644 --- a/lib/assert/empty.js +++ b/lib/assert/empty.js @@ -1,43 +1,44 @@ -const Assertion = require('../assert') -const AssertionFailedError = require('./error') -const { template } = require('../utils') -const output = require('../output') +import Assertion from '../assert.js'; +import AssertionFailedError from './error.js'; +import { template } from '../utils.js'; +import * as output from '../output.js'; 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 }), +export { EmptinessAssertion as Assertion }; + +export function empty(subject) { + return new EmptinessAssertion({ subject }); } diff --git a/lib/assert/equal.js b/lib/assert/equal.js index f8ef0a9d9..ab5917b11 100644 --- a/lib/assert/equal.js +++ b/lib/assert/equal.js @@ -1,57 +1,59 @@ -const Assertion = require('../assert') -const AssertionFailedError = require('./error') -const { template } = require('../utils') -const output = require('../output') +import Assertion from '../assert.js'; +import AssertionFailedError from './error.js'; +import { template } from '../utils.js'; +import * as output from '../output.js'; 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 }), - urlEquals: (baseUrl) => { - 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) - } - return actual === expected +export function fileEquals(file) { + return new EqualityAssertion({ file, jar: 'contents of {{file}}' }); +} + +export { EqualityAssertion as Assertion }; +export function equals(jar) { + return new EqualityAssertion({ jar }); +} + +export function urlEquals(baseUrl) { + 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); } - return assert - }, - fileEquals: (file) => new EqualityAssertion({ file, jar: 'contents of {{file}}' }), + return actual === expected; + }; + return assert; } diff --git a/lib/assert/error.js b/lib/assert/error.js index ed72233ec..4803e1b1c 100644 --- a/lib/assert/error.js +++ b/lib/assert/error.js @@ -1,4 +1,4 @@ -const subs = require('../utils').template +import { template as subs } from '../utils.js'; /** * 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 +export default AssertionFailedError; diff --git a/lib/assert/include.js b/lib/assert/include.js index 3899f4ca0..fde32657d 100644 --- a/lib/assert/include.js +++ b/lib/assert/include.js @@ -1,78 +1,79 @@ -const Assertion = require('../assert') -const AssertionFailedError = require('./error') -const { template } = require('../utils') -const output = require('../output') +import Assertion from '../assert.js'; +import AssertionFailedError from './error.js'; +import { template } from '../utils.js'; +import * as output from '../output.js'; -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('{{jar}}', output.output.colors.bold('{{jar}}')) + .replace('{{needle}}', output.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.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 }) - }, - fileIncludes: (file) => new InclusionAssertion({ file, jar: 'file {{file}}' }), +export { InclusionAssertion as Assertion }; +export function includes(needleType) { + needleType = needleType || 'string'; + return new InclusionAssertion({ jar: needleType }); } function escapeRegExp(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); +} + +export function fileIncludes(file) { + return new InclusionAssertion({ file, jar: 'file {{file}}' }); } diff --git a/lib/assert/throws.js b/lib/assert/throws.js index c0e92276c..731c7f075 100644 --- a/lib/assert/throws.js +++ b/lib/assert/throws.js @@ -1,22 +1,20 @@ 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 +export default errorThrown; diff --git a/lib/assert/truth.js b/lib/assert/truth.js index 51dbe2def..ed2874005 100644 --- a/lib/assert/truth.js +++ b/lib/assert/truth.js @@ -1,36 +1,39 @@ -const Assertion = require('../assert') -const AssertionFailedError = require('./error') -const { template } = require('../utils') -const output = require('../output') +import Assertion from '../assert.js'; +import AssertionFailedError from './error.js'; +import { template } from '../utils.js'; +import * as output from '../output.js'; 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 }), +export function truth(subject, type) { + return new TruthAssertion({ subject, type }); } +export default { + Assertion: TruthAssertion, +}; diff --git a/lib/cli.js b/lib/cli.js index 0d02c7765..b3e974436 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,9 +1,12 @@ -const { reporters: { Base } } = require('mocha'); -const ms = require('ms'); -const event = require('./event'); -const AssertionFailedError = require('./assert/error'); -const output = require('./output'); -const { MetaStep } = require('./step'); +import mocha from 'mocha'; +import ms from 'ms'; +import * as event from './event.js'; +import * as output from './output.js'; +import AssertionFailedError from './assert/error.js'; +import * as Codecept from './codecept.js'; +import container from './container.js'; + +const { reporters: { Base } } = mocha; const cursor = Base.cursor; let currentMetaStep = []; @@ -18,16 +21,16 @@ class Cli extends Base { if (opts.steps) level = 1; if (opts.debug) level = 2; if (opts.verbose) level = 3; - output.level(level); - output.print(`CodeceptJS v${require('./codecept').version()} ${output.standWithUkraine()}`); - output.print(`Using test root "${global.codecept_dir}"`); + output.output.level(level); + const version = Codecept.version(); + output.output.print(`CodeceptJS v${version} ${output.output.standWithUkraine()}`); + output.output.print(`Using test root "${global.codecept_dir}"`); const showSteps = level >= 1; if (level >= 2) { - const Containter = require('./container'); - output.print(output.styles.debug(`Helpers: ${Object.keys(Containter.helpers()).join(', ')}`)); - output.print(output.styles.debug(`Plugins: ${Object.keys(Containter.plugins()).join(', ')}`)); + output.output.print(output.output.styles.debug(`Helpers: ${Object.keys(container.helpers()).join(', ')}`)); + output.output.print(output.output.styles.debug(`Plugins: ${Object.keys(container.plugins()).join(', ')}`)); } runner.on('start', () => { @@ -35,7 +38,7 @@ class Cli extends Base { }); runner.on('suite', (suite) => { - output.suite.started(suite); + output.output.suite.started(suite); }); runner.on('fail', (test) => { @@ -43,10 +46,10 @@ class Cli extends Base { this.loadedTests.push(test.ctx.currentTest.uid); } if (showSteps && test.steps) { - return output.scenario.failed(test); + return output.output.scenario.failed(test); } cursor.CR(); - output.test.failed(test); + output.output.test.failed(test); }); runner.on('pending', (test) => { @@ -59,22 +62,22 @@ class Cli extends Base { } this.loadedTests.push(test.uid); cursor.CR(); - output.test.skipped(test); + output.output.test.skipped(test); }); runner.on('pass', (test) => { if (showSteps && test.steps) { - return output.scenario.passed(test); + return output.output.scenario.passed(test); } cursor.CR(); - output.test.passed(test); + output.output.test.passed(test); }); if (showSteps) { runner.on('test', (test) => { currentMetaStep = []; if (test.steps) { - output.test.started(test); + output.output.test.started(test); } }); @@ -82,8 +85,8 @@ class Cli extends Base { codeceptjsEventDispatchersRegistered = true; event.dispatcher.on(event.bddStep.started, (step) => { - output.stepShift = 2; - output.step(step); + output.output.stepShift = 2; + output.output.step(step); }); event.dispatcher.on(event.step.started, (step) => { @@ -97,22 +100,22 @@ class Cli extends Base { for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) { if (currentMetaStep[i] !== metaSteps[i]) { - output.stepShift = 3 + 2 * i; + output.output.stepShift = 3 + 2 * i; if (!metaSteps[i]) continue; // bdd steps are handled by bddStep.started if (metaSteps[i].isBDD()) continue; - output.step(metaSteps[i]); + output.output.step(metaSteps[i]); } } currentMetaStep = metaSteps; - output.stepShift = 3 + 2 * shift; + output.output.stepShift = 3 + 2 * shift; if (step.helper.constructor.name !== 'ExpectHelper') { - output.step(step); + output.output.step(step); } }); event.dispatcher.on(event.step.finished, () => { - output.stepShift = 0; + output.output.stepShift = 0; }); } } @@ -130,7 +133,7 @@ class Cli extends Base { test.opts.skipInfo = {}; } skipTestConfig(test, 'Skipped due to failure in \'before\' hook'); - output.test.skipped(test); + output.output.test.skipped(test); skippedCount += 1; } } @@ -145,12 +148,11 @@ class Cli extends Base { result() { const stats = this.stats; - stats.failedHooks = 0; console.log(); // passes if (stats.failures) { - output.print(output.styles.bold('-- FAILURES:')); + output.output.print(output.output.styles.bold('-- FAILURES:')); } const failuresLog = []; @@ -176,35 +178,31 @@ class Cli extends Base { // if (step.status === 'failed') line = '' + line; scenarioTrace += `\n${line}`; }); - log += `${output.styles.bold('Scenario Steps')}:${scenarioTrace}\n`; + log += `${output.output.styles.bold('Scenario Steps')}:${scenarioTrace}\n`; } // display artifacts in debug mode if (test?.artifacts && Object.keys(test.artifacts).length) { - log += `\n${output.styles.bold('Artifacts:')}`; + log += `\n${output.output.styles.bold('Artifacts:')}`; for (const artifact of Object.keys(test.artifacts)) { log += `\n- ${artifact}: ${test.artifacts[artifact]}`; } } - try { - let stack = err.stack ? err.stack.split('\n') : []; - if (stack[0] && stack[0].includes(err.message)) { - stack.shift(); - } + let stack = err.stack ? err.stack.split('\n') : []; + if (stack[0] && stack[0].includes(err.message)) { + stack.shift(); + } - if (output.level() < 3) { - stack = stack.slice(0, 3); - } + if (output.output.level() < 3) { + stack = stack.slice(0, 3); + } - err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`; + err.stack = `${stack.join('\n')}\n\n${output.output.colors.blue(log)}`; - // clone err object so stack trace adjustments won't affect test other reports - test.err = err; - return test; - } catch (e) { - throw Error(e); - } + // clone err object so stack trace adjustments won't affect test other reports + test.err = err; + return test; }); const originalLog = Base.consoleLog; @@ -217,17 +215,11 @@ class Cli extends Base { console.log(); } - this.failures.forEach((failure) => { - if (failure.constructor.name === 'Hook') { - stats.failures -= stats.failures - stats.failedHooks += 1 - } - }) event.emit(event.all.failures, { failuresLog, stats }); - output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks); + output.output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration)); - if (stats.failures && output.level() < 3) { - output.print(output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace')); + if (stats.failures && output.output.level() < 3) { + output.output.print(output.output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace')); } } } @@ -252,6 +244,6 @@ function skipTestConfig(test, message) { test.state = 'skipped'; } -module.exports = function (runner, opts) { +export default function cli(runner, opts) { return new Cli(runner, opts); -}; +} diff --git a/lib/codecept.js b/lib/codecept.js index fe749fd6e..4959457c6 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -1,215 +1,228 @@ -const { existsSync, readFileSync } = require('fs'); -const glob = require('glob'); -const fsPath = require('path'); -const { resolve } = require('path'); - -const container = require('./container'); -const Config = require('./config'); -const event = require('./event'); -const runHook = require('./hooks'); -const output = require('./output'); -const { emptyFolder } = require('./utils'); +import { existsSync, readFileSync } from 'fs'; +import glob from 'glob'; +import { resolve, dirname, isAbsolute, join } from 'path'; +import { fileURLToPath } from 'url'; +import { createRequire } from 'node:module'; +import container from './container.js'; +import Config from './config.js'; +import * as event from './event.js'; +import runHook from './hooks.js'; +import * as output from './output.js'; +import { emptyFolder } from './utils.js'; +import * as index from './index.js'; + +// Helpers and features +import * as actor from './actor.js'; +import pause from './pause.js'; +import within from './within.js'; +import session from './session.js'; +import data from './data/table.js'; +import Locator from './locator.js'; +import secret from './secret.js'; +import * as stepDefinitions from './interfaces/bdd.js'; + +// Listeners +import listener from './listener/steps.js'; +import listenerArtifacts from './listener/artifacts.js'; +import listenerConfig from './listener/config.js'; +import listenerHelpers from './listener/helpers.js'; +import listenerRetry from './listener/retry.js'; +import listenerTimeout from './listener/timeout.js'; +import listenerExit from './listener/exit.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); /** - * CodeceptJS runner + * CodeceptJS runner class. */ -class Codecept { +export default class Codecept { /** - * Create CodeceptJS runner. - * Config and options should be passed - * - * @param {*} config - * @param {*} opts + * Initializes CodeceptJS runner with config and options. + * @param {Object} config - Configuration object. + * @param {Object} opts - Options. */ constructor(config, opts) { this.config = Config.create(config); this.opts = opts; - this.testFiles = new Array(0); - this.requireModules(config.require); + this.testFiles = []; + this.requireModules(config?.require); } /** - * Require modules before codeceptjs running - * - * @param {string[]} requiringModules + * Requires necessary modules before running CodeceptJS. + * @param {string[]} requiringModules - List of modules to require. */ requireModules(requiringModules) { - if (requiringModules) { - requiringModules.forEach((requiredModule) => { - const isLocalFile = existsSync(requiredModule) || existsSync(`${requiredModule}.js`); - if (isLocalFile) { - requiredModule = resolve(requiredModule); - } - require(requiredModule); - }); - } + requiringModules?.forEach((requiredModule) => { + const isLocalFile = existsSync(requiredModule) || existsSync(`${requiredModule}.js`); + const modulePath = isLocalFile ? resolve(requiredModule) : requiredModule; + require(modulePath); + }); } /** - * Initialize CodeceptJS at specific directory. - * If async initialization is required, pass callback as second parameter. - * - * @param {string} dir + * Initializes CodeceptJS in a specific directory. + * @param {string} dir - Directory path. */ init(dir) { this.initGlobals(dir); - // initializing listeners container.create(this.config, this.opts); this.runHooks(); } /** - * Creates global variables - * - * @param {string} dir + * Initializes global variables. + * @param {string} dir - Directory path. */ initGlobals(dir) { global.codecept_dir = dir; - global.output_dir = fsPath.resolve(dir, this.config.output); + global.output_dir = resolve(dir, this.config.output); if (this.config.emptyOutputFolder) emptyFolder(global.output_dir); if (!this.config.noGlobals) { - global.Helper = global.codecept_helper = require('@codeceptjs/helper'); - global.actor = global.codecept_actor = require('./actor'); - global.pause = require('./pause'); - global.within = require('./within'); - global.session = require('./session'); - global.DataTable = require('./data/table'); - global.locate = locator => require('./locator').build(locator); - global.inject = container.support; - global.share = container.share; - global.secret = require('./secret').secret; - global.codecept_debug = output.debug; - global.codeceptjs = require('./index'); // load all objects - - // BDD - const stepDefinitions = require('./interfaces/bdd'); - global.Given = stepDefinitions.Given; - global.When = stepDefinitions.When; - global.Then = stepDefinitions.Then; - global.DefineParameterType = stepDefinitions.defineParameterType; - - // debug mode - global.debugMode = false; + this.initGlobalHelpers(); } } /** - * Executes hooks. + * Initializes global helpers and other CodeceptJS features. + */ + initGlobalHelpers() { + global.Helper = global.codecept_helper = index.generated; + global.actor = global.codecept_actor = actor; + global.pause = pause; + global.within = within; + global.session = session; + global.DataTable = data; + global.locate = (locator) => new Locator(locator); + global.inject = container.support; + global.share = container.share; + global.secret = secret; + global.codecept_debug = output.debug; + global.codeceptjs = index; + + // BDD + global.Given = stepDefinitions.Given; + global.When = stepDefinitions.When; + global.Then = stepDefinitions.Then; + global.DefineParameterType = stepDefinitions.defineParameterType; + global.debugMode = false; + } + + /** + * Runs all hooks, including custom and default. */ runHooks() { - // default hooks - runHook(require('./listener/steps')); - runHook(require('./listener/artifacts')); - runHook(require('./listener/config')); - runHook(require('./listener/helpers')); - runHook(require('./listener/retry')); - runHook(require('./listener/timeout')); - runHook(require('./listener/exit')); - - // custom hooks (previous iteration of plugins) - this.config.hooks.forEach(hook => runHook(hook)); + const listeners = [ + listener, + listenerArtifacts, + listenerConfig, + listenerHelpers, + listenerRetry, + listenerTimeout, + listenerExit, + ]; + + listeners.forEach(runHook); + + // Run custom hooks + this.config.hooks.forEach(runHook); } /** - * Executes bootstrap. - * + * Executes the bootstrap process. */ async bootstrap() { return runHook(this.config.bootstrap, 'bootstrap'); } /** - * Executes teardown. - + * Executes the teardown process. */ async teardown() { return runHook(this.config.teardown, 'teardown'); } /** - * Loads tests by pattern or by config.tests - * - * @param {string} [pattern] + * Loads test files based on the given pattern or config. + * @param {string} [pattern] - Optional pattern for loading tests. */ loadTests(pattern) { - const options = { - cwd: global.codecept_dir, - }; - - let patterns = [pattern]; - if (!pattern) { - patterns = []; - - // If the user wants to test a specific set of test files as an array or string. - if (this.config.tests && !this.opts.features) { - if (Array.isArray(this.config.tests)) { - patterns.push(...this.config.tests); - } else { - patterns.push(this.config.tests); + const patterns = this.getTestPatterns(pattern); + const options = { cwd: global.codecept_dir }; + + patterns.forEach((p) => { + glob.sync(p, options).forEach((file) => { + if (!file.includes('node_modules')) { + const fullPath = isAbsolute(file) ? file : join(global.codecept_dir, file); + const resolvedFile = resolve(fullPath); + + if (!this.testFiles.includes(resolvedFile)) { + this.testFiles.push(resolvedFile); + } } - } + }); + }); + } - if (this.config.gherkin.features && !this.opts.tests) { - if (Array.isArray(this.config.gherkin.features)) { - this.config.gherkin.features.forEach(feature => { - patterns.push(feature); - }); - } else { - patterns.push(this.config.gherkin.features); - } - } + /** + * Gets test patterns based on config and options. + * @param {string} [pattern] - Test pattern to match. + * @returns {string[]} - Array of test patterns. + */ + getTestPatterns(pattern) { + if (pattern) return [pattern]; + + const patterns = []; + const { tests, gherkin } = this.config; + + if (tests && !this.opts.features) { + patterns.push(...(Array.isArray(tests) ? tests : [tests])); } - for (pattern of patterns) { - glob.sync(pattern, options).forEach((file) => { - if (file.includes('node_modules')) return; - if (!fsPath.isAbsolute(file)) { - file = fsPath.join(global.codecept_dir, file); - } - if (!this.testFiles.includes(fsPath.resolve(file))) { - this.testFiles.push(fsPath.resolve(file)); - } - }); + if (gherkin?.features && !this.opts.tests) { + patterns.push(...(Array.isArray(gherkin.features) ? gherkin.features : [gherkin.features])); } + + return patterns; } /** - * Run a specific test or all loaded tests. - * - * @param {string} [test] + * Runs tests either specific to a file or all loaded tests. + * @param {string} [test] - Test file to run. * @returns {Promise} */ async run(test) { - return new Promise((resolve, reject) => { - const mocha = container.mocha(); - mocha.files = this.testFiles; - if (test) { - if (!fsPath.isAbsolute(test)) { - test = fsPath.join(global.codecept_dir, test); - } - mocha.files = mocha.files.filter(t => fsPath.basename(t, '.js') === test || t === test); - } - const done = () => { - event.emit(event.all.result, this); - event.emit(event.all.after, this); - resolve(); - }; + const mocha = container.mocha(); + mocha.files = this.testFiles; + if (test) { + const testPath = isAbsolute(test) ? test : join(global.codecept_dir, test); + mocha.files = mocha.files.filter((t) => resolve(t) === resolve(testPath)); + } + + return new Promise((resolve, reject) => { try { event.emit(event.all.before, this); - mocha.run(() => done()); - } catch (e) { - output.error(e.stack); - reject(e); + mocha.run(() => { + event.emit(event.all.result, this); + event.emit(event.all.after, this); + resolve(); + }); + } catch (error) { + output.output.error(error.stack); + reject(error); } }); } - - static version() { - return JSON.parse(readFileSync(`${__dirname}/../package.json`, 'utf8')).version; - } } -module.exports = Codecept; +/** + * Retrieves the version from package.json. + * @returns {string} - The version of the package. + */ +export function version() { + return JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8')).version; +} diff --git a/lib/colorUtils.js b/lib/colorUtils.js index ce1f6465a..c344d9df5 100644 --- a/lib/colorUtils.js +++ b/lib/colorUtils.js @@ -189,7 +189,7 @@ function convertHexColorToRgba(hex) { * * @param {string} color Color as a string, i.e. rgb(85,0,0) */ -function convertColorToRGBA(color) { +export function convertColorToRGBA(color) { const cstr = `${color}`.toLowerCase().trim() || ''; if (!/^rgba?\(.+?\)$/.test(cstr)) { @@ -221,7 +221,7 @@ function convertColorToRGBA(color) { * * @param {string} prop CSS Property name */ -function isColorProperty(prop) { +export function isColorProperty(prop) { return [ 'color', 'background', @@ -248,7 +248,7 @@ function isColorProperty(prop) { ].indexOf(prop) > -1; } -module.exports = { +export default { isColorProperty, convertColorToRGBA, convertColorNameToHex, diff --git a/lib/command/configMigrate.js b/lib/command/configMigrate.js index 0101c9cc2..1db0afb51 100644 --- a/lib/command/configMigrate.js +++ b/lib/command/configMigrate.js @@ -1,76 +1,70 @@ -const colors = require('chalk') -const fs = require('fs') -const inquirer = require('inquirer') -const mkdirp = require('mkdirp') -const path = require('path') -const util = require('util') +import colors from 'chalk'; +import fs from 'fs'; +import inquirer from 'inquirer'; +import mkdirp from 'mkdirp'; +import path from 'path'; +import util from 'util'; +import * as outputLib from '../output.js'; +import { fileExists } from '../utils.js'; +import { getTestRoot } from './utils.js'; -const { print, success, error } = require('../output') -const { fileExists } = require('../utils') -const { getTestRoot } = require('./utils') +export default function (initPath) { + const testsPath = getTestRoot(initPath); -module.exports = function (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() + outputLib.print(); + outputLib.print(` Welcome to ${colors.magenta.bold('CodeceptJS')} configuration migration tool`); + outputLib.print(` It will help you switch from ${colors.cyan.bold('.json')} to ${colors.magenta.bold('.js')} config format at ease`); + outputLib.print(); if (!path) { - print('No config file is specified.') - print(`Test root is assumed to be ${colors.yellow.bold(testsPath)}`) - print('----------------------------------') + outputLib.print('No config file is specified.'); + outputLib.print(`Test root is assumed to be ${colors.yellow.bold(testsPath)}`); + outputLib.print('----------------------------------'); } else { - print(`Migrating ${colors.magenta.bold('.js')} config to ${colors.bold(testsPath)}`) + outputLib.print(`Migrating ${colors.magenta.bold('.js')} config to ${colors.bold(testsPath)}`); } if (!fileExists(testsPath)) { - print(`Directory ${testsPath} does not exist, creating...`) - mkdirp.sync(testsPath) + outputLib.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 + outputLib.output.output.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'); + outputLib.output.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); + outputLib.output.success('JSON config file is deleted!'); } } - finish() - } - }) + }; + finish(); + } + }); } diff --git a/lib/command/definitions.js b/lib/command/definitions.js index cfa4ae459..86f35a976 100644 --- a/lib/command/definitions.js +++ b/lib/command/definitions.js @@ -1,11 +1,12 @@ -const fs = require('fs') -const path = require('path') +import fs from 'fs'; +import path from 'path'; +import { getConfig, getTestRoot } from './utils.js'; +import Codecept from '../codecept.js'; +import container from '../container.js'; +import * as output from '../output.js'; +import plugin from '../plugin/standardActingHelpers.js'; -const { getConfig, getTestRoot } = require('./utils') -const Codecept = require('../codecept') -const container = require('../container') -const output = require('../output') -const actingHelpers = [...require('../plugin/standardActingHelpers'), 'REST'] +const actingHelpers = [...(plugin), 'REST']; /** * Prepare data and generate content of definitions file @@ -29,27 +30,32 @@ 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, @@ -57,8 +63,8 @@ const getDefinitionsFileContent = ({ supportObjectsTypeFragment, methodsTypeFragment, translatedActionsFragment, - }) -} + }); +}; /** * Generate content for definitions file from fragments @@ -91,83 +97,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 +export default function (genPath, options) { + 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({ @@ -177,33 +183,30 @@ 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'); } /** @@ -213,13 +216,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( @@ -227,9 +230,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}`; } /** @@ -243,19 +246,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; } /** @@ -264,7 +267,5 @@ 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 0de23df4f..5f3c2a63f 100644 --- a/lib/command/dryRun.js +++ b/lib/command/dryRun.js @@ -1,122 +1,119 @@ -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 - - const testRoot = getTestRoot(configFile) - let config = getConfig(configFile) +import { getConfig, getTestRoot } from './utils.js'; +import Config from '../config.js'; +import Codecept from '../codecept.js'; +import { output } from '../output.js'; +import * as event from '../event.js'; +import { store } from '../store.js'; +import container from '../container.js'; +import colors from "chalk"; +import figures from "figures"; + +export default async function (test, options) { + if (options.grep) process.env.grep = options.grep.toLowerCase(); + const configFile = options.config; + let codecept; + + 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') + 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) { + for (const 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++ - - for (test of suite.tests) { - numOfTests++ - output.print(` ${output.styles.scenario(figures.checkboxOff)} ${test.title}`) + output.print(`${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')} -- ${mocha.suite.suites.length} tests`); + numOfSuites++; + + for (const test of suite.tests) { + 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 914b11bca..b3979a9cf 100644 --- a/lib/command/generate.js +++ b/lib/command/generate.js @@ -1,80 +1,81 @@ -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' +import colors from 'chalk'; +import fs from 'fs'; +import inquirer from 'inquirer'; +import mkdirp from 'mkdirp'; +import path from 'path'; +import { + fileExists, ucfirst, lcfirst, beautify, +} from '../utils.js'; +import { output } from '../output.js'; +import generateDefinitions from './definitions.js'; +import { + getConfig, getTestRoot, safeFileWrite, readConfig, +} from './utils.js'; +import container from '../container.js'; +import { __dirname } from '../dirname.js'; + +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, +export const 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; }, - { - 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', '') + }, + ]).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); + + 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 +83,7 @@ module.exports = { // insert your locators and methods here } -` +`; const poModuleTemplateTS = `const { I } = inject(); @@ -90,7 +91,7 @@ export = { // insert your locators and methods here } -` +`; const poClassTemplate = `const { I } = inject(); @@ -105,101 +106,96 @@ 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 +export const pageObject = function (genPath, opts) { + 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 - } - actor = `require('${actorPath}')` + 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}')`; + } - 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'); @@ -227,78 +223,73 @@ 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): +`; + +export const 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): 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) +export function heal(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') exports.config = { - // ... + // ... plugins: { heal: { enabled: true } } } - `) + `); - 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/gherkin/init.js b/lib/command/gherkin/init.js index a909c27bb..c84fee7fe 100644 --- a/lib/command/gherkin/init.js +++ b/lib/command/gherkin/init.js @@ -1,11 +1,10 @@ -const path = require('path'); -const mkdirp = require('mkdirp'); - -const output = require('../../output'); -const { fileExists } = require('../../utils'); -const { - getConfig, getTestRoot, updateConfig, safeFileWrite, findConfigFile, -} = require('../utils'); +import path from 'path'; +import mkdirp from 'mkdirp'; +import * as output from '../../output.js'; +import { fileExists } from '../../utils.js'; +import { + getConfig, getTestRoot, updateConfig, safeFileWrite, +} from '../utils.js'; const featureFile = `Feature: Business rules In order to achieve my goals @@ -24,25 +23,15 @@ Given('I have a defined step', () => { }); `; -module.exports = function (genPath) { +export default function (genPath) { const testsPath = getTestRoot(genPath); - const configFile = findConfigFile(testsPath); - - if (!configFile) { - output.error( - "Can't initialize Gherkin. This command must be run in an already initialized project.", - ); - process.exit(1); - } - const config = getConfig(testsPath); - const extension = path.extname(configFile).substring(1); output.print('Initializing Gherkin (Cucumber BDD) for CodeceptJS'); output.print('--------------------------'); if (config.gherkin && config.gherkin.steps) { - output.error('Gherkin is already initialized in this project. See `gherkin` section in the config'); + output.output.error('Gherkin is already initialized in this project. See `gherkin` section in the config'); process.exit(1); } @@ -63,19 +52,19 @@ module.exports = function (genPath) { output.success(`Created ${dir}, place step definitions into it`); } - if (safeFileWrite(path.join(dir, `steps.${extension}`), stepsFile)) { - output.success( - `Created sample steps file: step_definitions/steps.${extension}`, - ); + if (safeFileWrite(path.join(dir, 'steps.js'), stepsFile)) { + output.success('Created sample steps file: step_definitions/steps.js'); } config.gherkin = { features: './features/*.feature', - steps: [`./step_definitions/steps.${extension}`], + steps: [ + './step_definitions/steps.js', + ], }; - updateConfig(testsPath, config, extension); + updateConfig(testsPath, config); output.success('Gherkin setup is done.'); output.success('Start writing feature files and implement corresponding steps.'); -}; +} diff --git a/lib/command/gherkin/snippets.js b/lib/command/gherkin/snippets.js index e0f92e11a..461b11aee 100644 --- a/lib/command/gherkin/snippets.js +++ b/lib/command/gherkin/snippets.js @@ -1,14 +1,13 @@ -const escapeStringRegexp = require('escape-string-regexp'); -const fs = require('fs'); -const Gherkin = require('@cucumber/gherkin'); -const Messages = require('@cucumber/messages'); -const glob = require('glob'); -const fsPath = require('path'); - -const { getConfig, getTestRoot } = require('../utils'); -const Codecept = require('../../codecept'); -const output = require('../../output'); -const { matchStep } = require('../../interfaces/bdd'); +import escapeStringRegexp from 'escape-string-regexp'; +import fs from 'fs'; +import Gherkin from '@cucumber/gherkin'; +import * as Messages from '@cucumber/messages'; +import glob from 'glob'; +import fsPath from 'path'; +import { getConfig, getTestRoot } from '../utils.js'; +import Codecept from '../../codecept.js'; +import * as output from '../../output.js'; +import { matchStep } from '../../interfaces/bdd.js'; const uuidFn = Messages.IdGenerator.uuid(); const builder = new Gherkin.AstBuilder(uuidFn); @@ -16,7 +15,7 @@ const matcher = new Gherkin.GherkinClassicTokenMatcher(); const parser = new Gherkin.Parser(builder, matcher); parser.stopAtFirstError = false; -module.exports = function (genPath, options) { +export default function (genPath, options) { const configFile = options.config || genPath; const testsPath = getTestRoot(configFile); const config = getConfig(configFile); @@ -26,19 +25,19 @@ module.exports = function (genPath, options) { codecept.init(testsPath); if (!config.gherkin) { - output.error('Gherkin is not enabled in config. Run `codecept gherkin:init` to enable it'); + output.output.error('Gherkin is not enabled in config. Run `codecept gherkin:init` to enable it'); process.exit(1); } if (!config.gherkin.steps || !config.gherkin.steps[0]) { - output.error('No gherkin steps defined in config. Exiting'); + output.output.error('No gherkin steps defined in config. Exiting'); process.exit(1); } if (!options.feature && !config.gherkin.features) { - output.error('No gherkin features defined in config. Exiting'); + output.output.error('No gherkin features defined in config. Exiting'); process.exit(1); } if (options.path && !config.gherkin.steps.includes(options.path)) { - output.error(`You must include ${options.path} to the gherkin steps in your config file`); + output.output.error(`You must include ${options.path} to the gherkin steps in your config file`); process.exit(1); } @@ -87,6 +86,7 @@ module.exports = function (genPath, options) { }; const parseFile = (file) => { + console.log(file); const ast = parser.parse(fs.readFileSync(file).toString()); for (const child of ast.feature.children) { if (child.scenario.keyword === 'Scenario Outline') continue; // skip scenario outline @@ -100,7 +100,7 @@ module.exports = function (genPath, options) { let stepFile = options.path || config.gherkin.steps[0]; if (!fs.existsSync(stepFile)) { - output.error(`Please enter a valid step file path ${stepFile}`); + output.output.error(`Please enter a valid step file path ${stepFile}`); process.exit(1); } @@ -122,11 +122,11 @@ ${step.type}(${step.regexp ? '/^' : "'"}${step}${step.regexp ? '$/' : "'"}, () = output.print('No new snippets found'); return; } - output.success(`Snippets generated: ${snippets.length}`); + output.output.success(`Snippets generated: ${snippets.length}`); output.print(snippets.join('\n')); if (!options.dryRun) { - output.success(`Snippets added to ${output.colors.bold(stepFile)}`); + output.output.success(`Snippets added to ${output.output.colors.bold(stepFile)}`); fs.writeFileSync(stepFile, fs.readFileSync(stepFile).toString() + snippets.join('\n') + '\n'); // eslint-disable-line } -}; +} diff --git a/lib/command/gherkin/steps.js b/lib/command/gherkin/steps.js index ee149eb5b..2e6e63e47 100644 --- a/lib/command/gherkin/steps.js +++ b/lib/command/gherkin/steps.js @@ -1,9 +1,9 @@ -const { getConfig, getTestRoot } = require('../utils'); -const Codecept = require('../../codecept'); -const output = require('../../output'); -const { getSteps } = require('../../interfaces/bdd'); +import { getConfig, getTestRoot } from '../utils.js'; +import Codecept from '../../codecept.js'; +import * as output from '../../output.js'; +import { getSteps } from '../../interfaces/bdd.js'; -module.exports = function (genPath, options) { +export default function (genPath, options) { const configFile = options.config || genPath; const testsPath = getTestRoot(configFile); const config = getConfig(configFile); @@ -16,10 +16,10 @@ module.exports = function (genPath, options) { output.print(); const steps = getSteps(); for (const step of Object.keys(steps)) { - output.print(` ${output.colors.bold(step)} ${output.colors.green(steps[step].line || '')}`); + output.print(` ${output.output.colors.bold(step)} ${output.output.colors.green(steps[step].line || '')}`); } output.print(); if (!Object.keys(steps).length) { - output.error('No Gherkin steps defined'); + output.output.error('No Gherkin steps defined'); } -}; +} diff --git a/lib/command/info.js b/lib/command/info.js index 55f9856c3..3f7a7879f 100644 --- a/lib/command/info.js +++ b/lib/command/info.js @@ -1,46 +1,44 @@ -const envinfo = require('envinfo') +import envinfo from 'envinfo'; +import { getConfig, getTestRoot } from './utils.js'; +import Codecept from '../codecept.js'; +import * as codeceptInstance from '../codecept.js'; +import * as output from '../output.js'; -const { getConfig, getTestRoot } = require('./utils') -const Codecept = require('../codecept') -const output = require('../output') +export default async function (path) { + const testsPath = getTestRoot(path); + const config = getConfig(testsPath); + const codecept = new Codecept(config, {}); + codecept.init(testsPath); -module.exports = async function (path) { - 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 = codeceptInstance.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 () => { +export async function getMachineInfo() { const info = { nodeInfo: await envinfo.helpers.getNodeInfo(), osInfo: await envinfo.helpers.getOSInfo(), @@ -49,17 +47,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 dc7027a77..8254a0a1f 100644 --- a/lib/command/init.js +++ b/lib/command/init.js @@ -1,37 +1,38 @@ -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() +import colors from 'chalk'; +import fs from 'fs'; +import inquirer from 'inquirer'; +import mkdirp from 'mkdirp'; +import path from 'path'; +import { inspect } from 'util'; +import spawn from 'cross-spawn'; +import * as output from '../output.js'; +import { fileExists, beautify, installedLocally } from '../utils.js'; +import { getTestRoot } from './utils.js'; +import generateDefinitions from './definitions.js'; +import { test as generateTest } from './generate.js'; +import * as translations0 from '../../translations/index.js'; + +const isLocal = 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(translations0); -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 +42,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 +54,7 @@ module.exports = function() { }); } -` +`; const defaultActorTs = `// in this file you can append custom step methods to 'I' object @@ -65,357 +66,354 @@ 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() +`; + +export default 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(); 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') - } + 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 + const config = defaultConfig; + config.name = testsPath.split(path.sep).pop(); + config.output = result.output; - config.tests = result.tests - if (isTypeScript) { - config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}` - } + config.tests = result.tests; + if (isTypeScript) { + config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`; + } - // 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])) - } + // 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 + if (result.translation !== noTranslation) config.translation = result.translation; - const helperName = result.helper - config.helpers[helperName] = {} + const helperName = result.helper; + config.helpers[helperName] = {}; - if (result.jsonResponse === true) { - config.helpers.JSONResponse = {} - } + if (result.jsonResponse === true) { + config.helpers.JSONResponse = {}; + } - let helperConfigs = [] + let helperConfigs = []; - try { - const Helper = require(`../helper/${helperName}`) - if (Helper._checkRequirements) { - packages.concat(Helper._checkRequirements()) - } - - 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) + try { + const Helper = require(`../helper/${helperName}`); + if (Helper._checkRequirements) { + packages.concat(Helper._checkRequirements()); } - 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) + 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); + } - if (isTypeScript) { - config.include = _actorTranslation('./steps_file', config.translation) - } else { - config.include = _actorTranslation(stepFile, config.translation) - } + 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); - print(`Steps file created at ${stepFile}`) + if (isTypeScript) { + config.include = _actorTranslation('./steps_file', config.translation); + } else { + config.include = _actorTranslation(stepFile, config.translation); + } - let configSource - const hasConfigure = isLocal && !initPath + print(`Steps file created at ${stepFile}`); - if (isTypeScript) { - configSource = beautify(`export const config : CodeceptJS.MainConfig = ${inspect(config, false, 4, false)}`) + let configSource; + const hasConfigure = isLocal && !initPath; - if (hasConfigure) configSource = importCodeceptConfigure + configHeader + configSource + if (isTypeScript) { + configSource = beautify(`export const config : CodeceptJS.MainConfig = ${inspect(config, false, 4, false)}`); - fs.writeFileSync(typeScriptconfigFile, configSource, 'utf-8') - print(`Config created at ${typeScriptconfigFile}`) - } else { - configSource = beautify( - `/** @type {CodeceptJS.MainConfig} */\nexports.config = ${inspect(config, false, 4, false)}`, - ) + if (hasConfigure) configSource = importCodeceptConfigure + configHeader + configSource; - if (hasConfigure) configSource = requireCodeceptConfigure + configHeader + configSource + fs.writeFileSync(typeScriptconfigFile, configSource, 'utf-8'); + print(`Config created at ${typeScriptconfigFile}`); + } else { + configSource = beautify(`/** @type {CodeceptJS.MainConfig} */\nexports.config = ${inspect(config, false, 4, false)}`); - fs.writeFileSync(configFile, configSource, 'utf-8') - print(`Config created at ${configFile}`) - } + if (hasConfigure) configSource = requireCodeceptConfigure + 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}'`) - } else { - print(`Directory for temporary output files is already created at '${config.output}'`) - } - } + fs.writeFileSync(configFile, configSource, 'utf-8'); + print(`Config created at ${configFile}`); + } - const jsconfig = { - compilerOptions: { - allowJs: true, - }, + 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}'`); } + } - 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'], - } + 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 (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) - } + 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 { - 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}`) - } + fs.writeFileSync(tsconfigFile, tsconfigJson); } - - 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 - } + } else { + 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')}`); + + if (packages) { try { - generateDefinitions(testsPath, {}) + install(packages); } catch (err) { - print(colors.bold.red("Couldn't generate type definitions")) - print(colors.red(err.toString())) - print('Skipping type definitions...') - print(generateDefinitionsManually) + 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('') - 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 + 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); + } - helperResult.Playwright_electron = { - executablePath: '// require("electron") or require("electron-forge")', - args: ['path/to/your/main.js'], - } - } + 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'], + }; + } - 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() - }) - }) + 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 7f92cbec8..66d009710 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') +import { getConfig, getTestRoot } from './utils.js'; +import recorder from '../recorder.js'; +import Codecept from '../codecept.js'; +import Container from '../container.js'; +import * as event from '../event.js'; +import * as output from '../output.js'; +import webHelpers from '../plugin/standardActingHelpers.js'; -module.exports = async function (path, options) { +export default 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.output.error(`Error while running bootstrap file :${err}`); } } diff --git a/lib/command/list.js b/lib/command/list.js index 47a1047c9..3449b1b19 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') +import { getConfig, getTestRoot } from './utils.js'; +import Codecept from '../codecept.js'; +import container from '../container.js'; +import { getParamsToString } from '../parser.js'; +import { methodsOfObject } from '../utils.js'; +import * as output from '../output.js'; -module.exports = function (path) { - const testsPath = getTestRoot(path) - const config = getConfig(testsPath) - const codecept = new Codecept(config, {}) - codecept.init(testsPath) +export default function (path) { + 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.output.colors.grey(name)} I.${output.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.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 0600b4372..785a1c07d 100644 --- a/lib/command/run-multiple.js +++ b/lib/command/run-multiple.js @@ -1,206 +1,186 @@ -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 = {} +import { fork } from 'child_process'; +import path, { dirname } from 'path'; +import crypto from 'crypto'; +import { fileURLToPath } from 'url'; +import runHook from '../hooks.js'; +import * as event from '../event.js'; +import collection from './run-multiple/collection.js'; +import { clearString, replaceValueDeep } from '../utils.js'; +import { getConfig, getTestRoot, fail } from './utils.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const runner = path.join(__dirname, '../../bin/codecept.js'); +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) { +export default 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-multiple/chunk.js b/lib/command/run-multiple/chunk.js index d57fcafe4..ed1b5fb84 100644 --- a/lib/command/run-multiple/chunk.js +++ b/lib/command/run-multiple/chunk.js @@ -1,6 +1,6 @@ -const glob = require('glob'); -const path = require('path'); -const fs = require('fs'); +import glob from 'glob'; +import path from 'path'; +import fs from 'fs'; /** * Splits a list to (n) parts, defined via the size argument. @@ -61,7 +61,7 @@ const mapFileFormats = (files) => { * files by the passed number or executing a usder deifned function to perform * the splitting. */ -const createChunks = (config, patterns = []) => { +export const createChunks = (config, patterns = []) => { const files = patterns.filter(pattern => !!pattern).map((pattern) => { return findFiles(pattern).filter((file) => { return config.grep ? grepFile(file, config.grep) : true; @@ -86,6 +86,6 @@ const createChunks = (config, patterns = []) => { }); }; -module.exports = { +export default { createChunks, }; diff --git a/lib/command/run-multiple/collection.js b/lib/command/run-multiple/collection.js index a533029d3..e9a3e3368 100644 --- a/lib/command/run-multiple/collection.js +++ b/lib/command/run-multiple/collection.js @@ -1,5 +1,5 @@ -const { createChunks } = require('./chunk'); -const { createRun } = require('./run'); +import { createChunks } from './chunk.js'; +import { createRun } from './run.js'; /** * Bootstraps a collection of runs, it combines user defined selection of runs @@ -190,6 +190,6 @@ function guessBrowser(config) { return [config.helpers[firstHelper].browser]; } -module.exports = { +export default { createRuns, }; diff --git a/lib/command/run-multiple/run.js b/lib/command/run-multiple/run.js index ea06309df..991526f77 100644 --- a/lib/command/run-multiple/run.js +++ b/lib/command/run-multiple/run.js @@ -21,10 +21,6 @@ class Run { } } -const createRun = (name, config) => { +export function createRun(name, config) { return new Run(name, config); -}; - -module.exports = { - createRun, -}; +} diff --git a/lib/command/run-rerun.js b/lib/command/run-rerun.js index ccb18bcfd..8690bd118 100644 --- a/lib/command/run-rerun.js +++ b/lib/command/run-rerun.js @@ -1,34 +1,39 @@ -const { getConfig, getTestRoot } = require('./utils') -const { printError, createOutputDir } = require('./utils') -const Config = require('../config') -const Codecept = require('../rerun') +import { + getConfig, getTestRoot, printError, createOutputDir, +} from './utils.js'; +import Config from '../config.js'; +import Codecept from '../rerun.js'; -module.exports = async function (test, options) { +export async function runRerun(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); - const codecept = new Codecept(config, options) + function processError(err) { + printError(err); + process.exit(1); + } + 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 caae6f6cd..67ab5e4fe 100644 --- a/lib/command/run-workers.js +++ b/lib/command/run-workers.js @@ -1,89 +1,92 @@ // 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') - -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 +import { tryOrDefault } from '../utils.js'; +import { store } from '../store.js'; +import * as output from '../output.js'; +import * as event from '../event.js'; +import { Workers } from '../workers.js'; +import { getMachineInfo } from './info.js'; +import * as Codecept from '../codecept.js'; + +export default 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; 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() + const version = Codecept.version(); + output.print(`CodeceptJS v${version} ${output.output.standWithUkraine()}`); + output.print(`Running tests in ${output.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.output.test.failed(test); + }); workers.on(event.test.passed, (test) => { - passedTestArr.push(test) - output.test.passed(test) - }) + passedTestArr.push(test); + output.output.test.passed(test); + }); workers.on(event.test.skipped, (test) => { - skippedTestArr.push(test) - output.test.skipped(test) - }) + skippedTestArr.push(test); + output.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 +95,23 @@ 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; + await getMachineInfo(); } - await workers.bootstrapAll() - await workers.run() + await workers.bootstrapAll(); + await workers.run(); } catch (err) { - output.error(err) - process.exit(1) + output.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 e76257404..d93a25b29 100644 --- a/lib/command/run.js +++ b/lib/command/run.js @@ -1,46 +1,49 @@ -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) { +import importSync from 'import-sync'; +import { + getConfig, printError, getTestRoot, createOutputDir, +} from './utils.js'; +import Config from '../config.js'; +import Codecept from '../codecept.js'; +import { store } from '../store.js'; + +export default 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 codecept = new Codecept(config, options) + const testRoot = getTestRoot(configFile); + createOutputDir(config, testRoot); + 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 } = importSync('./info.js'); + 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 cb5620b79..23a1301c4 100644 --- a/lib/command/utils.js +++ b/lib/command/utils.js @@ -1,118 +1,103 @@ -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') - -// alias to deep merge -module.exports.deepMerge = require('../utils').deepMerge - -module.exports.getConfig = function (configFile) { +import fs from 'fs'; +import path from 'path'; +import util from 'util'; +import mkdirp from 'mkdirp'; +import * as output from '../output.js'; +import { fileExists, beautify } from '../utils.js'; +import * as config from '../config.js'; + +export const getConfig = function (configFile) { try { - return require('../config').load(configFile) + return config.default.load(configFile); } catch (err) { - fail(err.stack) + fail(err.stack); } -} +}; -module.exports.readConfig = function (configFile) { +export const 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.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 +export { getTestRoot }; function fail(msg) { - output.error(msg) - process.exit(1) + output.output.error(msg); + process.exit(1); } -module.exports.fail = fail +export { fail }; -function updateConfig(testsPath, config, extension) { - const configFile = path.join(testsPath, `codecept.conf.${extension}`) +function updateConfig(testsPath, config, key, extension = 'js') { + 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 + console.log(); + const msg = `codecept.conf.${extension} config can\'t be updated automatically`; + console.log(`${output.output.colors.bold.red(msg)}`); + console.log('Please update it manually:'); + console.log(); + console.log(`${key}: ${config[key]}`); + 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.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 +export { updateConfig }; function safeFileWrite(file, contents) { if (fileExists(file)) { - output.error(`File ${file} already exist, skipping...`) - return false + output.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 +export { safeFileWrite }; -module.exports.captureStream = (stream) => { - let oldStream - let buffer = '' +export const captureStream = (stream) => { + 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, ''))) -} +export const printError = (err) => { + output.print(''); + output.output.error(err.message); + output.print(''); + output.print(output.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) +export const createOutputDir = (config, testRoot) => { + 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) - } -} - -module.exports.findConfigFile = (testsPath) => { - const extensions = ['js', 'ts'] - for (const ext of extensions) { - const configFile = path.join(testsPath, `codecept.conf.${ext}`) - if (fileExists(configFile)) { - return configFile - } + output.print(`creating output directory: ${outputDir}`); + mkdirp.sync(outputDir); } - return null -} +}; diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js index 48ce85127..0df31193f 100644 --- a/lib/command/workers/runTests.js +++ b/lib/command/workers/runTests.js @@ -1,4 +1,10 @@ -const tty = require('tty'); +import tty from 'tty'; +import { parentPort, workerData } from 'worker_threads'; +import * as event from '../../event.js'; +import Container from '../../container.js'; +import { getConfig } from '../utils.js'; +import { deepMerge, tryOrDefault } from '../../utils.js'; +import Codecept from '../../codecept.js'; if (!tty.getWindowSize) { // this is really old method, long removed from Node, but Mocha @@ -7,19 +13,13 @@ if (!tty.getWindowSize) { tty.getWindowSize = () => [40, 80]; } -const { parentPort, workerData } = require('worker_threads'); -const event = require('../../event'); -const container = require('../../container'); -const { getConfig } = require('../utils'); -const { tryOrDefault, deepMerge } = require('../../utils'); - // eslint-disable-next-line no-unused-vars let stdout = ''; /* eslint-enable no-unused-vars */ const stderr = ''; // Requiring of Codecept need to be after tty.getWindowSize is available. -const Codecept = require(process.env.CODECEPT_CLASS_PATH || '../../codecept'); +// const Codecept = importSync(process.env.CODECEPT_CLASS_PATH || '../../codecept.js'); const { options, tests, testRoot, workerIndex, @@ -37,7 +37,7 @@ const config = deepMerge(getConfig(options.config || testRoot), overrideConfigs) const codecept = new Codecept(config, options); codecept.init(testRoot); codecept.loadTests(); -const mocha = container.mocha(); +const mocha = Container.mocha(); filterTests(); (async function () { @@ -287,6 +287,6 @@ function sendToParentThread(data) { function listenToParentThread() { parentPort.on('message', (eventData) => { - container.append({ support: eventData.data }); + Container.append({ support: eventData.data }); }); } diff --git a/lib/config.js b/lib/config.js index 3eaa49e55..0c3df5570 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,11 +1,9 @@ -const fs = require('fs'); -const path = require('path'); -const { - fileExists, - isFile, - deepMerge, - deepClone, -} = require('./utils'); +import fs from 'fs'; +import path from 'path'; +import importSync from 'import-sync'; +import { + fileExists, isFile, deepMerge, deepClone, +} from './utils.js'; const defaultConfig = { output: './_output', @@ -142,7 +140,7 @@ class Config { } } -module.exports = Config; +export default Config; function loadConfigFile(configFile) { const extensionName = path.extname(configFile); @@ -157,7 +155,8 @@ function loadConfigFile(configFile) { // .conf.js config file if (extensionName === '.js' || extensionName === '.ts' || extensionName === '.cjs') { - return Config.create(require(configFile).config); + const { config } = importSync(configFile); + return Config.create(config); } // json config provided diff --git a/lib/container.js b/lib/container.js index 2307b94e8..9bfc50048 100644 --- a/lib/container.js +++ b/lib/container.js @@ -1,23 +1,26 @@ -const glob = require('glob'); -const path = require('path'); -const { MetaStep } = require('./step'); -const { fileExists, isFunction, isAsyncFunction } = require('./utils'); -const Translation = require('./translation'); -const MochaFactory = require('./mochaFactory'); -const recorder = require('./recorder'); -const event = require('./event'); -const WorkerStorage = require('./workerStorage'); -const store = require('./store'); -const ai = require('./ai'); +import glob from 'glob'; +import path, { dirname } from 'path'; +import importSync from 'import-sync'; +import { fileURLToPath } from 'url'; +import { MetaStep } from './step.js'; +import { + fileExists, isFunction, isAsyncFunction, deepMerge, +} from './utils.js'; +import Translation from './translation.js'; +import { MochaFactory } from './mochaFactory.js'; +// eslint-disable-next-line import/no-named-as-default +import recorder from './recorder.js'; +import * as event from './event.js'; +import * as WorkerStorage from './workerStorage.js'; +import { store } from './store.js'; +import { actor } from './actor.js'; + +import ai from './ai.js'; let container = { helpers: {}, support: {}, plugins: {}, - /** - * @type {Mocha | {}} - * @ignore - */ mocha: {}, translation: {}, }; @@ -25,7 +28,7 @@ let container = { /** * Dependency Injection Container */ -class Container { +export default class Container { /** * Create container with all required helpers and support objects * @@ -59,10 +62,7 @@ class Container { * @returns { * } */ static plugins(name) { - if (!name) { - return container.plugins; - } - return container.plugins[name]; + return name ? container.plugins[name] : container.plugins; } /** @@ -119,7 +119,6 @@ class Container { * @param {Object} newContainer */ static append(newContainer) { - const deepMerge = require('./utils').deepMerge; container = deepMerge(container, newContainer); } @@ -151,8 +150,6 @@ class Container { } } -module.exports = Container; - function createHelpers(config) { const helpers = {}; let moduleName; @@ -160,29 +157,31 @@ function createHelpers(config) { try { if (config[helperName].require) { if (config[helperName].require.startsWith('.')) { + // @ts-ignore moduleName = path.resolve(global.codecept_dir, config[helperName].require); // custom helper } else { moduleName = config[helperName].require; // plugin helper } } else { - moduleName = `./helper/${helperName}`; // built-in helper + moduleName = `./helper/${helperName}.js`; // built-in helper } // @ts-ignore let HelperClass; // check if the helper is the built-in, use the require() syntax. if (moduleName.startsWith('./helper/')) { - HelperClass = require(moduleName); + const __dirname = dirname(fileURLToPath(import.meta.url)); + HelperClass = importSync(path.resolve(__dirname, moduleName)).default; } else { // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName. - HelperClass = require(moduleName).default || require(moduleName); + HelperClass = importSync(path.resolve(moduleName)).default; } - if (HelperClass._checkRequirements) { + if (HelperClass && HelperClass._checkRequirements) { const requirements = HelperClass._checkRequirements(); if (requirements) { let install; - if (require('./utils').installedLocally()) { + if (importSync('./utils.js').installedLocally()) { install = `npm install --save-dev ${requirements.join(' ')}`; } else { console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation'); @@ -203,7 +202,7 @@ function createHelpers(config) { return helpers; } -function createSupportObjects(config) { +export function createSupportObjects(config) { const objects = {}; for (const name in config) { @@ -211,7 +210,7 @@ function createSupportObjects(config) { } if (!config.I) { - objects.I = require('./actor')(); + objects.I = actor(); if (container.translation.I !== 'I') { objects[container.translation.I] = objects.I; @@ -295,12 +294,13 @@ function createPlugins(config, options = {}) { if (config[pluginName].require) { module = config[pluginName].require; if (module.startsWith('.')) { // local + // @ts-ignore module = path.resolve(global.codecept_dir, module); // custom plugin } } else { - module = `./plugin/${pluginName}`; + module = `./plugin/${pluginName}.js`; } - plugins[pluginName] = require(module)(config[pluginName]); + plugins[pluginName] = importSync(module).default(config[pluginName]); } catch (err) { throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`); } @@ -316,8 +316,10 @@ function getSupportObject(config, name) { return module; } -function loadGherkinSteps(paths) { +export function loadGherkinSteps(paths) { + // @ts-ignore global.Before = fn => event.dispatcher.on(event.test.started, fn); + // @ts-ignore global.After = fn => event.dispatcher.on(event.test.finished, fn); global.Fail = fn => event.dispatcher.on(event.test.failed, fn); @@ -329,6 +331,7 @@ function loadGherkinSteps(paths) { loadSupportObject(path, `Step Definition from ${path}`); } } else { + // @ts-ignore const folderPath = paths.startsWith('.') ? path.join(global.codecept_dir, paths) : ''; if (folderPath !== '') { glob.sync(folderPath).forEach((file) => { @@ -337,17 +340,20 @@ function loadGherkinSteps(paths) { } } + // @ts-ignore delete global.Before; + // @ts-ignore delete global.After; delete global.Fail; } function loadSupportObject(modulePath, supportObjectName) { if (modulePath.charAt(0) === '.') { + // @ts-ignore modulePath = path.join(global.codecept_dir, modulePath); } try { - const obj = require(modulePath); + const obj = importSync(modulePath).default || importSync(modulePath); if (typeof obj === 'function') { const fobj = obj(); @@ -405,7 +411,7 @@ function getObjectMethods(obj) { const methodsSet = new Set(); let protoObj = Reflect.getPrototypeOf(obj); do { - if (protoObj.constructor.prototype !== Object.prototype) { + if (protoObj?.constructor.prototype !== Object.prototype) { const keys = Reflect.ownKeys(protoObj); keys.forEach(k => methodsSet.add(k)); } @@ -424,16 +430,18 @@ function loadTranslation(locale, vocabularies) { } let translation; + locale = locale.replace('-', '_'); // check if it is a known translation if (Translation.langs[locale]) { translation = new Translation(Translation.langs[locale]); - } else if (fileExists(path.join(global.codecept_dir, locale))) { - // get from a provided file instead - translation = Translation.createDefault(); - translation.loadVocabulary(locale); - } else { + } else { // @ts-ignore translation = Translation.createDefault(); + // @ts-ignore + if (fileExists(path.join(global.codecept_dir, locale))) { + // get from a provided file instead + translation.loadVocabulary(locale); + } } vocabularies.forEach(v => translation.loadVocabulary(v)); diff --git a/lib/data/context.js b/lib/data/context.js index 82f3897f8..bbcdcfb0c 100644 --- a/lib/data/context.js +++ b/lib/data/context.js @@ -1,129 +1,128 @@ -const { isGenerator } = require('../utils') -const DataTable = require('./table') -const DataScenarioConfig = require('./dataScenarioConfig') -const Secret = require('../secret') +import { isGenerator } from '../utils.js'; +import DataTable from './table.js'; +import DataScenarioConfig from './dataScenarioConfig.js'; -module.exports = function (context) { +export default 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 9d6977819..46a8f5ff8 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 +export default DataScenarioConfig; diff --git a/lib/data/dataTableArgument.js b/lib/data/dataTableArgument.js index 7d1a72d32..ab9c66e43 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 +export default DataTableArgument; diff --git a/lib/data/table.js b/lib/data/table.js index 841e444dc..f97db1984 100644 --- a/lib/data/table.js +++ b/lib/data/table.js @@ -4,46 +4,40 @@ 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 +export default DataTable; diff --git a/lib/dirname.js b/lib/dirname.js new file mode 100644 index 000000000..f44a97cc8 --- /dev/null +++ b/lib/dirname.js @@ -0,0 +1,5 @@ +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +export const __dirname = dirname(__filename); diff --git a/lib/event.js b/lib/event.js index 676354be7..5e4c5e364 100644 --- a/lib/event.js +++ b/lib/event.js @@ -1,172 +1,176 @@ -const debug = require('debug')('codeceptjs:event'); -const events = require('events'); -const { error } = require('./output'); +import debug from 'debug'; + +import events from 'events'; +import * as output from './output.js'; + +debug('codeceptjs:event'); const dispatcher = new events.EventEmitter(); dispatcher.setMaxListeners(50); + /** - * @namespace - * @alias event + * @type {object} + * @constant + * @inner + * @property {'test.start'} started + * @property {'test.before'} before + * @property {'test.after'} after + * @property {'test.passed'} passed + * @property {'test.failed'} failed + * @property {'test.finish'} finished + * @property {'test.skipped'} skipped */ -module.exports = { - /** - * @type {NodeJS.EventEmitter} - * @constant - * @inner - */ - dispatcher, - /** - * @type {object} - * @constant - * @inner - * @property {'test.start'} started - * @property {'test.before'} before - * @property {'test.after'} after - * @property {'test.passed'} passed - * @property {'test.failed'} failed - * @property {'test.finish'} finished - * @property {'test.skipped'} skipped - */ - test: { - started: 'test.start', // sync - before: 'test.before', // async - after: 'test.after', // async - passed: 'test.passed', // sync - failed: 'test.failed', // sync - finished: 'test.finish', // sync - skipped: 'test.skipped', // sync - }, - /** - * @type {object} - * @constant - * @inner - * @property {'suite.before'} before - * @property {'suite.after'} after - */ - suite: { - before: 'suite.before', - after: 'suite.after', - }, - /** - * @type {object} - * @constant - * @inner - * @property {'hook.start'} started - * @property {'hook.passed'} passed - */ - hook: { - started: 'hook.start', - passed: 'hook.passed', - failed: 'hook.failed', - }, - /** - * @type {object} - * @constant - * @inner - * @property {'step.start'} started - * @property {'step.before'} before - * @property {'step.after'} after - * @property {'step.passed'} passed - * @property {'step.failed'} failed - * @property {'step.finish'} finished - * @property {'step.comment'} comment - */ - step: { - before: 'step.before', // async - after: 'step.after', // async - started: 'step.start', // sync - passed: 'step.passed', // sync - failed: 'step.failed', // sync - finished: 'step.finish', // sync - comment: 'step.comment', - }, - /** - * @type {object} - * @constant - * @inner - * @property {'suite.before'} before - * @property {'suite.after'} after - */ - bddStep: { - before: 'bddStep.before', - after: 'bddStep.after', - started: 'bddStep.started', - finished: 'bddStep.finished', - }, - /** - * @type {object} - * @constant - * @inner - * @property {'global.before'} before - * @property {'global.after'} after - * @property {'global.result'} result - * @property {'global.failures'} failures - */ - all: { - before: 'global.before', - after: 'global.after', - result: 'global.result', - failures: 'global.failures', - }, - /** - * @type {object} - * @constant - * @inner - * @property {'multiple.before'} before - * @property {'multiple.after'} after - */ - multiple: { - before: 'multiple.before', - after: 'multiple.after', - }, - - /** - * @type {object} - * @constant - * @inner - * @property {'workers.before'} before - * @property {'workers.after'} after - * @property {'workers.result'} result - */ - workers: { - before: 'workers.before', - after: 'workers.after', - result: 'workers.result', - }, +export const test = { + started: 'test.start', // sync + before: 'test.before', // async + after: 'test.after', // async + passed: 'test.passed', // sync + failed: 'test.failed', // sync + finished: 'test.finish', // sync + skipped: 'test.skipped', // sync +}; - /** - * @param {string} event - * @param {*} [param] - */ - emit(event, param) { - let msg = `Emitted | ${event}`; - if (param && param.toString()) { - msg += ` (${param.toString()})`; - } - debug(msg); - try { - this.dispatcher.emit.apply(this.dispatcher, arguments); - } catch (err) { - error(`Error processing ${event} event:`); - error(err.stack); - } - }, +/** + * @type {object} + * @constant + * @inner + * @property {'suite.before'} before + * @property {'suite.after'} after + */ +export const suite = { + before: 'suite.before', + after: 'suite.after', +}; +/** + * @type {object} + * @constant + * @inner + * @property {'hook.start'} started + * @property {'hook.passed'} passed + */ +export const hook = { + started: 'hook.start', + passed: 'hook.passed', + failed: 'hook.failed', +}; +/** + * @type {object} + * @constant + * @inner + * @property {'step.start'} started + * @property {'step.before'} before + * @property {'step.after'} after + * @property {'step.passed'} passed + * @property {'step.failed'} failed + * @property {'step.finish'} finished + * @property {'step.comment'} comment + */ +export const step = { + before: 'step.before', // async + after: 'step.after', // async + started: 'step.start', // sync + passed: 'step.passed', // sync + failed: 'step.failed', // sync + finished: 'step.finish', // sync + comment: 'step.comment', +}; +/** + * @type {object} + * @constant + * @inner + * @property {'suite.before'} before + * @property {'suite.after'} after + */ +export const bddStep = { + before: 'bddStep.before', + after: 'bddStep.after', + started: 'bddStep.started', + finished: 'bddStep.finished', +}; +/** + * @type {object} + * @constant + * @inner + * @property {'global.before'} before + * @property {'global.after'} after + * @property {'global.result'} result + * @property {'global.failures'} failures + */ +export const all = { + before: 'global.before', + after: 'global.after', + result: 'global.result', + failures: 'global.failures', +}; +/** + * @type {object} + * @constant + * @inner + * @property {'multiple.before'} before + * @property {'multiple.after'} after + */ +export const multiple = { + before: 'multiple.before', + after: 'multiple.after', +}; - /** for testing only! */ - cleanDispatcher: () => { - let event; - for (event in this.test) { - this.dispatcher.removeAllListeners(this.test[event]); - } - for (event in this.suite) { - this.dispatcher.removeAllListeners(this.test[event]); - } - for (event in this.step) { - this.dispatcher.removeAllListeners(this.test[event]); - } - for (event in this.all) { - this.dispatcher.removeAllListeners(this.test[event]); - } - }, +/** + * @type {object} + * @constant + * @inner + * @property {'workers.before'} before + * @property {'workers.after'} after + * @property {'workers.result'} result + */ +export const workers = { + before: 'workers.before', + after: 'workers.after', + result: 'workers.result', }; + +/** for testing only! */ +export function cleanDispatcher() { + let event; + for (event in test) { + dispatcher.removeAllListeners(test[event]); + } + for (event in suite) { + dispatcher.removeAllListeners(test[event]); + } + for (event in step) { + dispatcher.removeAllListeners(test[event]); + } + for (event in all) { + dispatcher.removeAllListeners(test[event]); + } +} + +/** + * @namespace + * @alias event + */ +/** + * @type {NodeJS.EventEmitter} + * @constant + * @inner + */ +export { dispatcher }; + +/** + * @param {string} event + * @param {*} [param] + */ +export function emit(event, param) { + let msg = `Emitted | ${event}`; + if (param && param.toString()) { + msg += ` (${param.toString()})`; + } + output.output.debug(msg); + try { + dispatcher.emit.apply(dispatcher, arguments); + } catch (err) { + output.output.error(`Error processing ${event} event:`); + output.output.error(err.stack); + } +} diff --git a/lib/heal.js b/lib/heal.js index b6071c456..e02877a4a 100644 --- a/lib/heal.js +++ b/lib/heal.js @@ -1,9 +1,13 @@ -const debug = require('debug')('codeceptjs:heal'); -const colors = require('chalk'); -const Container = require('./container'); -const recorder = require('./recorder'); -const output = require('./output'); -const event = require('./event'); +import debug from 'debug'; +import colors from 'chalk'; +import Container from './container.js'; +import recorder from './recorder.js'; +import * as event from './event.js'; +import { output } from './output.js'; + +const logger = debug('myapp:server'); + +logger('codeceptjs:heal'); /** * @class @@ -57,7 +61,7 @@ class Heal { const suggestions = []; const recipes = matchRecipes(this.recipes, this.contextName); - debug('Recipes', recipes); + logger('Recipes', recipes); const currentOutputLevel = output.level(); output.level(0); @@ -96,20 +100,20 @@ class Heal { const suggestions = await this.getCodeSuggestions(failureContext); if (suggestions.length === 0) { - debug('No healing suggestions found'); + logger('No healing suggestions found'); throw error; } output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`); - debug(suggestions); + logger(suggestions); for (const suggestion of suggestions) { for (const codeSnippet of suggestion.snippets) { try { - debug('Executing', codeSnippet); + logger('Executing', codeSnippet); recorder.catch((e) => { - debug(e); + logger(e); }); if (typeof codeSnippet === 'string') { @@ -131,7 +135,7 @@ class Heal { // recorder.session.restore(); return; } catch (err) { - debug('Failed to execute code', err); + logger('Failed to execute code', err); recorder.ignoreErr(err); // healing did not help recorder.catchWithoutStop(err); await recorder.promise(); // wait for all promises to resolve @@ -149,7 +153,7 @@ class Heal { const heal = new Heal(); -module.exports = heal; +export { heal }; function matchRecipes(recipes, contextName) { return Object.entries(recipes) diff --git a/lib/helper.js b/lib/helper.js index 3919046d1..3804e0695 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -1,2 +1,4 @@ // helper class was moved out from this repository to allow extending from base class -module.exports = require('@codeceptjs/helper'); +import generated from '@codeceptjs/helper'; + +export default generated; diff --git a/lib/helper/AI.js b/lib/helper/AI.js index 4b2d75978..d7da4ad6b 100644 --- a/lib/helper/AI.js +++ b/lib/helper/AI.js @@ -1,52 +1,39 @@ -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', -} +import Helper from '@codeceptjs/helper'; +import AiAssistant from '../ai.js'; +import standardActingHelpers from '../plugin/standardActingHelpers.js'; +import Container from '../container.js'; +import { splitByChunks, minifyHtml } from '../html.js'; /** * 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 WebDriver to ensure the HTML context is available. - * - * Use it only in development mode. It is recommended to run it only inside pause() mode. + * This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available. * * ## Configuration * - * This helper should be configured in codecept.conf.{js|ts} + * This helper should be configured in codecept.json or codecept.conf.js * * * `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 = AiAssistant; this.options = { chunkSize: 80000, - } - this.options = { ...this.options, ...config } - this.aiAssistant.enable(this.config) + }; + this.options = { ...this.options, ...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; } } } @@ -63,34 +50,30 @@ 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: gtpRole.user, content: prompt }, - { role: gtpRole.user, content: `Within this HTML: ${await minifyHtml(chunk)}` }, - ] + { role: 'user', content: prompt }, + { role: '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.aiAssistant.createCompletion(messages); - output.print(response) + console.log(response); - responses.push(response) + responses.push(response); } - return responses.join('\n\n') + return responses.join('\n\n'); } /** @@ -106,108 +89,36 @@ 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: gtpRole.user, content: prompt }, - { role: gtpRole.user, content: `Within this HTML: ${await minifyHtml(html)}` }, - ] + { role: 'user', content: prompt }, + { role: 'user', content: `Within this HTML: ${minifyHtml(html)}` }, + ]; - const response = await this._processAIRequest(messages) + const response = await this.aiAssistant.createCompletion(messages); - output.print(response) + console.log(response); - return response + return response; } /** - * Send a general request to AI and return response. + * Send a general request to ChatGPT and return response. * @param {string} prompt * @returns {Promise} - A Promise that resolves to the generated response from the GPT model. */ async askGptGeneralPrompt(prompt) { - const messages = [{ role: gtpRole.user, content: prompt }] - - const response = await this._processAIRequest(messages) - - output.print(response) - - return response - } - - /** - * Generates PageObject for current page using AI. - * - * It saves the PageObject to the output directory. You can review the page object and adjust it as needed and move to pages directory. - * Prompt can be customized in a global config file. - * - * ```js - * // create page object for whole page - * I.askForPageObject('home'); - * - * // create page object with extra prompt - * I.askForPageObject('home', 'implement signIn(username, password) method'); - * - * // create page object for a specific element - * I.askForPageObject('home', null, '.detail'); - * ``` - * - * Asks for a page object based on the provided page name, locator, and extra prompt. - * - * @async - * @param {string} pageName - The name of the page to retrieve the object for. - * @param {string|null} [extraPrompt=null] - An optional extra prompt for additional context or information. - * @param {string|null} [locator=null] - An optional locator to find a specific element on the page. - * @returns {Promise} A promise that resolves to the requested page object. - */ - async askForPageObject(pageName, extraPrompt = null, locator = null) { - const spinner = ora(' Processing AI request...').start() - - 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 '' - } - - const code = beautify(response[0]) - - output.print('----- Generated PageObject ----') - output.print(code) - output.print('-------------------------------') - - const fileName = path.join(output_dir, `${pageName}Page-${Date.now()}.js`) + const messages = [ + { role: 'user', content: prompt }, + ]; - output.print(output.styles.bold(`Page object for ${pageName} is saved to ${output.styles.bold(fileName)}`)) - fs.writeFileSync(fileName, code) + const response = await this.aiAssistant.createCompletion(messages); - 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}`) - } - } + console.log(response); - async _processAIRequest(messages) { - const spinner = ora(' Processing AI request...').start() - const response = await this.aiAssistant.createCompletion(messages) - spinner.stop() - return response + return response; } } -module.exports = AI +export default AI; diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js index ece565636..9f8b71011 100644 --- a/lib/helper/ApiDataFactory.js +++ b/lib/helper/ApiDataFactory.js @@ -1,7 +1,6 @@ -const path = require('path') - -const Helper = require('@codeceptjs/helper') -const REST = require('./REST') +import path from 'path'; +import Helper from '@codeceptjs/helper'; +import REST from './REST'; /** * Helper for managing remote data using REST API. @@ -184,67 +183,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 +265,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 +290,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 +321,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 +342,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,40 +373,37 @@ 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 +export default 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 58b17335b..6f943379d 100644 --- a/lib/helper/Appium.js +++ b/lib/helper/Appium.js @@ -1,26 +1,25 @@ -let webdriverio - -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 mobileRoot = '//*' -const webRoot = 'body' +import fs from 'fs'; +import { default as axios } from 'axios'; +import { v4 as uuidv4 } from 'uuid'; +import Webdriver from './WebDriver'; +import AssertionFailedError from '../assert/error.js'; +import { truth } from '../assert/truth'; +import recorder from '../recorder.js'; +import Locator from '../locator.js'; +import ConnectionRefused from './errors/ConnectionRefused.js'; + +let webdriverio; + +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,21 +174,19 @@ 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.'); } } @@ -206,7 +203,7 @@ class Appium extends Webdriver { } } } - `) + `); } // set defaults @@ -227,174 +224,166 @@ 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}`; } /** @@ -432,11 +421,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(); } /** @@ -474,11 +463,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(); } /** @@ -495,11 +484,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 */ @@ -508,21 +497,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(); } /** @@ -538,9 +527,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); } /** @@ -556,9 +545,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); } /** @@ -574,9 +563,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); } /** @@ -591,8 +580,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); } /** @@ -608,13 +597,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 }, - }) + }); } /** @@ -626,11 +615,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`, - }) + }); } /** @@ -645,9 +634,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); } /** @@ -662,9 +651,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); } /** @@ -679,9 +668,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); } /** @@ -699,15 +688,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); } /** @@ -723,13 +712,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 }, - }) + }); } /** @@ -744,8 +733,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(); } /** @@ -760,8 +749,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(); } /** @@ -776,8 +765,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(); } /** @@ -794,14 +783,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, - } + }; } /** @@ -816,10 +805,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; } /** @@ -834,10 +823,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; } /** @@ -846,7 +835,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); } /** @@ -866,17 +855,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'); } /** @@ -893,11 +882,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'); } /** @@ -914,8 +903,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); } /** @@ -940,8 +929,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); } /** @@ -956,8 +945,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); } /** @@ -978,9 +967,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); } /** @@ -997,8 +986,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); } /** @@ -1013,8 +1002,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(); } /** @@ -1033,13 +1022,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, - }) + }); } /** @@ -1056,7 +1045,7 @@ class Appium extends Webdriver { * @param {*} locator */ async tap(locator) { - return this.makeTouchAction(locator, 'tap') + return this.makeTouchAction(locator, 'tap'); } /** @@ -1079,13 +1068,10 @@ 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 */ @@ -1102,44 +1088,42 @@ 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); } /** @@ -1160,14 +1144,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); } /** @@ -1189,13 +1173,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); } /** @@ -1216,13 +1200,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); } /** @@ -1243,14 +1227,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); } /** @@ -1277,66 +1261,55 @@ 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`) + 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; } - 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, - ) + 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; } - }) + }); } /** @@ -1368,8 +1341,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); } /** @@ -1388,15 +1361,13 @@ 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; + })); } /** @@ -1411,8 +1382,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(); } /** @@ -1429,8 +1400,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); } /** @@ -1443,8 +1414,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); } /** @@ -1462,9 +1433,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); } /** @@ -1479,8 +1450,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(); } /** @@ -1488,8 +1459,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); } /** @@ -1497,8 +1468,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)); } /** @@ -1506,8 +1477,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)); } /** @@ -1515,16 +1486,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)); } /** @@ -1532,17 +1503,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)); } /** @@ -1550,9 +1521,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); } /** @@ -1560,8 +1531,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)); } /** @@ -1569,16 +1540,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)); } /** @@ -1587,8 +1558,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); } /** @@ -1596,8 +1567,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); } /** @@ -1605,8 +1576,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)); } /** @@ -1614,8 +1585,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)); } /** @@ -1630,7 +1601,7 @@ class Appium extends Webdriver { * @return {Promise} */ async saveScreenshot(fileName) { - return super.saveScreenshot(fileName, false) + return super.saveScreenshot(fileName, false); } /** @@ -1639,7 +1610,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); } /** @@ -1647,8 +1618,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)); } /** @@ -1656,8 +1627,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)); } /** @@ -1665,9 +1636,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); } /** @@ -1675,8 +1646,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)); } /** @@ -1685,8 +1656,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'); } /** @@ -1694,8 +1665,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); } /** @@ -1703,8 +1674,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); } /** @@ -1712,8 +1683,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); } /** @@ -1721,76 +1692,74 @@ 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 +export default Appium; diff --git a/lib/helper/Expect.js b/lib/helper/Expect.js new file mode 100644 index 000000000..f77e3fbb1 --- /dev/null +++ b/lib/helper/Expect.js @@ -0,0 +1,425 @@ +import * as chai from 'chai'; +import chai_json_schema from 'chai-json-schema'; +import chai_match_pattern from 'chai-match-pattern'; +import chai_exclude from 'chai-exclude'; +import chai_string from 'chai-string'; +import * as output from '../output.js'; + +chai.use(chai_string); +// @ts-ignore +chai.use(chai_exclude); +chai.use(chai_match_pattern); +chai.use(chai_json_schema); + +/** + * This helper allows performing assertions based on Chai. + * + * ### Examples + * + * Zero-configuration when paired with other helpers like REST, Playwright: + * + * ```js + * // inside codecept.conf.js + *{ + * helpers: { + * Playwright: {...}, + * Expect: {}, + * } + *} + * ``` + * + * ## Methods + */ +class ExpectHelper { + /** + * + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ + expectEqual(actualValue, expectedValue, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`); + return chai.expect(actualValue, customErrorMsg).to.equal(expectedValue); + } + + /** + * + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ + expectNotEqual(actualValue, expectedValue, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to not equal "${JSON.stringify(expectedValue)}"`); + return chai.expect(actualValue, customErrorMsg).not.to.equal(expectedValue); + } + + /** + * + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + + */ + expectDeepEqual(actualValue, expectedValue, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to deep equal "${JSON.stringify(expectedValue)}"`); + return chai.expect(actualValue, customErrorMsg).to.deep.equal(expectedValue); + } + + /** + * + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ + expectNotDeepEqual(actualValue, expectedValue, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to not deep equal "${JSON.stringify(expectedValue)}"`); + return chai.expect(actualValue, customErrorMsg).to.not.deep.equal(expectedValue); + } + + /** + * + * @param {*} actualValue + * @param {*} expectedValueToContain + * @param {*} [customErrorMsg] + */ + expectContain(actualValue, expectedValueToContain, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to contain "${JSON.stringify(expectedValueToContain)}"`); + return chai.expect(actualValue, customErrorMsg).to.contain( + expectedValueToContain, + ); + } + + /** + * + * @param {*} actualValue + * @param {*} expectedValueToNotContain + * @param {*} [customErrorMsg] + */ + expectNotContain( + actualValue, + expectedValueToNotContain, + customErrorMsg = '', + ) { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to not contain "${JSON.stringify(expectedValueToNotContain)}"`); + return chai.expect(actualValue, customErrorMsg).not.to.contain( + expectedValueToNotContain, + ); + } + + /** + * + * @param {*} actualValue + * @param {*} expectedValueToStartWith + * @param {*} [customErrorMsg] + */ + expectStartsWith(actualValue, expectedValueToStartWith, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to start with "${JSON.stringify(expectedValueToStartWith)}"`); + return chai.expect(actualValue, customErrorMsg).to.startsWith( + expectedValueToStartWith, + ); + } + + /** + * + * @param {*} actualValue + * @param {*} expectedValueToNotStartWith + * @param {*} [customErrorMsg] + */ + expectNotStartsWith( + actualValue, + expectedValueToNotStartWith, + customErrorMsg = '', + ) { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to not start with "${JSON.stringify(expectedValueToNotStartWith)}"`); + return chai.expect(actualValue, customErrorMsg).not.to.startsWith( + expectedValueToNotStartWith, + ); + } + + /** + * @param {*} actualValue + * @param {*} expectedValueToEndWith + * @param {*} [customErrorMsg] + */ + expectEndsWith(actualValue, expectedValueToEndWith, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to end with "${JSON.stringify(expectedValueToEndWith)}"`); + return chai.expect(actualValue, customErrorMsg).to.endsWith( + expectedValueToEndWith, + ); + } + + /** + * @param {*} actualValue + * @param {*} expectedValueToNotEndWith + * @param {*} [customErrorMsg] + */ + expectNotEndsWith( + actualValue, + expectedValueToNotEndWith, + customErrorMsg = '', + ) { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to not end with "${JSON.stringify(expectedValueToNotEndWith)}"`); + return chai.expect(actualValue, customErrorMsg).not.to.endsWith( + expectedValueToNotEndWith, + ); + } + + /** + * @param {*} targetData + * @param {*} jsonSchema + * @param {*} [customErrorMsg] + */ + expectJsonSchema(targetData, jsonSchema, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to match this JSON schema "${JSON.stringify(jsonSchema)}"`); + + return chai.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 }, + ) { + // @ts-ignore + output.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 chai.expect(targetData, customErrorMsg).to.be.jsonSchema(jsonSchema); + } + + /** + * @param {*} targetData + * @param {*} propertyName + * @param {*} [customErrorMsg] + */ + expectHasProperty(targetData, propertyName, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to have property: "${JSON.stringify(propertyName)}"`); + return chai.expect(targetData, customErrorMsg).to.have.property(propertyName); + } + + /** + * @param {*} targetData + * @param {*} propertyName + * @param {*} [customErrorMsg] + */ + expectHasAProperty(targetData, propertyName, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to have a property: "${JSON.stringify(propertyName)}"`); + return chai.expect(targetData, customErrorMsg).to.have.a.property(propertyName); + } + + /** + * @param {*} targetData + * @param {*} type + * @param {*} [customErrorMsg] + */ + expectToBeA(targetData, type, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to be a "${JSON.stringify(type)}"`); + return chai.expect(targetData, customErrorMsg).to.be.a(type); + } + + /** + * @param {*} targetData + * @param {*} type + * @param {*} [customErrorMsg] + */ + expectToBeAn(targetData, type, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to be an "${JSON.stringify(type)}"`); + return chai.expect(targetData, customErrorMsg).to.be.an(type); + } + + /** + * @param {*} targetData + * @param {*} regex + * @param {*} [customErrorMsg] + */ + expectMatchRegex(targetData, regex, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to match the regex "${JSON.stringify(regex)}"`); + return chai.expect(targetData, customErrorMsg).to.match(regex); + } + + /** + * @param {*} targetData + * @param {*} length + * @param {*} [customErrorMsg] + */ + expectLengthOf(targetData, length, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to have length of "${JSON.stringify(length)}"`); + return chai.expect(targetData, customErrorMsg).to.have.lengthOf(length); + } + + /** + * @param {*} targetData + * @param {*} [customErrorMsg] + */ + expectEmpty(targetData, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to be empty`); + return chai.expect(targetData, customErrorMsg).to.be.empty; + } + + /** + * @param {*} targetData + * @param {*} [customErrorMsg] + */ + expectTrue(targetData, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to be true`); + return chai.expect(targetData, customErrorMsg).to.be.true; + } + + /** + * @param {*} targetData + * @param {*} [customErrorMsg] + */ + expectFalse(targetData, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to be false`); + return chai.expect(targetData, customErrorMsg).to.be.false; + } + + /** + * @param {*} targetData + * @param {*} aboveThan + * @param {*} [customErrorMsg] + */ + expectAbove(targetData, aboveThan, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to be above ${JSON.stringify(aboveThan)}`); + return chai.expect(targetData, customErrorMsg).to.be.above(aboveThan); + } + + /** + * @param {*} targetData + * @param {*} belowThan + * @param {*} [customErrorMsg] + */ + expectBelow(targetData, belowThan, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to be below ${JSON.stringify(belowThan)}`); + return chai.expect(targetData, customErrorMsg).to.be.below(belowThan); + } + + /** + * @param {*} targetData + * @param {*} lengthAboveThan + * @param {*} [customErrorMsg] + */ + expectLengthAboveThan(targetData, lengthAboveThan, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to have length of above ${JSON.stringify(lengthAboveThan)}`); + return chai.expect(targetData, customErrorMsg).to.have.lengthOf.above( + lengthAboveThan, + ); + } + + /** + * @param {*} targetData + * @param {*} lengthBelowThan + * @param {*} [customErrorMsg] + */ + expectLengthBelowThan(targetData, lengthBelowThan, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(targetData)}" to have length of below ${JSON.stringify(lengthBelowThan)}`); + return chai.expect(targetData, customErrorMsg).to.have.lengthOf.below( + lengthBelowThan, + ); + } + + /** + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ + expectEqualIgnoreCase(actualValue, expectedValue, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect and ingore case "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`); + return chai.expect(actualValue, customErrorMsg).to.equalIgnoreCase( + expectedValue, + ); + } + + /** + * expects members of two arrays are deeply equal + * @param {*} actualValue + * @param {*} expectedValue + * @param {*} [customErrorMsg] + */ + expectDeepMembers(actualValue, expectedValue, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect members of "${JSON.stringify(actualValue)}" and "${JSON.stringify(expectedValue)}" arrays are deeply equal`); + return chai.expect(actualValue, customErrorMsg).to.have.deep.members( + expectedValue, + ); + } + + /** + * expects an array to be a superset of another array + * @param {*} superset + * @param {*} set + * @param {*} [customErrorMsg] + */ + expectDeepIncludeMembers(superset, set, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(superset)}" array to be a superset of "${JSON.stringify(set)}" array`); + return chai.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 = '', + ) { + // @ts-ignore + output.output.step(`I expect members of "${JSON.stringify(actualValue)}" and "${JSON.stringify(expectedValue)}" JSON objects are deeply equal excluding properties: ${JSON.stringify(fieldsToExclude)}`); + return chai.expect(actualValue, customErrorMsg) + .excludingEvery(fieldsToExclude) + .to.deep.equal(expectedValue); + } + + /** + * expects a JSON object matches a provided pattern + * @param {*} actualValue + * @param {*} expectedPattern + * @param {*} [customErrorMsg] + */ + expectMatchesPattern(actualValue, expectedPattern, customErrorMsg = '') { + // @ts-ignore + output.output.step(`I expect "${JSON.stringify(actualValue)}" to match the ${JSON.stringify(expectedPattern)} pattern`); + return chai.expect(actualValue, customErrorMsg).to.matchPattern(expectedPattern); + } +} + +export default ExpectHelper; diff --git a/lib/helper/ExpectHelper.js b/lib/helper/ExpectHelper.js index 8d891e0ac..34c033d96 100644 --- a/lib/helper/ExpectHelper.js +++ b/lib/helper/ExpectHelper.js @@ -1,9 +1,8 @@ -const output = require('../output') +import * as output from '../output.js'; -let expect +import { expect } from 'chai' import('chai').then((chai) => { - expect = chai.expect chai.use(require('chai-string')) // @ts-ignore chai.use(require('chai-exclude')) @@ -39,7 +38,7 @@ class ExpectHelper { */ expectEqual(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`) + output.output.step(`I expect "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`) return expect(actualValue, customErrorMsg).to.equal(expectedValue) } @@ -51,7 +50,7 @@ class ExpectHelper { */ expectNotEqual(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to not equal "${JSON.stringify(expectedValue)}"`) + output.output.step(`I expect "${JSON.stringify(actualValue)}" to not equal "${JSON.stringify(expectedValue)}"`) return expect(actualValue, customErrorMsg).not.to.equal(expectedValue) } @@ -64,7 +63,7 @@ class ExpectHelper { */ expectDeepEqual(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to deep equal "${JSON.stringify(expectedValue)}"`) + output.output.step(`I expect "${JSON.stringify(actualValue)}" to deep equal "${JSON.stringify(expectedValue)}"`) return expect(actualValue, customErrorMsg).to.deep.equal(expectedValue) } @@ -76,7 +75,7 @@ class ExpectHelper { */ expectNotDeepEqual(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to not deep equal "${JSON.stringify(expectedValue)}"`) + output.output.step(`I expect "${JSON.stringify(actualValue)}" to not deep equal "${JSON.stringify(expectedValue)}"`) return expect(actualValue, customErrorMsg).to.not.deep.equal(expectedValue) } @@ -88,7 +87,7 @@ class ExpectHelper { */ expectContain(actualValue, expectedValueToContain, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to contain "${JSON.stringify(expectedValueToContain)}"`) + output.output.step(`I expect "${JSON.stringify(actualValue)}" to contain "${JSON.stringify(expectedValueToContain)}"`) return expect(actualValue, customErrorMsg).to.contain(expectedValueToContain) } @@ -100,7 +99,7 @@ class ExpectHelper { */ expectNotContain(actualValue, expectedValueToNotContain, customErrorMsg = '') { // @ts-ignore - output.step( + output.output.step( `I expect "${JSON.stringify(actualValue)}" to not contain "${JSON.stringify(expectedValueToNotContain)}"`, ) return expect(actualValue, customErrorMsg).not.to.contain(expectedValueToNotContain) @@ -113,11 +112,17 @@ class ExpectHelper { * @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) + // Log the step output + output.output.step(`I expect "${JSON.stringify(actualValue)}" to start with "${JSON.stringify(expectedValueToStartWith)}"`); + + // Check if actualValue is a string + expect(actualValue, customErrorMsg).to.be.a('string'); + + // Check if actualValue starts with expectedValueToStartWith + expect(actualValue.startsWith(expectedValueToStartWith), customErrorMsg).to.be.true; } + /** * * @param {*} actualValue @@ -125,11 +130,14 @@ class ExpectHelper { * @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) + // Log the step output + output.output.step(`I expect "${JSON.stringify(actualValue)}" to not start with "${JSON.stringify(expectedValueToNotStartWith)}"`); + + // Check if actualValue is a string + expect(actualValue, customErrorMsg).to.be.a('string'); + + // Check if actualValue starts with expectedValueToStartWith + expect(actualValue.startsWith(expectedValueToNotStartWith), customErrorMsg).to.be.false; } /** @@ -139,7 +147,7 @@ class ExpectHelper { */ expectEndsWith(actualValue, expectedValueToEndWith, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to end with "${JSON.stringify(expectedValueToEndWith)}"`) + output.output.step(`I expect "${JSON.stringify(actualValue)}" to end with "${JSON.stringify(expectedValueToEndWith)}"`) return expect(actualValue, customErrorMsg).to.endsWith(expectedValueToEndWith) } @@ -150,7 +158,7 @@ class ExpectHelper { */ expectNotEndsWith(actualValue, expectedValueToNotEndWith, customErrorMsg = '') { // @ts-ignore - output.step( + output.output.step( `I expect "${JSON.stringify(actualValue)}" to not end with "${JSON.stringify(expectedValueToNotEndWith)}"`, ) return expect(actualValue, customErrorMsg).not.to.endsWith(expectedValueToNotEndWith) @@ -163,7 +171,7 @@ class ExpectHelper { */ expectJsonSchema(targetData, jsonSchema, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to match this JSON schema "${JSON.stringify(jsonSchema)}"`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to match this JSON schema "${JSON.stringify(jsonSchema)}"`) return expect(targetData, customErrorMsg).to.be.jsonSchema(jsonSchema) } @@ -176,7 +184,7 @@ class ExpectHelper { */ expectJsonSchemaUsingAJV(targetData, jsonSchema, customErrorMsg = '', ajvOptions = { allErrors: true }) { // @ts-ignore - output.step( + output.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)) @@ -190,7 +198,7 @@ class ExpectHelper { */ expectHasProperty(targetData, propertyName, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have property: "${JSON.stringify(propertyName)}"`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to have property: "${JSON.stringify(propertyName)}"`) return expect(targetData, customErrorMsg).to.have.property(propertyName) } @@ -201,7 +209,7 @@ class ExpectHelper { */ expectHasAProperty(targetData, propertyName, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have a property: "${JSON.stringify(propertyName)}"`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to have a property: "${JSON.stringify(propertyName)}"`) return expect(targetData, customErrorMsg).to.have.a.property(propertyName) } @@ -212,7 +220,7 @@ class ExpectHelper { */ expectToBeA(targetData, type, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be a "${JSON.stringify(type)}"`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to be a "${JSON.stringify(type)}"`) return expect(targetData, customErrorMsg).to.be.a(type) } @@ -223,7 +231,7 @@ class ExpectHelper { */ expectToBeAn(targetData, type, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be an "${JSON.stringify(type)}"`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to be an "${JSON.stringify(type)}"`) return expect(targetData, customErrorMsg).to.be.an(type) } @@ -234,7 +242,7 @@ class ExpectHelper { */ expectMatchRegex(targetData, regex, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to match the regex "${JSON.stringify(regex)}"`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to match the regex "${JSON.stringify(regex)}"`) return expect(targetData, customErrorMsg).to.match(regex) } @@ -245,7 +253,7 @@ class ExpectHelper { */ expectLengthOf(targetData, length, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have length of "${JSON.stringify(length)}"`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to have length of "${JSON.stringify(length)}"`) return expect(targetData, customErrorMsg).to.have.lengthOf(length) } @@ -255,7 +263,7 @@ class ExpectHelper { */ expectEmpty(targetData, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be empty`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to be empty`) return expect(targetData, customErrorMsg).to.be.empty } @@ -265,7 +273,7 @@ class ExpectHelper { */ expectTrue(targetData, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be true`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to be true`) return expect(targetData, customErrorMsg).to.be.true } @@ -275,7 +283,7 @@ class ExpectHelper { */ expectFalse(targetData, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be false`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to be false`) return expect(targetData, customErrorMsg).to.be.false } @@ -286,7 +294,7 @@ class ExpectHelper { */ expectAbove(targetData, aboveThan, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be above ${JSON.stringify(aboveThan)}`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to be above ${JSON.stringify(aboveThan)}`) return expect(targetData, customErrorMsg).to.be.above(aboveThan) } @@ -297,7 +305,7 @@ class ExpectHelper { */ expectBelow(targetData, belowThan, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to be below ${JSON.stringify(belowThan)}`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to be below ${JSON.stringify(belowThan)}`) return expect(targetData, customErrorMsg).to.be.below(belowThan) } @@ -308,7 +316,7 @@ class ExpectHelper { */ expectLengthAboveThan(targetData, lengthAboveThan, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have length of above ${JSON.stringify(lengthAboveThan)}`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to have length of above ${JSON.stringify(lengthAboveThan)}`) return expect(targetData, customErrorMsg).to.have.lengthOf.above(lengthAboveThan) } @@ -319,7 +327,7 @@ class ExpectHelper { */ expectLengthBelowThan(targetData, lengthBelowThan, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(targetData)}" to have length of below ${JSON.stringify(lengthBelowThan)}`) + output.output.step(`I expect "${JSON.stringify(targetData)}" to have length of below ${JSON.stringify(lengthBelowThan)}`) return expect(targetData, customErrorMsg).to.have.lengthOf.below(lengthBelowThan) } @@ -330,7 +338,7 @@ class ExpectHelper { */ expectEqualIgnoreCase(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step(`I expect and ingore case "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`) + output.output.step(`I expect and ingore case "${JSON.stringify(actualValue)}" to equal "${JSON.stringify(expectedValue)}"`) return expect(actualValue, customErrorMsg).to.equalIgnoreCase(expectedValue) } @@ -342,7 +350,7 @@ class ExpectHelper { */ expectDeepMembers(actualValue, expectedValue, customErrorMsg = '') { // @ts-ignore - output.step( + output.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) @@ -356,7 +364,7 @@ class ExpectHelper { */ expectDeepIncludeMembers(superset, set, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(superset)}" array to be a superset of "${JSON.stringify(set)}" array`) + output.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) } @@ -369,7 +377,7 @@ class ExpectHelper { */ expectDeepEqualExcluding(actualValue, expectedValue, fieldsToExclude, customErrorMsg = '') { // @ts-ignore - output.step( + output.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) @@ -383,9 +391,9 @@ class ExpectHelper { */ expectMatchesPattern(actualValue, expectedPattern, customErrorMsg = '') { // @ts-ignore - output.step(`I expect "${JSON.stringify(actualValue)}" to match the ${JSON.stringify(expectedPattern)} pattern`) + output.output.step(`I expect "${JSON.stringify(actualValue)}" to match the ${JSON.stringify(expectedPattern)} pattern`) return expect(actualValue, customErrorMsg).to.matchPattern(expectedPattern) } } -module.exports = ExpectHelper +export default ExpectHelper diff --git a/lib/helper/FileSystem.js b/lib/helper/FileSystem.js index 68d6d645d..0d6db48fb 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 Helper = require('@codeceptjs/helper') -const { fileExists } = require('../utils') -const { fileIncludes } = require('../assert/include') -const { fileEquals } = require('../assert/equal') +import assert from 'assert'; +import path from 'path'; +import fs from 'fs'; +import Helper from '@codeceptjs/helper'; +import { fileExists } from '../utils.js'; +import { fileIncludes } from '../assert/include.js'; +import { fileEquals } from '../assert/equal.js'; +import * as output from '../output.js'; /** * Helper for testing filesystem. @@ -30,15 +30,15 @@ const { fileEquals } = require('../assert/equal') * * ## Methods */ -class FileSystem extends Helper { - constructor() { - super() - this.dir = global.codecept_dir - this.file = '' +export default class FileSystem extends Helper { + constructor(config) { + super(config); + this.dir = global.codecept_dir; + this.file = ''; } _before() { - this.debugSection('Dir', this.dir) + output.output.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); + output.output.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); + output.output.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); + output.output.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 - /** * @param {string} file * @param {string} [encoding='utf8'] @@ -188,9 +187,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); } /** @@ -202,26 +201,26 @@ function getFileContents(file, encoding = 'utf8') { function isFileExists(file, timeout) { 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 4a5f39c29..4a17f0722 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') +import { default as axios } from 'axios'; +import Helper from '@codeceptjs/helper'; /** * GraphQL helper allows to send additional requests to a GraphQl endpoint during acceptance tests. @@ -38,35 +38,31 @@ 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' }, + ]; } /** @@ -75,39 +71,42 @@ 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; } /** @@ -123,7 +122,7 @@ class GraphQL extends Helper { method: 'POST', data: operation, headers, - } + }; } /** @@ -149,15 +148,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); } /** @@ -189,19 +188,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; } /** @@ -210,7 +209,7 @@ class GraphQL extends Helper { * @param {object} headers headers list */ haveRequestHeaders(headers) { - this.headers = { ...this.headers, ...headers } + this.headers = { ...this.headers, ...headers }; } /** @@ -224,7 +223,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 +export default GraphQL; diff --git a/lib/helper/GraphQLDataFactory.js b/lib/helper/GraphQLDataFactory.js index e45f7fe19..c241774f9 100644 --- a/lib/helper/GraphQLDataFactory.js +++ b/lib/helper/GraphQLDataFactory.js @@ -1,7 +1,6 @@ -const path = require('path') - -const Helper = require('@codeceptjs/helper') -const GraphQL = require('./GraphQL') +import path from 'path'; +import Helper from '@codeceptjs/helper'; +import GraphQL from './GraphQL'; /** * Helper for managing remote data using GraphQL queries. @@ -151,52 +150,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 +213,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 +234,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 +264,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 +276,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,15 +293,16 @@ 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 +export default GraphQLDataFactory; diff --git a/lib/helper/JSONResponse.js b/lib/helper/JSONResponse.js index 765b37fa9..26a12546b 100644 --- a/lib/helper/JSONResponse.js +++ b/lib/helper/JSONResponse.js @@ -1,13 +1,12 @@ -const Helper = require('@codeceptjs/helper') +import joi from 'joi'; +import Helper from '../helper.js'; -let expect +let expect; -import('chai').then((chai) => { - expect = chai.expect - chai.use(require('chai-deep-match')) -}) - -const joi = require('joi') +import('chai').then(chai => { + expect = chai.expect; + chai.use(require('chai-deep-match')); +}); /** * This helper allows performing assertions on JSON responses paired with following helpers: @@ -67,35 +66,33 @@ 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']; } } @@ -109,8 +106,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'); } /** @@ -123,35 +120,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); } /** @@ -163,9 +160,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); } /** @@ -186,19 +183,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); } } @@ -220,11 +217,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); } } @@ -248,11 +245,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); } } @@ -270,10 +267,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]}...`; } /** @@ -287,8 +284,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); } /** @@ -319,22 +316,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 +export default JSONResponse; diff --git a/lib/helper/Mochawesome.js b/lib/helper/Mochawesome.js index abc143af4..6d54c6f0d 100644 --- a/lib/helper/Mochawesome.js +++ b/lib/helper/Mochawesome.js @@ -1,71 +1,71 @@ -let addMochawesomeContext -let currentTest -let currentSuite +import Helper from '@codeceptjs/helper'; +import { clearString } from '../utils.js'; -const Helper = require('@codeceptjs/helper') -const { clearString } = require('../utils') +let addMochawesomeContext; +let currentTest; +let currentSuite; 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 +export default Mochawesome; diff --git a/lib/helper/MockServer.js b/lib/helper/MockServer.js index 7278c1021..762b62d20 100644 --- a/lib/helper/MockServer.js +++ b/lib/helper/MockServer.js @@ -1,4 +1,6 @@ -const { mock, settings } = require('pactum') +import pkg from 'pactum'; + +const { mock, settings } = pkg; /** * ## Configuration @@ -18,7 +20,7 @@ let config = { key: '', cert: '', }, -} +}; /** * MockServer @@ -141,10 +143,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 +157,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 +170,7 @@ class MockServer { * */ async stopMockServer() { - await mock.stop() + await mock.stop(); } /** @@ -214,8 +216,8 @@ class MockServer { * */ async addInteractionToMockServer(interaction) { - await mock.addInteraction(interaction) + await mock.addInteraction(interaction); } } -module.exports = MockServer +export default MockServer; diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js index d89fc520a..8adda7363 100644 --- a/lib/helper/Nightmare.js +++ b/lib/helper/Nightmare.js @@ -1,24 +1,25 @@ -const path = require('path') +import path from 'path'; -const urlResolve = require('url').resolve +import { resolve as urlResolve } from 'url'; -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') +import Helper from '@codeceptjs/helper'; +import { includes as stringIncludes } from '../assert/include.js'; +import { urlEquals, equals } from '../assert/equal.js'; +import empty from '../assert/empty.js'; +import { truth } from '../assert/truth'; +import Locator from '../locator.js'; +import ElementNotFound from './errors/ElementNotFound.js'; +import { + xpathLocator, fileExists, screenshotOutputFolder, toCamelCase, +} from '../utils.js'; 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 @@ -49,12 +50,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) { @@ -70,268 +71,235 @@ 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.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.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() + 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)); } } @@ -344,49 +312,45 @@ 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; + }); } /** @@ -409,14 +373,10 @@ 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); } /** @@ -428,7 +388,7 @@ class Nightmare extends Helper { * ``` */ haveHeader(header, value) { - return this.browser.header(header, value) + return this.browser.header(header, value); } /** @@ -437,237 +397,223 @@ 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 } /** @@ -676,7 +622,8 @@ 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 :( } /** @@ -686,7 +633,8 @@ 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 :( } /** @@ -694,68 +642,72 @@ 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); } /** @@ -764,12 +716,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); } /** @@ -783,21 +735,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); } /** @@ -806,171 +758,167 @@ 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')); } /** @@ -978,41 +926,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); } /** @@ -1022,7 +970,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); } /** @@ -1030,16 +978,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); } /** @@ -1050,7 +998,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); } /** @@ -1058,34 +1006,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); + })); } /** @@ -1093,136 +1041,113 @@ 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(); } /** @@ -1230,74 +1155,70 @@ 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(); } /** @@ -1305,40 +1226,28 @@ 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)); } /** @@ -1347,13 +1256,16 @@ 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 */ } @@ -1365,143 +1277,135 @@ 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 +export default 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 8e68fc74d..adb61f5f2 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1,18 +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 { +import path from 'path'; +import fs from 'fs'; +import Helper from '@codeceptjs/helper'; +import { v4 as uuidv4 } from 'uuid'; +import assert from 'assert'; +import promiseRetry from 'promise-retry'; +import Locator from '../locator.js'; +import recorder from '../recorder.js'; +import { includes as stringIncludes } from '../assert/include.js'; +import { urlEquals, equals } from '../assert/equal.js'; +import { empty } from '../assert/empty.js'; +import { truth } from '../assert/truth.js'; + +import { xpathLocator, ucfirst, fileExists, @@ -24,44 +23,40 @@ 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'] - -const { - setRestartStrategy, - restartsSession, - restartsContext, - restartsBrowser, -} = require('./extras/PlaywrightRestartOpts') -const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine') -const { +} from '../utils.js'; + +import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'; +import ElementNotFound from './errors/ElementNotFound.js'; +import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'; +import Popup from './extras/Popup.js'; +import Console from './extras/Console.js'; +import { findByPlaywrightLocator, findReact, findVue } from './extras/PlaywrightReactVueLocator.js'; + +import { + setRestartStrategy, restartsSession, restartsContext, restartsBrowser, +} from './extras/PlaywrightRestartOpts.js'; +import { createValueEngine, createDisabledEngine } from './extras/PlaywrightPropEngine.js'; + +import { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError, -} = require('./errors/ElementAssertion') -const { - dontSeeTraffic, - seeTraffic, - grabRecordedNetworkTraffics, - stopRecordingTraffic, - flushNetworkTraffics, -} = require('./network/actions') +} from './errors/ElementAssertion.js'; + +import { + dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics, +} from './network/actions.js'; + +let playwright; +let perfTiming; +let defaultSelectorEnginesInitialized = false; -const pathSeparator = path.sep +const popupStore = new Popup(); +const consoleLogStore = new Console(); +const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']; + +const pathSeparator = path.sep; /** * ## Configuration @@ -110,7 +105,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: @@ -332,34 +327,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) { @@ -386,65 +381,54 @@ 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) - - // 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) - } - this.options.recordVideo = { size } + this.options.recordVideo = { size: parseWindowSize(this.options.windowSize) }; } 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() { @@ -467,222 +451,216 @@ 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(); }, - } + }; } /** @@ -703,7 +681,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); } /** @@ -717,7 +695,7 @@ class Playwright extends Helper { * ``` */ amAcceptingPopups() { - popupStore.actionType = 'accept' + popupStore.actionType = 'accept'; } /** @@ -726,7 +704,7 @@ class Playwright extends Helper { * libraries](http://jster.net/category/windows-modals-popups). */ acceptPopup() { - popupStore.assertPopupActionType('accept') + popupStore.assertPopupActionType('accept'); } /** @@ -740,23 +718,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); } /** @@ -764,21 +742,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(); } /** @@ -790,33 +768,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); } /** @@ -829,48 +807,45 @@ 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; } /** @@ -879,72 +854,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; } /** @@ -952,22 +927,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, @@ -975,9 +950,9 @@ class Playwright extends Helper { 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd', - ) + ); - return this._waitForAction() + return this._waitForAction(); } /** @@ -998,11 +973,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(); } /** @@ -1018,9 +993,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); } /** @@ -1028,13 +1003,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(); } /** @@ -1042,11 +1017,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(); } /** @@ -1054,11 +1029,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. @@ -1070,14 +1045,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. @@ -1089,8 +1064,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); } /** @@ -1107,24 +1082,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(); } /** @@ -1142,16 +1117,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 }); } /** @@ -1172,13 +1147,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); } /** @@ -1186,8 +1161,8 @@ class Playwright extends Helper { */ scrollPageToTop() { return this.executeScript(() => { - window.scrollTo(0, 0) - }) + window.scrollTo(0, 0); + }); } /** @@ -1195,13 +1170,16 @@ 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, + )); + }); } /** @@ -1209,32 +1187,29 @@ 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); } /** @@ -1245,34 +1220,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(); } /** @@ -1284,11 +1259,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); } /** @@ -1300,8 +1275,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); } /** @@ -1313,10 +1288,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]; } /** @@ -1327,8 +1302,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); } /** @@ -1339,7 +1314,7 @@ class Playwright extends Helper { * ``` */ async _locateFields(locator) { - return findFields.call(this, locator) + return findFields.call(this, locator); } /** @@ -1347,7 +1322,7 @@ class Playwright extends Helper { * */ async grabWebElements(locator) { - return this._locate(locator) + return this._locate(locator); } /** @@ -1355,7 +1330,7 @@ class Playwright extends Helper { * */ async grabWebElement(locator) { - return this._locateElement(locator) + return this._locateElement(locator); } /** @@ -1370,20 +1345,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(); } /** @@ -1397,19 +1372,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(); } /** @@ -1421,12 +1396,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(); } /** @@ -1437,13 +1412,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(); } /** @@ -1462,20 +1437,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; } /** @@ -1483,12 +1458,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); } } @@ -1497,12 +1472,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); } } @@ -1510,11 +1485,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); } } @@ -1522,11 +1497,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); } } @@ -1549,18 +1524,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); + }); } /** @@ -1580,37 +1555,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' }); } /** @@ -1629,9 +1604,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(); } /** @@ -1649,41 +1624,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(); } /** @@ -1693,28 +1668,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(); } /** @@ -1722,13 +1697,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); } } @@ -1737,75 +1712,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: ``, `