UNPKG

temp-taiko

Version:

An easy to use wrapper over Chrome Remote Interface.

1,442 lines (1,389 loc) 116 kB
const cri = require('chrome-remote-interface'); const childProcess = require('child_process'); const { helper, wait, isString, isStrictObject, isFunction, waitUntil, xpath, waitForNavigation, timeouts, assertType, descEvent, isSelector, isElement, } = require('./helper'); const { createJsDialogEventName } = require('./util'); const inputHandler = require('./handlers/inputHandler'); const domHandler = require('./handlers/domHandler'); const networkHandler = require('./handlers/networkHandler'); const pageHandler = require('./handlers/pageHandler'); const targetHandler = require('./handlers/targetHandler'); const runtimeHandler = require('./handlers/runtimeHandler'); const browserHandler = require('./handlers/browserHandler'); const emulationHandler = require('./handlers/emulationHandler'); const logEvent = require('./logger'); const { match, $$, $$xpath, findElements, findFirstElement, isElementVisible, } = require('./elementSearch'); const { handleRelativeSearch, RelativeSearchElement, } = require('./proximityElementSearch'); const { prepareParameters, getElementGetter, desc, generateElementWrapper, } = require('./elementWrapperUtils'); const { setConfig, defaultConfig, setNavigationOptions, setClickOptions, setBrowserOptions, } = require('./config'); const fs = require('fs-extra'); const path = require('path'); const eventHandler = require('./eventBus'); let chromeProcess, temporaryUserDataDir, page, network, runtime, input, _client, dom, overlay, currentPort, currentHost, security, device, eventHandlerProxy, clientProxy, localProtocol = false; module.exports.emitter = descEvent; const connect_to_cri = async target => { if (process.env.LOCAL_PROTOCOL) { localProtocol = true; } if (_client) { await network.setRequestInterception({ patterns: [] }); _client.removeAllListeners(); } return new Promise(async function connect(resolve) { try { if (!target) { const browserTargets = await cri.List({ host: currentHost, port: currentPort, }); if (!browserTargets.length) throw new Error('No targets created yet!'); target = browserTargets.filter( target => target.type === 'page', )[0]; if (!target.length) throw new Error('No targets created yet!'); } await cri({ target, local: localProtocol }, async c => { _client = c; clientProxy = getEventProxy(_client); page = c.Page; network = c.Network; runtime = c.Runtime; input = c.Input; dom = c.DOM; overlay = c.Overlay; security = c.Security; await Promise.all([ runtime.enable(), network.enable(), page.enable(), dom.enable(), overlay.enable(), security.enable(), ]); if (defaultConfig.ignoreSSLErrors) security.setIgnoreCertificateErrors({ ignore: true }); _client.on('disconnect', reconnect); device = process.env.TAIKO_EMULATE_DEVICE; if (device) emulateDevice(device); // Should be emitted after enabling all domains. All handlers can then perform any action on domains properly. eventHandler.emit('createdSession', _client); logEvent('Session Created'); resolve(); }); } catch (e) { const timeoutId = setTimeout(() => { connect(resolve); }, 100); timeouts.push(timeoutId); } }); }; async function reconnect() { try { logEvent('Reconnecting'); eventHandler.emit('reconnecting'); _client.removeAllListeners(); _client = null; const browserTargets = await cri.List({ host: currentHost, port: currentPort, }); const pages = browserTargets.filter(target => { return target.type === 'page'; }); await connect_to_cri(pages[0]); await dom.getDocument(); logEvent('Reconnected'); eventHandler.emit('reconnected'); } catch (e) { console.log(e); } } eventHandler.addListener('targetCreated', async newTarget => { const browserTargets = await cri.List({ host: currentHost, port: currentPort, }); const pages = browserTargets.filter(target => { return target.targetId === newTarget.targetId; }); await connect_to_cri(pages[0]).then(() => { logEvent(`Target Navigated: Target id: ${newTarget.targetId}`); eventHandler.emit('targetNavigated'); }); }); /** * Launches a browser with a tab. The browser will be closed when the parent node.js process is closed.<br> * Note : `openBrowser` launches the browser in headless mode by default, but when `openBrowser` is called from {@link repl} it launches the browser in headful mode. * @example * await openBrowser({headless: false}) * await openBrowser() * await openBrowser({args:['--window-size=1440,900']}) * await openBrowser({args: [ * '--disable-gpu', * '--disable-dev-shm-usage', * '--disable-setuid-sandbox', * '--no-first-run', * '--no-sandbox', * '--no-zygote']}) # These are recommended args that has to be passed when running in docker * * @param {Object} [options={headless:true}] eg. {headless: true|false, args:['--window-size=1440,900']} * @param {boolean} [options.headless=true] - Option to open browser in headless/headful mode. * @param {Array<string>} [options.args=[]] - Args to open chromium. Refer https://peter.sh/experiments/chromium-command-line-switches/ for values. * @param {string} [options.host='127.0.0.1'] - Remote host to connect to. * @param {number} [options.port=0] - Remote debugging port, if not given connects to any open port. * @param {boolean} [options.ignoreCertificateErrors=false] - Option to ignore certificate errors. * @param {boolean} [options.observe=false] - Option to run each command after a delay. Useful to observe what is happening in the browser. * @param {number} [options.observeTime=3000] - Option to modify delay time for observe mode. Accepts value in milliseconds. * @param {boolean} [options.dumpio=false] - Option to dump IO from browser. * * @returns {Promise} */ module.exports.openBrowser = async (options = { headless: true }) => { if (!isStrictObject(options)) { throw new TypeError( 'Invalid option parameter. Refer https://taiko.gauge.org/#parameters for the correct format.', ); } if (chromeProcess && !chromeProcess.killed) { throw new Error( 'OpenBrowser cannot be called again as there is a chromium instance open.', ); } if (options.host && options.port) { currentHost = options.host; currentPort = options.port; } else { const BrowserFetcher = require('./browserFetcher'); const browserFetcher = new BrowserFetcher(); const chromeExecutable = browserFetcher.getExecutablePath(); options = setBrowserOptions(options); let args = [ `--remote-debugging-port=${options.port}`, '--disable-features=site-per-process,TranslateUI', '--enable-features=NetworkService,NetworkServiceInProcess', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--disable-background-timer-throttling', '--disable-background-networking', '--disable-breakpad', '--disable-default-apps', '--disable-hang-monitor', '--disable-prompt-on-repost', '--disable-sync', '--force-color-profile=srgb', '--safebrowsing-disable-auto-update', '--password-store=basic', '--use-mock-keychain', '--enable-automation', '--disable-notifications', 'about:blank', ]; if (options.args) args = args.concat(options.args); if (!args.some(arg => arg.startsWith('--user-data-dir'))) { const os = require('os'); const CHROME_PROFILE_PATH = path.join( os.tmpdir(), 'taiko_dev_profile-', ); const mkdtempAsync = helper.promisify(fs.mkdtemp); temporaryUserDataDir = await mkdtempAsync(CHROME_PROFILE_PATH); args.push(`--user-data-dir=${temporaryUserDataDir}`); } if (options.headless) args = args.concat(['--headless', '--window-size=1440,900']); chromeProcess = await childProcess.spawn(chromeExecutable, args); if (options.dumpio) { chromeProcess.stderr.pipe(process.stderr); chromeProcess.stdout.pipe(process.stdout); } const endpoint = await browserFetcher.waitForWSEndpoint( chromeProcess, defaultConfig.navigationTimeout, ); currentHost = endpoint.host; currentPort = endpoint.port; } await connect_to_cri(); var description = device ? `Browser opened with viewport ${device}` : 'Browser opened'; descEvent.emit('success', description); if (process.env.TAIKO_EMULATE_NETWORK) await module.exports.emulateNetwork( process.env.TAIKO_EMULATE_NETWORK, ); }; /** * Closes the browser and along with all of its tabs. * * @example * await closeBrowser() * * @returns {Promise} */ module.exports.closeBrowser = async () => { validate(); await _closeBrowser(); descEvent.emit('success', 'Browser closed'); }; const _closeBrowser = async () => { timeouts.forEach(timeout => { if (timeout) clearTimeout(timeout); }); networkHandler.resetInterceptors(); if (_client) { await reconnect(); await _client.removeAllListeners(); await page.close(); await _client.close(); _client = null; } if (chromeProcess) { chromeProcess.kill('SIGTERM'); const waitForChromeToClose = new Promise(fulfill => { chromeProcess.once('exit', () => { fulfill(); }); }); await waitForChromeToClose; if (temporaryUserDataDir) { try { fs.removeSync(temporaryUserDataDir); } catch (e) {} } } }; function getEventProxy(target) { let unsupportedClientMethods = [ 'removeListener', 'emit', 'removeAllListeners', 'setMaxListeners', 'off', ]; const handler = { get: (target, name) => { if (unsupportedClientMethods.includes(name)) throw new Error(`Unsupported action ${name} on client`); return target[name]; }, }; return new Proxy(target, handler); } /** * Gives CRI client object (a wrapper around Chrome DevTools Protocol). Refer https://github.com/cyrus-and/chrome-remote-interface * This is useful while writing plugins or if use some API of [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). * * @returns {Object} */ module.exports.client = () => clientProxy; /** * Allows switching between tabs using URL or page title. * * @example * await switchTo('https://taiko.gauge.org/') # switch using URL * await switchTo('Taiko') # switch using Title * * @param {string} targetUrl - URL/Page title of the tab to switch. * * @returns {Promise} */ module.exports.switchTo = async targetUrl => { validate(); let t = typeof targetUrl; if (t !== 'string') { throw new TypeError( 'The "targetUrl" argument must be of type string. Received type ' + t, ); } const targets = await targetHandler.getCriTargets( targetUrl, currentHost, currentPort, ); if (targets.matching.length === 0) { throw new Error('No target with given URL/Title found.'); } await connect_to_cri(targets.matching[0]); await dom.getDocument(); descEvent.emit('success', 'Switched to tab with URL ' + targetUrl); }; /** * Add interceptor for the network call. Helps in overriding request or to mock response of a network call. * * @example * # case 1: block URL : * await intercept(url) * # case 2: mockResponse : * await intercept(url, {mockObject}) * # case 3: override request : * await intercept(url, (request) => {request.continue({overrideObject})}) * # case 4: redirect always : * await intercept(url, redirectUrl) * # case 5: mockResponse based on request : * await intercept(url, (request) => { request.respond({mockResponseObject}) }) * # case 6: block URL twice: * await intercept(url, undefined, 2) * # case 7: mockResponse only 3 times : * await intercept(url, {mockObject}, 3) * * @param {string} requestUrl request URL to intercept * @param {function|Object} option action to be done after interception. For more examples refer to https://github.com/getgauge/taiko/issues/98#issuecomment-42024186 * @param {number} count number of times the request has to be intercepted . Optional parameter * * @returns {Promise} */ module.exports.intercept = async (requestUrl, option, count) => { await networkHandler.addInterceptor({ requestUrl: requestUrl, action: option, count, }); descEvent.emit('success', 'Interceptor added for ' + requestUrl); }; /** * Activates emulation of network conditions. * * @example * await emulateNetwork("Offline") * await emulateNetwork("Good2G") * * @param {string} networkType - 'GPRS','Regular2G','Good2G','Good3G','Regular3G','Regular4G','DSL','WiFi, Offline' * * @returns {Promise} */ module.exports.emulateNetwork = async networkType => { validate(); await networkHandler.setNetworkEmulation(networkType); descEvent.emit( 'success', 'Set network emulation with values ' + JSON.stringify(networkType), ); }; /** * Overrides the values of device screen dimensions according to a predefined list of devices. To provide custom device dimensions, use setViewPort API. * * @example * await emulateDevice('iPhone 6') * * @param {string} deviceModel - See [device model](https://github.com/getgauge/taiko/blob/master/lib/data/devices.js) for a list of all device models. * * @returns {Promise} */ module.exports.emulateDevice = emulateDevice; async function emulateDevice(deviceModel) { validate(); const devices = require('./data/devices').default; const deviceEmulate = devices[deviceModel]; let deviceNames = Object.keys(devices); if (deviceEmulate == undefined) throw new Error( `Please set one of the given device models \n${deviceNames.join( '\n', )}`, ); await Promise.all([ emulationHandler.setViewport(deviceEmulate.viewport), network.setUserAgentOverride({ userAgent: deviceEmulate.userAgent, }), ]); descEvent.emit('success', 'Device emulation set to ' + deviceModel); } /** * Overrides the values of device screen dimensions * * @example * await setViewPort({width:600, height:800}) * * @param {Object} options - See [chrome devtools setDeviceMetricsOverride](https://chromedevtools.github.io/devtools-protocol/tot/Emulation#method-setDeviceMetricsOverride) for a list of options * * @returns {Promise} */ module.exports.setViewPort = async options => { validate(); await emulationHandler.setViewport(options); descEvent.emit( 'success', 'ViewPort is set to width ' + options.width + ' and height ' + options.height, ); }; /** * Changes the timezone of the page. See [`metaZones.txt`](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) * for a list of supported timezone IDs. * @example * await emulateTimezone('America/Jamaica') */ module.exports.emulateTimezone = async timezoneId => { await emulationHandler.setTimeZone(timezoneId); descEvent.emit('success', 'Timezone set to ' + timezoneId); }; /** * Launches a new tab. If url is provided, the new tab is opened with the url loaded. * @example * await openTab('https://taiko.gauge.org/') * await openTab() # opens a blank tab. * * @param {string} [targetUrl=undefined] - Url of page to open in newly created tab. * @param {Object} options * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after the reload. Default navigation timeout is 5000 milliseconds, to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * @param {number} [options.navigationTimeout=5000] - Navigation timeout value in milliseconds for navigation after click. Accepts value in milliseconds. * @param {number} [options.waitForStart=100] - time to wait to check for occurrence of page load events. Accepts value in milliseconds. * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Page load events to implicitly wait for. Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint']] * * @returns {Promise} */ module.exports.openTab = async ( targetUrl, options = { navigationTimeout: defaultConfig.navigationTimeout }, ) => { validate(); if (!targetUrl) { _client.removeAllListeners(); let target = await cri.New({ host: currentHost, port: currentPort, }); await connect_to_cri(target); descEvent.emit('success', 'Opened a new tab'); return; } if (!/^https?:\/\//i.test(targetUrl) && !/^file/i.test(targetUrl)) targetUrl = 'http://' + targetUrl; options = setNavigationOptions(options); options.isPageNavigationAction = true; await doActionAwaitingNavigation(options, async () => { _client.removeAllListeners(); let target = await cri.New({ host: currentHost, port: currentPort, url: targetUrl, }); await connect_to_cri(target); }); descEvent.emit('success', 'Opened tab with URL ' + targetUrl); }; /** * Closes the given tab with given URL or closes current tab. * * @example * # Closes the current tab. * await closeTab() * # Closes all the tabs with Title 'Open Source Test Automation Framework | Gauge'. * await closeTab('Open Source Test Automation Framework | Gauge') * # Closes all the tabs with URL 'https://gauge.org'. * await closeTab('https://gauge.org') * * @param {string} [targetUrl=undefined] - URL/Page title of the tab to close. * * @returns {Promise} */ module.exports.closeTab = async targetUrl => { validate(); const { matching, others } = await targetHandler.getCriTargets( targetUrl, currentHost, currentPort, ); if (!others.length) { await _closeBrowser(); descEvent.emit('success', 'Closing last target and browser.'); return; } if (!matching.length) { throw new Error('No target with given URL/Title found.'); } let currentUrl = await currentURL(); let closedTabUrl; for (let target of matching) { closedTabUrl = target.url; await cri.Close({ host: currentHost, port: currentPort, id: target.id, }); } if (!targetHandler.isMatchingUrl(others[0], currentUrl)) { _client.removeAllListeners(); _client = null; await connect_to_cri(others[0]); await dom.getDocument(); } let message = targetUrl ? `Closed all tabs with URL ${targetUrl}` : `Closed current tab with URL ${closedTabUrl}`; descEvent.emit('success', message); }; /** * Override specific permissions to the given origin * * @example * await overridePermissions('http://maps.google.com',['geolocation']); * * @param {string} origin - url origin to override permissions * @param {Array<string>} permissions - See [chrome devtools permission types](https://chromedevtools.github.io/devtools-protocol/tot/Browser/#type-PermissionType) for a list of permission types. * * @returns {Promise} */ module.exports.overridePermissions = async (origin, permissions) => { validate(); await browserHandler.overridePermissions(origin, permissions); descEvent.emit( 'success', 'Override permissions with ' + permissions, ); }; /** * Clears all permission overrides for all origins. * * @example * await clearPermissionOverrides() * * @returns {Promise} */ module.exports.clearPermissionOverrides = async () => { validate(); await browserHandler.clearPermissionOverrides(); descEvent.emit('success', 'Cleared permission overrides'); }; /** * Sets a cookie with the given cookie data. It may overwrite equivalent cookie if it already exists. * * @example * await setCookie("CSRFToken","csrfToken", {url: "http://the-internet.herokuapp.com"}) * await setCookie("CSRFToken","csrfToken", {domain: "herokuapp.com"}) * * @param {string} name - Cookie name. * @param {string} value - Cookie value. * @param {Object} options * @param {string} [options.url=undefined] - sets cookie with the URL. * @param {string} [options.domain=undefined] - sets cookie with the exact domain. * @param {string} [options.path=undefined] - sets cookie with the exact path. * @param {boolean} [options.secure=undefined] - True if cookie to be set is secure. * @param {boolean} [options.httpOnly=undefined] - True if cookie to be set is http-only. * @param {string} [options.sameSite=undefined] - Represents the cookie's 'SameSite' status: Refer https://tools.ietf.org/html/draft-west-first-party-cookies. * @param {number} [options.expires=undefined] - UTC time in seconds, counted from January 1, 1970. eg: 2019-02-16T16:55:45.529Z * * @returns {Promise} */ module.exports.setCookie = async (name, value, options = {}) => { validate(); if (options.url === undefined && options.domain === undefined) throw new Error( 'At least URL or domain needs to be specified for setting cookies', ); options.name = name; options.value = value; let res = await network.setCookie(options); if (!res.success) throw new Error('Unable to set ' + name + ' cookie'); descEvent.emit('success', name + ' cookie set successfully'); }; /** * Deletes browser cookies with matching name and URL or domain/path pair. If cookie name is not given or empty, all browser cookies are deleted. * * @example * await deleteCookies() # clears all browser cookies * await deleteCookies("CSRFToken", {url: "http://the-internet.herokuapp.com"}) * await deleteCookies("CSRFToken", {domain: "herokuapp.com"}) * * @param {string} [cookieName=undefined] - Cookie name. * @param {Object} options * @param {string} [options.url=undefined] - deletes all the cookies with the given name where domain and path match provided URL. eg: https://google.com * @param {string} [options.domain=undefined] - deletes only cookies with the exact domain. eg: google.com * @param {string} [options.path=undefined] - deletes only cookies with the exact path. eg: Google/Chrome/Default/Cookies/.. * * @returns {Promise} */ module.exports.deleteCookies = async (cookieName, options = {}) => { validate(); if (!cookieName || !cookieName.trim()) { await network.clearBrowserCookies(); descEvent.emit('success', 'Browser cookies deleted successfully'); } else { if (options.url === undefined && options.domain === undefined) throw new Error( 'At least URL or domain needs to be specified for deleting cookies', ); options.name = cookieName; await network.deleteCookies(options); descEvent.emit( 'success', `"${cookieName}" cookie deleted successfully`, ); } }; /** * Get browser cookies * * @example * await getCookies() * await getCookies({urls:['https://the-internet.herokuapp.com']}) * * @param {Object} options * @param {Array} [options.urls=undefined] - The list of URLs for which applicable cookies will be fetched * * @returns {Promise<Object[]>} - Array of cookie objects */ module.exports.getCookies = async (options = {}) => { validate(); return (await network.getCookies(options)).cookies; }; /** * Overrides the Geolocation Position * * @example * await setLocation({ latitude: 27.1752868, longitude: 78.040009, accuracy:20 }) * * @param {Object} options Latitue, logitude and accuracy to set the location. * @param {number} options.latitude - Mock latitude * @param {number} options.longitude - Mock longitude * @param {number} options.accuracy - Mock accuracy * * @returns {Promise} */ module.exports.setLocation = async options => { validate(); await emulationHandler.setLocation(options); descEvent.emit('success', 'Geolocation set'); }; /** * Opens the specified URL in the browser's tab. Adds `http` protocol to the URL if not present. * @example * await goto('https://google.com') * await goto('google.com') * await goto({ navigationTimeout:10000, headers:{'Authorization':'Basic cG9zdG1hbjpwYXNzd29y2A=='}}) * * @param {string} url - URL to navigate page to. * @param {Object} options * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after the goto. Default navigationTimeout is 30 seconds to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint'] * @param {number} [options.navigationTimeout=30000] - Navigation timeout value in milliseconds for navigation after click. * @param {Object} options.headers - Map with extra HTTP headers. * @param {number} [options.waitForStart = 100] - time to wait for navigation to start. Accepts value in milliseconds. * * @returns {Promise} */ module.exports.goto = async ( url, options = { navigationTimeout: defaultConfig.navigationTimeout }, ) => { validate(); if (!/^https?:\/\//i.test(url) && !/^file/i.test(url)) url = 'http://' + url; if (options.headers) await network.setExtraHTTPHeaders({ headers: options.headers }); options = setNavigationOptions(options); options.isPageNavigationAction = true; await doActionAwaitingNavigation(options, async () => { await pageHandler.handleNavigation(url); }); descEvent.emit('success', 'Navigated to URL ' + url); }; /** * Reloads the page. * @example * await reload('https://google.com') * await reload('https://google.com', { navigationTimeout: 10000 }) * * @param {string} url - DEPRECATED URL to reload * @param {Object} options * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after the reload. Default navigation timeout is 30 seconds, to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint'] * @param {number} [options.navigationTimeout=30000] - Navigation timeout value in milliseconds for navigation after click. * @param {number} [options.waitForStart = 100] - time to wait for navigation to start. Accepts value in milliseconds. * @param {boolean} [options.ignoreCache = false] - Ignore Cache on reload - Default to false * * @returns {Promise} */ module.exports.reload = async ( url, options = { navigationTimeout: defaultConfig.navigationTimeout }, ) => { if (isString(url)) { console.warn('DEPRECATION WARNING: url is deprecated on reload'); } if (typeof url === 'object') options = Object.assign(url, options); validate(); options = setNavigationOptions(options); options.isPageNavigationAction = true; await doActionAwaitingNavigation(options, async () => { const value = options.ignoreCache || false; await page.reload({ ignoreCache: value }); }); let windowLocation = ( await runtimeHandler.runtimeEvaluate('window.location.toString()') ).result.value; descEvent.emit('success', windowLocation + 'reloaded'); }; /** * Mimics browser back button click functionality. * @example * await goBack() * * @param {Object} options * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after the goBack. Default navigation timeout is 30 seconds, to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint'] * @param {number} [options.navigationTimeout=30000] - Navigation timeout value in milliseconds for navigation after click. * @param {number} [options.waitForStart = 100] - time to wait for navigation to start. Accepts value in milliseconds. * * @returns {Promise} */ module.exports.goBack = async ( options = { navigationTimeout: defaultConfig.navigationTimeout }, ) => { validate(); await _go(-1, options); descEvent.emit( 'success', 'Performed clicking on browser back button', ); }; /** * Mimics browser forward button click functionality. * @example * await goForward() * * @param {Object} options * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after the goForward. Default navigation timeout is 30 seconds, to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint'] * @param {number} [options.navigationTimeout=30000] - Navigation timeout value in milliseconds for navigation after click. * @param {number} [options.waitForStart = 100] - time to wait for navigation to start. Accepts value in milliseconds. * * @returns {Promise} */ module.exports.goForward = async ( options = { navigationTimeout: defaultConfig.navigationTimeout }, ) => { validate(); await _go(+1, options); descEvent.emit( 'success', 'Performed clicking on browser forward button', ); }; const _go = async (delta, options) => { const history = await page.getNavigationHistory(); const entry = history.entries[history.currentIndex + delta]; if (!entry) return null; options = setNavigationOptions(options); await doActionAwaitingNavigation(options, async () => { await page.navigateToHistoryEntry({ entryId: entry.id }); }); }; /** * Returns window's current URL. * @example * await openBrowser(); * await goto("www.google.com"); * await currentURL(); # returns "https://www.google.com/?gws_rd=ssl" * * @returns {Promise<string>} - The URL of the current window. */ const currentURL = async () => { validate(); const locationObj = await runtimeHandler.runtimeEvaluate( 'window.location.toString()', ); return locationObj.result.value; }; module.exports.currentURL = currentURL; /** * Returns page's title. * @example * await openBrowser(); * await goto("www.google.com"); * await title(); # returns "Google" * * @returns {Promise<string>} - The title of the current page. */ module.exports.title = async () => { validate(); const result = await runtimeHandler.runtimeEvaluate( 'document.querySelector("title").textContent', ); return result.result.value; }; const checkIfElementAtPointOrChild = async e => { function isElementAtPointOrChild() { function getDirectParent(nodes, elem) { return nodes.find(node => node.contains(elem)); } let value, elem = this; if (elem.nodeType === Node.TEXT_NODE) { let range = document.createRange(); range.selectNodeContents(elem); value = range.getClientRects()[0]; elem = elem.parentElement; } else value = elem.getBoundingClientRect(); const y = (value.top + value.bottom) / 2; const x = (value.left + value.right) / 2; const nodes = document.elementsFromPoint(x, y); const isElementCoveredByAnotherElement = nodes[0] !== elem; let node = null; if (isElementCoveredByAnotherElement) { node = document.elementFromPoint(x, y); } else { node = getDirectParent(nodes, elem); } return ( elem.contains(node) || node.contains(elem) || window.getComputedStyle(node).getPropertyValue('opacity') < 0.1 || window.getComputedStyle(elem).getPropertyValue('opacity') < 0.1 ); } const nodeId = e.get(); const res = await runtimeHandler.runtimeCallFunctionOn( isElementAtPointOrChild, null, { nodeId: nodeId }, ); return res.result.value; }; const checkIfElementIsCovered = async (elem, isElemAtPoint) => { isElemAtPoint = await checkIfElementAtPointOrChild(elem); return isElemAtPoint; }; async function _click(selector, options, ...args) { const elems = await handleRelativeSearch( await findElements(selector), args, ); let elemsLength = elems.length; let isElemAtPoint; let X; let Y; options = setClickOptions(options); if (elemsLength > options.elementsToMatch) { elems.splice(options.elementsToMatch, elems.length); } for (let elem of elems) { const nodeId = elem.get(); isElemAtPoint = false; await scrollTo(elem); let { x, y } = await domHandler.boundingBoxCenter(nodeId); (X = x), (Y = y); isElemAtPoint = await checkIfElementIsCovered( elem, isElemAtPoint, ); let isDisabled = ( await evaluate(elem, function() { return this.hasAttribute('disabled') ? this.disabled : false; }) ).value; if (isDisabled) { throw Error(description(selector) + 'is disabled'); } if (isElemAtPoint) { const type = ( await evaluate(elem, function getType() { return this.type; }) ).value; assertType( nodeId, () => type !== 'file', 'Unsupported operation, use `attach` on file input field', ); if (defaultConfig.headful) await highlightElemOnAction(nodeId); break; } } if (!isElemAtPoint && elemsLength != elems.length) throw Error( 'Please provide a more specific selector, too many matches.', ); if (!isElemAtPoint) throw Error( description(selector) + ' is covered by other element', ); (options.x = X), (options.y = Y); options.noOfClicks = options.noOfClicks || 1; for (let count = 0; count < options.noOfClicks; count++) { await waitForMouseActions(options); } } /** * Fetches an element with the given selector, scrolls it into view if needed, and then clicks in the center of the element. If there's no element matching selector, the method throws an error. * @example * await click('Get Started') * await click(link('Get Started')) * await click({x : 170, y : 567}) * * @param {selector|string|Object} selector - A selector to search for element to click / coordinates of the elemets to click on. If there are multiple elements satisfying the selector, the first will be clicked. * @param {Object} options * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after the click. Default navigation timeout is 30 seconds, to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * @param {number} [options.navigationTimeout=30000] - Navigation timeout value in milliseconds for navigation after click. * @param {string} [options.button='left'] - `left`, `right`, or `middle`. * @param {number} [options.clickCount=1] - Number of times to click on the element. * @param {number} [options.elementsToMatch=10] - Number of elements to loop through to match the element with given selector. * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint'] * @param {number} [options.waitForStart=100] - time to wait for navigation to start. Accepts time in milliseconds. * @param {relativeSelector[]} args * * @returns {Promise} */ module.exports.click = click; async function click(selector, options = {}, ...args) { validate(); if (options instanceof RelativeSearchElement) { args = [options].concat(args); options = {}; } if ( isSelector(selector) || isString(selector) || isElement(selector) ) { options.noOfClicks = options.clickCount || 1; await _click(selector, options, ...args); descEvent.emit( 'success', 'Clicked ' + description(selector, true) + ' ' + options.noOfClicks + ' times', ); } else { options = setClickOptions(options, selector.x, selector.y); options.noOfClicks = options.clickCount || 1; for (let count = 0; count < options.noOfClicks; count++) { await waitForMouseActions(options); } descEvent.emit( 'success', 'Clicked ' + options.noOfClicks + ' times on coordinates x : ' + selector.x + ' and y : ' + selector.y, ); } } /** * Fetches an element with the given selector, scrolls it into view if needed, and then double clicks the element. If there's no element matching selector, the method throws an error. * * @example * await doubleClick('Get Started') * await doubleClick(button('Get Started')) * * @param {selector|string} selector - A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be double clicked. * @param {Object} options * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after the click. Default navigation timout is 30 seconds, to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * @param {relativeSelector[]} args * * @returns {Promise} */ module.exports.doubleClick = async ( selector, options = {}, ...args ) => { validate(); if (options instanceof RelativeSearchElement) { args = [options].concat(args); options = {}; } options = { waitForNavigation: options.waitForNavigation, clickCount: 2, }; await _click(selector, options, ...args); descEvent.emit( 'success', 'Double clicked ' + description(selector, true), ); }; /** * Fetches an element with the given selector, scrolls it into view if needed, and then right clicks the element. If there's no element matching selector, the method throws an error. * * @example * await rightClick('Get Started') * await rightClick(text('Get Started')) * * @param {selector|string} selector - A selector to search for element to right click. If there are multiple elements satisfying the selector, the first will be clicked. * @param {Object} options - Click options. * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after the click. Default navigation timout is 30 seconds, to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * * @returns {Promise} */ module.exports.rightClick = async ( selector, options = {}, ...args ) => { validate(); if (options instanceof RelativeSearchElement) { args = [options].concat(args); options = {}; } options = { waitForNavigation: options.waitForNavigation, button: 'right', }; await _click(selector, options, ...args); descEvent.emit( 'success', 'Right clicked ' + description(selector, true), ); }; /** * Fetches the source element with given selector and moves it to given destination selector or moves for given distance. If there's no element matching selector, the method throws an error. *Drag and drop of HTML5 draggable does not work as expected. Issue tracked here https://github.com/getgauge/taiko/issues/279 * * @example * await dragAndDrop($("work"),into($('work done'))) * await dragAndDrop($("work"),{up:10,down:10,left:10,right:10}) * * @param {selector|string} source - Element to be Dragged * @param {selector|string} destination - Element for dropping the dragged element * @param {Object} distance - Distance to be moved from position of source element * * @returns {Promise} */ module.exports.dragAndDrop = async (source, destination) => { validate(); let sourceElem = await findFirstElement(source); let dest = isSelector(destination) || isString(destination) || isElement(destination) ? await findFirstElement(destination) : destination; let options = setClickOptions({}); await doActionAwaitingNavigation(options, async () => { if (defaultConfig.headful) { await highlightElemOnAction(sourceElem.get()); if (isElement(dest)) await highlightElemOnAction(dest.get()); } await dragAndDrop(options, sourceElem, dest); }); const desc = isElement(dest) ? `Dragged and dropped ${description( sourceElem, true, )} to ${description(dest, true)}}` : `Dragged and dropped ${description( sourceElem, true, )} at ${JSON.stringify(destination)}`; descEvent.emit('success', desc); }; const dragAndDrop = async (options, sourceElem, dest) => { let sourcePosition = await domHandler.boundingBoxCenter( sourceElem.get(), ); await scrollTo(sourceElem); options.x = sourcePosition.x; options.y = sourcePosition.y; options.type = 'mouseMoved'; await input.dispatchMouseEvent(options); options.type = 'mousePressed'; await input.dispatchMouseEvent(options); let destPosition = await calculateDestPosition( sourceElem.get(), dest, ); await inputHandler.mouse_move(sourcePosition, destPosition); options.x = destPosition.x; options.y = destPosition.y; options.type = 'mouseReleased'; await input.dispatchMouseEvent(options); }; const calculateDestPosition = async (sourceElemNodeId, dest) => { if (isElement(dest)) { await scrollTo(dest); return await domHandler.boundingBoxCenter(dest.get()); } const destPosition = await domHandler.calculateNewCenter( sourceElemNodeId, dest, ); const newBoundary = destPosition.newBoundary; if (defaultConfig.headful) { await overlay.highlightQuad({ quad: [ newBoundary.right, newBoundary.top, newBoundary.right, newBoundary.bottom, newBoundary.left, newBoundary.bottom, newBoundary.left, newBoundary.top, ], outlineColor: { r: 255, g: 0, b: 0 }, }); await waitFor(1000); await overlay.hideHighlight(); } return destPosition; }; /** * Fetches an element with the given selector, scrolls it into view if needed, and then hovers over the center of the element. If there's no element matching selector, the method throws an error. * * @example * await hover('Get Started') * await hover(link('Get Started')) * * @param {selector|string} selector - A selector to search for element to right click. If there are multiple elements satisfying the selector, the first will be hovered. * @param {Object} options * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint'] */ module.exports.hover = async (selector, options = {}) => { validate(); options = setNavigationOptions(options); const e = await findFirstElement(selector); await scrollTo(e); if (defaultConfig.headful) await highlightElemOnAction(e.get()); const { x, y } = await domHandler.boundingBoxCenter(e.get()); const option = { x: x, y: y, }; await doActionAwaitingNavigation(options, async () => { Promise.resolve() .then(() => { option.type = 'mouseMoved'; return input.dispatchMouseEvent(option); }) .catch(err => { throw new Error(err); }); }); descEvent.emit( 'success', 'Hovered over the ' + description(selector, true), ); }; /** * Fetches an element with the given selector and focuses it. If there's no element matching selector, the method throws an error. * * @example * await focus(textBox('Username:')) * * @param {selector|string} selector - A selector of an element to focus. If there are multiple elements satisfying the selector, the first will be focused. * @param {Object} options * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint'] */ module.exports.focus = async (selector, options = {}) => { validate(); options = setNavigationOptions(options); await doActionAwaitingNavigation(options, async () => { if (defaultConfig.headful) await highlightElemOnAction( (await findFirstElement(selector)).get(), ); await _focus(selector); }); descEvent.emit( 'success', 'Focussed on the ' + description(selector, true), ); }; /** * Types the given text into the focused or given element. * @example * await write('admin', into('Username:')) * await write('admin', 'Username:') * await write('admin') * * @param {string} text - Text to type into the element. * @param {selector|string} into - A selector of an element to write into. * @param {Object} options * @param {number} [options.delay = 10] - Time to wait between key presses in milliseconds. * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after the click. Default navigation timeout is 15 seconds, to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * @param {number} [options.waitForStart=100] - wait for navigation to start. Accepts time in milliseconds. * @param {number} [options.navigationTimeout=30000] - Navigation timeout value in milliseconds for navigation after click. * @param {boolean} [options.hideText=false] - Prevent given text from being written to log output. * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint'] * * @returns {Promise} */ module.exports.write = async ( text, into, options = { delay: 10 }, ) => { if (text == null || text == undefined) { console.warn( `Invalid text value ${text}, setting value to empty ''`, ); text = ''; } else { if (!isString(text)) text = text.toString(); } validate(); let desc; if (into && !isSelector(into) && !isElement(into)) { if (!into.delay) into.delay = options.delay; options = into; into = undefined; } options = setNavigationOptions(options); await doActionAwaitingNavigation(options, async () => { if (into) { const selector = isString(into) ? textBox(into) : into, elems = await handleRelativeSearch( await findElements(selector), [], ); await waitUntil( async () => { for (let elem of elems) { try { await _focus(elem); let activeElement = await runtimeHandler.activeElement(); if (activeElement.notWritable) continue; return true; } catch (e) {} } return false; }, 100, 1000, ).catch(() => { throw new Error('Element focused is not writable'); }); const textDesc = await _write(text, options); const d = description(selector, true); desc = `Wrote ${textDesc} into the ${ d === '' ? 'focused element' : d }`; } else { const textDesc = await _write(text, options); desc = `Wrote ${textDesc} into the focused element.`; } }); descEvent.emit('success', desc); }; const _write = async (text, options) => { let activeElement = await runtimeHandler.activeElement(); if (activeElement.notWritable) { await waitUntil( async () => !(await runtimeHandler.activeElement()).notWritable, 100, 10000, ).catch(() => { throw new Error('Element focused is not writable'); }); activeElement = await runtimeHandler.activeElement(); } const showOrMaskText = activeElement.isPassword || options.hideText ? '*****' : text; if (defaultConfig.headful) await highlightElemOnAction(activeElement.nodeId); for (const char of text) { await inputHandler.down(char); await inputHandler.up(char); await new Promise(resolve => { const timeoutId = setTimeout(resolve, options.delay); timeouts.push(timeoutId); }); } return showOrMaskText; }; /** * Clears the value of given selector. If no selector is given clears the current active element. * * @example * await clear() * await clear(textBox({placeholder:'Email'})) * * @param {selector} selector - A selector to search for element to clear. If there are multiple elements satisfying the selector, the first will be cleared. * @param {Object} options - Click options. * @param {boolean} [options.waitForNavigation=true] - Wait for navigation after clear. Default navigation timeout is 30 seconds, to override pass `{ navigationTimeout: 10000 }` in `options` parameter. * @param {number} [options.waitForStart=100] - wait for navigation to start. Accepts time in milliseconds. * @param {number} [options.navigationTimeout=30000] - Navigation timeout value in milliseconds for navigation after click. * @param {string[]} [options.waitForEvents = ['firstMeaningfulPaint']] - Events available to wait for ['DOMContentLoaded', 'loadEventFired', 'networkAlmostIdle', 'networkIdle', 'firstPaint', 'firstContentfulPaint', 'firstMeaningfulPaint'] * * @returns {Promise} */ module.exports.clear = async (selector, options = {}) => { validate(); if (selector && !isSelector(selector) && !isElement(selector)) { options = selector; selector = undefined; } options = setNavigationOptions(options); let activeElement = await runtimeHandler.activeElement(); if (activeElement.notWritable || selector) { await waitUntil( async () => { try { if (selector) await _focus(selector); } catch (_) {} return !(await runtimeHandler.activeElement()).notWritable; }, 100, 10000, ).catch(() => { throw new Error('Element cannot be cleared'); }); activeElement = await runtimeHandler.activeElement(); } if (activeElement.notWritable) throw new Error('Elem