From 492d76bc4e46a579e41dddd746b4bb35566a642a Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 31 Jan 2025 17:37:50 +0200 Subject: [PATCH 01/10] fix register retryTo globally --- lib/plugin/retryTo.js | 23 ++++++----------------- lib/plugin/tryTo.js | 21 ++++++--------------- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/lib/plugin/retryTo.js b/lib/plugin/retryTo.js index 71405d974..49f365b86 100644 --- a/lib/plugin/retryTo.js +++ b/lib/plugin/retryTo.js @@ -1,23 +1,12 @@ const { retryTo } = require('../effects') -module.exports = function (config) { - console.log(` -Deprecation Warning: 'retryTo' has been moved to the effects module. -You should update your tests to use it as follows: - -\`\`\`javascript -const { retryTo } = require('codeceptjs/effects'); - -// Example: Retry these steps 5 times before failing -await retryTo((tryNum) => { - I.switchTo('#editor frame'); - I.click('Open'); - I.see('Opened'); -}, 5); -\`\`\` +const defaultConfig = { + registerGlobal: true, +} -For more details, refer to the documentation. - `) +module.exports = function (config) { + config = Object.assign(defaultConfig, config) + console.log(`Deprecation Warning: 'retryTo' has been moved to the 'codeceptjs/effects' module`) if (config.registerGlobal) { global.retryTo = retryTo diff --git a/lib/plugin/tryTo.js b/lib/plugin/tryTo.js index 2eb77245c..a38a277ab 100644 --- a/lib/plugin/tryTo.js +++ b/lib/plugin/tryTo.js @@ -1,21 +1,12 @@ const { tryTo } = require('../effects') -module.exports = function (config) { - console.log(` -Deprecated Warning: 'tryTo' has been moved to the effects module. -You should update your tests to use it as follows: - -\`\`\`javascript -const { tryTo } = require('codeceptjs/effects'); - -// Example: failed step won't fail a test but will return true/false -await tryTo(() => { - I.switchTo('#editor frame'); -}); -\`\`\` +const defaultConfig = { + registerGlobal: true, +} -For more details, refer to the documentation. - `) +module.exports = function (config) { + config = Object.assign(defaultConfig, config) + console.log(`Deprecation Warning: 'tryTo' has been moved to the 'codeceptjs/effects' module`) if (config.registerGlobal) { global.tryTo = tryTo From 202fc4228c1127978c9a508510770f4670c88c1e Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 31 Jan 2025 17:44:43 +0200 Subject: [PATCH 02/10] fixed serializing step args --- lib/step/base.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/step/base.js b/lib/step/base.js index 27ea059dd..42962093b 100644 --- a/lib/step/base.js +++ b/lib/step/base.js @@ -190,8 +190,8 @@ class Step { args.push(arg.name) } else if (typeof arg == 'string') { args.push(arg) - } else { - args.push(JSON.stringify(arg).slice(0, 300)) + } else if (arg) { + args.push((JSON.stringify(arg) || '').slice(0, 300)) } } } From 8e0bc1a7de9a18a175961c77d0e225c932cc786c Mon Sep 17 00:00:00 2001 From: DavertMik Date: Wed, 5 Feb 2025 04:11:48 +0200 Subject: [PATCH 03/10] improved export/types --- lib/step/base.js | 10 ++++++++-- package.json | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/step/base.js b/lib/step/base.js index d5dd6aaa6..3be7442ef 100644 --- a/lib/step/base.js +++ b/lib/step/base.js @@ -25,8 +25,7 @@ class Step { this.opts = {} /** @member {string} */ this.actor = 'I' // I = actor - /** @member {string} */ - this.helperMethod = name // helper method + /** @member {string} */ this.status = 'pending' /** @member {string} */ @@ -38,6 +37,13 @@ class Step { /** @member {string} */ this.stack = '' + // These are part of HelperStep class + // but left here for types compatibility + /** @member {any} */ + this.helper = null + /** @member {string} */ + this.helperMethod = name + this.startTime = 0 this.endTime = 0 diff --git a/package.json b/package.json index abcc7620a..043ec4bdb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "3.7.0-beta.8", + "version": "3.7.0-beta.14", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ "acceptance", @@ -31,6 +31,7 @@ "main": "lib/index.js", "exports": { ".": "./lib/index.js", + "./lib/*": "./lib/*.js", "./els": "./lib/els.js", "./effects": "./lib/effects.js", "./steps": "./lib/steps.js" From a581d6dd2c5c2a245ca6b9c233a04a0c568f1035 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 6 Feb 2025 08:31:43 +0200 Subject: [PATCH 04/10] fixed tryTo/retryTo regressions --- bin/codecept.js | 2 +- lib/command/check.js | 4 +- lib/effects.js | 9 +++- lib/helper/Playwright.js | 2 +- lib/helper/Puppeteer.js | 2 +- lib/plugin/retryFailedStep.js | 25 +++++------ lib/recorder.js | 1 + lib/store.js | 34 ++++++++++++--- lib/within.js | 2 + package.json | 2 +- test/unit/plugin/retryFailedStep_test.js | 54 +++++++++++++++++++----- 11 files changed, 100 insertions(+), 37 deletions(-) diff --git a/bin/codecept.js b/bin/codecept.js index 985da3890..5a4752129 100755 --- a/bin/codecept.js +++ b/bin/codecept.js @@ -62,7 +62,7 @@ program .command('check') .option(commandFlags.config.flag, commandFlags.config.description) .description('Checks configuration and environment before running tests') - .option('-t, --timeout [ms]', 'timeout for checks in ms, 20000 by default') + .option('-t, --timeout [ms]', 'timeout for checks in ms, 50000 by default') .action(errorHandler(require('../lib/command/check'))) program diff --git a/lib/command/check.js b/lib/command/check.js index 6e3dc3e45..020072e03 100644 --- a/lib/command/check.js +++ b/lib/command/check.js @@ -87,9 +87,9 @@ module.exports = async function (options) { if (config?.ai?.request) { checks.ai = true - printCheck('ai', checks['ai'], 'AI configuration is enabled, request function is set') + printCheck('ai', checks['ai'], 'Configuration is enabled, request function is set') } else { - printCheck('ai', checks['ai'], 'AI is disabled') + printCheck('ai', checks['ai'], 'Disabled') } printCheck('tests', checks['tests'], `Total: ${numTests} tests`) diff --git a/lib/effects.js b/lib/effects.js index 3e3a7c462..b6a08aef1 100644 --- a/lib/effects.js +++ b/lib/effects.js @@ -2,6 +2,7 @@ const recorder = require('./recorder') const { debug } = require('./output') const store = require('./store') const event = require('./event') +const within = require('./within') /** * A utility function for CodeceptJS tests that acts as a soft assertion. @@ -178,11 +179,14 @@ async function tryTo(callback) { const sessionName = 'tryTo' let result = false + let hasAutoRetriesEnabled = store.autoRetries return recorder.add( sessionName, () => { recorder.session.start(sessionName) - store.tryTo = true + hasAutoRetriesEnabled = store.autoRetries + if (hasAutoRetriesEnabled) debug('Auto retries disabled inside tryTo effect') + store.autoRetries = false callback() recorder.add(() => { result = true @@ -199,7 +203,7 @@ async function tryTo(callback) { return recorder.add( 'result', () => { - store.tryTo = undefined + store.autoRetries = hasAutoRetriesEnabled return result }, true, @@ -215,4 +219,5 @@ module.exports = { hopeThat, retryTo, tryTo, + within, } diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 8bb30d1fb..b02692822 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -484,7 +484,7 @@ class Playwright extends Helper { this.currentRunningTest = test recorder.retry({ - retries: process.env.FAILED_STEP_RETRIES || 3, + retries: test.opts?.conditionalRetries || 3, when: err => { if (!err || typeof err.message !== 'string') { return false diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 3ea66872d..824756707 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -312,7 +312,7 @@ class Puppeteer extends Helper { this.sessionPages = {} this.currentRunningTest = test recorder.retry({ - retries: process.env.FAILED_STEP_RETRIES || 3, + retries: test.opts?.conditionalRetries || 3, when: err => { if (!err || typeof err.message !== 'string') { return false diff --git a/lib/plugin/retryFailedStep.js b/lib/plugin/retryFailedStep.js index 7d6371514..ce43a1a33 100644 --- a/lib/plugin/retryFailedStep.js +++ b/lib/plugin/retryFailedStep.js @@ -1,8 +1,6 @@ const event = require('../event') const recorder = require('../recorder') -const container = require('../container') -const { log } = require('../output') - +const store = require('../store') const defaultConfig = { retries: 3, defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'], @@ -70,9 +68,9 @@ const defaultConfig = { * Use scenario configuration to disable plugin for a test * * ```js - * Scenario('scenario tite', () => { + * Scenario('scenario tite', { disableRetryFailedStep: true }, () => { * // test goes here - * }).config(test => test.disableRetryFailedStep = true) + * }) * ``` * */ @@ -85,19 +83,14 @@ module.exports = config => { const when = err => { if (!enableRetry) return - const store = require('../store') if (store.debugMode) return false + if (!store.autoRetries) return false if (customWhen) return customWhen(err) return true } config.when = when event.dispatcher.on(event.step.started, step => { - if (process.env.TRY_TO === 'true') { - log('Info: RetryFailedStep plugin is disabled inside tryTo block') - return - } - // if a step is ignored - return for (const ignored of config.ignoredSteps) { if (step.name === ignored) return @@ -113,9 +106,13 @@ module.exports = config => { }) event.dispatcher.on(event.test.before, test => { - if (test && test.disableRetryFailedStep) return // disable retry when a test is not active - // this env var is used to set the retries inside _before() block of helpers - process.env.FAILED_STEP_RETRIES = config.retries + if (test.opts.disableRetryFailedStep) { + store.autoRetries = false + return // disable retry when a test is not active + } + // this option is used to set the retries inside _before() block of helpers + store.autoRetries = true + test.opts.conditionalRetries = config.retries recorder.retry(config) }) } diff --git a/lib/recorder.js b/lib/recorder.js index 5f7dbd59b..a86453775 100644 --- a/lib/recorder.js +++ b/lib/recorder.js @@ -192,6 +192,7 @@ module.exports = { .pop() // no retries or unnamed tasks debug(`${currentQueue()} Running | ${taskName} | Timeout: ${timeout || 'None'}`) + if (retryOpts) debug(`${currentQueue()} Retry opts`, JSON.stringify(retryOpts)) if (!retryOpts || !taskName || !retry) { const [promise, timer] = getTimeoutPromise(timeout, taskName) diff --git a/lib/store.js b/lib/store.js index 18f918ae7..ef554fce0 100644 --- a/lib/store.js +++ b/lib/store.js @@ -3,17 +3,41 @@ * @namespace */ const store = { - /** @type {boolean} */ + /** + * If we are in --debug mode + * @type {boolean} + */ debugMode: false, - /** @type {boolean} */ + + /** + * Is timeouts enabled + * @type {boolean} + */ timeouts: true, - /** @type {boolean} */ + + /** + * If auto-retries are enabled by retryFailedStep plugin + * tryTo effect disables them + * @type {boolean} + */ + autoRetries: false, + + /** + * Tests are executed via dry-run + * @type {boolean} + */ dryRun: false, - /** @type {boolean} */ + /** + * If we are in pause mode + * @type {boolean} + */ onPause: false, + + // current object states + /** @type {CodeceptJS.Test | null} */ currentTest: null, - /** @type {any} */ + /** @type {CodeceptJS.Step | null} */ currentStep: null, /** @type {CodeceptJS.Suite | null} */ currentSuite: null, diff --git a/lib/within.js b/lib/within.js index 119b45406..2ffdeab29 100644 --- a/lib/within.js +++ b/lib/within.js @@ -7,6 +7,8 @@ const MetaStep = require('./step/meta') const { isAsyncFunction } = require('./utils') /** + * TODO: move to effects + * * @param {CodeceptJS.LocatorOrString} context * @param {Function} fn * @return {Promise<*> | undefined} diff --git a/package.json b/package.json index e45ae9b47..4d9ece947 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "3.7.0-beta.14", + "version": "3.7.0-beta.15", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ "acceptance", diff --git a/test/unit/plugin/retryFailedStep_test.js b/test/unit/plugin/retryFailedStep_test.js index e8012868e..55cc2eea9 100644 --- a/test/unit/plugin/retryFailedStep_test.js +++ b/test/unit/plugin/retryFailedStep_test.js @@ -4,9 +4,10 @@ import('chai').then(chai => { }) const retryFailedStep = require('../../../lib/plugin/retryFailedStep') -const tryTo = require('../../../lib/plugin/tryTo') -const within = require('../../../lib/within') +const { tryTo, within } = require('../../../lib/effects') +const { createTest } = require('../../../lib/mocha/test') const session = require('../../../lib/session') +const store = require('../../../lib/store') const container = require('../../../lib/container') const event = require('../../../lib/event') const recorder = require('../../../lib/recorder') @@ -19,16 +20,18 @@ describe('retryFailedStep', () => { _session: () => {}, }, }) + store.autoRetries = false recorder.start() }) afterEach(() => { + store.autoRetries = false event.dispatcher.emit(event.step.finished, {}) }) it('should retry failed step', async () => { retryFailedStep({ retries: 2, minTimeout: 1 }) - event.dispatcher.emit(event.test.before, {}) + event.dispatcher.emit(event.test.before, createTest('test')) event.dispatcher.emit(event.step.started, { name: 'click' }) let counter = 0 @@ -48,7 +51,8 @@ describe('retryFailedStep', () => { it('should not retry within', async () => { retryFailedStep({ retries: 1, minTimeout: 1 }) - event.dispatcher.emit(event.test.before, {}) + const test = createTest('test') + event.dispatcher.emit(event.test.before, test) let counter = 0 event.dispatcher.emit(event.step.started, { name: 'click' }) @@ -69,14 +73,14 @@ describe('retryFailedStep', () => { await recorder.catchWithoutStop(err => err) } - expect(process.env.FAILED_STEP_RETRIES).to.equal('1') + expect(test.opts.conditionalRetries).to.equal(1) // expects to retry only once counter.should.equal(2) }) it('should not retry steps with wait*', async () => { retryFailedStep({ retries: 2, minTimeout: 1 }) - event.dispatcher.emit(event.test.before, {}) + event.dispatcher.emit(event.test.before, createTest('test')) let counter = 0 event.dispatcher.emit(event.step.started, { name: 'waitForElement' }) @@ -103,7 +107,7 @@ describe('retryFailedStep', () => { it('should not retry steps with amOnPage', async () => { retryFailedStep({ retries: 2, minTimeout: 1 }) - event.dispatcher.emit(event.test.before, {}) + event.dispatcher.emit(event.test.before, createTest('test')) let counter = 0 event.dispatcher.emit(event.step.started, { name: 'amOnPage' }) @@ -130,7 +134,7 @@ describe('retryFailedStep', () => { it('should add custom steps to ignore', async () => { retryFailedStep({ retries: 2, minTimeout: 1, ignoredSteps: ['somethingNew*'] }) - event.dispatcher.emit(event.test.before, {}) + event.dispatcher.emit(event.test.before, createTest('test')) let counter = 0 event.dispatcher.emit(event.step.started, { name: 'somethingNew' }) @@ -157,7 +161,7 @@ describe('retryFailedStep', () => { it('should add custom regexp steps to ignore', async () => { retryFailedStep({ retries: 2, minTimeout: 1, ignoredSteps: [/somethingNew/] }) - event.dispatcher.emit(event.test.before, {}) + event.dispatcher.emit(event.test.before, createTest('test')) let counter = 0 event.dispatcher.emit(event.step.started, { name: 'somethingNew' }) @@ -184,7 +188,7 @@ describe('retryFailedStep', () => { it('should not retry session', async () => { retryFailedStep({ retries: 1, minTimeout: 1 }) - event.dispatcher.emit(event.test.before, {}) + event.dispatcher.emit(event.test.before, createTest('test')) event.dispatcher.emit(event.step.started, { name: 'click' }) let counter = 0 @@ -246,4 +250,34 @@ describe('retryFailedStep', () => { ) return recorder.promise() }) + + it('should not retry failed step when tryTo plugin is enabled', async () => { + retryFailedStep({ retries: 2, minTimeout: 1 }) + event.dispatcher.emit(event.test.before, createTest('test')) + + let counter = 0 + + // without tryTo effect + event.dispatcher.emit(event.step.started, { name: 'click' }) + recorder.add('failed step', () => { + counter++ + if (counter < 3) throw new Error('Ups') + }) + await recorder.promise() + + expect(counter).to.equal(3) + counter = 0 + + // with tryTo effect + let res = await tryTo(async () => { + event.dispatcher.emit(event.step.started, { name: 'click' }) + recorder.add('failed step', () => { + counter++ + throw new Error('Ups') + }) + return recorder.promise() + }) + expect(counter).to.equal(1) + expect(res).to.equal(false) + }) }) From 4b29e699c63c2ee66b2acf059e79d528474c1334 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 6 Feb 2025 08:33:47 +0200 Subject: [PATCH 05/10] fixed subtitles test --- docs/plugins.md | 1075 +++++++--------------------- test/unit/plugin/subtitles_test.js | 35 +- 2 files changed, 285 insertions(+), 825 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 9c88deeab..ec4bbc604 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -5,107 +5,37 @@ sidebar: auto title: Plugins --- - +## analyze### Parameters* `config` **any** (optional, default `{}`)## authLogs user in for the first test and reuses session for next tests. -## analyze - -### Parameters - -- `config` **any** (optional, default `{}`) - -## autoDelay - -Sometimes it takes some time for a page to respond to user's actions. -Depending on app's performance this can be either slow or fast. - -For instance, if you click a button and nothing happens - probably JS event is not attached to this button yet -Also, if you fill field and input validation doesn't accept your input - maybe because you typed value too fast. - -This plugin allows to slow down tests execution when a test running too fast. -It puts a tiny delay for before and after action commands. - -Commands affected (by default): - -- `click` -- `fillField` -- `checkOption` -- `pressKey` -- `doubleClick` -- `rightClick` - -#### Configuration - -```js -plugins: { - autoDelay: { - enabled: true - } -} -``` - -Possible config options: - -- `methods`: list of affected commands. Can be overridden -- `delayBefore`: put a delay before a command. 100ms by default -- `delayAfter`: put a delay after a command. 200ms by default - -### Parameters - -- `config` - -## autoLogin - -Logs user in for the first test and reuses session for next tests. Works by saving cookies into memory or file. -If a session expires automatically logs in again. - -> For better development experience cookies can be saved into file, so a session can be reused while writing tests. - -#### Usage - -1. Enable this plugin and configure as described below -2. Define user session names (example: `user`, `editor`, `admin`, etc). -3. Define how users are logged in and how to check that user is logged in -4. Use `login` object inside your tests to log in: - -```js +If a session expires automatically logs in again.> For better development experience cookies can be saved into file, so a session can be reused while writing tests.#### Usage1. Enable this plugin and configure as described below 2. Define user session names (example: `user`, `editor`, `admin`, etc). 3. Define how users are logged in and how to check that user is logged in 4. Use `login` object inside your tests to log in:```js // inside a test file // use login to inject auto-login function -Feature('Login') +Feature('Login'); Before(({ login }) => { - login('user') // login using user session -}) +login('user'); // login using user session +}); // Alternatively log in for one scenario. -Scenario('log me in', ({ I, login }) => { - login('admin') - I.see('I am logged in') -}) -``` - -#### Configuration +Scenario('log me in', ( { I, login } ) => { +login('admin'); +I.see('I am logged in'); +}); +```#### Configuration* `saveToFile` (default: false) - save cookies to file. Allows to reuse session between execution. -- `saveToFile` (default: false) - save cookies to file. Allows to reuse session between execution. - `inject` (default: `login`) - name of the login function to use - `users` - an array containing different session names and functions to: - `login` - sign in into the system - `check` - check that user is logged in - `fetch` - to get current cookies (by default `I.grabCookie()`) - - `restore` - to set cookies (by default `I.amOnPage('/'); I.setCookie(cookie)`) + - `restore` - to set cookies (by default `I.amOnPage('/'); I.setCookie(cookie)`)#### How It Works1. `restore` method is executed. It should open a page and set credentials. -#### How It Works - -1. `restore` method is executed. It should open a page and set credentials. 2. `check` method is executed. It should reload a page (so cookies are applied) and check that this page belongs to logged-in user. When you pass the second args `session`, you could perform the validation using passed session. 3. If `restore` and `check` were not successful, `login` is executed 4. `login` should fill in login form -5. After successful login, `fetch` is executed to save cookies into memory or file. - -#### Example: Simple login - -```js -autoLogin: { +5. After successful login, `fetch` is executed to save cookies into memory or file.#### Example: Simple login``js +auth: { enabled: true, saveToFile: true, inject: 'login', @@ -121,51 +51,42 @@ autoLogin: { } } } -``` - -#### Example: Multiple users - -```js -autoLogin: { +``#### Example: Multiple users```js +auth: { enabled: true, saveToFile: true, inject: 'loginAs', // use `loginAs` instead of login - users: { + users: { user: { - login: (I) => { - I.amOnPage('/login'); - I.fillField('email', 'user@site.com'); - I.fillField('password', '123456'); - I.click('Login'); - }, - check: (I) => { - I.amOnPage('/'); - I.see('User', '.navbar'); - }, + login: (I) => { + I.amOnPage('/login'); + I.fillField('email', 'user@site.com'); + I.fillField('password', '123456'); + I.click('Login'); + }, + check: (I) => { + I.amOnPage('/'); + I.see('User', '.navbar'); + }, }, admin: { - login: (I) => { - I.amOnPage('/login'); - I.fillField('email', 'admin@site.com'); - I.fillField('password', '123456'); - I.click('Login'); - }, - check: (I) => { - I.amOnPage('/'); - I.see('Admin', '.navbar'); - }, + login: (I) => { + I.amOnPage('/login'); + I.fillField('email', 'admin@site.com'); + I.fillField('password', '123456'); + I.click('Login'); }, - } -} -``` - -#### Example: Keep cookies between tests + check: (I) => { + I.amOnPage('/'); + I.see('Admin', '.navbar'); + }, + }, + } + } -If you decide to keep cookies between tests you don't need to save/retrieve cookies between tests. +```````#### Example: Keep cookies between testsIf you decide to keep cookies between tests you don't need to save/retrieve cookies between tests. But you need to login once work until session expires. -For this case, disable `fetch` and `restore` methods. - -```js +For this case, disable `fetch` and `restore` methods.```js helpers: { WebDriver: { // config goes here @@ -173,7 +94,7 @@ helpers: { } }, plugins: { - autoLogin: { + auth: { users: { admin: { login: (I) => { @@ -192,15 +113,9 @@ plugins: { } } } -``` - -#### Example: Getting sessions from local storage - -If your session is stored in local storage instead of cookies you still can obtain sessions. - -```js +```#### Example: Getting sessions from local storageIf your session is stored in local storage instead of cookies you still can obtain sessions.```js plugins: { - autoLogin: { + auth: { admin: { login: (I) => I.loginAsAdmin(), check: (I) => I.see('Admin', '.navbar'), @@ -214,20 +129,14 @@ plugins: { } } } -``` - -#### Tips: Using async function in the autoLogin - -If you use async functions in the autoLogin plugin, login function should be used with `await` keyword. - -```js -autoLogin: { +```#### Tips: Using async function in the authIf you use async functions in the auth plugin, login function should be used with `await` keyword.```js +auth: { enabled: true, saveToFile: true, inject: 'login', users: { admin: { - login: async (I) => { // If you use async function in the autoLogin plugin + login: async (I) => { // If you use async function in the auth plugin const phrase = await I.grabTextFrom('#phrase') I.fillField('username', 'admin'), I.fillField('password', 'password') @@ -240,26 +149,18 @@ autoLogin: { } } } -``` - -```js -Scenario('login', async ({ I, login }) => { +``````js +Scenario('login', async ( {I, login} ) => { await login('admin') // you should use `await` }) -``` - -#### Tips: Using session to validate user - -Instead of asserting on page elements for the current user in `check`, you can use the `session` you saved in `fetch` - -```js -autoLogin: { +```#### Tips: Using session to validate userInstead of asserting on page elements for the current user in `check`, you can use the `session` you saved in `fetch````js +auth: { enabled: true, saveToFile: true, inject: 'login', users: { admin: { - login: async (I) => { // If you use async function in the autoLogin plugin + login: async (I) => { // If you use async function in the auth plugin const phrase = await I.grabTextFrom('#phrase') I.fillField('username', 'admin'), I.fillField('password', 'password') @@ -274,128 +175,121 @@ autoLogin: { } } } -``` - -```js -Scenario('login', async ({ I, login }) => { +``````js +Scenario('login', async ( {I, login} ) => { await login('admin') // you should use `await` }) -``` - -### Parameters - -- `config` - -## commentStep - -Add descriptive nested steps for your tests: - -```js -Scenario('project update test', async I => { - __`Given` - const projectId = await I.have('project') - - __`When` - projectPage.update(projectId, { title: 'new title' }) - - __`Then` - projectPage.open(projectId) - I.see('new title', 'h1') -}) -``` - -Steps prefixed with `__` will be printed as nested steps in `--steps` output: - - Given - I have "project" - When - projectPage update - Then - projectPage open - I see "new title", "h1" - -Also those steps will be exported to allure reports. - -This plugin can be used +```### Parameters* `config` ## autoDelaySometimes it takes some time for a page to respond to user's actions. +Depending on app's performance this can be either slow or fast.For instance, if you click a button and nothing happens - probably JS event is not attached to this button yet +Also, if you fill field and input validation doesn't accept your input - maybe because you typed value too fast.This plugin allows to slow down tests execution when a test running too fast. +It puts a tiny delay for before and after action commands.Commands affected (by default):* `click` +* `fillField` +* `checkOption` +* `pressKey` +* `doubleClick` +* `rightClick`#### Configuration```js +plugins: { + autoDelay: { + enabled: true + } +} +```Possible config options:* `methods`: list of affected commands. Can be overridden +* `delayBefore`: put a delay before a command. 100ms by default +* `delayAfter`: put a delay after a command. 200ms by default### Parameters* `config` ## commentStep### Parameters* `config` **Meta*** **deprecated**: Add descriptive nested steps for your tests: -### Config + ```js + Scenario('project update test', async (I) => { + __`Given`; + const projectId = await I.have('project'); -- `enabled` - (default: false) enable a plugin -- `registerGlobal` - (default: false) register `__` template literal function globally. You can override function global name by providing a name as a value. + __`When`; + projectPage.update(projectId, { title: 'new title' }); -### Examples + __`Then`; + projectPage.open(projectId); + I.see('new title', 'h1'); + }) + ``` -Registering `__` globally: + Steps prefixed with `__` will be printed as nested steps in `--steps` output: -```js -plugins: { - commentStep: { - enabled: true, - registerGlobal: true - } -} -``` + Given + I have "project" + When + projectPage update + Then + projectPage open + I see "new title", "h1" -Registering `Step` globally: + Also those steps will be exported to allure reports. -```js -plugins: { - commentStep: { - enabled: true, - registerGlobal: 'Step' - } -} -``` + This plugin can be used -Using only local function names: + ### Config -```js -plugins: { - commentStep: { - enabled: true - } -} -``` + * `enabled` - (default: false) enable a plugin + * `registerGlobal` - (default: false) register `__` template literal function globally. You can override function global name by providing a name as a value. -Then inside a test import a comment function from a plugin. -For instance, you can prepare Given/When/Then functions to use them inside tests: + ### Examples -```js -// inside a test -const step = codeceptjs.container.plugins('commentStep') + Registering `__` globally: -const Given = () => step`Given` -const When = () => step`When` -const Then = () => step`Then` -``` + ```js + plugins: { + commentStep: { + enabled: true, + registerGlobal: true + } + } + ``` -Scenario('project update test', async (I) => { -Given(); -const projectId = await I.have('project'); + Registering `Step` globally: -When(); -projectPage.update(projectId, { title: 'new title' }); + ```js + plugins: { + commentStep: { + enabled: true, + registerGlobal: 'Step' + } + } + ``` -Then(); -projectPage.open(projectId); -I.see('new title', 'h1'); -}); + Using only local function names: -``` + ```js + plugins: { + commentStep: { + enabled: true + } + } + ``` -``` + Then inside a test import a comment function from a plugin. + For instance, you can prepare Given/When/Then functions to use them inside tests: -### Parameters + ```js + // inside a test + const step = codeceptjs.container.plugins('commentStep'); -- `config` + const Given = () => step`Given`; + const When = () => step`When`; + const Then = () => step`Then`; + ``` -## coverage + Scenario('project update test', async (I) => { + Given(); + const projectId = await I.have('project'); -Dumps code coverage from Playwright/Puppeteer after every test. + When(); + projectPage.update(projectId, { title: 'new title' }); -#### Configuration + Then(); + projectPage.open(projectId); + I.see('new title', 'h1'); + }); -```js + ``` + ```## coverageDumps code coverage from Playwright/Puppeteer after every test.#### Configuration```js plugins: { coverage: { enabled: true, @@ -404,49 +298,21 @@ plugins: { outputDir: 'output/coverage' } } -``` - -Possible config options, More could be found at [monocart-coverage-reports][1] - -- `debug`: debug info. By default, false. -- `name`: coverage report name. -- `outputDir`: path to coverage report. -- `sourceFilter`: filter the source files. -- `sourcePath`: option to resolve a custom path. - -### Parameters - -- `config` - -## customLocator - -Creates a [custom locator][2] by using special attributes in HTML. - -If you have a convention to use `data-test-id` or `data-qa` attributes to mark active elements for e2e tests, -you can enable this plugin to simplify matching elements with these attributes: - -```js +```Possible config options, More could be found at [monocart-coverage-reports][1]* `debug`: debug info. By default, false. +* `name`: coverage report name. +* `outputDir`: path to coverage report. +* `sourceFilter`: filter the source files. +* `sourcePath`: option to resolve a custom path.### Parameters* `config` ## customLocatorCreates a [custom locator][2] by using special attributes in HTML.If you have a convention to use `data-test-id` or `data-qa` attributes to mark active elements for e2e tests, +you can enable this plugin to simplify matching elements with these attributes:```js // replace this: I.click({ css: '[data-test-id=register_button]'); // with this: I.click('$register_button'); -``` - -This plugin will create a valid XPath locator for you. - -#### Configuration - -- `enabled` (default: `false`) should a locator be enabled -- `prefix` (default: `$`) sets a prefix for a custom locator. -- `attribute` (default: `data-test-id`) to set an attribute to be matched. -- `strategy` (default: `xpath`) actual locator strategy to use in query (`css` or `xpath`). -- `showActual` (default: false) show in the output actually produced XPath or CSS locator. By default shows custom locator value. - -#### Examples: - -Using `data-test` attribute with `$` prefix: - -```js +```This plugin will create a valid XPath locator for you.#### Configuration* `enabled` (default: `false`) should a locator be enabled +* `prefix` (default: `$`) sets a prefix for a custom locator. +* `attribute` (default: `data-test-id`) to set an attribute to be matched. +* `strategy` (default: `xpath`) actual locator strategy to use in query (`css` or `xpath`). +* `showActual` (default: false) show in the output actually produced XPath or CSS locator. By default shows custom locator value.#### Examples:Using `data-test` attribute with `$` prefix:```js // in codecept.conf.js plugins: { customLocator: { @@ -454,18 +320,10 @@ plugins: { attribute: 'data-test' } } -``` - -In a test: - -```js -I.seeElement('$user') // matches => [data-test=user] -I.click('$sign-up') // matches => [data-test=sign-up] -``` - -Using `data-qa` attribute with `=` prefix: - -```js +```In a test:```js +I.seeElement('$user'); // matches => [data-test=user] +I.click('$sign-up'); // matches => [data-test=sign-up] +```Using `data-qa` attribute with `=` prefix:```js // in codecept.conf.js plugins: { customLocator: { @@ -474,18 +332,10 @@ plugins: { attribute: 'data-qa' } } -``` - -In a test: - -```js -I.seeElement('=user') // matches => [data-qa=user] -I.click('=sign-up') // matches => [data-qa=sign-up] -``` - -Using `data-qa` OR `data-test` attribute with `=` prefix: - -```js +```In a test:```js +I.seeElement('=user'); // matches => [data-qa=user] +I.click('=sign-up'); // matches => [data-qa=sign-up] +```Using `data-qa` OR `data-test` attribute with `=` prefix:```js // in codecept.conf.js plugins: { customLocator: { @@ -495,16 +345,10 @@ plugins: { strategy: 'xpath' } } -``` - -In a test: - -```js -I.seeElement('=user') // matches => //*[@data-qa=user or @data-test=user] -I.click('=sign-up') // matches => //*[data-qa=sign-up or @data-test=sign-up] -``` - -```js +```In a test:```js +I.seeElement('=user'); // matches => //*[@data-qa=user or @data-test=user] +I.click('=sign-up'); // matches => //*[data-qa=sign-up or @data-test=sign-up] +``````js // in codecept.conf.js plugins: { customLocator: { @@ -514,119 +358,41 @@ plugins: { strategy: 'css' } } -``` - -In a test: - -```js -I.seeElement('=user') // matches => [data-qa=user],[data-test=user] -I.click('=sign-up') // matches => [data-qa=sign-up],[data-test=sign-up] -``` - -### Parameters - -- `config` - -## customReporter - -Sample custom reporter for CodeceptJS. - -### Parameters - -- `config` - -## eachElement - -Provides `eachElement` global function to iterate over found elements to perform actions on them. - -`eachElement` takes following args: - -- `purpose` - the goal of an action. A comment text that will be displayed in output. -- `locator` - a CSS/XPath locator to match elements -- `fn(element, index)` - **asynchronous** function which will be executed for each matched element. - -Example of usage: - -```js +```In a test:```js +I.seeElement('=user'); // matches => [data-qa=user],[data-test=user] +I.click('=sign-up'); // matches => [data-qa=sign-up],[data-test=sign-up] +```### Parameters* `config` ## customReporterSample custom reporter for CodeceptJS.### Parameters* `config` ## eachElementProvides `eachElement` global function to iterate over found elements to perform actions on them.`eachElement` takes following args:* `purpose` - the goal of an action. A comment text that will be displayed in output. +* `locator` - a CSS/XPath locator to match elements +* `fn(element, index)` - **asynchronous** function which will be executed for each matched element.Example of usage:```js // this example works with Playwright and Puppeteer helper -await eachElement('click all checkboxes', 'form input[type=checkbox]', async el => { - await el.click() -}) -``` - -Click odd elements: - -```js +await eachElement('click all checkboxes', 'form input[type=checkbox]', async (el) => { + await el.click(); +}); +```Click odd elements:```js // this example works with Playwright and Puppeteer helper await eachElement('click odd buttons', '.button-select', async (el, index) => { - if (index % 2) await el.click() -}) -``` - -Check all elements for visibility: - -```js + if (index % 2) await el.click(); +}); +```Check all elements for visibility:```js // this example works with Playwright and Puppeteer helper -const assert = require('assert') -await eachElement('check all items are visible', '.item', async el => { - assert(await el.isVisible()) -}) -``` - -This method works with WebDriver, Playwright, Puppeteer, Appium helpers. - -Function parameter `el` represents a matched element. -Depending on a helper API of `el` can be different. Refer to API of corresponding browser testing engine for a complete API list: - -- [Playwright ElementHandle][3] -- [Puppeteer][4] -- [webdriverio element][5] - -#### Configuration - -- `registerGlobal` - to register `eachElement` function globally, true by default - -If `registerGlobal` is false you can use eachElement from the plugin: - -```js -const eachElement = codeceptjs.container.plugins('eachElement') -``` - -### Parameters - -- `purpose` **[string][6]** -- `locator` **CodeceptJS.LocatorOrString** -- `fn` **[Function][7]** - -Returns **([Promise][8]\ | [undefined][9])** - -## fakerTransform - -Use the `@faker-js/faker` package to generate fake data inside examples on your gherkin tests - -#### Usage - -To start please install `@faker-js/faker` package - - npm install -D @faker-js/faker - - - - yarn add -D @faker-js/faker - -Add this plugin to config file: - -```js +const assert = require('assert'); +await eachElement('check all items are visible', '.item', async (el) => { + assert(await el.isVisible()); +}); +```This method works with WebDriver, Playwright, Puppeteer, Appium helpers.Function parameter `el` represents a matched element. +Depending on a helper API of `el` can be different. Refer to API of corresponding browser testing engine for a complete API list:* [Playwright ElementHandle][3] +* [Puppeteer][4] +* [webdriverio element][5]#### Configuration* `registerGlobal` - to register `eachElement` function globally, true by defaultIf `registerGlobal` is false you can use eachElement from the plugin:```js +const eachElement = codeceptjs.container.plugins('eachElement'); +```### Parameters* `purpose` **[string][6]** +* `locator` **CodeceptJS.LocatorOrString** +* `fn` **[Function][7]** Returns **([Promise][8]\ | [undefined][9])** ## fakerTransformUse the `@faker-js/faker` package to generate fake data inside examples on your gherkin tests#### UsageTo start please install `@faker-js/faker` package npm install -D @faker-js/faker yarn add -D @faker-js/fakerAdd this plugin to config file:```js plugins: { - fakerTransform: { - enabled: true - } + fakerTransform: { + enabled: true + } } -``` - -Add the faker API using a mustache string format inside examples tables in your gherkin scenario outline - -```feature +```Add the faker API using a mustache string format inside examples tables in your gherkin scenario outline```feature Scenario Outline: ... Given ... When ... @@ -634,118 +400,48 @@ Scenario Outline: ... Examples: | productName | customer | email | anythingMore | | {{commerce.product}} | Dr. {{name.findName}} | {{internet.email}} | staticData | -``` - -### Parameters - -- `config` - -## heal - -Self-healing tests with AI. - -Read more about heaking in [Self-Healing Tests][10] - -```js +```### Parameters* `config` ## healSelf-healing tests with AI.Read more about heaking in [Self-Healing Tests][10]```js plugins: { heal: { enabled: true, } } -``` - -More config options are available: - -- `healLimit` - how many steps can be healed in a single test (default: 2) - -### Parameters - -- `config` (optional, default `{}`) - -## pageInfo - -Collects information from web page after each failed test and adds it to the test as an artifact. +```More config options are available:* `healLimit` - how many steps can be healed in a single test (default: 2)### Parameters* `config` (optional, default `{}`)## pageInfoCollects information from web page after each failed test and adds it to the test as an artifact. It is suggested to enable this plugin if you run tests on CI and you need to debug failed tests. -This plugin can be paired with `analyze` plugin to provide more context. - -It collects URL, HTML errors (by classes), and browser logs. - -Enable this plugin in config: - -```js +This plugin can be paired with `analyze` plugin to provide more context.It collects URL, HTML errors (by classes), and browser logs.Enable this plugin in config:```js plugins: { pageInfo: { enabled: true, } -``` - -Additional config options: - -- `errorClasses` - list of classes to search for errors (default: `['error', 'warning', 'alert', 'danger']`) -- `browserLogs` - list of types of errors to search for in browser logs (default: `['error']`) - -### Parameters - -- `config` (optional, default `{}`) - -## pauseOnFail - -Automatically launches [interactive pause][11] when a test fails. - -Useful for debugging flaky tests on local environment. -Add this plugin to config file: - -```js +```Additional config options:* `errorClasses` - list of classes to search for errors (default: `['error', 'warning', 'alert', 'danger']`) +* `browserLogs` - list of types of errors to search for in browser logs (default: `['error']`)### Parameters* `config` (optional, default `{}`)## pauseOnFailAutomatically launches [interactive pause][11] when a test fails.Useful for debugging flaky tests on local environment. +Add this plugin to config file:```js plugins: { pauseOnFail: {}, } -``` - -Unlike other plugins, `pauseOnFail` is not recommended to be enabled by default. -Enable it manually on each run via `-p` option: - - npx codeceptjs run -p pauseOnFail - -## retryFailedStep - -Retries each failed step in a test. - -Add this plugin to config file: - -```js +```Unlike other plugins, `pauseOnFail` is not recommended to be enabled by default. +Enable it manually on each run via `-p` option: npx codeceptjs run -p pauseOnFail## retryFailedStepRetries each failed step in a test.Add this plugin to config file:```js plugins: { - retryFailedStep: { - enabled: true - } + retryFailedStep: { + enabled: true + } } -``` - -Run tests with plugin enabled: - - npx codeceptjs run --plugins retryFailedStep - -#### Configuration: - -- `retries` - number of retries (by default 3), -- `when` - function, when to perform a retry (accepts error as parameter) -- `factor` - The exponential factor to use. Default is 1.5. -- `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000. -- `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity. -- `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false. -- `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes: - - `amOnPage` - - `wait*` - - `send*` - - `execute*` - - `run*` - - `have*` -- `ignoredSteps` - an array for custom steps to ignore on retry. Use it to append custom steps to ignored list. - You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`. - To append your own steps to ignore list - copy and paste a default steps list. Regexp values are accepted as well. - -#### Example - -```js +```Run tests with plugin enabled: npx codeceptjs run --plugins retryFailedStep#### Configuration:* `retries` - number of retries (by default 3), +* `when` - function, when to perform a retry (accepts error as parameter) +* `factor` - The exponential factor to use. Default is 1.5. +* `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000. +* `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity. +* `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false. +* `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes: + * `amOnPage` + * `wait*` + * `send*` + * `execute*` + * `run*` + * `have*` +* `ignoredSteps` - an array for custom steps to ignore on retry. Use it to append custom steps to ignored list. + You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`. + To append your own steps to ignore list - copy and paste a default steps list. Regexp values are accepted as well.#### Example```js plugins: { retryFailedStep: { enabled: true, @@ -755,78 +451,20 @@ plugins: { ] } } -``` - -#### Disable Per Test - -This plugin can be disabled per test. In this case you will need to stet `I.retry()` to all flaky steps: - -Use scenario configuration to disable plugin for a test - -```js -Scenario('scenario tite', () => { - // test goes here -}).config(test => (test.disableRetryFailedStep = true)) -``` - -### Parameters - -- `config` - -## screenshotOnFail - -Creates screenshot on failure. Screenshot is saved into `output` directory. - -Initially this functionality was part of corresponding helper but has been moved into plugin since 1.4 - -This plugin is **enabled by default**. - -#### Configuration - -Configuration can either be taken from a corresponding helper (deprecated) or a from plugin config (recommended). - -```js +```#### Disable Per TestThis plugin can be disabled per test. In this case you will need to stet `I.retry()` to all flaky steps:Use scenario configuration to disable plugin for a test```js +Scenario('scenario tite', { disableRetryFailedStep: true }, () => { + // test goes here +}) +```### Parameters* `config` ## screenshotOnFailCreates screenshot on failure. Screenshot is saved into `output` directory.Initially this functionality was part of corresponding helper but has been moved into plugin since 1.4This plugin is **enabled by default**.#### ConfigurationConfiguration can either be taken from a corresponding helper (deprecated) or a from plugin config (recommended).```js plugins: { - screenshotOnFail: { - enabled: true - } + screenshotOnFail: { + enabled: true + } } -``` - -Possible config options: - -- `uniqueScreenshotNames`: use unique names for screenshot. Default: false. -- `fullPageScreenshots`: make full page screenshots. Default: false. - -### Parameters - -- `config` - -## selenoid - -[Selenoid][12] plugin automatically starts browsers and video recording. -Works with WebDriver helper. - -### Prerequisite - -This plugin **requires Docker** to be installed. - -> If you have issues starting Selenoid with this plugin consider using the official [Configuration Manager][13] tool from Selenoid - -### Usage - -Selenoid plugin can be started in two ways: - -1. **Automatic** - this plugin will create and manage selenoid container for you. -2. **Manual** - you create the conatainer and configure it with a plugin (recommended). - -#### Automatic - -If you are new to Selenoid and you want plug and play setup use automatic mode. - -Add plugin configuration in `codecept.conf.js`: - -```js +```Possible config options:* `uniqueScreenshotNames`: use unique names for screenshot. Default: false. +* `fullPageScreenshots`: make full page screenshots. Default: false.### Parameters* `config` ## selenoid[Selenoid][12] plugin automatically starts browsers and video recording. +Works with WebDriver helper.### PrerequisiteThis plugin **requires Docker** to be installed.> If you have issues starting Selenoid with this plugin consider using the official [Configuration Manager][13] tool from Selenoid### UsageSelenoid plugin can be started in two ways:1. **Automatic** - this plugin will create and manage selenoid container for you. +2. **Manual** - you create the conatainer and configure it with a plugin (recommended).#### AutomaticIf you are new to Selenoid and you want plug and play setup use automatic mode.Add plugin configuration in `codecept.conf.js`:```js plugins: { selenoid: { enabled: true, @@ -838,28 +476,10 @@ plugins: { enableLog: true, }, } -``` - -When `autoCreate` is enabled it will pull the [latest Selenoid from DockerHub][14] and start Selenoid automatically. -It will also create `browsers.json` file required by Selenoid. - -In automatic mode the latest version of browser will be used for tests. It is recommended to specify exact version of each browser inside `browsers.json` file. - -> **If you are using Windows machine or if `autoCreate` does not work properly, create container manually** - -#### Manual - -While this plugin can create containers for you for better control it is recommended to create and launch containers manually. -This is especially useful for Continous Integration server as you can configure scaling for Selenoid containers. - -> Use [Selenoid Configuration Manager][13] to create and start containers semi-automatically. - -1. Create `browsers.json` file in the same directory `codecept.conf.js` is located - [Refer to Selenoid documentation][15] to know more about browsers.json. - -_Sample browsers.json_ - -```js +```When `autoCreate` is enabled it will pull the [latest Selenoid from DockerHub][14] and start Selenoid automatically. +It will also create `browsers.json` file required by Selenoid.In automatic mode the latest version of browser will be used for tests. It is recommended to specify exact version of each browser inside `browsers.json` file.> **If you are using Windows machine or if `autoCreate` does not work properly, create container manually**#### ManualWhile this plugin can create containers for you for better control it is recommended to create and launch containers manually. +This is especially useful for Continous Integration server as you can configure scaling for Selenoid containers.> Use [Selenoid Configuration Manager][13] to create and start containers semi-automatically.1. Create `browsers.json` file in the same directory `codecept.conf.js` is located + [Refer to Selenoid documentation][15] to know more about browsers.json.*Sample browsers.json*```js { "chrome": { "default": "latest", @@ -872,17 +492,7 @@ _Sample browsers.json_ } } } -``` - -> It is recommended to use specific versions of browsers in `browsers.json` instead of latest. This will prevent tests fail when browsers will be updated. - -**⚠ At first launch selenoid plugin takes extra time to download all Docker images before tests starts**. - -2. Create Selenoid container - -Run the following command to create a container. To know more [refer here][16] - -```bash +```> It is recommended to use specific versions of browsers in `browsers.json` instead of latest. This will prevent tests fail when browsers will be updated.**⚠ At first launch selenoid plugin takes extra time to download all Docker images before tests starts**.2. Create Selenoid containerRun the following command to create a container. To know more [refer here][16]```bash docker create \ --name selenoid \ -p 4444:4444 \ @@ -891,20 +501,8 @@ docker create \ -v `pwd`/output/video/:/opt/selenoid/video/ \ -e OVERRIDE_VIDEO_OUTPUT_DIR=`pwd`/output/video/ \ aerokube/selenoid:latest-release -``` - -### Video Recording - -This plugin allows to record and save video per each executed tests. - -When `enableVideo` is `true` this plugin saves video in `output/videos` directory with each test by name -To save space videos for all succesful tests are deleted. This can be changed by `deletePassed` option. - -When `allure` plugin is enabled a video is attached to report automatically. - -### Options: - -| Param | Description | +```### Video RecordingThis plugin allows to record and save video per each executed tests.When `enableVideo` is `true` this plugin saves video in `output/videos` directory with each test by name +To save space videos for all succesful tests are deleted. This can be changed by `deletePassed` option.When `allure` plugin is enabled a video is attached to report automatically.### Options:| Param | Description | | ---------------- | ------------------------------------------------------------------------------ | | name | Name of the container (default : selenoid) | | port | Port of selenium server (default : 4444) | @@ -913,85 +511,39 @@ When `allure` plugin is enabled a video is attached to report automatically. | enableVideo | Enable video recording and use `video` folder of output (default: false) | | enableLog | Enable log recording and use `logs` folder of output (default: false) | | deletePassed | Delete video and logs of passed tests (default : true) | -| additionalParams | example: `additionalParams: '--env TEST=test'` [Refer here][17] to know more | - -### Parameters - -- `config` - -## stepByStepReport - -![step-by-step-report][18] - -Generates step by step report for a test. +| additionalParams | example: `additionalParams: '--env TEST=test'` [Refer here][17] to know more |### Parameters* `config` ## stepByStepReport![step-by-step-report][18]Generates step by step report for a test. After each step in a test a screenshot is created. After test executed screenshots are combined into slideshow. -By default, reports are generated only for failed tests. - -Run tests with plugin enabled: - - npx codeceptjs run --plugins stepByStepReport - -#### Configuration - -```js +By default, reports are generated only for failed tests.Run tests with plugin enabled: npx codeceptjs run --plugins stepByStepReport#### Configuration```js "plugins": { "stepByStepReport": { "enabled": true } } -``` - -Possible config options: - -- `deleteSuccessful`: do not save screenshots for successfully executed tests. Default: true. -- `animateSlides`: should animation for slides to be used. Default: true. -- `ignoreSteps`: steps to ignore in report. Array of RegExps is expected. Recommended to skip `grab*` and `wait*` steps. -- `fullPageScreenshots`: should full page screenshots be used. Default: false. -- `output`: a directory where reports should be stored. Default: `output`. -- `screenshotsForAllureReport`: If Allure plugin is enabled this plugin attaches each saved screenshot to allure report. Default: false. -- \`disableScreenshotOnFail : Disables the capturing of screeshots after the failed step. Default: true. - -### Parameters - -- `config` **any** - -## stepTimeout - -Set timeout for test steps globally. - -Add this plugin to config file: - -```js +```Possible config options:* `deleteSuccessful`: do not save screenshots for successfully executed tests. Default: true. +* `animateSlides`: should animation for slides to be used. Default: true. +* `ignoreSteps`: steps to ignore in report. Array of RegExps is expected. Recommended to skip `grab*` and `wait*` steps. +* `fullPageScreenshots`: should full page screenshots be used. Default: false. +* `output`: a directory where reports should be stored. Default: `output`. +* `screenshotsForAllureReport`: If Allure plugin is enabled this plugin attaches each saved screenshot to allure report. Default: false. +* \`disableScreenshotOnFail : Disables the capturing of screeshots after the failed step. Default: true.### Parameters* `config` **any** ## stepTimeoutSet timeout for test steps globally.Add this plugin to config file:```js plugins: { - stepTimeout: { - enabled: true - } + stepTimeout: { + enabled: true + } } -``` - -Run tests with plugin enabled: - - npx codeceptjs run --plugins stepTimeout - -#### Configuration: - -- `timeout` - global step timeout, default 150 seconds +```Run tests with plugin enabled: npx codeceptjs run --plugins stepTimeout#### Configuration:* `timeout` - global step timeout, default 150 seconds -- `overrideStepLimits` - whether to use timeouts set in plugin config to override step timeouts set in code with I.limitTime(x).action(...), default false +* `overrideStepLimits` - whether to use timeouts set in plugin config to override step timeouts set in code with I.limitTime(x).action(...), default false -- `noTimeoutSteps` - an array of steps with no timeout. Default: +* `noTimeoutSteps` - an array of steps with no timeout. Default: - - `amOnPage` - - `wait*` + * `amOnPage` + * `wait*` - you could set your own noTimeoutSteps which would replace the default one. + you could set your own noTimeoutSteps which would replace the default one. -- `customTimeoutSteps` - an array of step actions with custom timeout. Use it to override or extend noTimeoutSteps. - You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`. - -#### Example - -```js +* `customTimeoutSteps` - an array of step actions with custom timeout. Use it to override or extend noTimeoutSteps. + You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.#### Example```js plugins: { stepTimeout: { enabled: true, @@ -1006,56 +558,20 @@ plugins: { ] } } -``` - -### Parameters - -- `config` - -## subtitles - -Automatically captures steps as subtitle, and saves it as an artifact when a video is found for a failed test - -#### Configuration - -```js +```### Parameters* `config` ## subtitlesAutomatically captures steps as subtitle, and saves it as an artifact when a video is found for a failed test#### Configuration```js plugins: { - subtitles: { - enabled: true - } + subtitles: { + enabled: true + } } -``` - -## wdio - -Webdriverio services runner. - -This plugin allows to run webdriverio services like: - -- selenium-standalone -- sauce -- testingbot -- browserstack -- appium - -A complete list of all available services can be found on [webdriverio website][19]. - -#### Setup - -1. Install a webdriverio service +```## wdioWebdriverio services runner.This plugin allows to run webdriverio services like:* selenium-standalone +* sauce +* testingbot +* browserstack +* appiumA complete list of all available services can be found on [webdriverio website][19].#### Setup1. Install a webdriverio service 2. Enable `wdio` plugin in config -3. Add service name to `services` array inside wdio plugin config. - -See examples below: - -#### Selenium Standalone Service - -Install ` @wdio/selenium-standalone-service` package, as [described here][20]. -It is important to make sure it is compatible with current webdriverio version. - -Enable `wdio` plugin in plugins list and add `selenium-standalone` service: - -```js +3. Add service name to `services` array inside wdio plugin config.See examples below:#### Selenium Standalone ServiceInstall ` @wdio/selenium-standalone-service` package, as [described here][20]. +It is important to make sure it is compatible with current webdriverio version.Enable `wdio` plugin in plugins list and add `selenium-standalone` service:```js plugins: { wdio: { enabled: true, @@ -1063,16 +579,8 @@ plugins: { // additional config for service can be passed here } } -``` - -#### Sauce Service - -Install `@wdio/sauce-service` package, as [described here][21]. -It is important to make sure it is compatible with current webdriverio version. - -Enable `wdio` plugin in plugins list and add `sauce` service: - -```js +```#### Sauce ServiceInstall `@wdio/sauce-service` package, as [described here][21]. +It is important to make sure it is compatible with current webdriverio version.Enable `wdio` plugin in plugins list and add `sauce` service:```js plugins: { wdio: { enabled: true, @@ -1082,39 +590,6 @@ plugins: { // additional config, from sauce service } } -``` - ---- - -In the same manner additional services from webdriverio can be installed, enabled, and configured. - -#### Configuration - -- `services` - list of enabled services -- ... - additional configuration passed into services. - -### Parameters - -- `config` - -[1]: https://github.com/cenfun/monocart-coverage-reports?tab=readme-ov-file#default-options -[2]: https://codecept.io/locators#custom-locators -[3]: https://playwright.dev/docs/api/class-elementhandle -[4]: https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle -[5]: https://webdriver.io/docs/api -[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function -[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise -[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined -[10]: https://codecept.io/heal/ -[11]: /basics/#pause -[12]: https://aerokube.com/selenoid/ -[13]: https://aerokube.com/cm/latest/ -[14]: https://hub.docker.com/u/selenoid -[15]: https://aerokube.com/selenoid/latest/#_prepare_configuration -[16]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container -[17]: https://docs.docker.com/engine/reference/commandline/create/ -[18]: https://codecept.io/img/codeceptjs-slideshow.gif -[19]: https://webdriver.io -[20]: https://webdriver.io/docs/selenium-standalone-service.html -[21]: https://webdriver.io/docs/sauce-service.html +```***In the same manner additional services from webdriverio can be installed, enabled, and configured.#### Configuration* `services` - list of enabled services +* ... - additional configuration passed into services.### Parameters* `config` [1]: https://github.com/cenfun/monocart-coverage-reports?tab=readme-ov-file#default-options[2]: https://codecept.io/locators#custom-locators[3]: https://playwright.dev/docs/api/class-elementhandle[4]: https://pptr.dev/#?product=Puppeteer&show=api-class-elementhandle[5]: https://webdriver.io/docs/api[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined[10]: https://codecept.io/heal/[11]: /basics/#pause[12]: https://aerokube.com/selenoid/[13]: https://aerokube.com/cm/latest/[14]: https://hub.docker.com/u/selenoid[15]: https://aerokube.com/selenoid/latest/#_prepare_configuration[16]: https://aerokube.com/selenoid/latest/#_option_2_start_selenoid_container[17]: https://docs.docker.com/engine/reference/commandline/create/[18]: https://codecept.io/img/codeceptjs-slideshow.gif[19]: https://webdriver.io[20]: https://webdriver.io/docs/selenium-standalone-service.html[21]: https://webdriver.io/docs/sauce-service.html +``````` diff --git a/test/unit/plugin/subtitles_test.js b/test/unit/plugin/subtitles_test.js index 60db3691d..c3ee2be4a 100644 --- a/test/unit/plugin/subtitles_test.js +++ b/test/unit/plugin/subtitles_test.js @@ -4,6 +4,7 @@ const fsPromises = require('fs').promises const subtitles = require('../../../lib/plugin/subtitles') const container = require('../../../lib/container') const event = require('../../../lib/event') +const { createTest } = require('../../../lib/mocha/test') const recorder = require('../../../lib/recorder') function sleep(ms) { @@ -28,7 +29,7 @@ describe('subtitles', () => { it('should not capture subtitle as video artifact was missing', async () => { const fsMock = sinon.mock(fsPromises) - const test = { notes: [] } + const test = createTest('test') fsMock.expects('writeFile').never() @@ -43,12 +44,8 @@ describe('subtitles', () => { it('should capture subtitle as video artifact is present', async () => { const fsMock = sinon.mock(fsPromises) - const test = { - notes: [], - artifacts: { - video: '../../lib/output/failedTest1.webm', - }, - } + const test = createTest('test') + test.artifacts.video = '../../lib/output/failedTest1.webm' fsMock .expects('writeFile') @@ -71,12 +68,8 @@ describe('subtitles', () => { it('should capture mutiple steps as subtitle', async () => { const fsMock = sinon.mock(fsPromises) - const test = { - notes: [], - artifacts: { - video: '../../lib/output/failedTest1.webm', - }, - } + const test = createTest('test') + test.artifacts.video = '../../lib/output/failedTest1.webm' fsMock .expects('writeFile') @@ -106,12 +99,8 @@ describe('subtitles', () => { it('should capture separate steps for separate tests', async () => { const fsMock = sinon.mock(fsPromises) - const test1 = { - notes: [], - artifacts: { - video: '../../lib/output/failedTest1.webm', - }, - } + const test1 = createTest('test') + test1.artifacts.video = '../../lib/output/failedTest1.webm' fsMock .expects('writeFile') @@ -151,12 +140,8 @@ describe('subtitles', () => { return value.match(/^1\n[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\s-->\s[0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3}\nI\.click\(Login\)\n\n$/gm) }), ) - const test2 = { - notes: [], - artifacts: { - video: '../../lib/output/failedTest2.webm', - }, - } + const test2 = createTest('test') + test2.artifacts.video = '../../lib/output/failedTest2.webm' event.dispatcher.emit(event.test.before, test2) const step3 = { name: 'click', actor: 'I', args: ['Login'] } From 6fe0fafc302cc46c89d58117a16e30e1e2502cf5 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 6 Feb 2025 08:34:22 +0200 Subject: [PATCH 06/10] new beta tag --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d9ece947..717a297e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "3.7.0-beta.15", + "version": "3.7.0-beta.16", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ "acceptance", From a7c50ac15af41ba4f44c372b8d43e6d152eed3b5 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 6 Feb 2025 08:37:53 +0200 Subject: [PATCH 07/10] added BC compatibility for disableRetryFailedStep --- lib/plugin/retryFailedStep.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/plugin/retryFailedStep.js b/lib/plugin/retryFailedStep.js index ce43a1a33..6d43bf8da 100644 --- a/lib/plugin/retryFailedStep.js +++ b/lib/plugin/retryFailedStep.js @@ -106,7 +106,9 @@ module.exports = config => { }) event.dispatcher.on(event.test.before, test => { - if (test.opts.disableRetryFailedStep) { + // pass disableRetryFailedStep is a preferred way to disable retries + // test.disableRetryFailedStep is used for backward compatibility + if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) { store.autoRetries = false return // disable retry when a test is not active } From 518d5fc2c66a63724906e834ad5a11c0e68e0a91 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 6 Feb 2025 08:42:33 +0200 Subject: [PATCH 08/10] fixed playwright/puppeteer issues --- lib/helper/Playwright.js | 2 +- lib/helper/Puppeteer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index b02692822..194a17d16 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -484,7 +484,7 @@ class Playwright extends Helper { this.currentRunningTest = test recorder.retry({ - retries: test.opts?.conditionalRetries || 3, + retries: test?.opts?.conditionalRetries || 3, when: err => { if (!err || typeof err.message !== 'string') { return false diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 824756707..c023bc84a 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -312,7 +312,7 @@ class Puppeteer extends Helper { this.sessionPages = {} this.currentRunningTest = test recorder.retry({ - retries: test.opts?.conditionalRetries || 3, + retries: test?.opts?.conditionalRetries || 3, when: err => { if (!err || typeof err.message !== 'string') { return false From a40026ddc40624df16a4e7910dee11baa9b481bd Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 6 Feb 2025 09:53:40 +0200 Subject: [PATCH 09/10] check improvements --- lib/command/check.js | 29 ++++++++++++++++++++++------- lib/plugin/retryTo.js | 2 +- lib/plugin/tryTo.js | 2 +- package.json | 2 +- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/command/check.js b/lib/command/check.js index 020072e03..de9aa3c8c 100644 --- a/lib/command/check.js +++ b/lib/command/check.js @@ -25,6 +25,7 @@ module.exports = async function (options) { ai: true, // we don't need to check AI helpers: false, setup: false, + teardown: false, tests: false, def: false, } @@ -131,22 +132,36 @@ module.exports = async function (options) { if (Object.keys(helpers).length) { const suite = container.mocha().suite const test = createTest('test', () => {}) - try { - for (const helper of Object.values(helpers)) { + checks.setup = true + for (const helper of Object.values(helpers)) { + try { if (helper._beforeSuite) await helper._beforeSuite(suite) if (helper._before) await helper._before(test) + } catch (err) { + err.message = `${helper.constructor.name} helper: ${err.message}` + if (checks.setup instanceof Error) err.message = `${err.message}\n\n${checks.setup?.message || ''}`.trim() + checks.setup = err + } + } + + printCheck('Helpers Before', checks['setup'], standardActingHelpers.some(h => Object.keys(helpers).includes(h)) ? 'Initializing browser' : '') + + checks.teardown = true + for (const helper of Object.values(helpers).reverse()) { + try { if (helper._passed) await helper._passed(test) if (helper._after) await helper._after(test) if (helper._finishTest) await helper._finishTest(suite) if (helper._afterSuite) await helper._afterSuite(suite) + } catch (err) { + err.message = `${helper.constructor.name} helper: ${err.message}` + if (checks.teardown instanceof Error) err.message = `${err.message}\n\n${checks.teardown?.message || ''}`.trim() + checks.teardown = err } - checks.setup = true - } catch (err) { - checks.setup = err } - } - printCheck('Helpers Before/After', checks['setup'], standardActingHelpers.some(h => Object.keys(helpers).includes(h)) ? 'Initializing and closing browser' : '') + printCheck('Helpers After', checks['teardown'], standardActingHelpers.some(h => Object.keys(helpers).includes(h)) ? 'Closing browser' : '') + } try { definitions(configFile, { dryRun: true }) diff --git a/lib/plugin/retryTo.js b/lib/plugin/retryTo.js index 49f365b86..815b1bc23 100644 --- a/lib/plugin/retryTo.js +++ b/lib/plugin/retryTo.js @@ -6,7 +6,7 @@ const defaultConfig = { module.exports = function (config) { config = Object.assign(defaultConfig, config) - console.log(`Deprecation Warning: 'retryTo' has been moved to the 'codeceptjs/effects' module`) + console.log(`Deprecation Warning: 'retryTo' has been moved to the 'codeceptjs/effects' module. Disable retryTo plugin to remove this warning.`) if (config.registerGlobal) { global.retryTo = retryTo diff --git a/lib/plugin/tryTo.js b/lib/plugin/tryTo.js index a38a277ab..84a16028e 100644 --- a/lib/plugin/tryTo.js +++ b/lib/plugin/tryTo.js @@ -6,7 +6,7 @@ const defaultConfig = { module.exports = function (config) { config = Object.assign(defaultConfig, config) - console.log(`Deprecation Warning: 'tryTo' has been moved to the 'codeceptjs/effects' module`) + console.log(`Deprecation Warning: 'tryTo' has been moved to the 'codeceptjs/effects' module. Disable tryTo plugin to remove this warning.`) if (config.registerGlobal) { global.tryTo = tryTo diff --git a/package.json b/package.json index 717a297e3..c2e68676a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "3.7.0-beta.16", + "version": "3.7.0-beta.19", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ "acceptance", From 02aa67b43759fb9be65a418b760cd91f1353e595 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 6 Feb 2025 12:40:47 +0200 Subject: [PATCH 10/10] rename var --- lib/effects.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/effects.js b/lib/effects.js index b6a08aef1..5416e9d06 100644 --- a/lib/effects.js +++ b/lib/effects.js @@ -179,13 +179,13 @@ async function tryTo(callback) { const sessionName = 'tryTo' let result = false - let hasAutoRetriesEnabled = store.autoRetries + let isAutoRetriesEnabled = store.autoRetries return recorder.add( sessionName, () => { recorder.session.start(sessionName) - hasAutoRetriesEnabled = store.autoRetries - if (hasAutoRetriesEnabled) debug('Auto retries disabled inside tryTo effect') + isAutoRetriesEnabled = store.autoRetries + if (isAutoRetriesEnabled) debug('Auto retries disabled inside tryTo effect') store.autoRetries = false callback() recorder.add(() => { @@ -203,7 +203,7 @@ async function tryTo(callback) { return recorder.add( 'result', () => { - store.autoRetries = hasAutoRetriesEnabled + store.autoRetries = isAutoRetriesEnabled return result }, true,