UNPKG

testplane

Version:

Tests framework based on mocha and wdio

469 lines 19.5 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; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExistingBrowser = exports.getActivePuppeteerPage = void 0; const url_1 = __importDefault(require("url")); const lodash_1 = __importDefault(require("lodash")); const webdriverio_1 = require("@testplane/webdriverio"); const wdio_utils_1 = require("@testplane/wdio-utils"); const browser_1 = require("./browser"); const commands_1 = require("./commands"); const camera_1 = require("./camera"); const client_bridge_1 = require("./client-bridge"); const history = __importStar(require("./history")); const logger = __importStar(require("../utils/logger")); const config_1 = require("../constants/config"); const browser_2 = require("../constants/browser"); const browser_3 = require("../utils/browser"); const config_2 = require("../utils/config"); const help_1 = require("../constants/help"); const history_1 = require("./history"); const cdp_1 = require("./cdp"); const OPTIONAL_SESSION_OPTS = ["transformRequest", "transformResponse"]; const BROWSER_SESSION_HINT = "browser session"; const CDP_CONNECTION_HINT = "cdp connection"; const CLIENT_BRIDGE_HINT = "client bridge"; function ensure(value, hint) { if (!value) { throw new Error(`Execution can't proceed, because a crucial component was not initialized${hint ? " (" + hint + ")" : ""}. This is likely due to a bug on our side.\n` + `\nPlease file an issue at ${help_1.NEW_ISSUE_LINK}, we will try to fix it as soon as possible.`); } } const isClientBridgeErrorData = (data) => { return Boolean(data && data.error && data.message); }; const getActivePuppeteerPage = async (session) => { const puppeteer = await session.getPuppeteer(); if (!puppeteer) { return; } const pages = await puppeteer.pages(); if (!pages.length) { return; } const active = await session.getWindowHandle(); for (const page of pages) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error need private _targetId if (page.target()._targetId === active) { return page; } } return pages[pages.length - 1]; }; exports.getActivePuppeteerPage = getActivePuppeteerPage; class ExistingBrowser extends browser_1.Browser { constructor(config, opts) { super(config, opts); this._cdp = null; this._tags = new Set(); this._camera = camera_1.Camera.create(this._config.screenshotMode, () => this._takeScreenshot()); this._meta = this._initMeta(); } async init({ sessionId, sessionCaps, sessionOpts }, calibrator) { this._session = await this._attachSession({ sessionId, sessionCaps, sessionOpts }); const cdpPromise = cdp_1.CDP.create(this).then(cdp => { this._cdp = cdp; }); if (!(0, config_2.isRunInNodeJsEnv)(this._config)) { this._startCollectingCustomCommands(); } const isolationPromise = cdpPromise.then(() => this._performIsolation({ sessionCaps, sessionOpts })); this._extendStacktrace(); this._addSteps(); this._addHistory(); await history.runGroup({ session: this._session, snapshotsPromiseRef: this._snapshotsPromiseRef, callstack: this._callstackHistory, config: this._config, }, "testplane: init browser", async () => { this._addCommands(); await isolationPromise; this._callstackHistory?.clear(); try { this.config.prepareBrowser && this.config.prepareBrowser(this.publicAPI); } catch (e) { logger.warn(`WARN: couldn't prepare browser ${this.id}\n`, e?.stack); } await this._prepareSession(); await this._performCalibration(calibrator); await this._buildClientScripts(); }); return this; } markAsBroken({ stubBrowserCommands = false } = {}) { if (this.state.isBroken) { return; } this.applyState({ isBroken: true }); if (stubBrowserCommands) { this._stubCommands(); } } quit() { this._cdp?.close(); this._meta = this._initMeta(); } async prepareScreenshot(selectors, opts = {}) { // Running this fragment with history causes rrweb snapshots to break on pages with iframes return (0, history_1.runWithoutHistory)({ callstack: this._callstackHistory }, async () => { opts = lodash_1.default.extend(opts, { usePixelRatio: this._calibration ? this._calibration.usePixelRatio : true, }); ensure(this._clientBridge, CLIENT_BRIDGE_HINT); const result = await this._clientBridge.call("prepareScreenshot", [selectors, opts]); if (isClientBridgeErrorData(result)) { throw new Error(`Prepare screenshot failed with error type '${result.error}' and error message: ${result.message}`); } // https://github.com/webdriverio/webdriverio/issues/11396 if (this._config.automationProtocol === config_1.WEBDRIVER_PROTOCOL && opts.disableAnimation) { await this._disableIframeAnimations(); } return result; }); } async cleanupScreenshot(opts = {}) { if (opts.disableAnimation) { return (0, history_1.runWithoutHistory)({ callstack: this._callstackHistory }, async () => { await this._cleanupPageAnimations(); }); } } open(url) { ensure(this._session, BROWSER_SESSION_HINT); return this._session.url(url); } evalScript(script) { ensure(this._session, BROWSER_SESSION_HINT); return this._session.execute(`return ${script}`); } injectScript(script) { ensure(this._session, BROWSER_SESSION_HINT); return this._session.execute(script); } async captureViewportImage(page, screenshotDelay) { if (screenshotDelay) { await new Promise(resolve => setTimeout(resolve, screenshotDelay)); } return this._camera.captureViewportImage(page); } scrollBy(params) { ensure(this._session, BROWSER_SESSION_HINT); return this._session.execute(function (params) { // eslint-disable-next-line no-var var elem, xVal, yVal; if (params.selector) { elem = document.querySelector(params.selector); if (!elem) { throw new Error("Scroll screenshot failed with: " + 'Could not find element with css selector specified in "selectorToScroll" option: ' + params.selector); } xVal = elem.scrollLeft + params.x; yVal = elem.scrollTop + params.y; } else { elem = window; xVal = window.pageXOffset + params.x; yVal = window.pageYOffset + params.y; } return elem.scrollTo(xVal, yVal); }, params); } async _attachSession({ sessionId, sessionCaps, sessionOpts = { capabilities: {} }, }) { const detectedSessionEnvFlags = (0, wdio_utils_1.sessionEnvironmentDetector)({ capabilities: sessionCaps, requestedCapabilities: sessionOpts.capabilities, }); const opts = { sessionId, ...sessionOpts, ...this._getSessionOptsFromConfig(OPTIONAL_SESSION_OPTS), ...detectedSessionEnvFlags, ...this._config.sessionEnvFlags, options: sessionOpts, capabilities: { ...sessionOpts.capabilities, ...sessionCaps }, requestedCapabilities: sessionOpts.capabilities, }; return (0, webdriverio_1.attach)(opts); } _initMeta() { return { pid: process.pid, browserVersion: this.version, testXReqId: this.state.testXReqId, traceparent: this.state.traceparent, ...this._config.meta, }; } _takeScreenshot() { ensure(this._session, BROWSER_SESSION_HINT); return this._session.takeScreenshot(); } _addCommands() { ensure(this._session, BROWSER_SESSION_HINT); this._addMetaAccessCommands(this._session); this._decorateUrlMethod(this._session); // The reason for doing this is that in webdriverio 8.26.2 there was a breaking change that made ElementsList an async iterator // https://github.com/webdriverio/webdriverio/pull/11874 this._overrideGetElementsList(this._session); // eslint-disable-next-line @typescript-eslint/no-var-requires commands_1.customCommandFileNames.forEach(command => require(`./commands/${command}`).default(this)); super._addCommands(); } _overrideGetElementsList(session) { // prettier-ignore for (const attachToElement of [false, true]) { // @ts-expect-error This is a temporary hack to patch wdio's breaking changes. session.overwriteCommand("$$", async (origCommand, selector) => { const arr = []; const res = await origCommand(selector); for await (const el of res) arr.push(el); arr.parent = res.parent; arr.foundWith = res.foundWith; arr.selector = res.selector; arr.props = res.props; return arr; }, attachToElement); } } _addMetaAccessCommands(session) { session.addCommand("setMeta", (key, value) => (this._meta[key] = value)); session.addCommand("getMeta", key => (key ? this._meta[key] : this._meta)); session.addCommand("addTag", (tag) => { if (Array.isArray(tag)) { tag.forEach(element => this._tags?.add(element)); } else { this._tags?.add(tag); } }); } _decorateUrlMethod(session) { session.overwriteCommand("url", async (origUrlFn, uri, params) => { if (!uri) { return session.getUrl(); } const newUri = this._resolveUrl(uri); this._meta.url = newUri; if (this._config.urlHttpTimeout) { this.setHttpTimeout(this._config.urlHttpTimeout); } const result = await origUrlFn(newUri, this._session?.isBidi ? params : undefined); if (this._config.urlHttpTimeout) { this.restoreHttpTimeout(); } if (this._clientBridge) { await this._clientBridge.call("resetZoom"); } return result; }); } _resolveUrl(uri) { return this._config.baseUrl ? url_1.default.resolve(this._config.baseUrl, uri) : uri; } async _performBidiIsolation(sessionOpts) { ensure(this._session, BROWSER_SESSION_HINT); const puppeteer = await this._session.getPuppeteer(); const browserCtxs = puppeteer.browserContexts(); const incognitoCtx = await puppeteer.createIncognitoBrowserContext(); const page = await incognitoCtx.newPage(); if (sessionOpts?.automationProtocol === config_1.WEBDRIVER_PROTOCOL) { const windowIds = await this._session.getWindowHandles(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const incognitoWindowId = windowIds.find(id => id.includes(page.target()._targetId)); await Promise.all([this._session.switchToWindow(incognitoWindowId), page.bringToFront()]); } if (this._session.isBidi) { return; } for (const ctx of browserCtxs) { if (ctx.isIncognito()) { await ctx.close(); continue; } for (const page of await ctx.pages()) { await page.close(); } } } async _performCdpIsolation(sessionOpts) { ensure(this._session, BROWSER_SESSION_HINT); ensure(this._cdp, CDP_CONNECTION_HINT); const session = this._session; const cdpTarget = this._cdp.target; const [browserContextIds, currentTargets, browserContextId] = await Promise.all([ cdpTarget.getBrowserContexts().then(res => res.browserContextIds), cdpTarget.getTargets().then(res => res.targetInfos), cdpTarget.createBrowserContext().then(res => res.browserContextId), ]); const incognitoWindowId = await cdpTarget.createTarget({ browserContextId }).then(res => res.targetId); const switchWindowPromise = sessionOpts?.automationProtocol === config_1.WEBDRIVER_PROTOCOL ? session .getWindowHandles() .then(ids => session.switchToWindow(ids.find(id => id.includes(incognitoWindowId)))) : null; const browserContextIdsToClose = browserContextIds.filter(id => id !== browserContextId); await Promise.all([ switchWindowPromise, cdpTarget.activateTarget(incognitoWindowId), ...browserContextIdsToClose.map(contextId => cdpTarget.disposeBrowserContext(contextId)), ...currentTargets.map(target => cdpTarget.closeTarget(target.targetId).catch(() => { })), ]); } async _performIsolation({ sessionCaps, sessionOpts, }) { ensure(this._session, BROWSER_SESSION_HINT); if (!this._config.isolation) { return; } const { browserName, browserVersion = "", version = "", } = sessionCaps || {}; if (!(0, browser_3.isSupportIsolation)(browserName, browserVersion)) { logger.warn(`WARN: test isolation works only with chrome@${browser_2.MIN_CHROME_VERSION_SUPPORT_ISOLATION} and higher, ` + `but got ${browserName}@${browserVersion || version}`); return; } if (this._session.isBidi) { return this._performBidiIsolation(sessionOpts); } else if (this._cdp) { return this._performCdpIsolation(sessionOpts); } else { logger.warn("Unable to get CDP endpoint, skip performing isolation"); } } async _prepareSession() { await this._setOrientation(this.config.orientation); await this._setWindowSize(this.config.windowSize); } async _setOrientation(orientation) { if (orientation) { ensure(this._session, BROWSER_SESSION_HINT); await this._session.setOrientation(orientation); } } async _setWindowSize(size) { if (size) { ensure(this._session, BROWSER_SESSION_HINT); if (typeof size === "string") { const [width, height] = size.split("x").map(v => parseInt(v, 10)); await this._session.setWindowSize(width, height); } else { await this._session.setWindowSize(size.width, size.height); } } } async _performCalibration(calibrator) { if (!this.config.calibrate || this._calibration) { return Promise.resolve(); } return calibrator.calibrate(this).then(calibration => { this._calibration = calibration; this._camera.calibrate(calibration); }); } async _buildClientScripts() { return (0, client_bridge_1.build)(this, { calibration: this._calibration }).then(clientBridge => (this._clientBridge = clientBridge)); } async _runInEachDisplayedIframe(cb) { ensure(this._session, BROWSER_SESSION_HINT); const session = this._session; const iframes = await session.findElements("css selector", "iframe[src]"); const displayedIframes = []; await Promise.all(iframes.map(async (iframe) => { const isIframeDisplayed = await session.$(iframe).isDisplayed(); if (isIframeDisplayed) { displayedIframes.push(iframe); } })); try { for (const iframe of displayedIframes) { await session.switchToFrame(iframe); await cb(); // switchToParentFrame does not work in ios - https://github.com/appium/appium/issues/14882 await session.switchToFrame(null); } } catch (e) { await session.switchToFrame(null); throw e; } } async _disableFrameAnimations() { ensure(this._clientBridge, CLIENT_BRIDGE_HINT); const result = await this._clientBridge.call("disableFrameAnimations"); if (isClientBridgeErrorData(result)) { throw new Error(`Disable animations failed with error type '${result.error}' and error message: ${result.message}`); } return result; } async _disableIframeAnimations() { await this._runInEachDisplayedIframe(() => this._disableFrameAnimations()); } async _cleanupFrameAnimations() { ensure(this._clientBridge, CLIENT_BRIDGE_HINT); return this._clientBridge.call("cleanupFrameAnimations"); } async _cleanupIframeAnimations() { await this._runInEachDisplayedIframe(() => this._cleanupFrameAnimations()); } async _cleanupPageAnimations() { await this._cleanupFrameAnimations(); if (this._config.automationProtocol === config_1.WEBDRIVER_PROTOCOL) { await this._cleanupIframeAnimations(); } } _stubCommands() { if (!this._session) { return; } for (const commandName of this._session.commandList) { if (commandName === "deleteSession") { continue; } if (lodash_1.default.isFunction(this._session[commandName])) { this._session.overwriteCommand(commandName, () => { }); } } } get meta() { return this._meta; } get tags() { return [...this._tags]; } get cdp() { return this._cdp; } } exports.ExistingBrowser = ExistingBrowser; //# sourceMappingURL=existing-browser.js.map