UNPKG

@applitools/spec-driver-webdriverio

Version:
541 lines (540 loc) 21.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.build = exports.switchWorld = exports.getWorlds = exports.getCurrentWorld = exports.performAction = exports.setElementText = exports.getElementText = exports.getElementAttribute = exports.getElementRegion = exports.setOrientation = exports.getOrientation = exports.getSystemBars = exports.waitUntilDisplayed = exports.hover = exports.click = exports.takeScreenshot = exports.visit = exports.getUrl = exports.getTitle = exports.getCapabilities = exports.getDriverInfo = exports.getCookies = exports.setWindowSize = exports.getWindowSize = exports.findElements = exports.findElement = exports.childContext = exports.parentContext = exports.mainContext = exports.executeScript = exports.extractHostName = exports.isEqualElements = exports.isStaleElementError = exports.extractSelector = exports.untransformSelector = exports.transformSelector = exports.transformElement = exports.transformDriver = exports.isSelector = exports.isElement = exports.isDriver = void 0; const utils = __importStar(require("@applitools/utils")); // #region HELPERS const LEGACY_ELEMENT_ID = 'ELEMENT'; const ELEMENT_ID = 'element-6066-11e4-a52e-4f735466cecf'; const DIRECT_SELECTOR_REGEXP = /^(id|css selector|xpath|link text|partial link text|name|tag name|class name|-android uiautomator|-android datamatcher|-android viewmatcher|-android viewtag|-ios uiautomation|-ios predicate string|-ios class chain|accessibility id):(.+)/; function extractElementId(element) { var _a, _b; return ((_b = (_a = element.elementId) !== null && _a !== void 0 ? _a : element[ELEMENT_ID]) !== null && _b !== void 0 ? _b : element[LEGACY_ELEMENT_ID]); } function transformShadowRoot(shadowRoot) { return isElement(shadowRoot) ? shadowRoot : { [ELEMENT_ID]: shadowRoot['shadow-6066-11e4-a52e-4f735466cecf'] }; } function transformArgument(arg) { if (!arg) return []; const elements = []; const argWithElementMarkers = transform(arg); return [argWithElementMarkers, ...elements]; function transform(arg) { if (isElement(arg)) { elements.push(arg); return { isElement: true }; } else if (utils.types.isArray(arg)) { return arg.map(transform); } else if (utils.types.isObject(arg)) { return Object.entries(arg).reduce((object, [key, value]) => { return Object.assign(object, { [key]: transform(value) }); }, {}); } else { return arg; } } } // NOTE: // A few things to note: // - this function runs inside of the browser process // - evaluations in Puppeteer accept multiple arguments (not just one like in Playwright) // - an element reference (a.k.a. an ElementHandle) can only be sent as its // own argument. To account for this, we use a wrapper function to receive all // of the arguments in a serialized structure, deserialize them, and call the script, // and pass the arguments as originally intended function scriptRunner(script, arg, ...elements) { const func = new Function(script.startsWith('function') ? `return (${script}).apply(null, arguments)` : script); return func(transform(arg)); function transform(arg) { if (!arg) { return arg; } else if (arg.isElement) { return elements.shift(); } else if (Array.isArray(arg)) { return arg.map(transform); } else if (typeof arg === 'object') { return Object.entries(arg).reduce((object, [key, value]) => { return Object.assign(object, { [key]: transform(value) }); }, {}); } else { return arg; } } } function loadCommand() { let commandPath; try { commandPath = require.resolve('webdriver/build/command', { paths: [`${process.cwd()}/node_modules`] }); } catch { commandPath = 'webdriver/build/command'; } return Number(process.env.APPLITOOLS_FRAMEWORK_MAJOR_VERSION) < 8 ? require(commandPath).default : (method, url, body) => { const webdriver = import('webdriver'); return async function (...args) { return (await webdriver).command(method, url, body).apply(this, args); }; }; } // #endregion // #region UTILITY function isDriver(browser) { if (!browser) return false; return browser.constructor.name === 'Browser'; } exports.isDriver = isDriver; function isElement(element) { if (!element) return false; return Boolean(element.elementId || element[ELEMENT_ID] || element[LEGACY_ELEMENT_ID]); } exports.isElement = isElement; function isSelector(selector) { return (utils.types.isString(selector) || utils.types.isFunction(selector) || utils.types.has(selector, ['using', 'value'])); } exports.isSelector = isSelector; function transformDriver(driver) { const command = loadCommand(); const additionalCommands = { _getWindowSize: command('GET', '/session/:sessionId/window/current/size', { command: '_getWindowSize', parameters: [], }), _setWindowSize: command('POST', '/session/:sessionId/window/current/size', { command: '_setWindowSize', parameters: [ { name: 'width', type: 'number', required: true }, { name: 'height', type: 'number', required: true }, ], }), setWindowPosition: command('POST', '/session/:sessionId/window/current/position', { command: 'setWindowPosition', parameters: [ { name: 'x', type: 'number', required: true }, { name: 'y', type: 'number', required: true }, ], }), }; Object.entries(additionalCommands).forEach(([name, cmd]) => driver.addCommand(name, cmd)); return driver; } exports.transformDriver = transformDriver; function transformElement(element) { const elementId = extractElementId(element); return { [ELEMENT_ID]: elementId, [LEGACY_ELEMENT_ID]: elementId }; } exports.transformElement = transformElement; function transformSelector(selector) { if (utils.types.has(selector, 'selector')) { if (!utils.types.has(selector, 'type')) return selector.selector; if (selector.type === 'css') return `css selector:${selector.selector}`; else return `${selector.type}:${selector.selector}`; } return selector; } exports.transformSelector = transformSelector; function untransformSelector(selector) { if (utils.types.isFunction(selector)) return null; else if (utils.types.isString(selector)) { const match = selector.match(DIRECT_SELECTOR_REGEXP); if (!match) return { selector }; const [, using, value] = match; selector = { using, value }; } if (utils.types.has(selector, ['using', 'value'])) { return { type: selector.using === 'css selector' ? 'css' : selector.using, selector: selector.value }; } return selector; } exports.untransformSelector = untransformSelector; function extractSelector(element) { return element.selector; } exports.extractSelector = extractSelector; function isStaleElementError(error) { if (!error) return false; const errOrResult = error.originalError || error; return errOrResult instanceof Error && errOrResult.name === 'stale element reference'; } exports.isStaleElementError = isStaleElementError; async function isEqualElements(_browser, element1, element2) { if (!element1 || !element2) return false; const elementId1 = extractElementId(element1); const elementId2 = extractElementId(element2); return elementId1 === elementId2; } exports.isEqualElements = isEqualElements; function extractHostName(driver) { var _a, _b; return (_b = (_a = driver.options) === null || _a === void 0 ? void 0 : _a.hostname) !== null && _b !== void 0 ? _b : null; } exports.extractHostName = extractHostName; // #endregion // #region COMMANDS async function executeScript(browser, script, arg) { if (browser.isDevTools) { script = utils.types.isString(script) ? script : script.toString(); return browser.execute(scriptRunner, script, ...transformArgument(arg)); } else { return browser.execute(script, arg); } } exports.executeScript = executeScript; async function mainContext(browser) { await browser.switchToFrame(null); return browser; } exports.mainContext = mainContext; async function parentContext(browser) { await browser.switchToParentFrame(); return browser; } exports.parentContext = parentContext; async function childContext(browser, element) { await browser.switchToFrame(element); return browser; } exports.childContext = childContext; async function findElement(browser, selector, parent) { selector = utils.types.has(selector, ['using', 'value']) ? `${selector.using}:${selector.value}` : selector; const root = parent ? await browser.$(transformShadowRoot(parent)) : browser; try { const element = await root.$(selector); return !utils.types.has(element, 'error') ? element : null; } catch (error) { if (/element could not be located/i.test(error.message) || /cannot locate an element/i.test(error.message) || /wasn\'t found/i.test(error.message)) { return null; } throw error; } } exports.findElement = findElement; async function findElements(browser, selector, parent) { selector = utils.types.has(selector, ['using', 'value']) ? `${selector.using}:${selector.value}` : selector; const root = parent ? await browser.$(transformShadowRoot(parent)) : browser; const elements = await root.$$(selector); return Array.from(elements); } exports.findElements = findElements; async function getWindowSize(browser) { try { const rect = await browser.getWindowRect(); return { width: rect.width, height: rect.height }; } catch { return browser._getWindowSize(); } } exports.getWindowSize = getWindowSize; async function setWindowSize(browser, size) { try { await browser.setWindowRect(0, 0, size.width, size.height); } catch { await browser.setWindowPosition(0, 0); await browser._setWindowSize(size.width, size.height); } } exports.setWindowSize = setWindowSize; async function getCookies(browser, context) { if (context) return browser.getCookies(); let cookies; if (browser.isDevTools) { const puppeteer = await browser.getPuppeteer(); const [page] = await puppeteer.pages(); const cdpSession = await page.target().createCDPSession(); const response = await cdpSession.send('Network.getAllCookies'); cookies = response.cookies; } else { const response = await browser.sendCommandAndGetResult('Network.getAllCookies', {}); cookies = response.cookies; } return cookies.map((cookie) => { const copy = { ...cookie, expiry: cookie.expires }; delete copy.expires; delete copy.size; delete copy.priority; delete copy.session; delete copy.sameParty; delete copy.sourceScheme; delete copy.sourcePort; return copy; }); } exports.getCookies = getCookies; async function getDriverInfo(driver) { return { sessionId: driver.sessionId }; } exports.getDriverInfo = getDriverInfo; async function getCapabilities(browser) { var _a, _b; try { return (_b = (await ((_a = browser.getSession) === null || _a === void 0 ? void 0 : _a.call(browser)))) !== null && _b !== void 0 ? _b : browser.capabilities; } catch (error) { if (/cannot call non W3C standard command/i.test(error.message)) return browser.capabilities; throw error; } } exports.getCapabilities = getCapabilities; async function getTitle(browser) { return browser.getTitle(); } exports.getTitle = getTitle; async function getUrl(browser) { return browser.getUrl(); } exports.getUrl = getUrl; async function visit(browser, url) { await browser.url(url); } exports.visit = visit; async function takeScreenshot(browser) { if (browser.isDevTools) { const puppeteer = await browser.getPuppeteer(); const [page] = await puppeteer.pages(); const result = await page.screenshot({ captureBeyondViewport: false }); return result; } return browser.takeScreenshot(); } exports.takeScreenshot = takeScreenshot; async function click(browser, element) { const resolvedElement = isSelector(element) ? await findElement(browser, element) : element; const extendedElement = await browser.$(resolvedElement); await extendedElement.click(); } exports.click = click; async function hover(browser, element) { const resolvedElement = isSelector(element) ? await findElement(browser, element) : element; if (browser.isDevTools) { const { x, y, width, height } = await browser.execute((element) => { const rect = element.getBoundingClientRect(); return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; }, resolvedElement); const puppeteer = await browser.getPuppeteer(); const [page] = await puppeteer.pages(); await page.mouse.move(x + width / 2, y + height / 2); } else { const extendedElement = await browser.$(resolvedElement); await extendedElement.moveTo(); } } exports.hover = hover; async function waitUntilDisplayed(browser, selector, timeout) { const element = await findElement(browser, selector); if (process.env.APPLITOOLS_FRAMEWORK_MAJOR_VERSION === '5') { // @ts-ignore await element.waitForDisplayed(timeout); } else { // @ts-ignore await element.waitForDisplayed({ timeout }); } } exports.waitUntilDisplayed = waitUntilDisplayed; // #endregion // #region MOBILE COMMANDS async function getSystemBars(browser) { return browser.getSystemBars(); } exports.getSystemBars = getSystemBars; async function getOrientation(browser) { const orientation = await browser.getOrientation(); return orientation.toLowerCase(); } exports.getOrientation = getOrientation; async function setOrientation(browser, orientation) { return await browser.setOrientation(orientation); } exports.setOrientation = setOrientation; async function getElementRegion(browser, element) { const extendedElement = await browser.$(element); if (utils.types.isFunction(extendedElement, 'getRect')) { return extendedElement.getRect(); } else { const region = { x: 0, y: 0, width: 0, height: 0 }; if (utils.types.isFunction(extendedElement.getLocation)) { const location = await extendedElement.getLocation(); region.x = location.x; region.y = location.y; } if (utils.types.isFunction(extendedElement.getSize)) { const size = await extendedElement.getSize(); region.width = size.width; region.height = size.height; } return region; } } exports.getElementRegion = getElementRegion; async function getElementAttribute(browser, element, attr) { return (await browser.getElementAttribute(extractElementId(element), attr)); } exports.getElementAttribute = getElementAttribute; async function getElementText(browser, element) { const extendedElement = await browser.$(element); return extendedElement.getText(); } exports.getElementText = getElementText; async function setElementText(browser, element, text) { const resolvedElement = isSelector(element) ? await findElement(browser, element) : element; const extendedElement = await browser.$(resolvedElement); await extendedElement.setValue(text); } exports.setElementText = setElementText; async function performAction(browser, steps) { return browser.touchAction(steps); } exports.performAction = performAction; async function getCurrentWorld(driver) { const context = await driver.getContext(); return utils.types.isString(context) ? context : context.id; } exports.getCurrentWorld = getCurrentWorld; async function getWorlds(driver) { const contexts = await driver.getContexts(); return contexts.map(context => (utils.types.isString(context) ? context : context.id)); } exports.getWorlds = getWorlds; async function switchWorld(driver, name) { return driver.switchContext(name); } exports.switchWorld = switchWorld; // #endregion // #region TESTING const browserOptionsNames = { chrome: 'goog:chromeOptions', firefox: 'moz:firefoxOptions', }; /* * Spawn a browser with a given configuration (INTERNAL USE ONLY) * * NOTE: * This function is intended for internal use only. As a result it relies on some dev dependencies. * When wiring the spec-driver up to an SDK and calling this function, if you don't have the same dev deps * installed in the SDK, then this function will error. */ async function build(env) { let frameworkPath; try { frameworkPath = require.resolve('webdriverio', { paths: [`${process.cwd()}/node_modules`] }); } catch { frameworkPath = 'webdriverio'; } const { remote } = require(frameworkPath); const chromedriver = require('chromedriver'); const parseEnv = require('@applitools/test-utils/src/parse-env'); const { protocol, browser = '', capabilities, url, attach, proxy, configurable = true, args = [], headless, logLevel = 'silent', } = parseEnv(env, process.env.APPLITOOLS_WEBDRIVERIO_PROTOCOL); const options = { capabilities: { browserName: browser, ...capabilities }, logLevel, connectionRetryCount: 5, connectionRetryTimeout: 180000, }; if (browser === 'chrome' && protocol === 'cdp') { options.automationProtocol = 'devtools'; options.capabilities[browserOptionsNames.chrome] = { args }; options.capabilities['wdio:devtoolsOptions'] = { headless, ignoreDefaultArgs: ['--hide-scrollbars'], }; } else if (protocol === 'wd') { options.automationProtocol = 'webdriver'; options.protocol = url.protocol ? url.protocol.replace(/:$/, '') : undefined; options.hostname = url.hostname; if (url.port) options.port = Number(url.port); else if (options.protocol === 'http') options.port = 80; else if (options.protocol === 'https') options.port = 443; options.path = url.pathname; if (configurable) { if (browser === 'chrome' && attach) { await chromedriver.start(['--port=9515'], true); options.protocol = 'http'; options.hostname = 'localhost'; options.port = 9515; options.path = '/'; } const browserOptionsName = browserOptionsNames[browser || options.capabilities.browserName]; if (browserOptionsName) { const browserOptions = options.capabilities[browserOptionsName] || {}; browserOptions.args = [...(browserOptions.args || []), ...args]; if (headless) browserOptions.args.push('headless'); if (attach) { browserOptions.debuggerAddress = attach === true ? 'localhost:9222' : attach; if (browser !== 'firefox') browserOptions.w3c = false; } options.capabilities[browserOptionsName] = browserOptions; } } } if (proxy) { options.capabilities.proxy = { proxyType: 'manual', httpProxy: proxy.http || proxy.server, sslProxy: proxy.https || proxy.server, ftpProxy: proxy.ftp, noProxy: proxy.bypass.join(','), }; } const driver = await remote(options); return [driver, () => driver.deleteSession().then(() => chromedriver.stop())]; } exports.build = build; // #endregion