UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

1,392 lines (1,183 loc) 38.6 kB
// @ts-nocheck const fs = require('fs') const assert = require('assert') const path = require('path') const qrcode = require('qrcode-terminal') const createTestCafe = require('testcafe') const { Selector, ClientFunction } = require('testcafe') const Helper = require('@codeceptjs/helper') 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, normalizeSpacesInString } = require('../utils') const Locator = require('../locator') /** * 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.conf.ts 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 = { 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 { require('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() } /** * Use [TestCafe](https://devexpress.github.io/testcafe/documentation/test-api/) API inside a test. * * First argument is a description of an action. * Second argument is async function that gets this helper as parameter. * * { [`t`](https://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#test-controller)) } object from TestCafe API is available. * * ```js * I.useTestCafeTo('handle browser dialog', async ({ t }) { * await t.setNativeDialogHandler(() => true); * }); * ``` * * * * @param {string} description used to show in logs. * @param {function} fn async functuion that executed with TestCafe helper as argument */ useTestCafeTo(description, fn) { return this._useTo(...arguments) } /** * 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) } /** * {{> focus }} * */ async focus(locator) { const els = await this._locate(locator) await assertElementExists(els, locator, 'Element to focus') const element = await els.nth(0) const focusElement = ClientFunction(() => element().focus(), { boundTestRun: this.t, dependencies: { element }, }) return focusElement() } /** * {{> blur }} * */ async blur(locator) { const els = await this._locate(locator) await assertElementExists(els, locator, 'Element to blur') const element = await els.nth(0) const blurElement = ClientFunction(() => element().blur(), { boundTestRun: this.t, dependencies: { element } }) return blurElement() } /** * {{> click }} * */ async click(locator, context = null) { return proceedClick.call(this, locator, context) } /** * {{> refreshPage }} */ async refreshPage() { 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.toString(), { 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, locator) 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, locator) 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).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).catch(mapError) continue } } catch (err) {} try { const sel = `[value="${opt}"]` optEl = el.find(sel) if (await optEl.count) { await this.t.click(optEl).catch(mapError) } } 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(normalizeSpacesInString(text)) } else { els = (await findElements.call(this, this.context, '*')).withText(normalizeSpacesInString(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 "${new Locator(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 "${new Locator(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 "${new Locator(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 "${new Locator(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 _value = typeof value === 'boolean' ? value : value.toString() // 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 _value = typeof value === 'boolean' ? value : value.toString() // 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) } /** * {{> saveElementScreenshot }} * */ async saveElementScreenshot(locator, fileName) { const outputFile = path.join(global.output_dir, fileName) const sel = await findElements.call(this, this.context, locator) assertElementExists(sel, locator) const firstElement = await sel.filterVisible().nth(0) this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`) return this.t.takeElementScreenshot(firstElement, fileName) } /** * {{> saveScreenshot }} */ // TODO Implement full page screenshots async saveScreenshot(fileName) { 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 its resolution. */ async executeScript(fn, ...args) { const browserFn = createClientFunction(fn, args).with({ boundTestRun: this.t }) return browserFn() } /** * {{> grabTextFromAll }} */ async grabTextFromAll(locator) { const sel = await findElements.call(this, this.context, locator) const length = await sel.count const texts = [] for (let i = 0; i < length; i++) { texts.push(await sel.nth(i).innerText) } return texts } /** * {{> grabTextFrom }} */ async grabTextFrom(locator) { const sel = await findElements.call(this, this.context, locator) assertElementExists(sel, locator) const texts = await this.grabTextFromAll(locator) if (texts.length > 1) { this.debugSection('GrabText', `Using first element out of ${texts.length}`) } return texts[0] } /** * {{> grabAttributeFrom }} */ async grabAttributeFromAll(locator, attr) { const sel = await findElements.call(this, this.context, locator) const length = await sel.count const attrs = [] for (let i = 0; i < length; i++) { attrs.push(await (await sel.nth(i)).getAttribute(attr)) } return attrs } /** * {{> grabAttributeFrom }} */ async grabAttributeFrom(locator, attr) { const sel = await findElements.call(this, this.context, locator) assertElementExists(sel, locator) const attrs = await this.grabAttributeFromAll(locator, attr) if (attrs.length > 1) { this.debugSection('GrabAttribute', `Using first element out of ${attrs.length}`) } return attrs[0] } /** * {{> grabValueFromAll }} */ async grabValueFromAll(locator) { const sel = await findElements.call(this, this.context, locator) const length = await sel.count const values = [] for (let i = 0; i < length; i++) { values.push(await (await sel.nth(i)).value) } return values } /** * {{> grabValueFrom }} */ async grabValueFrom(locator) { const sel = await findElements.call(this, this.context, locator) assertElementExists(sel, locator) const values = await this.grabValueFromAll(locator) if (values.length > 1) { this.debugSection('GrabValue', `Using first element out of ${values.length}`) } return values[0] } /** * {{> 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( () => { 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 () => { 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 () => { 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(fn, args).with({ boundTestRun: this.t }) 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 (${new Locator(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() while (true) { let result try { result = await browserFn() } 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.filterVisible().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