UNPKG

codeceptjs

Version:

Modern Era Acceptance Testing Framework for NodeJS

1,307 lines (1,109 loc) 35.9 kB
// @ts-nocheck const fs = require('fs'); const assert = require('assert'); const path = require('path'); const qrcode = require('qrcode-terminal'); const requireg = require('requireg'); const createTestCafe = require('testcafe'); const { Selector, ClientFunction } = require('testcafe'); const ElementNotFound = require('./errors/ElementNotFound'); const testControllerHolder = require('./testcafe/testControllerHolder'); const { mapError, createTestFile, createClientFunction, } = require('./testcafe/testcafe-utils'); const stringIncludes = require('../assert/include').includes; const { urlEquals } = require('../assert/equal'); const { empty } = require('../assert/empty'); const { truth } = require('../assert/truth'); const { xpathLocator, } = require('../utils'); const Locator = require('../locator'); const Helper = require('../helper'); /** * Client Functions */ const getPageUrl = t => ClientFunction(() => document.location.href).with({ boundTestRun: t }); const getHtmlSource = t => ClientFunction(() => document.getElementsByTagName('html')[0].innerHTML).with({ boundTestRun: t }); /** * Uses [TestCafe](https://github.com/DevExpress/testcafe) library to run cross-browser tests. * The browser version you want to use in tests must be installed on your system. * * Requires `testcafe` package to be installed. * * ``` * npm i testcafe --save-dev * ``` * * ## Configuration * * This helper should be configured in codecept.json or codecept.conf.js * * * `url`: base url of website to be tested * * `show`: (optional, default: false) - show browser window. * * `windowSize`: (optional) - set browser window width and height * * `getPageTimeout` (optional, default: '30000') config option to set maximum navigation time in milliseconds. * * `waitForTimeout`: (optional) default wait* timeout in ms. Default: 5000. * * `browser`: (optional, default: chrome) - See https://devexpress.github.io/testcafe/documentation/using-testcafe/common-concepts/browsers/browser-support.html * * * #### Example #1: Show chrome browser window * * ```js * { * helpers: { * TestCafe : { * url: "http://localhost", * waitForTimeout: 15000, * show: true, * browser: "chrome" * } * } * } * ``` * * To use remote device you can provide 'remote' as browser parameter this will display a link with QR Code * See https://devexpress.github.io/testcafe/documentation/recipes/test-on-remote-computers-and-mobile-devices.html * #### Example #2: Remote browser connection * * ```js * { * helpers: { * TestCafe : { * url: "http://localhost", * waitForTimeout: 15000, * browser: "remote" * } * } * } * ``` * * ## Access From Helpers * * Call Testcafe methods directly using the testcafe controller. * * ```js * const testcafeTestController = this.helpers['TestCafe'].t; * const comboBox = Selector('.combo-box'); * await testcafeTestController * .hover(comboBox) // hover over combo box * .click('#i-prefer-both') // click some other element * ``` * * ## Methods */ class TestCafe extends Helper { constructor(config) { super(config); this.testcafe = undefined; // testcafe instance this.t = undefined; // testcafe test controller this.dummyTestcafeFile; // generated testcafe test file // context is used for within() function. // It requires to have _withinBeginand _withinEnd implemented. // Inside _withinBegin we should define that all next element calls should be started from a specific element (this.context). this.context = undefined; // TODO Not sure if this applies to testcafe this.options = Object.assign({ url: 'http://localhost', show: false, browser: 'chrome', restart: true, // TODO Test if restart false works manualStart: false, keepBrowserState: false, waitForTimeout: 5000, getPageTimeout: 30000, fullPageScreenshots: false, disableScreenshots: false, windowSize: undefined, }, config); } // TOOD Do a requirements check static _checkRequirements() { try { requireg('testcafe'); } catch (e) { return ['testcafe@^1.1.0']; } } static _config() { return [ { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' }, { name: 'browser', message: 'Browser to be used', default: 'chrome' }, { name: 'show', message: 'Show browser window', default: true, type: 'confirm', }, ]; } async _configureAndStartBrowser() { this.dummyTestcafeFile = createTestFile(global.output_dir); // create a dummy test file to get hold of the test controller this.iteration += 2; // Use different ports for each test run // @ts-ignore this.testcafe = await createTestCafe('', null, null); this.debugSection('_before', 'Starting testcafe browser...'); this.isRunning = true; // TODO Do we have to cleanup the runner? const runner = this.testcafe.createRunner(); this.options.browser !== 'remote' ? this._startBrowser(runner) : this._startRemoteBrowser(runner); this.t = await testControllerHolder.get(); assert(this.t, 'Expected to have the testcafe test controller'); if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0) { const dimensions = this.options.windowSize.split('x'); await this.t.resizeWindow(parseInt(dimensions[0], 10), parseInt(dimensions[1], 10)); } } async _startBrowser(runner) { runner .src(this.dummyTestcafeFile) .screenshots(global.output_dir, !this.options.disableScreenshots) // .video(global.output_dir) // TODO Make this configurable .browsers(this.options.show ? this.options.browser : `${this.options.browser}:headless`) .reporter('minimal') .run({ skipJsErrors: true, skipUncaughtErrors: true, quarantineMode: false, // debugMode: true, // debugOnFail: true, // developmentMode: true, pageLoadTimeout: this.options.getPageTimeout, selectorTimeout: this.options.waitForTimeout, assertionTimeout: this.options.waitForTimeout, takeScreenshotsOnFails: true, }) .catch((err) => { this.debugSection('_before', `Error ${err.toString()}`); this.isRunning = false; this.testcafe.close(); }); } async _startRemoteBrowser(runner) { const remoteConnection = await this.testcafe.createBrowserConnection(); console.log('Connect your device to the following URL or scan QR Code: ', remoteConnection.url); qrcode.generate(remoteConnection.url); remoteConnection.once('ready', () => { runner .src(this.dummyTestcafeFile) .browsers(remoteConnection) .reporter('minimal') .run({ selectorTimeout: this.options.waitForTimeout, skipJsErrors: true, skipUncaughtErrors: true, }) .catch((err) => { this.debugSection('_before', `Error ${err.toString()}`); this.isRunning = false; this.testcafe.close(); }); }); } async _stopBrowser() { this.debugSection('_after', 'Stopping testcafe browser...'); testControllerHolder.free(); if (this.testcafe) { this.testcafe.close(); } fs.unlinkSync(this.dummyTestcafeFile); // remove the dummy test this.t = undefined; this.isRunning = false; } _init() { } async _beforeSuite() { if (!this.options.restart && !this.options.manualStart && !this.isRunning) { this.debugSection('Session', 'Starting singleton browser session'); return this._configureAndStartBrowser(); } } async _before() { if (this.options.restart && !this.options.manualStart) return this._configureAndStartBrowser(); if (!this.isRunning && !this.options.manualStart) return this._configureAndStartBrowser(); this.context = null; } async _after() { if (!this.isRunning) return; if (this.options.restart) { this.isRunning = false; return this._stopBrowser(); } if (this.options.keepBrowserState) return; if (!this.options.keepCookies) { this.debugSection('Session', 'cleaning cookies and localStorage'); await this.clearCookie(); // TODO IMHO that should only happen when await this.executeScript(() => localStorage.clear()) .catch((err) => { if (!(err.message.indexOf("Storage is disabled inside 'data:' URLs.") > -1)) throw err; }); } } _afterSuite() { } async _finishTest() { if (!this.options.restart && this.isRunning) return this._stopBrowser(); } /** * Get elements by different locator types, including strict locator * Should be used in custom helpers: * * ```js * const elements = await this.helpers['TestCafe']._locate('.item'); * ``` * */ async _locate(locator) { return findElements.call(this, this.context, locator).catch(mapError); } async _withinBegin(locator) { const els = await this._locate(locator); assertElementExists(els, locator); this.context = await els.nth(0); } async _withinEnd() { this.context = null; } /** * {{> amOnPage }} */ async amOnPage(url) { if (!(/^\w+\:\/\//.test(url))) { url = this.options.url + url; } return this.t.navigateTo(url) .catch(mapError); } /** * {{> resizeWindow }} */ async resizeWindow(width, height) { if (width === 'maximize') { return this.t.maximizeWindow().catch(mapError); } return this.t.resizeWindow(width, height).catch(mapError); } /** * {{> click }} * */ async click(locator, context = null) { return proceedClick.call(this, locator, context); } /** * {{> refreshPage }} */ async refreshPage() { // eslint-disable-next-line no-restricted-globals return this.t.eval(() => location.reload(true), { boundTestRun: this.t }).catch(mapError); } /** * {{> waitForVisible }} * */ async waitForVisible(locator, sec) { const timeout = sec ? sec * 1000 : undefined; return (await findElements.call(this, this.context, locator)) .with({ visibilityCheck: true, timeout })() .catch(mapError); } /** * {{> fillField }} */ async fillField(field, value) { const els = await findFields.call(this, field); assertElementExists(els, field, 'Field'); const el = await els.nth(0); return this.t .typeText(el, value.toString(), { replace: true }) .catch(mapError); } /** * {{> clearField }} */ async clearField(field) { const els = await findFields.call(this, field); assertElementExists(els, field, 'Field'); const el = await els.nth(0); const res = await this.t .selectText(el) .pressKey('delete'); return res; } /** * {{> appendField }} * */ async appendField(field, value) { const els = await findFields.call(this, field); assertElementExists(els, field, 'Field'); const el = await els.nth(0); return this.t .typeText(el, value, { replace: false }) .catch(mapError); } /** * {{> attachFile }} * */ async attachFile(field, pathToFile) { const els = await findFields.call(this, field); assertElementExists(els, field, 'Field'); const el = await els.nth(0); const file = path.join(global.codecept_dir, pathToFile); return this.t .setFilesToUpload(el, [file]) .catch(mapError); } /** * {{> pressKey }} * * {{ keys }} */ async pressKey(key) { assert(key, 'Expected a sequence of keys or key combinations'); return this.t .pressKey(key.toLowerCase()) // testcafe keys are lowercase .catch(mapError); } /** * {{> moveCursorTo }} * */ async moveCursorTo(locator, offsetX = 0, offsetY = 0) { const els = (await findElements.call(this, this.context, locator)).filterVisible(); await assertElementExists(els); return this.t .hover(els.nth(0), { offsetX, offsetY }) .catch(mapError); } /** * {{> doubleClick }} * */ async doubleClick(locator, context = null) { let matcher; if (context) { const els = await this._locate(context); await assertElementExists(els, context); matcher = await els.nth(0); } const els = (await findClickable.call(this, matcher, locator)).filterVisible(); return this.t .doubleClick(els.nth(0)) .catch(mapError); } /** * {{> rightClick }} * */ async rightClick(locator, context = null) { let matcher; if (context) { const els = await this._locate(context); await assertElementExists(els, context); matcher = await els.nth(0); } const els = (await findClickable.call(this, matcher, locator)).filterVisible(); assertElementExists(els); return this.t .rightClick(els.nth(0)) .catch(mapError); } /** * {{> checkOption }} */ async checkOption(field, context = null) { const el = await findCheckable.call(this, field, context); return this.t .click(el) .catch(mapError); } /** * {{> uncheckOption }} */ async uncheckOption(field, context = null) { const el = await findCheckable.call(this, field, context); if (await el.checked) { return this.t .click(el) .catch(mapError); } } /** * {{> seeCheckboxIsChecked }} */ async seeCheckboxIsChecked(field) { return proceedIsChecked.call(this, 'assert', field); } /** * {{> dontSeeCheckboxIsChecked }} */ async dontSeeCheckboxIsChecked(field) { return proceedIsChecked.call(this, 'negate', field); } /** * {{> selectOption }} */ async selectOption(select, option) { const els = await findFields.call(this, select); assertElementExists(els, select, 'Selectable field'); const el = await els.filterVisible().nth(0); if ((await el.tagName).toLowerCase() !== 'select') { throw new Error('Element is not <select>'); } if (!Array.isArray(option)) option = [option]; // TODO As far as I understand the testcafe docs this should do a multi-select // but it does not work const clickOpts = { ctrl: option.length > 1 }; await this.t.click(el, clickOpts).catch(mapError); for (const key of option) { const opt = key; let optEl; try { optEl = el.child('option').withText(opt); if (await optEl.count) { await this.t.click(optEl, clickOpts).catch(mapError); continue; } // eslint-disable-next-line no-empty } catch (err) { } try { const sel = `[value="${opt}"]`; optEl = el.find(sel); if (await optEl.count) { await this.t.click(optEl, clickOpts).catch(mapError); } // eslint-disable-next-line no-empty } catch (err) { } } } /** * {{> seeInCurrentUrl }} */ async seeInCurrentUrl(url) { stringIncludes('url').assert(url, await getPageUrl(this.t)().catch(mapError)); } /** * {{> dontSeeInCurrentUrl }} */ async dontSeeInCurrentUrl(url) { stringIncludes('url').negate(url, await getPageUrl(this.t)().catch(mapError)); } /** * {{> seeCurrentUrlEquals }} */ async seeCurrentUrlEquals(url) { urlEquals(this.options.url).assert(url, await getPageUrl(this.t)().catch(mapError)); } /** * {{> dontSeeCurrentUrlEquals }} */ async dontSeeCurrentUrlEquals(url) { urlEquals(this.options.url).negate(url, await getPageUrl(this.t)().catch(mapError)); } /** * {{> see }} * */ async see(text, context = null) { let els; if (context) { els = (await findElements.call(this, this.context, context)).withText(text); } else { els = (await findElements.call(this, this.context, '*')).withText(text); } return this.t .expect(els.filterVisible().count).gt(0, `No element with text "${text}" found`) .catch(mapError); } /** * {{> dontSee }} * */ async dontSee(text, context = null) { let els; if (context) { els = (await findElements.call(this, this.context, context)).withText(text); } else { els = (await findElements.call(this, this.context, 'body')).withText(text); } return this.t .expect(els.filterVisible().count).eql(0, `Element with text "${text}" can still be seen`) .catch(mapError); } /** * {{> seeElement }} */ async seeElement(locator) { const exists = (await findElements.call(this, this.context, locator)).filterVisible().exists; return this.t .expect(exists).ok(`No element "${locator}" found`) .catch(mapError); } /** * {{> dontSeeElement }} */ async dontSeeElement(locator) { const exists = (await findElements.call(this, this.context, locator)).filterVisible().exists; return this.t .expect(exists).notOk(`Element "${locator}" is still visible`) .catch(mapError); } /** * {{> seeElementInDOM }} */ async seeElementInDOM(locator) { const exists = (await findElements.call(this, this.context, locator)).exists; return this.t .expect(exists).ok(`No element "${locator}" found in DOM`) .catch(mapError); } /** * {{> dontSeeElementInDOM }} */ async dontSeeElementInDOM(locator) { const exists = (await findElements.call(this, this.context, locator)).exists; return this.t .expect(exists).notOk(`Element "${locator}" is still in DOM`) .catch(mapError); } /** * {{> seeNumberOfVisibleElements }} * */ async seeNumberOfVisibleElements(locator, num) { const count = (await findElements.call(this, this.context, locator)).filterVisible().count; return this.t .expect(count).eql(num) .catch(mapError); } /** * {{> grabNumberOfVisibleElements }} */ async grabNumberOfVisibleElements(locator) { const count = (await findElements.call(this, this.context, locator)).filterVisible().count; return count; } /** * {{> seeInField }} */ async seeInField(field, value) { // const expectedValue = findElements.call(this, this.context, field).value; const els = await findFields.call(this, field); assertElementExists(els, field, 'Field'); const el = await els.nth(0); return this.t .expect(await el.value).eql(value) .catch(mapError); } /** * {{> dontSeeInField }} */ async dontSeeInField(field, value) { // const expectedValue = findElements.call(this, this.context, field).value; const els = await findFields.call(this, field); assertElementExists(els, field, 'Field'); const el = await els.nth(0); return this.t .expect(el.value).notEql(value) .catch(mapError); } /** * Checks that text is equal to provided one. * * ```js * I.seeTextEquals('text', 'h1'); * ``` */ async seeTextEquals(text, context = null) { const expectedText = findElements.call(this, context, undefined).textContent; return this.t .expect(expectedText).eql(text) .catch(mapError); } /** * {{> seeInSource }} */ async seeInSource(text) { const source = await getHtmlSource(this.t)(); stringIncludes('HTML source of a page').assert(text, source); } /** * {{> dontSeeInSource }} */ async dontSeeInSource(text) { const source = await getHtmlSource(this.t)(); stringIncludes('HTML source of a page').negate(text, source); } /** * {{> saveScreenshot }} */ async saveScreenshot(fileName, fullPage) { // TODO Implement full page screenshots const fullPageOption = fullPage || this.options.fullPageScreenshots; const outputFile = path.join(global.output_dir, fileName); this.debug(`Screenshot is saving to ${outputFile}`); // TODO testcafe automatically creates thumbnail images (which cant be turned off) return this.t.takeScreenshot(fileName); } /** * {{> wait }} */ async wait(sec) { return new Promise(((done) => { setTimeout(done, sec * 1000); })); } /** * {{> executeScript }} * * If a function returns a Promise It will wait for it resolution. */ async executeScript(fn, ...args) { const browserFn = createClientFunction(fn, args).with({ boundTestRun: this.t }); return browserFn(); } /** * {{> grabTextFrom }} */ async grabTextFrom(locator) { const sel = await findElements.call(this, this.context, locator); assertElementExists(sel); const num = await sel.count; if (num) { const res = []; for (let i = 0; i < num; i++) { res.push(await sel.nth(i).innerText); } return res; } return sel.nth(0).innerText; } /** * {{> grabAttributeFrom }} */ async grabAttributeFrom(locator, attr) { const sel = await findElements.call(this, this.context, locator); assertElementExists(sel); return (await sel.nth(0)).value; } /** * {{> grabValueFrom }} */ async grabValueFrom(locator) { return this.grabAttributeFrom(locator, 'value'); } /** * {{> grabSource }} */ async grabSource() { return ClientFunction(() => document.documentElement.innerHTML).with({ boundTestRun: this.t })(); } /** * Get JS log from browser. * * ```js * let logs = await I.grabBrowserLogs(); * console.log(JSON.stringify(logs)) * ``` */ async grabBrowserLogs() { // TODO Must map? return this.t.getBrowserConsoleMessages(); } /** * {{> grabCurrentUrl }} */ async grabCurrentUrl() { return ClientFunction(() => document.location.href).with({ boundTestRun: this.t })(); } /** * {{> grabPageScrollPosition }} */ async grabPageScrollPosition() { return ClientFunction(() => ({ x: window.pageXOffset, y: window.pageYOffset })).with({ boundTestRun: this.t })(); } /** * {{> scrollPageToTop }} */ scrollPageToTop() { return ClientFunction(() => window.scrollTo(0, 0)).with({ boundTestRun: this.t })().catch(mapError); } /** * {{> scrollPageToBottom }} */ scrollPageToBottom() { return ClientFunction(() => { const body = document.body; const html = document.documentElement; window.scrollTo(0, Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight, )); }).with({ boundTestRun: this.t })().catch(mapError); } /** * {{> scrollTo }} */ async scrollTo(locator, offsetX = 0, offsetY = 0) { if (typeof locator === 'number' && typeof offsetX === 'number') { offsetY = offsetX; offsetX = locator; locator = null; } const scrollBy = ClientFunction((offset) => { if (window && window.scrollBy && offset) { window.scrollBy(offset.x, offset.y); } }).with({ boundTestRun: this.t }); if (locator) { const els = await this._locate(locator); assertElementExists(els, locator, 'Element'); const el = await els.nth(0); const x = (await el.offsetLeft) + offsetX; const y = (await el.offsetTop) + offsetY; return scrollBy({ x, y }).catch(mapError); } const x = offsetX; const y = offsetY; return scrollBy({ x, y }).catch(mapError); } /** * {{> switchTo }} */ async switchTo(locator) { if (Number.isInteger(locator)) { throw new Error('Not supported switching to iframe by number'); } if (!locator) { return this.t.switchToMainWindow(); } const el = await findElements.call(this, this.context, locator); return this.t.switchToIframe(el); } // TODO Add url assertions /** * {{> setCookie }} */ async setCookie(cookie) { if (Array.isArray(cookie)) { throw new Error('cookie array is not supported'); } cookie.path = cookie.path || '/'; // cookie.expires = cookie.expires || (new Date()).toUTCString(); const setCookie = ClientFunction(() => { document.cookie = `${cookie.name}=${cookie.value};path=${cookie.path};expires=${cookie.expires};`; }, { dependencies: { cookie } }).with({ boundTestRun: this.t }); return setCookie(); } /** * {{> seeCookie }} * */ async seeCookie(name) { const cookie = await this.grabCookie(name); empty(`cookie ${name} to be set`).negate(cookie); } /** * {{> dontSeeCookie }} */ async dontSeeCookie(name) { const cookie = await this.grabCookie(name); empty(`cookie ${name} not to be set`).assert(cookie); } /** * {{> grabCookie }} * * Returns cookie in JSON format. If name not passed returns all cookies for this domain. */ async grabCookie(name) { if (!name) { const getCookie = ClientFunction(() => { return document.cookie.split(';').map(c => c.split('=')); }).with({ boundTestRun: this.t }); const cookies = await getCookie(); return cookies.map(cookie => ({ name: cookie[0].trim(), value: cookie[1] })); } const getCookie = ClientFunction(() => { // eslint-disable-next-line prefer-template const v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)'); return v ? v[2] : null; }, { dependencies: { name } }).with({ boundTestRun: this.t }); const value = await getCookie(); if (value) return { name, value }; } /** * {{> clearCookie }} */ async clearCookie(cookieName) { const clearCookies = ClientFunction(() => { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i]; const eqPos = cookie.indexOf('='); const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; if (cookieName === undefined || name === cookieName) { document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`; } } }, { dependencies: { cookieName } }).with({ boundTestRun: this.t }); return clearCookies(); } /** * {{> waitInUrl }} */ async waitInUrl(urlPart, sec = null) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; const clientFn = createClientFunction((urlPart) => { const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href))); return currUrl.indexOf(urlPart) > -1; }, [urlPart]).with({ boundTestRun: this.t }); return waitForFunction(clientFn, waitTimeout).catch(async (err) => { const currUrl = await this.grabCurrentUrl(); throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`); }); } /** * {{> waitUrlEquals }} */ async waitUrlEquals(urlPart, sec = null) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; const baseUrl = this.options.url; if (urlPart.indexOf('http') < 0) { urlPart = baseUrl + urlPart; } const clientFn = createClientFunction((urlPart) => { const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href))); return currUrl === urlPart; }, [urlPart]).with({ boundTestRun: this.t }); return waitForFunction(clientFn, waitTimeout).catch(async (err) => { const currUrl = await this.grabCurrentUrl(); throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`); }); } /** * {{> waitForFunction }} */ async waitForFunction(fn, argsOrSec = null, sec = null) { let args = []; if (argsOrSec) { if (Array.isArray(argsOrSec)) { args = argsOrSec; } else if (typeof argsOrSec === 'number') { sec = argsOrSec; } } const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; const clientFn = createClientFunction((urlPart) => { const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href))); return currUrl.indexOf(urlPart) > -1; }, args); return waitForFunction(clientFn, waitTimeout); } /** * {{> waitNumberOfVisibleElements }} */ async waitNumberOfVisibleElements(locator, num, sec) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; return this.t .expect(createSelector(locator).with({ boundTestRun: this.t }).filterVisible().count) .eql(num, `The number of elements (${locator}) is not ${num} after ${sec} sec`, { timeout: waitTimeout }) .catch(mapError); } /** * {{> waitForElement }} */ async waitForElement(locator, sec) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; return this.t .expect(createSelector(locator).with({ boundTestRun: this.t }).exists) .ok({ timeout: waitTimeout }); } /** * {{> waitToHide }} */ async waitToHide(locator, sec) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; return this.t .expect(createSelector(locator).filterHidden().with({ boundTestRun: this.t }).exists) .notOk({ timeout: waitTimeout }); } /** * {{> waitForInvisible }} */ async waitForInvisible(locator, sec) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; return this.t .expect(createSelector(locator).filterVisible().with({ boundTestRun: this.t }).exists) .ok({ timeout: waitTimeout }); } /** * {{> waitForText }} * */ async waitForText(text, sec = null, context = null) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout; let els; if (context) { els = (await findElements.call(this, this.context, context)); await this.t .expect(els.exists) .ok(`Context element ${context} not found`, { timeout: waitTimeout }); } else { els = (await findElements.call(this, this.context, '*')); } return this.t .expect(els.withText(text).filterVisible().exists) .ok(`No element with text "${text}" found in ${context || 'body'}`, { timeout: waitTimeout }) .catch(mapError); } } async function waitForFunction(browserFn, waitTimeout) { const pause = () => new Promise((done => setTimeout(done, 50))); const start = Date.now(); // eslint-disable-next-line no-constant-condition while (true) { let result; try { result = await browserFn(); // eslint-disable-next-line no-empty } catch (err) { throw new Error(`Error running function ${err.toString()}`); } if (result) return result; const duration = (Date.now() - start); if (duration > waitTimeout) { throw new Error('waitForFunction timed out'); } await pause(); // make polling } } const createSelector = (locator) => { locator = new Locator(locator, 'css'); if (locator.isXPath()) return elementByXPath(locator.value); return Selector(locator.simplify()); }; const elementByXPath = (xpath) => { assert(xpath, 'xpath is required'); return Selector(() => { const iterator = document.evaluate(xpath, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); const items = []; let item = iterator.iterateNext(); while (item) { items.push(item); item = iterator.iterateNext(); } return items; }, { dependencies: { xpath } }); }; const assertElementExists = async (res, locator, prefix, suffix) => { if (!res || !(await res.count) || !(await res.nth(0).tagName)) { throw new ElementNotFound(locator, prefix, suffix); } }; async function findElements(matcher, locator) { if (locator && locator.react) throw new Error('react locators are not yet supported'); locator = new Locator(locator, 'css'); if (!locator.isXPath()) { return matcher ? matcher.find(locator.simplify()) : Selector(locator.simplify()).with({ timeout: 0, boundTestRun: this.t }); } if (!matcher) return elementByXPath(locator.value).with({ timeout: 0, boundTestRun: this.t }); return matcher.find((node, idx, originNode) => { const found = document.evaluate(xpath, originNode, null, 5, null); let current = null; while (current = found.iterateNext()) { if (current === node) return true; } return false; }, { xpath: locator.value }); } async function proceedClick(locator, context = null) { let matcher; if (context) { const els = await this._locate(context); await assertElementExists(els, context); matcher = await els.nth(0); } const els = await findClickable.call(this, matcher, locator); if (context) { await assertElementExists(els, locator, 'Clickable element', `was not found inside element ${new Locator(context).toString()}`); } else { await assertElementExists(els, locator, 'Clickable element'); } const firstElement = await els.nth(0); return this.t .click(firstElement) .catch(mapError); } async function findClickable(matcher, locator) { if (locator && locator.react) throw new Error('react locators are not yet supported'); locator = new Locator(locator); if (!locator.isFuzzy()) return (await findElements.call(this, matcher, locator)).filterVisible(); let els; // try to use native TestCafe locator els = matcher ? matcher.find('a,button') : createSelector('a,button'); els = await els.withExactText(locator.value).with({ timeout: 0, boundTestRun: this.t }); if (await els.count) return els; const literal = xpathLocator.literal(locator.value); els = (await findElements.call(this, matcher, Locator.clickable.narrow(literal))).filterVisible(); if (await els.count) return els; els = (await findElements.call(this, matcher, Locator.clickable.wide(literal))).filterVisible(); if (await els.count) return els; els = (await findElements.call(this, matcher, Locator.clickable.self(literal))).filterVisible(); if (await els.count) return els; return findElements.call(this, matcher, locator.value); // by css or xpath } async function proceedIsChecked(assertType, option) { const els = await findCheckable.call(this, option); assertElementExists(els, option, 'Checkable'); const selected = await els.checked; return truth(`checkable ${option}`, 'to be checked')[assertType](selected); } async function findCheckable(locator, context) { assert(locator, 'locator is required'); assert(this.t, 'this.t is required'); let contextEl = await this.context; if (typeof context === 'string') { contextEl = (await findElements.call(this, contextEl, (new Locator(context, 'css')).simplify())).filterVisible(); contextEl = await contextEl.nth(0); } const matchedLocator = new Locator(locator); if (!matchedLocator.isFuzzy()) { return (await findElements.call(this, contextEl, matchedLocator.simplify())).filterVisible(); } const literal = xpathLocator.literal(locator); let els = (await findElements.call(this, contextEl, Locator.checkable.byText(literal))).filterVisible(); if (await els.count) { return els; } els = (await findElements.call(this, contextEl, Locator.checkable.byName(literal))).filterVisible(); if (await els.count) { return els; } return (await findElements.call(this, contextEl, locator)).filterVisible(); } async function findFields(locator) { const matchedLocator = new Locator(locator); if (!matchedLocator.isFuzzy()) { return this._locate(matchedLocator); } const literal = xpathLocator.literal(locator); let els = await this._locate({ xpath: Locator.field.labelEquals(literal) }); if (await els.count) { return els; } els = await this._locate({ xpath: Locator.field.labelContains(literal) }); if (await els.count) { return els; } els = await this._locate({ xpath: Locator.field.byName(literal) }); if (await els.count) { return els; } return this._locate({ css: locator }); } module.exports = TestCafe;