@applitools/spec-driver-webdriverio
Version:
541 lines (540 loc) • 21.3 kB
JavaScript
;
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