From 699fb24126f77bce1187a3e9c19d09829b698ab3 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 31 Dec 2024 03:33:19 +0200 Subject: [PATCH 1/6] added els functions --- docs/els.md | 289 ++++++++++++++++++++++++++++++++++++++++++ lib/els.js | 173 +++++++++++++++++++++++++ test/unit/els_test.js | 174 +++++++++++++++++++++++++ 3 files changed, 636 insertions(+) create mode 100644 docs/els.md create mode 100644 lib/els.js create mode 100644 test/unit/els_test.js diff --git a/docs/els.md b/docs/els.md new file mode 100644 index 000000000..91acc10f3 --- /dev/null +++ b/docs/els.md @@ -0,0 +1,289 @@ +## Element Access + +The `els` module provides low-level element manipulation functions for CodeceptJS tests, allowing for more granular control over element interactions and assertions. However, because element representation differs between frameworks, tests using element functions are not portable between helpers. So if you set to use Playwright you won't be able to witch to WebDriver with one config change in CodeceptJS. + +### Usage + +Import the els functions in your test file: + +```js +const { element, eachElement, expectElement, expectAnyElement, expectAllElements } = require('codeceptjs/els'); +``` + +## element + +The `element` function allows you to perform custom operations on the first matching element found by a locator. It provides a low-level way to interact with elements when the built-in helper methods aren't sufficient. + +### Syntax + +```js +element(purpose, locator, fn); +// or +element(locator, fn); +``` + +### Parameters + +- `purpose` (optional) - A string describing the operation being performed. If omitted, a default purpose will be generated from the function. +- `locator` - A locator string/object to find the element(s). +- `fn` - An async function that receives the element as its argument and performs the desired operation. `el` argument represents an element of an underlying engine used: Playwright, WebDriver, or Puppeteer. + +### Returns + +Returns the result of the provided async function executed on the first matching element. + +### Example + +```js +Scenario('my test', async ({ I }) => { + // combine element function with standard steps: + I.amOnPage('/cart'); + + // but use await every time you use element function + await element( + // with explicit purpose + 'check custom attribute', + '.button', + async el => await el.getAttribute('data-test'), + ); + + // or simply + await element('.button', async el => { + return await el.isEnabled(); + }); +}); +``` + +### Notes + +- Only works with helpers that implement the `_locate` method +- The function will only operate on the first element found, even if multiple elements match the locator +- The provided callback must be an async function +- Throws an error if no helper with `_locate` method is enabled + +## eachElement + +The `eachElement` function allows you to perform operations on each element that matches a locator. It's useful for iterating through multiple elements and performing the same operation on each one. + +### Syntax + +```js +eachElement(purpose, locator, fn); +// or +eachElement(locator, fn); +``` + +### Parameters + +- `purpose` (optional) - A string describing the operation being performed. If omitted, a default purpose will be generated from the function. +- `locator` - A locator string/object to find the element(s). +- `fn` - An async function that receives two arguments: + - `el` - The current element being processed + - `index` - The index of the current element in the collection + +### Returns + +Returns a promise that resolves when all elements have been processed. If any element operation fails, the function will throw the first encountered error. + +### Example + +```js +Scenario('my test', async ({ I }) => { + // combine element function with standard steps: + I.click('/hotels'); + + // iterate over elements but don't forget to put await + await eachElement( + 'validate list items', // explain your actions for future review + '.list-item', // locator + async (el, index) => { + const text = await el.getText(); + console.log(`Item ${index}: ${text}`); + }, + ); + + // Or simply check if all checkboxes are checked + await eachElement('input[type="checkbox"]', async el => { + const isChecked = await el.isSelected(); + if (!isChecked) { + throw new Error('Found unchecked checkbox'); + } + }); +}); +``` + +### Notes + +- Only works with helpers that implement the `_locate` method +- The function will process all elements that match the locator +- The provided callback must be an async function +- If an operation fails on any element, the error is logged and the function continues processing remaining elements +- After all elements are processed, if any errors occurred, the first error is thrown +- Throws an error if no helper with `_locate` method is enabled + +## expectElement + +The `expectElement` function allows you to perform assertions on the first element that matches a locator. It's designed for validating element properties or states and will throw an assertion error if the condition is not met. + +### Syntax + +```js +expectElement(locator, fn); +``` + +### Parameters + +- `locator` - A locator string/object to find the element(s). +- `fn` - An async function that receives the element as its argument and should return a boolean value: + - `true` - The assertion passed + - `false` - The assertion failed + +### Returns + +Returns a promise that resolves when the assertion is complete. Throws an assertion error if the condition is not met. + +### Example + +```js +// Check if a button is enabled +await expectElement('.submit-button', async el => { + return await el.isEnabled(); +}); + +// Verify element has specific text content +await expectElement('.header', async el => { + const text = await el.getText(); + return text === 'Welcome'; +}); + +// Check for specific attribute value +await expectElement('#user-profile', async el => { + const role = await el.getAttribute('role'); + return role === 'button'; +}); +``` + +### Notes + +- Only works with helpers that implement the `_locate` method +- The function will only check the first element found, even if multiple elements match the locator +- The provided callback must be an async function that returns a boolean +- The assertion message will include both the locator and the function used for validation +- Throws an error if no helper with `_locate` method is enabled + +## expectAnyElement + +The `expectAnyElement` function allows you to perform assertions where at least one element from a collection should satisfy the condition. It's useful when you need to verify that at least one element among many matches your criteria. + +### Syntax + +```js +expectAnyElement(locator, fn); +``` + +### Parameters + +- `locator` - A locator string/object to find the element(s). +- `fn` - An async function that receives the element as its argument and should return a boolean value: + - `true` - The assertion passed for this element + - `false` - The assertion failed for this element + +### Returns + +Returns a promise that resolves when the assertion is complete. Throws an assertion error if no elements satisfy the condition. + +### Example + +```js +Scenario('validate any element matches criteria', async ({ I }) => { + // Navigate to the page + I.amOnPage('/products'); + + // Check if any product is marked as "in stock" + await expectAnyElement('.product-item', async el => { + const status = await el.getAttribute('data-status'); + return status === 'in-stock'; + }); + + // Verify at least one price is below $100 + await expectAnyElement('.price-tag', async el => { + const price = await el.getText(); + return parseFloat(price.replace('$', '')) < 100; + }); + + // Check if any button in the list is enabled + await expectAnyElement('.action-button', async el => { + return await el.isEnabled(); + }); +}); +``` + +### Notes + +- Only works with helpers that implement the `_locate` method +- The function will check all matching elements until it finds one that satisfies the condition +- Stops checking elements once the first matching condition is found +- The provided callback must be an async function that returns a boolean +- Throws an assertion error if no elements satisfy the condition +- Throws an error if no helper with `_locate` method is enabled + +## expectAllElements + +The `expectAllElements` function verifies that every element matching the locator satisfies the given condition. It's useful when you need to ensure that all elements in a collection meet specific criteria. + +### Syntax + +```js +expectAllElements(locator, fn); +``` + +### Parameters + +- `locator` - A locator string/object to find the element(s). +- `fn` - An async function that receives the element as its argument and should return a boolean value: + - `true` - The assertion passed for this element + - `false` - The assertion failed for this element + +### Returns + +Returns a promise that resolves when all assertions are complete. Throws an assertion error as soon as any element fails the condition. + +### Example + +```js +Scenario('validate all elements meet criteria', async ({ I }) => { + // Navigate to the page + I.amOnPage('/dashboard'); + + // Verify all required fields have the required attribute + await expectAllElements('.required-field', async el => { + const required = await el.getAttribute('required'); + return required !== null; + }); + + // Check if all checkboxes in a form are checked + await expectAllElements('input[type="checkbox"]', async el => { + return await el.isSelected(); + }); + + // Verify all items in a list have non-empty text + await expectAllElements('.list-item', async el => { + const text = await el.getText(); + return text.trim().length > 0; + }); + + // Ensure all buttons in a section are enabled + await expectAllElements('#action-section button', async el => { + return await el.isEnabled(); + }); +}); +``` + +### Notes + +- Only works with helpers that implement the `_locate` method +- The function checks every element that matches the locator +- Fails fast: stops checking elements as soon as one fails the condition +- The provided callback must be an async function that returns a boolean +- The assertion message will include which element number failed (e.g., "element #2 of...") +- Throws an error if no helper with `_locate` method is enabled diff --git a/lib/els.js b/lib/els.js new file mode 100644 index 000000000..f44b3db96 --- /dev/null +++ b/lib/els.js @@ -0,0 +1,173 @@ +const output = require('./output'); +const store = require('./store'); +const recorder = require('./recorder'); +const container = require('./container'); +const event = require('./event'); +const Step = require('./step'); +const { truth } = require('./assert/truth'); +const { isAsyncFunction } = require('./utils'); + +function element(purpose, locator, fn) { + if (!fn) { + fn = locator; + locator = purpose; + purpose = `element (${fn.toString()})`; + } + + const step = prepareStep(purpose, locator, fn); + if (!step) return; + + return executeStep(step, async () => { + const els = await step.helper._locate(locator); + output.debug(`Found ${els.length} elements, using first element`); + + return fn(els[0]); + }); +} + +function eachElement(purpose, locator, fn) { + if (!fn) { + fn = locator; + locator = purpose; + purpose = `each element (${fn.toString()})`; + } + + const step = prepareStep(purpose, locator, fn); + if (!step) return; + + return executeStep(step, async () => { + const els = await step.helper._locate(locator); + output.debug(`Found ${els.length} elements for each elements to iterate`); + + const errs = []; + let i = 0; + for (const el of els) { + try { + await fn(el, i); + } catch (err) { + output.error(`eachElement: failed operation on element #${i} ${el}`); + errs.push(err); + } + i++; + } + + if (errs.length) { + throw errs[0]; + } + }); +} + +function expectElement(locator, fn) { + const step = prepareStep(`expect element to be (${fn.toString()})`, locator, fn); + if (!step) return; + + return executeStep(step, async () => { + const els = await step.helper._locate(locator); + output.debug(`Found ${els.length} elements, first will be used for assertion`); + + const result = await fn(els[0]); + const assertion = truth(`element (${locator})`, fn.toString()); + assertion.assert(result); + }); +} + +function expectAnyElement(locator, fn) { + const step = prepareStep(`expect any element to be (${fn.toString()})`, locator, fn); + if (!step) return; + + return executeStep(step, async () => { + const els = await step.helper._locate(locator); + output.debug(`Found ${els.length} elements, at least one should pass the assertion`); + + const assertion = truth(`any element of (${locator})`, fn.toString()); + + let found = false; + for (const el of els) { + const result = await fn(el); + if (result) { + found = true; + break; + } + } + if (!found) throw assertion.getException(); + }); +} + +function expectAllElements(locator, fn) { + const step = prepareStep(`expect all elements to be (${fn.toString()})`, locator, fn); + if (!step) return; + + return executeStep(step, async () => { + const els = await step.helper._locate(locator); + output.debug(`Found ${els.length} elements, all should pass the assertion`); + + let i = 1; + for (const el of els) { + output.debug(`checking element #${i}: ${el}`); + const result = await fn(el); + const assertion = truth(`element #${i} of (${locator})`, fn.toString()); + assertion.assert(result); + i++; + } + }); +} + +module.exports = { + element, + eachElement, + expectElement, + expectAnyElement, + expectAllElements, +}; + +function prepareStep(purpose, locator, fn) { + if (store.dryRun) return; + const helpers = Object.values(container.helpers()); + + const helper = helpers.filter(h => !!h._locate)[0]; + + if (!helper) { + throw new Error('No helper enabled with _locate method with returns a list of elements.'); + } + + if (!isAsyncFunction(fn)) { + throw new Error('Async function should be passed into each element'); + } + + const step = new Step(helper, `${purpose} within "${locator}"`); + step.helperMethod = '_locate'; + + return step; +} + +async function executeStep(step, action) { + let error; + const promise = recorder.add('register element wrapper', async () => { + event.emit(event.step.started, step); + + try { + await action(); + } catch (err) { + recorder.throw(err); + event.emit(event.step.failed, step, err); + event.emit(event.step.finished, step); + // event.emit(event.step.after, step) + error = err; + // await recorder.promise(); + return; + } + + event.emit(event.step.after, step); + event.emit(event.step.passed, step); + event.emit(event.step.finished, step); + }); + + // await recorder.promise(); + + // if (error) { + // console.log('error', error.inspect()) + // return recorder.throw(error); + // } + + return promise; +} diff --git a/test/unit/els_test.js b/test/unit/els_test.js new file mode 100644 index 000000000..35a28d2c8 --- /dev/null +++ b/test/unit/els_test.js @@ -0,0 +1,174 @@ +const assert = require('assert'); +const { expect } = require('chai'); +const els = require('../../lib/els'); +const recorder = require('../../lib/recorder'); +const Container = require('../../lib/container'); +const Helper = require('../../lib/helper'); + +class TestHelper extends Helper { + constructor() { + super(); + this.elements = []; + } + + async _locate(locator) { + return this.elements; + } +} + +describe('els', () => { + let helper; + + beforeEach(() => { + helper = new TestHelper(); + Container.clear(); + Container.append({ + helpers: { + test: helper, + }, + }); + recorder.reset(); + recorder.startUnlessRunning(); + }); + + describe('#element', () => { + it('should execute function on first found element', async () => { + helper.elements = ['el1', 'el2', 'el3']; + let elementUsed; + + await els.element('my test', '.selector', async el => { + elementUsed = el; + }); + + await recorder.promise(); + + assert.equal(elementUsed, 'el1'); + }); + + it('should work without purpose parameter', async () => { + helper.elements = ['el1', 'el2']; + let elementUsed; + + await els.element('.selector', async el => { + elementUsed = el; + }); + + assert.equal(elementUsed, 'el1'); + }); + + it('should throw error when no helper with _locate available', async () => { + Container.clear(); + try { + await els.element('.selector', async () => {}); + throw new Error('should have thrown error'); + } catch (e) { + expect(e.message).to.include('No helper enabled with _locate method'); + } + }); + }); + + describe('#eachElement', () => { + it('should execute function on each element', async () => { + helper.elements = ['el1', 'el2', 'el3']; + const usedElements = []; + + await els.eachElement('.selector', async el => { + usedElements.push(el); + }); + + assert.deepEqual(usedElements, ['el1', 'el2', 'el3']); + }); + + it('should provide index as second parameter', async () => { + helper.elements = ['el1', 'el2']; + const indices = []; + + await els.eachElement('.selector', async (el, i) => { + indices.push(i); + }); + + assert.deepEqual(indices, [0, 1]); + }); + + it('should work without purpose parameter', async () => { + helper.elements = ['el1', 'el2']; + const usedElements = []; + + await els.eachElement('.selector', async el => { + usedElements.push(el); + }); + + assert.deepEqual(usedElements, ['el1', 'el2']); + }); + + it('should throw first error if operation fails', async () => { + helper.elements = ['el1', 'el2']; + + try { + await els.eachElement('.selector', async el => { + throw new Error(`failed on ${el}`); + }); + throw new Error('should have thrown error'); + } catch (e) { + expect(e.message).to.equal('failed on el1'); + } + }); + }); + + describe('#expectElement', () => { + it('should pass when condition is true', async () => { + helper.elements = ['el1']; + + await els.expectElement('.selector', async () => true); + }); + + it('should fail when condition is false', async () => { + helper.elements = ['el1']; + + try { + await els.expectElement('.selector', async () => false); + throw new Error('should have thrown error'); + } catch (e) { + expect(e.message).to.include('element (.selector)'); + } + }); + }); + + describe('#expectAnyElement', () => { + it('should pass when any element matches condition', async () => { + helper.elements = ['el1', 'el2', 'el3']; + + await els.expectAnyElement('.selector', async el => el === 'el2'); + }); + + it('should fail when no element matches condition', async () => { + helper.elements = ['el1', 'el2']; + + try { + await els.expectAnyElement('.selector', async () => false); + throw new Error('should have thrown error'); + } catch (e) { + expect(e.message).to.include('any element of (.selector)'); + } + }); + }); + + describe('#expectAllElements', () => { + it('should pass when all elements match condition', async () => { + helper.elements = ['el1', 'el2']; + + await els.expectAllElements('.selector', async () => true); + }); + + it('should fail when any element does not match condition', async () => { + helper.elements = ['el1', 'el2', 'el3']; + + try { + await els.expectAllElements('.selector', async el => el !== 'el2'); + throw new Error('should have thrown error'); + } catch (e) { + expect(e.message).to.include('element #2 of (.selector)'); + } + }); + }); +}); From 608022bc76a8f8ed683875dcbb3bfbf97feadc9f Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 3 Jan 2025 01:03:36 +0200 Subject: [PATCH 2/6] implemented els module --- test/unit/els_test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/unit/els_test.js b/test/unit/els_test.js index 35a28d2c8..2b128f778 100644 --- a/test/unit/els_test.js +++ b/test/unit/els_test.js @@ -108,6 +108,7 @@ describe('els', () => { await els.eachElement('.selector', async el => { throw new Error(`failed on ${el}`); }); + await recorder.promise(); throw new Error('should have thrown error'); } catch (e) { expect(e.message).to.equal('failed on el1'); @@ -127,9 +128,10 @@ describe('els', () => { try { await els.expectElement('.selector', async () => false); + await recorder.promise(); throw new Error('should have thrown error'); } catch (e) { - expect(e.message).to.include('element (.selector)'); + expect(e.cliMessage()).to.include('element (.selector)'); } }); }); @@ -146,9 +148,10 @@ describe('els', () => { try { await els.expectAnyElement('.selector', async () => false); + await recorder.promise(); throw new Error('should have thrown error'); } catch (e) { - expect(e.message).to.include('any element of (.selector)'); + expect(e.cliMessage()).to.include('any element of (.selector)'); } }); }); @@ -165,9 +168,10 @@ describe('els', () => { try { await els.expectAllElements('.selector', async el => el !== 'el2'); + await recorder.promise(); throw new Error('should have thrown error'); } catch (e) { - expect(e.message).to.include('element #2 of (.selector)'); + expect(e.cliMessage()).to.include('element #2 of (.selector)'); } }); }); From 062d1972d58a473fefbba220b7585bc525e268d5 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 3 Jan 2025 01:12:32 +0200 Subject: [PATCH 3/6] added els to package.json --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index c676a4ac7..e7696f915 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,10 @@ "docs/webapi/**" ], "main": "lib/index.js", + "exports": { + ".": "./lib/index.js", + "./els": "./lib/els.js" + }, "types": "typings/index.d.ts", "bin": { "codeceptjs": "./bin/codecept.js" From 6850dfd12859eed60c28d7957cf49776dab20f5d Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 3 Jan 2025 03:52:18 +0200 Subject: [PATCH 4/6] els functions & terminal improvements --- lib/els.js | 20 +- lib/helper/Playwright.js | 2428 ++++++++++++++++++-------------------- lib/output.js | 4 +- lib/step.js | 13 +- lib/utils.js | 80 +- 5 files changed, 1248 insertions(+), 1297 deletions(-) diff --git a/lib/els.js b/lib/els.js index f44b3db96..31242a969 100644 --- a/lib/els.js +++ b/lib/els.js @@ -5,13 +5,13 @@ const container = require('./container'); const event = require('./event'); const Step = require('./step'); const { truth } = require('./assert/truth'); -const { isAsyncFunction } = require('./utils'); +const { isAsyncFunction, humanizeFunction } = require('./utils'); function element(purpose, locator, fn) { if (!fn) { fn = locator; locator = purpose; - purpose = `element (${fn.toString()})`; + purpose = 'first element'; } const step = prepareStep(purpose, locator, fn); @@ -29,7 +29,7 @@ function eachElement(purpose, locator, fn) { if (!fn) { fn = locator; locator = purpose; - purpose = `each element (${fn.toString()})`; + purpose = 'for each element'; } const step = prepareStep(purpose, locator, fn); @@ -58,7 +58,7 @@ function eachElement(purpose, locator, fn) { } function expectElement(locator, fn) { - const step = prepareStep(`expect element to be (${fn.toString()})`, locator, fn); + const step = prepareStep('expect element to be', locator, fn); if (!step) return; return executeStep(step, async () => { @@ -72,7 +72,7 @@ function expectElement(locator, fn) { } function expectAnyElement(locator, fn) { - const step = prepareStep(`expect any element to be (${fn.toString()})`, locator, fn); + const step = prepareStep('expect any element to be', locator, fn); if (!step) return; return executeStep(step, async () => { @@ -94,7 +94,7 @@ function expectAnyElement(locator, fn) { } function expectAllElements(locator, fn) { - const step = prepareStep(`expect all elements to be (${fn.toString()})`, locator, fn); + const step = prepareStep('expect all elements', locator, fn); if (!step) return; return executeStep(step, async () => { @@ -105,7 +105,7 @@ function expectAllElements(locator, fn) { for (const el of els) { output.debug(`checking element #${i}: ${el}`); const result = await fn(el); - const assertion = truth(`element #${i} of (${locator})`, fn.toString()); + const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn)); assertion.assert(result); i++; } @@ -134,7 +134,11 @@ function prepareStep(purpose, locator, fn) { throw new Error('Async function should be passed into each element'); } - const step = new Step(helper, `${purpose} within "${locator}"`); + const isAssertion = purpose.startsWith('expect'); + + const step = new Step(helper, `${purpose} within "${locator}" ${isAssertion ? 'to be' : 'to'}`); + step.setActor('EL'); + step.setArguments([humanizeFunction(fn)]); step.helperMethod = '_locate'; return step; diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 37c23dd35..fc846ff8d 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1,17 +1,18 @@ -const path = require('path') -const fs = require('fs') - -const Helper = require('@codeceptjs/helper') -const { v4: uuidv4 } = require('uuid') -const assert = require('assert') -const promiseRetry = require('promise-retry') -const Locator = require('../locator') -const recorder = require('../recorder') -const stringIncludes = require('../assert/include').includes -const { urlEquals } = require('../assert/equal') -const { equals } = require('../assert/equal') -const { empty } = require('../assert/empty') -const { truth } = require('../assert/truth') +const path = require('path'); +const fs = require('fs'); + +const Helper = require('@codeceptjs/helper'); +const { v4: uuidv4 } = require('uuid'); +const assert = require('assert'); +const promiseRetry = require('promise-retry'); +const Locator = require('../locator'); +const recorder = require('../recorder'); +const stringIncludes = require('../assert/include').includes; +const store = require('../store'); +const { urlEquals } = require('../assert/equal'); +const { equals } = require('../assert/equal'); +const { empty } = require('../assert/empty'); +const { truth } = require('../assert/truth'); const { xpathLocator, ucfirst, @@ -24,44 +25,28 @@ const { clearString, requireWithFallback, normalizeSpacesInString, -} = require('../utils') -const { isColorProperty, convertColorToRGBA } = require('../colorUtils') -const ElementNotFound = require('./errors/ElementNotFound') -const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused') -const Popup = require('./extras/Popup') -const Console = require('./extras/Console') -const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator') - -let playwright -let perfTiming -let defaultSelectorEnginesInitialized = false - -const popupStore = new Popup() -const consoleLogStore = new Console() -const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron'] +} = require('../utils'); +const { isColorProperty, convertColorToRGBA } = require('../colorUtils'); +const ElementNotFound = require('./errors/ElementNotFound'); +const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused'); +const Popup = require('./extras/Popup'); +const Console = require('./extras/Console'); +const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator'); -const { - setRestartStrategy, - restartsSession, - restartsContext, - restartsBrowser, -} = require('./extras/PlaywrightRestartOpts') -const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine') -const { - seeElementError, - dontSeeElementError, - dontSeeElementInDOMError, - seeElementInDOMError, -} = require('./errors/ElementAssertion') -const { - dontSeeTraffic, - seeTraffic, - grabRecordedNetworkTraffics, - stopRecordingTraffic, - flushNetworkTraffics, -} = require('./network/actions') +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 { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } = require('./extras/PlaywrightRestartOpts'); +const { createValueEngine, createDisabledEngine } = require('./extras/PlaywrightPropEngine'); +const { seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError } = require('./errors/ElementAssertion'); +const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions'); + +const pathSeparator = path.sep; /** * ## Configuration @@ -110,7 +95,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 +317,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 +371,61 @@ class Playwright extends Helper { use: { actionTimeout: 0 }, ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors, highlightElement: false, - } + }; - process.env.testIdAttribute = 'data-testid' - config = Object.assign(defaults, config) + process.env.testIdAttribute = 'data-testid'; + config = Object.assign(defaults, config); if (availableBrowsers.indexOf(config.browser) < 0) { - throw new Error( - `Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`, - ) + throw new Error(`Invalid config. Can't use browser "${config.browser}". Accepted values: ${availableBrowsers.join(', ')}`); } - return config + return config; } _getOptionsForBrowser(config) { if (config[config.browser]) { if (config[config.browser].browserWSEndpoint && config[config.browser].browserWSEndpoint.wsEndpoint) { - config[config.browser].browserWSEndpoint = config[config.browser].browserWSEndpoint.wsEndpoint + config[config.browser].browserWSEndpoint = config[config.browser].browserWSEndpoint.wsEndpoint; } return { ...config[config.browser], wsEndpoint: config[config.browser].browserWSEndpoint, - } + }; } - return {} + return {}; } _setConfig(config) { - this.options = this._validateConfig(config) - setRestartStrategy(this.options) + this.options = this._validateConfig(config); + setRestartStrategy(this.options); this.playwrightOptions = { headless: !this.options.show, ...this._getOptionsForBrowser(config), - } + }; if (this.options.channel && this.options.browser === 'chromium') { - this.playwrightOptions.channel = this.options.channel + this.playwrightOptions.channel = this.options.channel; } if (this.options.video) { // set the video resolution with window size - let size = parseWindowSize(this.options.windowSize) + let size = parseWindowSize(this.options.windowSize); // if the video resolution is passed, set the record resoultion with that resolution if (this.options.recordVideo && this.options.recordVideo.size) { - size = parseWindowSize(this.options.recordVideo.size) + size = parseWindowSize(this.options.recordVideo.size); } - this.options.recordVideo = { size } + this.options.recordVideo = { size }; } if (this.options.recordVideo && !this.options.recordVideo.dir) { - this.options.recordVideo.dir = `${global.output_dir}/videos/` + this.options.recordVideo.dir = `${global.output_dir}/videos/`; } - this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint - this.isElectron = this.options.browser === 'electron' - this.userDataDir = this.playwrightOptions.userDataDir - ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` - : undefined - this.isCDPConnection = this.playwrightOptions.cdpConnection - popupStore.defaultAction = this.options.defaultPopupAction + this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint; + this.isElectron = this.options.browser === 'electron'; + this.userDataDir = this.playwrightOptions.userDataDir ? `${this.playwrightOptions.userDataDir}_${Date.now().toString()}` : undefined; + this.isCDPConnection = this.playwrightOptions.cdpConnection; + popupStore.defaultAction = this.options.defaultPopupAction; } static _config() { @@ -458,231 +439,225 @@ class Playwright extends Helper { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost', - when: (answers) => answers.Playwright_browser !== 'electron', + when: answers => answers.Playwright_browser !== 'electron', }, { name: 'show', message: 'Show browser window', default: true, type: 'confirm', - when: (answers) => answers.Playwright_browser !== 'electron', + 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) => { + when: err => { if (!err || typeof err.message !== 'string') { - return false + 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) => { + 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 + restoreVars: async session => { + 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 +678,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 +692,7 @@ class Playwright extends Helper { * ``` */ amAcceptingPopups() { - popupStore.actionType = 'accept' + popupStore.actionType = 'accept'; } /** @@ -726,7 +701,7 @@ class Playwright extends Helper { * libraries](http://jster.net/category/windows-modals-popups). */ acceptPopup() { - popupStore.assertPopupActionType('accept') + popupStore.assertPopupActionType('accept'); } /** @@ -740,23 +715,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 +739,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 +765,33 @@ class Playwright extends Helper { */ _addPopupListener(page) { if (!page) { - return + return; } - page.removeAllListeners('dialog') - page.on('dialog', async (dialog) => { - popupStore.popup = dialog - const action = popupStore.actionType || this.options.defaultPopupAction - await this._waitForAction() + page.removeAllListeners('dialog'); + page.on('dialog', async dialog => { + 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 +804,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.browser.on('targetchanged', target => { + this.debugSection('Url', target.url()); + }); - this.isRunning = true - return this.browser + this.isRunning = true; + return this.browser; } /** @@ -879,72 +851,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 = {} - dataNames.forEach((name) => { - extractedData[name] = timing[name] - navigationStart - }) + const extractedData = {}; + dataNames.forEach(name => { + extractedData[name] = timing[name] - navigationStart; + }); - return extractedData + return extractedData; } /** @@ -952,32 +924,26 @@ 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}`) + url = this.options.url + (url.startsWith('/') ? url : `/${url}`); } 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, - 'responseEnd', - 'domInteractive', - 'domContentLoadedEventEnd', - 'loadEventEnd', - ) + perfTiming = this._extractDataFromPerformanceTiming(performanceTiming, 'responseEnd', 'domInteractive', 'domContentLoadedEventEnd', 'loadEventEnd'); - return this._waitForAction() + return this._waitForAction(); } /** @@ -998,11 +964,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 +984,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 +994,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 +1008,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 +1020,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 +1036,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 +1055,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 +1073,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 +1108,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 +1138,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 +1152,8 @@ class Playwright extends Helper { */ scrollPageToTop() { return this.executeScript(() => { - window.scrollTo(0, 0) - }) + window.scrollTo(0, 0); + }); } /** @@ -1195,13 +1161,10 @@ 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 +1172,32 @@ class Playwright extends Helper { */ async scrollTo(locator, offsetX = 0, offsetY = 0) { if (typeof locator === 'number' && typeof offsetX === 'number') { - offsetY = offsetX - offsetX = locator - locator = null + offsetY = offsetX; + offsetX = locator; + locator = null; } if (locator) { - const el = await this._locateElement(locator) - assertElementExists(el, locator, 'Element') - await el.scrollIntoViewIfNeeded() - const elementCoordinates = await clickablePoint(el) + 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,33 +1208,33 @@ class Playwright extends Helper { return { x: window.pageXOffset, y: window.pageYOffset, - } + }; } - 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(); } /** @@ -1283,11 +1246,40 @@ class Playwright extends Helper { * ``` */ async _locate(locator) { - const context = await this._getContext() + const context = await this._getContext(); + + if (this.frame) return findElements(this.frame, locator); + + const els = await findElements(context, locator); + + if (store.debugMode) { + const previewElements = els.slice(0, 3); + let htmls = await Promise.all( + previewElements.map(async el => { + const html = await el.evaluate(node => node.outerHTML); + return ( + html + .replace(/\n/g, '') + .replace(/\s+/g, ' ') + .substring(0, 100 / previewElements.length) + .trim() + '...' + ); + }), + ); + if (els.length > 3) { + htmls.push('...'); + } - if (this.frame) return findElements(this.frame, locator) + if (els.length > 1) { + this.debugSection(`Elements (${els.length})`, htmls.join('|').trim()); + } else if (els.length === 1) { + this.debugSection('Element', htmls.join('|').trim()); + } else { + this.debug('No elements found'); + } + } - return findElements(context, locator) + return els; } /** @@ -1299,8 +1291,8 @@ class Playwright extends Helper { * ``` */ async _locateElement(locator) { - const context = await this._getContext() - return findElement(context, locator) + const context = await this._getContext(); + return findElement(context, locator); } /** @@ -1312,10 +1304,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]; } /** @@ -1326,8 +1318,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); } /** @@ -1338,7 +1330,7 @@ class Playwright extends Helper { * ``` */ async _locateFields(locator) { - return findFields.call(this, locator) + return findFields.call(this, locator); } /** @@ -1346,7 +1338,7 @@ class Playwright extends Helper { * */ async grabWebElements(locator) { - return this._locate(locator) + return this._locate(locator); } /** @@ -1354,7 +1346,7 @@ class Playwright extends Helper { * */ async grabWebElement(locator) { - return this._locateElement(locator) + return this._locateElement(locator); } /** @@ -1369,20 +1361,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(); } /** @@ -1396,19 +1388,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(); } /** @@ -1420,12 +1412,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(); } /** @@ -1436,13 +1428,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(); } /** @@ -1461,20 +1453,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; } /** @@ -1482,12 +1474,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); } } @@ -1496,12 +1488,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); } } @@ -1509,11 +1501,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); } } @@ -1521,11 +1513,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); } } @@ -1547,19 +1539,19 @@ class Playwright extends Helper { * @return {Promise} */ async handleDownloads(fileName) { - this.page.waitForEvent('download').then(async (download) => { - const filePath = await download.path() - fileName = fileName || `downloads/${path.basename(filePath)}` + this.page.waitForEvent('download').then(async download => { + 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); + }); } /** @@ -1579,37 +1571,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' }); } /** @@ -1628,9 +1620,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(); } /** @@ -1648,41 +1640,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(); } /** @@ -1692,28 +1684,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(); } /** @@ -1721,13 +1713,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); } } @@ -1736,17 +1728,17 @@ 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(); } /** @@ -1767,44 +1759,44 @@ class Playwright extends Helper { * @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument. */ async clearField(locator, options = {}) { - const els = await findFields.call(this, locator) - assertElementExists(els, locator, 'Field to clear') + const els = await findFields.call(this, locator); + assertElementExists(els, locator, 'Field to clear'); - const el = els[0] + const el = els[0]; - await highlightActiveElement.call(this, el) + await highlightActiveElement.call(this, el); - await el.clear() + await el.clear(); - return this._waitForAction() + return this._waitForAction(); } /** * {{> appendField }} */ async appendField(field, value) { - const els = await findFields.call(this, field) - assertElementExists(els, field, 'Field') - await highlightActiveElement.call(this, els[0]) - await els[0].press('End') - await els[0].type(value.toString(), { delay: this.options.pressKeyDelay }) - return this._waitForAction() + const els = await findFields.call(this, field); + assertElementExists(els, field, 'Field'); + await highlightActiveElement.call(this, els[0]); + await els[0].press('End'); + await els[0].type(value.toString(), { delay: this.options.pressKeyDelay }); + return this._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); } /** @@ -1812,38 +1804,38 @@ class Playwright extends Helper { * */ async attachFile(locator, pathToFile) { - const file = path.join(global.codecept_dir, pathToFile) + const file = path.join(global.codecept_dir, pathToFile); 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`); } - const els = await findFields.call(this, locator) - assertElementExists(els, locator, 'Field') - await els[0].setInputFiles(file) - return this._waitForAction() + const els = await findFields.call(this, locator); + assertElementExists(els, locator, 'Field'); + await els[0].setInputFiles(file); + return this._waitForAction(); } /** * {{> selectOption }} */ async selectOption(select, option) { - const els = await findFields.call(this, select) - assertElementExists(els, select, 'Selectable field') - const el = els[0] + const els = await findFields.call(this, select); + assertElementExists(els, select, 'Selectable field'); + const el = els[0]; - await highlightActiveElement.call(this, el) - let optionToSelect = '' + await highlightActiveElement.call(this, el); + let optionToSelect = ''; try { - optionToSelect = (await el.locator('option', { hasText: option }).textContent()).trim() + optionToSelect = (await el.locator('option', { hasText: option }).textContent()).trim(); } catch (e) { - optionToSelect = option + optionToSelect = option; } - if (!Array.isArray(option)) option = [optionToSelect] + if (!Array.isArray(option)) option = [optionToSelect]; - await el.selectOption(option) - return this._waitForAction() + await el.selectOption(option); + return this._waitForAction(); } /** @@ -1851,37 +1843,37 @@ class Playwright extends Helper { * */ async grabNumberOfVisibleElements(locator) { - let els = await this._locate(locator) - els = await Promise.all(els.map((el) => el.isVisible())) - return els.filter((v) => v).length + let els = await this._locate(locator); + els = await Promise.all(els.map(el => el.isVisible())); + return els.filter(v => v).length; } /** * {{> seeInCurrentUrl }} */ async seeInCurrentUrl(url) { - stringIncludes('url').assert(url, await this._getPageUrl()) + stringIncludes('url').assert(url, await this._getPageUrl()); } /** * {{> dontSeeInCurrentUrl }} */ async dontSeeInCurrentUrl(url) { - stringIncludes('url').negate(url, await this._getPageUrl()) + stringIncludes('url').negate(url, await this._getPageUrl()); } /** * {{> seeCurrentUrlEquals }} */ async seeCurrentUrlEquals(url) { - urlEquals(this.options.url).assert(url, await this._getPageUrl()) + urlEquals(this.options.url).assert(url, await this._getPageUrl()); } /** * {{> dontSeeCurrentUrlEquals }} */ async dontSeeCurrentUrlEquals(url) { - urlEquals(this.options.url).negate(url, await this._getPageUrl()) + urlEquals(this.options.url).negate(url, await this._getPageUrl()); } /** @@ -1890,14 +1882,14 @@ class Playwright extends Helper { * */ async see(text, context = null) { - return proceedSee.call(this, 'assert', text, context) + return proceedSee.call(this, 'assert', text, context); } /** * {{> seeTextEquals }} */ async seeTextEquals(text, context = null) { - return proceedSee.call(this, 'assert', text, context, true) + return proceedSee.call(this, 'assert', text, context, true); } /** @@ -1906,14 +1898,14 @@ class Playwright extends Helper { * */ async dontSee(text, context = null) { - return proceedSee.call(this, 'negate', text, context) + return proceedSee.call(this, 'negate', text, context); } /** * {{> grabSource }} */ async grabSource() { - return this.page.content() + return this.page.content(); } /** @@ -1928,32 +1920,32 @@ class Playwright extends Helper { * @return {Promise} */ async grabBrowserLogs() { - const logs = consoleLogStore.entries - consoleLogStore.clear() - return logs + const logs = consoleLogStore.entries; + consoleLogStore.clear(); + return logs; } /** * {{> grabCurrentUrl }} */ async grabCurrentUrl() { - return this._getPageUrl() + return this._getPageUrl(); } /** * {{> seeInSource }} */ async seeInSource(text) { - const source = await this.page.content() - stringIncludes('HTML source of a page').assert(text, source) + const source = await this.page.content(); + stringIncludes('HTML source of a page').assert(text, source); } /** * {{> dontSeeInSource }} */ async dontSeeInSource(text) { - const source = await this.page.content() - stringIncludes('HTML source of a page').negate(text, source) + const source = await this.page.content(); + stringIncludes('HTML source of a page').negate(text, source); } /** @@ -1962,10 +1954,8 @@ class Playwright extends Helper { * */ 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); } /** @@ -1974,11 +1964,8 @@ class Playwright extends Helper { * */ 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); } /** @@ -1986,9 +1973,9 @@ class Playwright extends Helper { */ async setCookie(cookie) { if (Array.isArray(cookie)) { - return this.browserContext.addCookies(cookie) + return this.browserContext.addCookies(cookie); } - return this.browserContext.addCookies([cookie]) + return this.browserContext.addCookies([cookie]); } /** @@ -1996,16 +1983,16 @@ class Playwright extends Helper { * */ async seeCookie(name) { - const cookies = await this.browserContext.cookies() - empty(`cookie ${name} to be set`).negate(cookies.filter((c) => c.name === name)) + const cookies = await this.browserContext.cookies(); + empty(`cookie ${name} to be set`).negate(cookies.filter(c => c.name === name)); } /** * {{> dontSeeCookie }} */ async dontSeeCookie(name) { - const cookies = await this.browserContext.cookies() - empty(`cookie ${name} not to be set`).assert(cookies.filter((c) => c.name === name)) + const cookies = await this.browserContext.cookies(); + empty(`cookie ${name} not to be set`).assert(cookies.filter(c => c.name === name)); } /** @@ -2014,10 +2001,10 @@ class Playwright extends Helper { * {{> grabCookie }} */ async grabCookie(name) { - const cookies = await this.browserContext.cookies() - if (!name) return cookies - const cookie = cookies.filter((c) => c.name === name) - if (cookie[0]) return cookie[0] + const cookies = await this.browserContext.cookies(); + if (!name) return cookies; + const cookie = cookies.filter(c => c.name === name); + if (cookie[0]) return cookie[0]; } /** @@ -2026,8 +2013,8 @@ class Playwright extends Helper { async clearCookie() { // Playwright currently doesn't support to delete a certain cookie // https://github.com/microsoft/playwright/blob/main/docs/src/api/class-browsercontext.md#async-method-browsercontextclearcookies - if (!this.browserContext) return - return this.browserContext.clearCookies() + if (!this.browserContext) return; + return this.browserContext.clearCookies(); } /** @@ -2057,9 +2044,9 @@ class Playwright extends Helper { async executeScript(fn, arg) { if (this.context && this.context.constructor.name === 'FrameLocator') { // switching to iframe context - return this.context.locator(':root').evaluate(fn, arg) + return this.context.locator(':root').evaluate(fn, arg); } - return this.page.evaluate.apply(this.page, [fn, arg]) + return this.page.evaluate.apply(this.page, [fn, arg]); } /** @@ -2068,14 +2055,14 @@ class Playwright extends Helper { * @param {*} locator */ _contextLocator(locator) { - locator = buildLocatorString(new Locator(locator, 'css')) + locator = buildLocatorString(new Locator(locator, 'css')); if (this.contextLocator) { - const contextLocator = buildLocatorString(new Locator(this.contextLocator, 'css')) - locator = `${contextLocator} >> ${locator}` + const contextLocator = buildLocatorString(new Locator(this.contextLocator, 'css')); + locator = `${contextLocator} >> ${locator}`; } - return locator + return locator; } /** @@ -2083,11 +2070,11 @@ class Playwright extends Helper { * */ async grabTextFrom(locator) { - locator = this._contextLocator(locator) - const text = await this.page.textContent(locator) - assertElementExists(text, locator) - this.debugSection('Text', text) - return text + locator = this._contextLocator(locator); + const text = await this.page.textContent(locator); + assertElementExists(text, locator); + this.debugSection('Text', text); + return text; } /** @@ -2095,51 +2082,51 @@ class Playwright extends Helper { * */ async grabTextFromAll(locator) { - const els = await this._locate(locator) - const texts = [] + const els = await this._locate(locator); + const texts = []; for (const el of els) { - texts.push(await await el.innerText()) + texts.push(await await el.innerText()); } - this.debug(`Matched ${els.length} elements`) - return texts + this.debug(`Matched ${els.length} elements`); + return texts; } /** * {{> grabValueFrom }} */ async grabValueFrom(locator) { - const values = await this.grabValueFromAll(locator) - assertElementExists(values, locator) - this.debugSection('Value', values[0]) - return values[0] + const values = await this.grabValueFromAll(locator); + assertElementExists(values, locator); + this.debugSection('Value', values[0]); + return values[0]; } /** * {{> grabValueFromAll }} */ async grabValueFromAll(locator) { - const els = await findFields.call(this, locator) - this.debug(`Matched ${els.length} elements`) - return Promise.all(els.map((el) => el.inputValue())) + const els = await findFields.call(this, locator); + this.debug(`Matched ${els.length} elements`); + return Promise.all(els.map(el => el.inputValue())); } /** * {{> grabHTMLFrom }} */ async grabHTMLFrom(locator) { - const html = await this.grabHTMLFromAll(locator) - assertElementExists(html, locator) - this.debugSection('HTML', html[0]) - return html[0] + const html = await this.grabHTMLFromAll(locator); + assertElementExists(html, locator); + this.debugSection('HTML', html[0]); + return html[0]; } /** * {{> grabHTMLFromAll }} */ async grabHTMLFromAll(locator) { - const els = await this._locate(locator) - this.debug(`Matched ${els.length} elements`) - return Promise.all(els.map((el) => el.innerHTML())) + const els = await this._locate(locator); + this.debug(`Matched ${els.length} elements`); + return Promise.all(els.map(el => el.innerHTML())); } /** @@ -2147,10 +2134,10 @@ class Playwright extends Helper { * */ async grabCssPropertyFrom(locator, cssProperty) { - const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty) - assertElementExists(cssValues, locator) - this.debugSection('CSS', cssValues[0]) - return cssValues[0] + const cssValues = await this.grabCssPropertyFromAll(locator, cssProperty); + assertElementExists(cssValues, locator); + this.debugSection('CSS', cssValues[0]); + return cssValues[0]; } /** @@ -2158,15 +2145,11 @@ class Playwright extends Helper { * */ async grabCssPropertyFromAll(locator, cssProperty) { - const els = await this._locate(locator) - this.debug(`Matched ${els.length} elements`) - const cssValues = await Promise.all( - els.map((el) => - el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty), - ), - ) + const els = await this._locate(locator); + this.debug(`Matched ${els.length} elements`); + const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty))); - return cssValues + return cssValues; } /** @@ -2174,37 +2157,35 @@ class Playwright extends Helper { * */ async seeCssPropertiesOnElements(locator, cssProperties) { - const res = await this._locate(locator) - assertElementExists(res, locator) + const res = await this._locate(locator); + assertElementExists(res, locator); - const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties) - const elemAmount = res.length - let props = [] + const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties); + const elemAmount = res.length; + let props = []; for (const element of res) { for (const prop of Object.keys(cssProperties)) { - const cssProp = await this.grabCssPropertyFrom(locator, prop) + const cssProp = await this.grabCssPropertyFrom(locator, prop); if (isColorProperty(prop)) { - props.push(convertColorToRGBA(cssProp)) + props.push(convertColorToRGBA(cssProp)); } else { - props.push(cssProp) + props.push(cssProp); } } } - const values = Object.keys(cssPropertiesCamelCase).map((key) => cssPropertiesCamelCase[key]) - if (!Array.isArray(props)) props = [props] - let chunked = chunkArray(props, values.length) - chunked = chunked.filter((val) => { + const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]); + if (!Array.isArray(props)) props = [props]; + let chunked = chunkArray(props, values.length); + chunked = chunked.filter(val => { for (let i = 0; i < val.length; ++i) { // eslint-disable-next-line eqeqeq - if (val[i] != values[i]) return false + if (val[i] != values[i]) return false; } - return true - }) - return equals( - `all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`, - ).assert(chunked.length, elemAmount) + return true; + }); + return equals(`all elements (${new Locator(locator)}) to have CSS property ${JSON.stringify(cssProperties)}`).assert(chunked.length, elemAmount); } /** @@ -2212,33 +2193,30 @@ class Playwright extends Helper { * */ async seeAttributesOnElements(locator, attributes) { - const res = await this._locate(locator) - assertElementExists(res, locator) - - const elemAmount = res.length - const commands = [] - res.forEach((el) => { - Object.keys(attributes).forEach((prop) => { - commands.push(el.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop)) - }) - }) - let attrs = await Promise.all(commands) - const values = Object.keys(attributes).map((key) => attributes[key]) - if (!Array.isArray(attrs)) attrs = [attrs] - let chunked = chunkArray(attrs, values.length) - chunked = chunked.filter((val) => { + const res = await this._locate(locator); + assertElementExists(res, locator); + + const elemAmount = res.length; + const commands = []; + res.forEach(el => { + Object.keys(attributes).forEach(prop => { + commands.push(el.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop)); + }); + }); + let attrs = await Promise.all(commands); + const values = Object.keys(attributes).map(key => attributes[key]); + if (!Array.isArray(attrs)) attrs = [attrs]; + let chunked = chunkArray(attrs, values.length); + chunked = chunked.filter(val => { for (let i = 0; i < val.length; ++i) { // the attribute could be a boolean - if (typeof val[i] === 'boolean') return val[i] === values[i] + if (typeof val[i] === 'boolean') return val[i] === values[i]; // if the attribute doesn't exist, returns false as well - if (!val[i] || !val[i].includes(values[i])) return false + if (!val[i] || !val[i].includes(values[i])) return false; } - return true - }) - return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert( - chunked.length, - elemAmount, - ) + return true; + }); + return equals(`all elements (${new Locator(locator)}) to have attributes ${JSON.stringify(attributes)}`).assert(chunked.length, elemAmount); } /** @@ -2246,21 +2224,21 @@ class Playwright extends Helper { * */ async dragSlider(locator, offsetX = 0) { - const src = await this._locateElement(locator) - assertElementExists(src, locator, 'Slider Element') + const src = await this._locateElement(locator); + assertElementExists(src, locator, 'Slider Element'); // Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets! - const sliderSource = await clickablePoint(src) + const sliderSource = await clickablePoint(src); // Drag start point - await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 }) - await this.page.mouse.down() + await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 }); + await this.page.mouse.down(); // Drag destination - await this.page.mouse.move(sliderSource.x + offsetX, sliderSource.y, { steps: 5 }) - await this.page.mouse.up() + await this.page.mouse.move(sliderSource.x + offsetX, sliderSource.y, { steps: 5 }); + await this.page.mouse.up(); - return this._waitForAction() + return this._waitForAction(); } /** @@ -2268,10 +2246,10 @@ class Playwright extends Helper { * */ async grabAttributeFrom(locator, attr) { - const attrs = await this.grabAttributeFromAll(locator, attr) - assertElementExists(attrs, locator) - this.debugSection('Attribute', attrs[0]) - return attrs[0] + const attrs = await this.grabAttributeFromAll(locator, attr); + assertElementExists(attrs, locator); + this.debugSection('Attribute', attrs[0]); + return attrs[0]; } /** @@ -2279,15 +2257,15 @@ class Playwright extends Helper { * */ async grabAttributeFromAll(locator, attr) { - const els = await this._locate(locator) - this.debug(`Matched ${els.length} elements`) - const array = [] + const els = await this._locate(locator); + this.debug(`Matched ${els.length} elements`); + const array = []; for (let index = 0; index < els.length; index++) { - array.push(await els[index].getAttribute(attr)) + array.push(await els[index].getAttribute(attr)); } - return array + return array; } /** @@ -2295,43 +2273,43 @@ class Playwright extends Helper { * */ async saveElementScreenshot(locator, fileName) { - const outputFile = screenshotOutputFolder(fileName) + const outputFile = screenshotOutputFolder(fileName); - const res = await this._locateElement(locator) - assertElementExists(res, locator) - const elem = res - this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`) - return elem.screenshot({ path: outputFile, type: 'png' }) + const res = await this._locateElement(locator); + assertElementExists(res, locator); + const elem = res; + this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`); + return elem.screenshot({ path: outputFile, type: 'png' }); } /** * {{> saveScreenshot }} */ async saveScreenshot(fileName, fullPage) { - const fullPageOption = fullPage || this.options.fullPageScreenshots - let outputFile = screenshotOutputFolder(fileName) + const fullPageOption = fullPage || this.options.fullPageScreenshots; + let outputFile = screenshotOutputFolder(fileName); - this.debug(`Screenshot is saving to ${outputFile}`) + this.debug(`Screenshot is saving to ${outputFile}`); await this.page.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png', - }) + }); if (this.activeSessionName) { for (const sessionName in this.sessionPages) { - const activeSessionPage = this.sessionPages[sessionName] - outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`) + const activeSessionPage = this.sessionPages[sessionName]; + outputFile = screenshotOutputFolder(`${sessionName}_${fileName}`); - this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`) + this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`); if (activeSessionPage) { await activeSessionPage.screenshot({ path: outputFile, fullPage: fullPageOption, type: 'png', - }) + }); } } } @@ -2355,23 +2333,21 @@ class Playwright extends Helper { * @returns {Promise} response */ async makeApiRequest(method, url, options) { - method = method.toLowerCase() - const allowedMethods = ['get', 'post', 'patch', 'head', 'fetch', 'delete'] + method = method.toLowerCase(); + const allowedMethods = ['get', 'post', 'patch', 'head', 'fetch', 'delete']; if (!allowedMethods.includes(method)) { - throw new Error( - `Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`, - ) + throw new Error(`Method ${method} is not allowed, use the one from a list ${allowedMethods} or switch to using REST helper`); } if (url.startsWith('/')) { // local url - url = this.options.url + url - this.debugSection('URL', url) + url = this.options.url + url; + this.debugSection('URL', url); } - const response = await this.page.request[method](url, options) - this.debugSection('Status', response.status()) - this.debugSection('Response', await response.text()) + const response = await this.page.request[method](url, options); + this.debugSection('Status', response.status()); + this.debugSection('Response', await response.text()); // hook to allow JSON response handle this if (this.config.onResponse) { @@ -2380,83 +2356,71 @@ class Playwright extends Helper { status: response.status(), statusText: response.statusText(), headers: response.headers(), - } - this.config.onResponse(axiosResponse) + }; + this.config.onResponse(axiosResponse); } - return response + return response; } async _failed(test) { - await this._withinEnd() + await this._withinEnd(); if (!test.artifacts) { - test.artifacts = {} + test.artifacts = {}; } if (this.options.recordVideo && this.page && this.page.video()) { - test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`) + test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`); for (const sessionName in this.sessionPages) { - test.artifacts[`video_${sessionName}`] = saveVideoForPage( - this.sessionPages[sessionName], - `${test.title}_${sessionName}.failed`, - ) + test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.failed`); } } if (this.options.trace) { - test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`) + test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`); for (const sessionName in this.sessionPages) { - if (!this.sessionPages[sessionName].context) continue - test.artifacts[`trace_${sessionName}`] = await saveTraceForContext( - this.sessionPages[sessionName].context, - `${test.title}_${sessionName}.failed`, - ) + if (!this.sessionPages[sessionName].context) continue; + test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`); } } if (this.options.recordHar) { - test.artifacts.har = this.currentRunningTest.artifacts.har + test.artifacts.har = this.currentRunningTest.artifacts.har; } } async _passed(test) { if (this.options.recordVideo && this.page && this.page.video()) { if (this.options.keepVideoForPassedTests) { - test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`) + test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`); for (const sessionName of Object.keys(this.sessionPages)) { - test.artifacts[`video_${sessionName}`] = saveVideoForPage( - this.sessionPages[sessionName], - `${test.title}_${sessionName}.passed`, - ) + test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.passed`); } } else { this.page .video() .delete() - .catch((e) => {}) + .catch(e => {}); } } if (this.options.trace) { if (this.options.keepTraceForPassedTests) { if (this.options.trace) { - test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`) + test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`); for (const sessionName in this.sessionPages) { - if (!this.sessionPages[sessionName].context) continue - test.artifacts[`trace_${sessionName}`] = await saveTraceForContext( - this.sessionPages[sessionName].context, - `${test.title}_${sessionName}.passed`, - ) + if (!this.sessionPages[sessionName].context) continue; + test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`); } } } else { - await this.browserContext.tracing.stop() + await this.browserContext.tracing.stop(); } } if (this.options.recordHar) { - test.artifacts.har = this.currentRunningTest.artifacts.har + test.artifacts.har = this.currentRunningTest.artifacts.har; } } @@ -2464,99 +2428,90 @@ class Playwright extends Helper { * {{> wait }} */ async wait(sec) { - return new Promise((done) => { - setTimeout(done, sec * 1000) - }) + return new Promise(done => { + setTimeout(done, sec * 1000); + }); } /** * {{> waitForEnabled }} */ async waitForEnabled(locator, sec) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - locator = new Locator(locator, 'css') + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + locator = new Locator(locator, 'css'); - let waiter - const context = await this._getContext() + let waiter; + const context = await this._getContext(); if (!locator.isXPath()) { const valueFn = function ([locator]) { - return Array.from(document.querySelectorAll(locator)).filter((el) => !el.disabled).length > 0 - } - waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout }) + return Array.from(document.querySelectorAll(locator)).filter(el => !el.disabled).length > 0; + }; + waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout }); } else { const enabledFn = function ([locator, $XPath]) { - eval($XPath) // eslint-disable-line no-eval - return $XPath(null, locator).filter((el) => !el.disabled).length > 0 - } - waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout }) + eval($XPath); // eslint-disable-line no-eval + return $XPath(null, locator).filter(el => !el.disabled).length > 0; + }; + waiter = context.waitForFunction(enabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout }); } - return waiter.catch((err) => { - throw new Error( - `element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`, - ) - }) + return waiter.catch(err => { + throw new Error(`element (${locator.toString()}) still not enabled after ${waitTimeout / 1000} sec\n${err.message}`); + }); } /** * {{> waitForDisabled }} */ async waitForDisabled(locator, sec) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - locator = new Locator(locator, 'css') + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + locator = new Locator(locator, 'css'); - let waiter - const context = await this._getContext() + let waiter; + const context = await this._getContext(); if (!locator.isXPath()) { const valueFn = function ([locator]) { - return Array.from(document.querySelectorAll(locator)).filter((el) => el.disabled).length > 0 - } - waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout }) + return Array.from(document.querySelectorAll(locator)).filter(el => el.disabled).length > 0; + }; + waiter = context.waitForFunction(valueFn, [locator.value], { timeout: waitTimeout }); } else { const disabledFn = function ([locator, $XPath]) { - eval($XPath) // eslint-disable-line no-eval - return $XPath(null, locator).filter((el) => el.disabled).length > 0 - } - waiter = context.waitForFunction(disabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout }) + eval($XPath); // eslint-disable-line no-eval + return $XPath(null, locator).filter(el => el.disabled).length > 0; + }; + waiter = context.waitForFunction(disabledFn, [locator.value, $XPath.toString()], { timeout: waitTimeout }); } - return waiter.catch((err) => { - throw new Error( - `element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`, - ) - }) + return waiter.catch(err => { + throw new Error(`element (${locator.toString()}) is still enabled after ${waitTimeout / 1000} sec\n${err.message}`); + }); } /** * {{> waitForValue }} */ async waitForValue(field, value, sec) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - const locator = new Locator(field, 'css') - const matcher = await this.context - let waiter - const context = await this._getContext() + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + const locator = new Locator(field, 'css'); + const matcher = await this.context; + let waiter; + const context = await this._getContext(); if (!locator.isXPath()) { const valueFn = function ([locator, value]) { - return ( - Array.from(document.querySelectorAll(locator)).filter((el) => (el.value || '').indexOf(value) !== -1).length > - 0 - ) - } - waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout }) + return Array.from(document.querySelectorAll(locator)).filter(el => (el.value || '').indexOf(value) !== -1).length > 0; + }; + waiter = context.waitForFunction(valueFn, [locator.value, value], { timeout: waitTimeout }); } else { const valueFn = function ([locator, $XPath, value]) { - eval($XPath) // eslint-disable-line no-eval - return $XPath(null, locator).filter((el) => (el.value || '').indexOf(value) !== -1).length > 0 - } + eval($XPath); // eslint-disable-line no-eval + return $XPath(null, locator).filter(el => (el.value || '').indexOf(value) !== -1).length > 0; + }; waiter = context.waitForFunction(valueFn, [locator.value, $XPath.toString(), value], { timeout: waitTimeout, - }) + }); } - return waiter.catch((err) => { - const loc = locator.toString() - throw new Error( - `element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`, - ) - }) + return waiter.catch(err => { + const loc = locator.toString(); + throw new Error(`element (${loc}) is not in DOM or there is no element(${loc}) with value "${value}" after ${waitTimeout / 1000} sec\n${err.message}`); + }); } /** @@ -2564,44 +2519,40 @@ class Playwright extends Helper { * */ async waitNumberOfVisibleElements(locator, num, sec) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - locator = new Locator(locator, 'css') + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + locator = new Locator(locator, 'css'); - let waiter - const context = await this._getContext() + let waiter; + const context = await this._getContext(); if (locator.isCSS()) { const visibleFn = function ([locator, num]) { - const els = document.querySelectorAll(locator) + const els = document.querySelectorAll(locator); if (!els || els.length === 0) { - return false + return false; } - return Array.prototype.filter.call(els, (el) => el.offsetParent !== null).length === num - } - waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout }) + return Array.prototype.filter.call(els, el => el.offsetParent !== null).length === num; + }; + waiter = context.waitForFunction(visibleFn, [locator.value, num], { timeout: waitTimeout }); } else { const visibleFn = function ([locator, $XPath, num]) { - eval($XPath) // eslint-disable-line no-eval - return $XPath(null, locator).filter((el) => el.offsetParent !== null).length === num - } + eval($XPath); // eslint-disable-line no-eval + return $XPath(null, locator).filter(el => el.offsetParent !== null).length === num; + }; waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString(), num], { timeout: waitTimeout, - }) + }); } - return waiter.catch((err) => { - throw new Error( - `The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`, - ) - }) + return waiter.catch(err => { + throw new Error(`The number of elements (${locator.toString()}) is not ${num} after ${waitTimeout / 1000} sec\n${err.message}`); + }); } /** * {{> waitForClickable }} */ async waitForClickable(locator, waitTimeout) { - console.log( - 'I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable', - ) - console.log('Remove usage of this function') + console.log('I.waitForClickable is DEPRECATED: This is no longer needed, Playwright automatically waits for element to be clickable'); + console.log('Remove usage of this function'); } /** @@ -2609,16 +2560,14 @@ class Playwright extends Helper { * */ async waitForElement(locator, sec) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - locator = new Locator(locator, 'css') + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + locator = new Locator(locator, 'css'); - const context = await this._getContext() + const context = await this._getContext(); try { - await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' }) + await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' }); } catch (e) { - throw new Error( - `element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`, - ) + throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`); } } @@ -2628,28 +2577,28 @@ class Playwright extends Helper { * {{> waitForVisible }} */ async waitForVisible(locator, sec) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - locator = new Locator(locator, 'css') - const context = await this._getContext() - let count = 0 + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + locator = new Locator(locator, 'css'); + const context = await this._getContext(); + let count = 0; // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented - let waiter + let waiter; if (this.frame) { do { - waiter = await this.frame.locator(buildLocatorString(locator)).first().isVisible() - await this.wait(1) - count += 1000 - if (waiter) break - } while (count <= waitTimeout) + waiter = await this.frame.locator(buildLocatorString(locator)).first().isVisible(); + await this.wait(1); + count += 1000; + if (waiter) break; + } while (count <= waitTimeout); - if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`) + if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`); } try { - await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'visible' }) + await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'visible' }); } catch (e) { - throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${e.message}`) + throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${e.message}`); } } @@ -2657,29 +2606,29 @@ class Playwright extends Helper { * {{> waitForInvisible }} */ async waitForInvisible(locator, sec) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - locator = new Locator(locator, 'css') - const context = await this._getContext() - let waiter - let count = 0 + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + locator = new Locator(locator, 'css'); + const context = await this._getContext(); + let waiter; + let count = 0; // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented if (this.frame) { do { - waiter = await this.frame.locator(buildLocatorString(locator)).first().isHidden() - await this.wait(1) - count += 1000 - if (waiter) break - } while (count <= waitTimeout) + waiter = await this.frame.locator(buildLocatorString(locator)).first().isHidden(); + await this.wait(1); + count += 1000; + if (waiter) break; + } while (count <= waitTimeout); - if (!waiter) throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec.`) - return + if (!waiter) throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec.`); + return; } try { - await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'hidden' }) + await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'hidden' }); } catch (e) { - throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${e.message}`) + throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${e.message}`); } } @@ -2687,136 +2636,134 @@ class Playwright extends Helper { * {{> waitToHide }} */ async waitToHide(locator, sec) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - locator = new Locator(locator, 'css') - const context = await this._getContext() - let waiter - let count = 0 + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + locator = new Locator(locator, 'css'); + const context = await this._getContext(); + let waiter; + let count = 0; // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented if (this.frame) { do { - waiter = await this.frame.locator(buildLocatorString(locator)).first().isHidden() - await this.wait(1) - count += 1000 - if (waiter) break - } while (count <= waitTimeout) + waiter = await this.frame.locator(buildLocatorString(locator)).first().isHidden(); + await this.wait(1); + count += 1000; + if (waiter) break; + } while (count <= waitTimeout); - if (!waiter) throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec.`) - return + if (!waiter) throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec.`); + return; } return context .locator(buildLocatorString(locator)) .first() .waitFor({ timeout: waitTimeout, state: 'hidden' }) - .catch((err) => { - throw new Error( - `element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`, - ) - }) + .catch(err => { + throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`); + }); } /** * {{> waitForNumberOfTabs }} */ async waitForNumberOfTabs(expectedTabs, sec) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - let currentTabs - let count = 0 + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + let currentTabs; + let count = 0; do { - currentTabs = await this.grabNumberOfOpenTabs() - await this.wait(1) - count += 1000 - if (currentTabs >= expectedTabs) return - } while (count <= waitTimeout) + currentTabs = await this.grabNumberOfOpenTabs(); + await this.wait(1); + count += 1000; + if (currentTabs >= expectedTabs) return; + } while (count <= waitTimeout); - throw new Error(`Expected ${expectedTabs} tabs are not met after ${waitTimeout / 1000} sec.`) + throw new Error(`Expected ${expectedTabs} tabs are not met after ${waitTimeout / 1000} sec.`); } async _getContext() { if ((this.context && this.context.constructor.name === 'FrameLocator') || this.context) { - return this.context + return this.context; } - return this.page + return this.page; } /** * {{> waitInUrl }} */ async waitInUrl(urlPart, sec = null) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; return this.page .waitForFunction( - (urlPart) => { - const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href))) - return currUrl.indexOf(urlPart) > -1 + urlPart => { + const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href))); + return currUrl.indexOf(urlPart) > -1; }, urlPart, { timeout: waitTimeout }, ) - .catch(async (e) => { - const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data. + .catch(async e => { + const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data. if (/Timeout/i.test(e.message)) { - throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`) + throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`); } else { - throw e + throw e; } - }) + }); } /** * {{> waitUrlEquals }} */ async waitUrlEquals(urlPart, sec = null) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; - const baseUrl = this.options.url + const baseUrl = this.options.url; if (urlPart.indexOf('http') < 0) { - urlPart = baseUrl + urlPart + urlPart = baseUrl + urlPart; } return this.page .waitForFunction( - (urlPart) => { - const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href))) - return currUrl.indexOf(urlPart) > -1 + urlPart => { + const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href))); + return currUrl.indexOf(urlPart) > -1; }, urlPart, { timeout: waitTimeout }, ) - .catch(async (e) => { - const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data. + .catch(async e => { + const currUrl = await this._getPageUrl(); // Required because the waitForFunction can't return data. if (/Timeout/i.test(e.message)) { - throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`) + throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`); } else { - throw e + throw e; } - }) + }); } /** * {{> waitForText }} */ async waitForText(text, sec = null, context = null) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - const errorMessage = `Text "${text}" was not found on page after ${waitTimeout / 1000} sec.` - let waiter + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + const errorMessage = `Text "${text}" was not found on page after ${waitTimeout / 1000} sec.`; + let waiter; - const contextObject = await this._getContext() + const contextObject = await this._getContext(); if (context) { - const locator = new Locator(context, 'css') + const locator = new Locator(context, 'css'); if (!locator.isXPath()) { try { await contextObject .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`) .first() - .waitFor({ timeout: waitTimeout, state: 'visible' }) + .waitFor({ timeout: waitTimeout, state: 'visible' }); } catch (e) { - throw new Error(`${errorMessage}\n${e.message}`) + throw new Error(`${errorMessage}\n${e.message}`); } } @@ -2824,34 +2771,34 @@ class Playwright extends Helper { try { await contextObject.waitForFunction( ([locator, text, $XPath]) => { - eval($XPath) // eslint-disable-line no-eval - const el = $XPath(null, locator) - if (!el.length) return false - return el[0].innerText.indexOf(text) > -1 + eval($XPath); // eslint-disable-line no-eval + const el = $XPath(null, locator); + if (!el.length) return false; + return el[0].innerText.indexOf(text) > -1; }, [locator.value, text, $XPath.toString()], { timeout: waitTimeout }, - ) + ); } catch (e) { - throw new Error(`${errorMessage}\n${e.message}`) + throw new Error(`${errorMessage}\n${e.message}`); } } } else { // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented - const _contextObject = this.frame ? this.frame : contextObject - let count = 0 + const _contextObject = this.frame ? this.frame : contextObject; + let count = 0; do { waiter = await _contextObject .locator(`:has-text(${JSON.stringify(text)})`) .first() - .isVisible() - if (waiter) break - await this.wait(1) - count += 1000 - } while (count <= waitTimeout) + .isVisible(); + if (waiter) break; + await this.wait(1); + count += 1000; + } while (count <= waitTimeout); - if (!waiter) throw new Error(`${errorMessage}`) + if (!waiter) throw new Error(`${errorMessage}`); } } @@ -2867,8 +2814,8 @@ class Playwright extends Helper { * @param {?number} [sec=null] seconds to wait */ async waitForRequest(urlOrPredicate, sec = null) { - const timeout = sec ? sec * 1000 : this.options.waitForTimeout - return this.page.waitForRequest(urlOrPredicate, { timeout }) + const timeout = sec ? sec * 1000 : this.options.waitForTimeout; + return this.page.waitForRequest(urlOrPredicate, { timeout }); } /** @@ -2883,8 +2830,8 @@ class Playwright extends Helper { * @param {?number} [sec=null] number of seconds to wait */ async waitForResponse(urlOrPredicate, sec = null) { - const timeout = sec ? sec * 1000 : this.options.waitForTimeout - return this.page.waitForResponse(urlOrPredicate, { timeout }) + const timeout = sec ? sec * 1000 : this.options.waitForTimeout; + return this.page.waitForResponse(urlOrPredicate, { timeout }); } /** @@ -2894,51 +2841,51 @@ class Playwright extends Helper { if (Number.isInteger(locator)) { // Select by frame index of current context - let childFrames = null + let childFrames = null; if (this.context && typeof this.context.childFrames === 'function') { - childFrames = this.context.childFrames() + childFrames = this.context.childFrames(); } else { - childFrames = this.page.mainFrame().childFrames() + childFrames = this.page.mainFrame().childFrames(); } if (locator >= 0 && locator < childFrames.length) { - this.context = await this.page.frameLocator('iframe').nth(locator) - this.contextLocator = locator + this.context = await this.page.frameLocator('iframe').nth(locator); + this.contextLocator = locator; } else { - throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath') + throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath'); } - return + return; } if (!locator) { - this.context = this.page - this.contextLocator = null - this.frame = null - return + this.context = this.page; + this.contextLocator = null; + this.frame = null; + return; } // iframe by selector - locator = buildLocatorString(new Locator(locator, 'css')) - const frame = await this._locateElement(locator) + locator = buildLocatorString(new Locator(locator, 'css')); + const frame = await this._locateElement(locator); if (!frame) { - throw new Error(`Frame ${JSON.stringify(locator)} was not found by text|CSS|XPath`) + throw new Error(`Frame ${JSON.stringify(locator)} was not found by text|CSS|XPath`); } if (this.frame) { - this.frame = await this.frame.frameLocator(locator) + this.frame = await this.frame.frameLocator(locator); } else { - this.frame = await this.page.frameLocator(locator) + this.frame = await this.page.frameLocator(locator); } - const contentFrame = this.frame + const contentFrame = this.frame; if (contentFrame) { - this.context = contentFrame - this.contextLocator = null + this.context = contentFrame; + this.contextLocator = null; } else { - this.context = this.page.frame(this.page.frames()[1].name()) - this.contextLocator = locator + this.context = this.page.frame(this.page.frames()[1].name()); + this.contextLocator = locator; } } @@ -2946,17 +2893,17 @@ class Playwright extends Helper { * {{> 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; } } - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - const context = await this._getContext() - return context.waitForFunction(fn, args, { timeout: waitTimeout }) + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + const context = await this._getContext(); + return context.waitForFunction(fn, args, { timeout: waitTimeout }); } /** @@ -2968,13 +2915,13 @@ class Playwright extends Helper { */ async waitForNavigation(options = {}) { console.log(`waitForNavigation deprecated: - * This method is inherently racy, please use 'waitForURL' instead.`) + * This method is inherently racy, please use 'waitForURL' instead.`); options = { timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation, ...options, - } - return this.page.waitForNavigation(options) + }; + return this.page.waitForNavigation(options); } /** @@ -2990,44 +2937,44 @@ class Playwright extends Helper { timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation, ...options, - } - return this.page.waitForURL(url, options) + }; + return this.page.waitForURL(url, options); } 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) { - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - locator = new Locator(locator, 'css') + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; + locator = new Locator(locator, 'css'); - let waiter - const context = await this._getContext() + let waiter; + const context = await this._getContext(); if (!locator.isXPath()) { try { await context .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()}`) .first() - .waitFor({ timeout: waitTimeout, state: 'detached' }) + .waitFor({ timeout: waitTimeout, state: 'detached' }); } catch (e) { - throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${e.message}`) + throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${e.message}`); } } else { const visibleFn = function ([locator, $XPath]) { - eval($XPath) // eslint-disable-line no-eval - return $XPath(null, locator).length === 0 - } - waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout }) - return waiter.catch((err) => { - throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`) - }) + eval($XPath); // eslint-disable-line no-eval + return $XPath(null, locator).length === 0; + }; + waiter = context.waitForFunction(visibleFn, [locator.value, $XPath.toString()], { timeout: waitTimeout }); + return waiter.catch(err => { + throw new Error(`element (${locator.toString()}) still on page after ${waitTimeout / 1000} sec\n${err.message}`); + }); } } @@ -3036,56 +2983,56 @@ class Playwright extends Helper { */ async waitForCookie(name, sec) { // by default, we will retry 3 times - let retries = 3 - const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + let retries = 3; + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; if (sec) { - retries = sec + retries = sec; } else { - retries = Math.ceil(waitTimeout / 1000) - 1 + retries = Math.ceil(waitTimeout / 1000) - 1; } return promiseRetry( async (retry, number) => { - const _grabCookie = async (name) => { - const cookies = await this.browserContext.cookies() - const cookie = cookies.filter((c) => c.name === name) - if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`) - } + const _grabCookie = async name => { + const cookies = await this.browserContext.cookies(); + const cookie = cookies.filter(c => c.name === name); + if (cookie.length === 0) throw Error(`Cookie ${name} is not found after ${retries}s`); + }; - this.debugSection('Wait for cookie: ', name) - if (number > 1) this.debugSection('Retrying... Attempt #', number) + this.debugSection('Wait for cookie: ', name); + if (number > 1) this.debugSection('Retrying... Attempt #', number); try { - await _grabCookie(name) + await _grabCookie(name); } catch (e) { - retry(e) + retry(e); } }, { retries, maxTimeout: 1000 }, - ) + ); } async _waitForAction() { - return this.wait(this.options.waitForAction / 1000) + return this.wait(this.options.waitForAction / 1000); } /** * {{> grabDataFromPerformanceTiming }} */ async grabDataFromPerformanceTiming() { - return perfTiming + return perfTiming; } /** * {{> grabElementBoundingRect }} */ async grabElementBoundingRect(locator, prop) { - const el = await this._locateElement(locator) - assertElementExists(el, locator) - const rect = await el.boundingBox() - if (prop) return rect[prop] - return rect + const el = await this._locateElement(locator); + assertElementExists(el, locator); + const rect = await el.boundingBox(); + if (prop) return rect[prop]; + return rect; } /** @@ -3100,7 +3047,7 @@ class Playwright extends Helper { * @param {function} [handler] a function to process request */ async mockRoute(url, handler) { - return this.browserContext.route(...arguments) + return this.browserContext.route(...arguments); } /** @@ -3116,7 +3063,7 @@ class Playwright extends Helper { * @param {function} [handler] a function to process request */ async stopMockingRoute(url, handler) { - return this.browserContext.unroute(...arguments) + return this.browserContext.unroute(...arguments); } /** @@ -3124,27 +3071,27 @@ class Playwright extends Helper { * */ startRecordingTraffic() { - this.flushNetworkTraffics() - this.recording = true - this.recordedAtLeastOnce = true + this.flushNetworkTraffics(); + this.recording = true; + this.recordedAtLeastOnce = true; - this.page.on('requestfinished', async (request) => { + this.page.on('requestfinished', async request => { const information = { url: request.url(), method: request.method(), requestHeaders: request.headers(), requestPostData: request.postData(), response: request.response(), - } + }; - this.debugSection('REQUEST: ', JSON.stringify(information)) + this.debugSection('REQUEST: ', JSON.stringify(information)); if (typeof information.requestPostData === 'object') { - information.requestPostData = JSON.parse(information.requestPostData) + information.requestPostData = JSON.parse(information.requestPostData); } - this.requests.push(information) - }) + this.requests.push(information); + }); } /** @@ -3167,21 +3114,21 @@ class Playwright extends Helper { */ blockTraffic(urls) { if (Array.isArray(urls)) { - urls.forEach((url) => { - this.page.route(url, (route) => { + urls.forEach(url => { + this.page.route(url, route => { route .abort() // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then. - .catch((e) => {}) - }) - }) + .catch(e => {}); + }); + }); } else { - this.page.route(urls, (route) => { + this.page.route(urls, route => { route .abort() // Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then. - .catch((e) => {}) - }) + .catch(e => {}); + }); } } @@ -3203,26 +3150,26 @@ class Playwright extends Helper { */ mockTraffic(urls, responseString, contentType = 'application/json') { // Required to mock cross-domain requests - const headers = { 'access-control-allow-origin': '*' } + const headers = { 'access-control-allow-origin': '*' }; if (typeof urls === 'string') { - urls = [urls] + urls = [urls]; } - urls.forEach((url) => { - this.page.route(url, (route) => { + urls.forEach(url => { + this.page.route(url, route => { if (this.page.isClosed()) { // Sometimes it happens that browser has been closed in the meantime. // In this case we just don't fulfill to prevent error in test scenario. - return + return; } route.fulfill({ contentType, headers, body: responseString, - }) - }) - }) + }); + }); + }); } /** @@ -3230,7 +3177,7 @@ class Playwright extends Helper { * {{> flushNetworkTraffics }} */ flushNetworkTraffics() { - flushNetworkTraffics.call(this) + flushNetworkTraffics.call(this); } /** @@ -3238,7 +3185,7 @@ class Playwright extends Helper { * {{> stopRecordingTraffic }} */ stopRecordingTraffic() { - stopRecordingTraffic.call(this) + stopRecordingTraffic.call(this); } /** @@ -3256,23 +3203,21 @@ class Playwright extends Helper { */ grabTrafficUrl(urlMatch) { if (!this.recordedAtLeastOnce) { - throw new Error( - 'Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.', - ) + throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.'); } for (const i in this.requests) { // eslint-disable-next-line no-prototype-builtins if (this.requests.hasOwnProperty(i)) { - const request = this.requests[i] + const request = this.requests[i]; if (request.url && request.url.match(new RegExp(urlMatch))) { - return request.url + return request.url; } } } - assert.fail(`Method "getTrafficUrl" failed: No request found in traffic that matches ${urlMatch}`) + assert.fail(`Method "getTrafficUrl" failed: No request found in traffic that matches ${urlMatch}`); } /** @@ -3280,7 +3225,7 @@ class Playwright extends Helper { * {{> grabRecordedNetworkTraffics }} */ async grabRecordedNetworkTraffics() { - return grabRecordedNetworkTraffics.call(this) + return grabRecordedNetworkTraffics.call(this); } /** @@ -3288,7 +3233,7 @@ class Playwright extends Helper { * {{> seeTraffic }} */ async seeTraffic({ name, url, parameters, requestPostData, timeout = 10 }) { - await seeTraffic.call(this, ...arguments) + await seeTraffic.call(this, ...arguments); } /** @@ -3297,42 +3242,42 @@ class Playwright extends Helper { * */ dontSeeTraffic({ name, url }) { - dontSeeTraffic.call(this, ...arguments) + dontSeeTraffic.call(this, ...arguments); } /** * {{> startRecordingWebSocketMessages }} */ async startRecordingWebSocketMessages() { - this.flushWebSocketMessages() - this.recordingWebSocketMessages = true - this.recordedWebSocketMessagesAtLeastOnce = true + this.flushWebSocketMessages(); + this.recordingWebSocketMessages = true; + this.recordedWebSocketMessagesAtLeastOnce = true; - this.cdpSession = await this.getNewCDPSession() - await this.cdpSession.send('Network.enable') - await this.cdpSession.send('Page.enable') + this.cdpSession = await this.getNewCDPSession(); + await this.cdpSession.send('Network.enable'); + await this.cdpSession.send('Page.enable'); - this.cdpSession.on('Network.webSocketFrameReceived', (payload) => { - this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload)) - }) + this.cdpSession.on('Network.webSocketFrameReceived', payload => { + this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload)); + }); - this.cdpSession.on('Network.webSocketFrameSent', (payload) => { - this._logWebsocketMessages(this._getWebSocketLog('SENT', payload)) - }) + this.cdpSession.on('Network.webSocketFrameSent', payload => { + this._logWebsocketMessages(this._getWebSocketLog('SENT', payload)); + }); - this.cdpSession.on('Network.webSocketFrameError', (payload) => { - this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload)) - }) + this.cdpSession.on('Network.webSocketFrameError', payload => { + this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload)); + }); } /** * {{> stopRecordingWebSocketMessages }} */ async stopRecordingWebSocketMessages() { - await this.cdpSession.send('Network.disable') - await this.cdpSession.send('Page.disable') - this.page.removeAllListeners('Network') - this.recordingWebSocketMessages = false + await this.cdpSession.send('Network.disable'); + await this.cdpSession.send('Page.disable'); + this.page.removeAllListeners('Network'); + this.recordingWebSocketMessages = false; } /** @@ -3344,19 +3289,17 @@ class Playwright extends Helper { grabWebSocketMessages() { if (!this.recordingWebSocketMessages) { if (!this.recordedWebSocketMessagesAtLeastOnce) { - throw new Error( - 'Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.', - ) + throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.'); } } - return this.webSocketMessages + return this.webSocketMessages; } /** * Resets all recorded WS messages. */ flushWebSocketMessages() { - this.webSocketMessages = [] + this.webSocketMessages = []; } /** @@ -3414,350 +3357,342 @@ class Playwright extends Helper { * @return {Promise>} */ async grabMetrics() { - const client = await this.page.context().newCDPSession(this.page) - await client.send('Performance.enable') - const perfMetricObject = await client.send('Performance.getMetrics') - return perfMetricObject?.metrics + const client = await this.page.context().newCDPSession(this.page); + await client.send('Performance.enable'); + const perfMetricObject = await client.send('Performance.getMetrics'); + return perfMetricObject?.metrics; } _getWebSocketMessage(payload) { if (payload.errorMessage) { - return payload.errorMessage + return payload.errorMessage; } - return payload.response.payloadData + return payload.response.payloadData; } _getWebSocketLog(prefix, payload) { - return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n` + return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`; } async getNewCDPSession() { - return this.page.context().newCDPSession(this.page) + return this.page.context().newCDPSession(this.page); } _logWebsocketMessages(message) { - this.webSocketMessages.push(message) + this.webSocketMessages.push(message); } } -module.exports = Playwright +module.exports = Playwright; function buildLocatorString(locator) { if (locator.isCustom()) { - return `${locator.type}=${locator.value}` + return `${locator.type}=${locator.value}`; } if (locator.isXPath()) { - return `xpath=${locator.value}` + return `xpath=${locator.value}`; } - return locator.simplify() + return locator.simplify(); } async function findElements(matcher, locator) { - if (locator.react) return findReact(matcher, locator) - if (locator.vue) return findVue(matcher, locator) - if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator) - locator = new Locator(locator, 'css') + if (locator.react) return findReact(matcher, locator); + if (locator.vue) return findVue(matcher, locator); + if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator); + locator = new Locator(locator, 'css'); - return matcher.locator(buildLocatorString(locator)).all() + return matcher.locator(buildLocatorString(locator)).all(); } async function findElement(matcher, locator) { - if (locator.react) return findReact(matcher, locator) - if (locator.vue) return findVue(matcher, locator) - if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator) - locator = new Locator(locator, 'css') + if (locator.react) return findReact(matcher, locator); + if (locator.vue) return findVue(matcher, locator); + if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator); + locator = new Locator(locator, 'css'); - return matcher.locator(buildLocatorString(locator)).first() + return matcher.locator(buildLocatorString(locator)).first(); } async function getVisibleElements(elements) { - const visibleElements = [] + const visibleElements = []; for (const element of elements) { if (await element.isVisible()) { - visibleElements.push(element) + visibleElements.push(element); } } if (visibleElements.length === 0) { - return elements + return elements; } - return visibleElements + return visibleElements; } async function proceedClick(locator, context = null, options = {}) { - let matcher = await this._getContext() + let matcher = await this._getContext(); if (context) { - const els = await this._locate(context) - assertElementExists(els, context) - matcher = els[0] + const els = await this._locate(context); + assertElementExists(els, context); + matcher = els[0]; } - const els = await findClickable.call(this, matcher, locator) + const els = await findClickable.call(this, matcher, locator); if (context) { - assertElementExists( - els, - locator, - 'Clickable element', - `was not found inside element ${new Locator(context).toString()}`, - ) + assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`); } else { - assertElementExists(els, locator, 'Clickable element') + assertElementExists(els, locator, 'Clickable element'); } - await highlightActiveElement.call(this, els[0]) + await highlightActiveElement.call(this, els[0]); /* using the force true options itself but instead dispatching a click */ if (options.force) { - await els[0].dispatchEvent('click') + await els[0].dispatchEvent('click'); } else { - const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0] - await element.click(options) + const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]; + await element.click(options); } - const promises = [] + const promises = []; if (options.waitForNavigation) { - promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation })) + promises.push(this.waitForURL(/.*/, { waitUntil: options.waitForNavigation })); } - promises.push(this._waitForAction()) + promises.push(this._waitForAction()); - return Promise.all(promises) + return Promise.all(promises); } async function findClickable(matcher, locator) { - if (locator.react) return findReact(matcher, locator) - if (locator.vue) return findVue(matcher, locator) - if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator) + if (locator.react) return findReact(matcher, locator); + if (locator.vue) return findVue(matcher, locator); + if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator); - locator = new Locator(locator) - if (!locator.isFuzzy()) return findElements.call(this, matcher, locator) + locator = new Locator(locator); + if (!locator.isFuzzy()) return findElements.call(this, matcher, locator); - let els - const literal = xpathLocator.literal(locator.value) + let els; + const literal = xpathLocator.literal(locator.value); - els = await findElements.call(this, matcher, Locator.clickable.narrow(literal)) - if (els.length) return els + els = await findElements.call(this, matcher, Locator.clickable.narrow(literal)); + if (els.length) return els; - els = await findElements.call(this, matcher, Locator.clickable.wide(literal)) - if (els.length) return els + els = await findElements.call(this, matcher, Locator.clickable.wide(literal)); + if (els.length) return els; try { - els = await findElements.call(this, matcher, Locator.clickable.self(literal)) - if (els.length) return els + els = await findElements.call(this, matcher, Locator.clickable.self(literal)); + if (els.length) return els; } catch (err) { // Do nothing } - return findElements.call(this, matcher, locator.value) // by css or xpath + return findElements.call(this, matcher, locator.value); // by css or xpath } async function proceedSee(assertType, text, context, strict = false) { - let description - let allText + let description; + let allText; if (!context) { - const el = await this.context + const el = await this.context; - allText = el.constructor.name !== 'Locator' ? [await el.locator('body').innerText()] : [await el.innerText()] + allText = el.constructor.name !== 'Locator' ? [await el.locator('body').innerText()] : [await el.innerText()]; - description = 'web application' + description = 'web application'; } else { - const locator = new Locator(context, 'css') - description = `element ${locator.toString()}` - const els = await this._locate(locator) - assertElementExists(els, locator.toString()) - allText = await Promise.all(els.map((el) => el.innerText())) + const locator = new Locator(context, 'css'); + description = `element ${locator.toString()}`; + const els = await this._locate(locator); + assertElementExists(els, locator.toString()); + allText = await Promise.all(els.map(el => el.innerText())); } if (strict) { - return allText.map((elText) => equals(description)[assertType](text, elText)) + return allText.map(elText => equals(description)[assertType](text, elText)); } - return stringIncludes(description)[assertType]( - normalizeSpacesInString(text), - normalizeSpacesInString(allText.join(' | ')), - ) + return stringIncludes(description)[assertType](normalizeSpacesInString(text), normalizeSpacesInString(allText.join(' | '))); } async function findCheckable(locator, context) { - let contextEl = await this.context + let contextEl = await this.context; if (typeof context === 'string') { - contextEl = await findElements.call(this, contextEl, new Locator(context, 'css').simplify()) - contextEl = contextEl[0] + contextEl = await findElements.call(this, contextEl, new Locator(context, 'css').simplify()); + contextEl = contextEl[0]; } - const matchedLocator = new Locator(locator) + const matchedLocator = new Locator(locator); if (!matchedLocator.isFuzzy()) { - return findElements.call(this, contextEl, matchedLocator.simplify()) + return findElements.call(this, contextEl, matchedLocator.simplify()); } - const literal = xpathLocator.literal(locator) - let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal)) + const literal = xpathLocator.literal(locator); + let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal)); if (els.length) { - return els + return els; } - els = await findElements.call(this, contextEl, Locator.checkable.byName(literal)) + els = await findElements.call(this, contextEl, Locator.checkable.byName(literal)); if (els.length) { - return els + return els; } - return findElements.call(this, contextEl, locator) + return findElements.call(this, contextEl, locator); } async function proceedIsChecked(assertType, option) { - let els = await findCheckable.call(this, option) - assertElementExists(els, option, 'Checkable') - els = await Promise.all(els.map((el) => el.isChecked())) - const selected = els.reduce((prev, cur) => prev || cur) - return truth(`checkable ${option}`, 'to be checked')[assertType](selected) + let els = await findCheckable.call(this, option); + assertElementExists(els, option, 'Checkable'); + els = await Promise.all(els.map(el => el.isChecked())); + const selected = els.reduce((prev, cur) => prev || cur); + return truth(`checkable ${option}`, 'to be checked')[assertType](selected); } async function findFields(locator) { - const matchedLocator = new Locator(locator) + const matchedLocator = new Locator(locator); if (!matchedLocator.isFuzzy()) { - return this._locate(matchedLocator) + return this._locate(matchedLocator); } - const literal = xpathLocator.literal(locator) + const literal = xpathLocator.literal(locator); - let els = await this._locate({ xpath: Locator.field.labelEquals(literal) }) + let els = await this._locate({ xpath: Locator.field.labelEquals(literal) }); if (els.length) { - return els + return els; } - els = await this._locate({ xpath: Locator.field.labelContains(literal) }) + els = await this._locate({ xpath: Locator.field.labelContains(literal) }); if (els.length) { - return els + return els; } - els = await this._locate({ xpath: Locator.field.byName(literal) }) + els = await this._locate({ xpath: Locator.field.byName(literal) }); if (els.length) { - return els + return els; } - return this._locate({ css: locator }) + return this._locate({ css: locator }); } async function proceedSeeInField(assertType, field, value) { - const els = await findFields.call(this, field) - assertElementExists(els, field, 'Field') - const el = els[0] - const tag = await el.evaluate((e) => e.tagName) - const fieldType = await el.getAttribute('type') + const els = await findFields.call(this, field); + assertElementExists(els, field, 'Field'); + const el = els[0]; + const tag = await el.evaluate(e => e.tagName); + const fieldType = await el.getAttribute('type'); - const proceedMultiple = async (elements) => { - const fields = Array.isArray(elements) ? elements : [elements] + const proceedMultiple = async elements => { + const fields = Array.isArray(elements) ? elements : [elements]; - const elementValues = [] + const elementValues = []; for (const element of fields) { - elementValues.push(await element.inputValue()) + elementValues.push(await element.inputValue()); } if (typeof value === 'boolean') { - equals(`no. of items matching > 0: ${field}`)[assertType](value, !!elementValues.length) + equals(`no. of items matching > 0: ${field}`)[assertType](value, !!elementValues.length); } else { if (assertType === 'assert') { - equals(`select option by ${field}`)[assertType](true, elementValues.length > 0) + equals(`select option by ${field}`)[assertType](true, elementValues.length > 0); } - elementValues.forEach((val) => stringIncludes(`fields by ${field}`)[assertType](value, val)) + elementValues.forEach(val => stringIncludes(`fields by ${field}`)[assertType](value, val)); } - } + }; if (tag === 'SELECT') { if (await el.getAttribute('multiple')) { - const selectedOptions = await el.all('option:checked') - if (!selectedOptions.length) return null + const selectedOptions = await el.all('option:checked'); + if (!selectedOptions.length) return null; - const options = await filterFieldsByValue(selectedOptions, value, true) - return proceedMultiple(options) + const options = await filterFieldsByValue(selectedOptions, value, true); + return proceedMultiple(options); } - return el.inputValue() + return el.inputValue(); } if (tag === 'INPUT') { if (fieldType === 'checkbox' || fieldType === 'radio') { if (typeof value === 'boolean') { // Filter by values - const options = await filterFieldsBySelectionState(els, true) - return proceedMultiple(options) + const options = await filterFieldsBySelectionState(els, true); + return proceedMultiple(options); } - const options = await filterFieldsByValue(els, value, true) - return proceedMultiple(options) + const options = await filterFieldsByValue(els, value, true); + return proceedMultiple(options); } - return proceedMultiple(els[0]) + return proceedMultiple(els[0]); } - let fieldVal + let fieldVal; try { - fieldVal = await el.inputValue() + fieldVal = await el.inputValue(); } catch (e) { if (e.message.includes('Error: Node is not an ,