diff --git a/package-lock.json b/package-lock.json index ac73acde3..226c88759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,7 @@ "@types/sharp": "0.31.1", "@types/sinon": "17.0.1", "@types/sinonjs__fake-timers": "8.1.2", + "@types/strftime": "0.9.8", "@types/urijs": "1.19.25", "@types/url-join": "4.0.3", "@types/yallist": "4.0.4", @@ -2655,6 +2656,12 @@ "version": "2.0.1", "license": "MIT" }, + "node_modules/@types/strftime": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@types/strftime/-/strftime-0.9.8.tgz", + "integrity": "sha512-QIvDlGAKyF3YJbT3QZnfC+RIvV5noyDbi+ZJ5rkaSRqxCGrYJefgXm3leZAjtoQOutZe1hCXbAg+p89/Vj4HlQ==", + "dev": true + }, "node_modules/@types/unist": { "version": "2.0.6", "dev": true, @@ -16631,6 +16638,12 @@ "@types/stack-utils": { "version": "2.0.1" }, + "@types/strftime": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@types/strftime/-/strftime-0.9.8.tgz", + "integrity": "sha512-QIvDlGAKyF3YJbT3QZnfC+RIvV5noyDbi+ZJ5rkaSRqxCGrYJefgXm3leZAjtoQOutZe1hCXbAg+p89/Vj4HlQ==", + "dev": true + }, "@types/unist": { "version": "2.0.6", "dev": true diff --git a/package.json b/package.json index 4b72de0a6..650c05507 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "@types/sharp": "0.31.1", "@types/sinon": "17.0.1", "@types/sinonjs__fake-timers": "8.1.2", + "@types/strftime": "0.9.8", "@types/urijs": "1.19.25", "@types/url-join": "4.0.3", "@types/yallist": "4.0.4", @@ -151,8 +152,8 @@ "eslint": "8.25.0", "eslint-config-gemini-testing": "2.8.0", "eslint-config-prettier": "8.7.0", - "glob-extra": "5.0.2", "execa": "5.1.1", + "glob-extra": "5.0.2", "husky": "0.11.4", "js-levenshtein": "1.1.6", "jsdom": "^24.0.0", diff --git a/src/browser-installer/registry/index.ts b/src/browser-installer/registry/index.ts index a656066bf..ad71bb35b 100644 --- a/src/browser-installer/registry/index.ts +++ b/src/browser-installer/registry/index.ts @@ -14,7 +14,7 @@ import { type DownloadProgressCallback, } from "../utils"; import { getFirefoxBuildId } from "../firefox/utils"; -import logger from "../../utils/logger"; +import * as logger from "../../utils/logger"; import { BrowserName } from "../../browser/types"; type VersionToPathMap = Record>; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/index.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/index.ts index e70107a79..0c318e486 100644 --- a/src/browser-installer/ubuntu-packages/collect-dependencies/index.ts +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/index.ts @@ -5,7 +5,7 @@ import { Cache } from "./cache"; import { fetchBrowsersMilestones } from "./browser-versions/index"; import { downloadBrowserVersions } from "./browser-downloader"; import { getUbuntuMilestone, writeUbuntuPackageDependencies } from ".."; -import logger from "../../../utils/logger"; +import * as logger from "../../../utils/logger"; const createResolveSharedObjectToPackageName = (cache: Cache) => diff --git a/src/browser-installer/ubuntu-packages/index.ts b/src/browser-installer/ubuntu-packages/index.ts index ce2a95d8a..252cb3db6 100644 --- a/src/browser-installer/ubuntu-packages/index.ts +++ b/src/browser-installer/ubuntu-packages/index.ts @@ -4,7 +4,7 @@ import path from "path"; import { getOsPackagesDir, type DownloadProgressCallback, browserInstallerDebug } from "../utils"; import { installUbuntuPackages } from "./apt"; import { getUbuntuMilestone } from "./utils"; -import logger from "../../utils/logger"; +import * as logger from "../../utils/logger"; import { LINUX_RUNTIME_LIBRARIES_PATH_ENV_NAME, LINUX_UBUNTU_RELEASE_ID } from "../constants"; import registry from "../registry"; diff --git a/src/browser-pool/basic-pool.ts b/src/browser-pool/basic-pool.ts index 2988e9a7f..fcc64774b 100644 --- a/src/browser-pool/basic-pool.ts +++ b/src/browser-pool/basic-pool.ts @@ -32,7 +32,7 @@ export class BasicPool implements Pool { } async getBrowser(id: string, opts: BrowserOpts = {}): Promise { - const browser = NewBrowser.create(this._config, { ...opts, id, wdPool: this._wdPool }); + const browser = NewBrowser.create(this._config, { ...opts, id, wdPool: this._wdPool, emitter: this._emitter }); try { await browser.init(); diff --git a/src/browser/browser.ts b/src/browser/browser.ts index f2498852f..aa4df3380 100644 --- a/src/browser/browser.ts +++ b/src/browser/browser.ts @@ -4,14 +4,14 @@ import _ from "lodash"; import { SAVE_HISTORY_MODE } from "../constants/config"; import { X_REQUEST_ID_DELIMITER } from "../constants/browser"; -import history from "./history"; +import * as history from "./history"; import { enhanceStacktraces } from "./stacktrace"; import { getBrowserCommands, getElementCommands } from "./history/commands"; import addRunStepCommand from "./commands/runStep"; import { Config } from "../config"; import { AsyncEmitter } from "../events"; import { BrowserConfig } from "../config/browser-config"; -import Callstack from "./history/callstack"; +import type { Callstack } from "./history/callstack"; import type { WdProcess, WebdriverPool } from "../browser-pool/webdriver-pool"; import type { Capabilities } from "@wdio/types"; @@ -33,7 +33,7 @@ export type BrowserOpts = { id: string; version?: string; state?: Record; - emitter?: AsyncEmitter; + emitter: AsyncEmitter; wdPool?: WebdriverPool; }; @@ -46,6 +46,7 @@ export type BrowserState = { export type CustomCommend = { name: string; elementScope: boolean }; export class Browser { + protected _emitter: AsyncEmitter; protected _config: BrowserConfig; protected _debug: boolean; protected _session: WebdriverIO.Browser | null; @@ -80,6 +81,7 @@ export class Browser { }; this._customCommands = new Set(); this._wdPool = opts.wdPool; + this._emitter = opts.emitter; } setHttpTimeout(timeout: number | null): void { @@ -112,7 +114,7 @@ export class Browser { protected _addHistory(): void { if (this._config.saveHistoryMode !== SAVE_HISTORY_MODE.NONE) { - this._callstackHistory = history.initCommandHistory(this._session); + this._callstackHistory = history.initCommandHistory(this._session as WebdriverIO.Browser); } } @@ -196,4 +198,8 @@ export class Browser { const allCustomCommands = Array.from(this._customCommands); return _.uniqWith(allCustomCommands, _.isEqual); } + + get emitter(): AsyncEmitter { + return this._emitter; + } } diff --git a/src/browser/calibrator.js b/src/browser/calibrator.js deleted file mode 100644 index ff37e9b30..000000000 --- a/src/browser/calibrator.js +++ /dev/null @@ -1,134 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const Promise = require("bluebird"); -const _ = require("lodash"); -const looksSame = require("looks-same"); -const { CoreError } = require("./core-error"); - -const DIRECTION = { FORWARD: "forward", REVERSE: "reverse" }; - -module.exports = class Calibrator { - constructor() { - this._cache = {}; - this._script = fs.readFileSync(path.join(__dirname, "client-scripts", "calibrate.js"), "utf8"); - } - - /** - * @param {Browser} browser - * @returns {Promise.} - */ - calibrate(browser) { - if (this._cache[browser.id]) { - return Promise.resolve(this._cache[browser.id]); - } - - return Promise.resolve(browser.open("about:blank")) - .then(() => browser.evalScript(this._script)) - .then(features => [features, browser.captureViewportImage()]) - .spread(async (features, image) => { - const { innerWidth, pixelRatio } = features; - const hasPixelRatio = Boolean(pixelRatio && pixelRatio > 1.0); - const imageFeatures = await this._analyzeImage(image, { calculateColorLength: hasPixelRatio }); - - if (!imageFeatures) { - return Promise.reject( - new CoreError( - "Could not calibrate. This could be due to calibration page has failed to open properly", - ), - ); - } - - features = _.extend(features, { - top: imageFeatures.viewportStart.y, - left: imageFeatures.viewportStart.x, - usePixelRatio: hasPixelRatio && imageFeatures.colorLength > innerWidth, - }); - - this._cache[browser.id] = features; - - return features; - }); - } - - async _analyzeImage(image, params) { - const imageHeight = (await image.getSize()).height; - - for (var y = 0; y < imageHeight; y++) { - var result = await analyzeRow(y, image, params); - - if (result) { - return result; - } - } - - return null; - } -}; - -async function analyzeRow(row, image, params = {}) { - const markerStart = await findMarkerInRow(row, image, DIRECTION.FORWARD); - - if (markerStart === -1) { - return null; - } - - const result = { viewportStart: { x: markerStart, y: row } }; - - if (!params.calculateColorLength) { - return result; - } - - const markerEnd = await findMarkerInRow(row, image, DIRECTION.REVERSE); - const colorLength = markerEnd - markerStart + 1; - - return _.extend(result, { colorLength }); -} - -async function findMarkerInRow(row, image, searchDirection) { - const imageWidth = (await image.getSize()).width; - const searchColor = { R: 148, G: 250, B: 0 }; - - if (searchDirection === DIRECTION.REVERSE) { - return searchReverse_(); - } else { - return searchForward_(); - } - - async function searchForward_() { - for (var x = 0; x < imageWidth; x++) { - var isSame = await compare_(x); - - if (isSame) { - return x; - } - } - return -1; - } - - async function searchReverse_() { - for (var x = imageWidth - 1; x >= 0; x--) { - var isSame = await compare_(x); - - if (isSame) { - return x; - } - } - return -1; - } - - async function compare_(x) { - var pixel = await image.getRGBA(x, row); - var color = pickRGB(pixel); - return looksSame.colors(color, searchColor); - } -} - -function pickRGB(rgba) { - return { - R: rgba.r, - G: rgba.g, - B: rgba.b, - }; -} diff --git a/src/browser/calibrator.ts b/src/browser/calibrator.ts new file mode 100644 index 000000000..80df29b09 --- /dev/null +++ b/src/browser/calibrator.ts @@ -0,0 +1,153 @@ +import fs from "fs"; +import path from "path"; +import looksSame from "looks-same"; +import { CoreError } from "./core-error"; +import { ExistingBrowser } from "./existing-browser"; +import { RGBA } from "../image"; + +const DIRECTION = { FORWARD: "forward", REVERSE: "reverse" } as const; + +interface BrowserFeatures { + needsCompatLib: boolean; + pixelRatio: number; + innerWidth: number; +} + +export interface CalibrationResult extends BrowserFeatures { + top: number; + left: number; + usePixelRatio: boolean; +} + +interface ViewportStart { + x: number; + y: number; +} + +interface ImageAnalysisResult { + viewportStart: ViewportStart; + colorLength?: number; +} + +interface Image { + getSize(): Promise<{ width: number; height: number }>; + getRGBA(x: number, y: number): Promise<{ r: number; g: number; b: number; a: number }>; +} + +export class Calibrator { + private _cache: Record; + private _script: string; + + constructor() { + this._cache = {}; + this._script = fs.readFileSync(path.join(__dirname, "client-scripts", "calibrate.js"), "utf8"); + } + + async calibrate(browser: ExistingBrowser): Promise { + if (this._cache[browser.id]) { + return this._cache[browser.id]; + } + + await browser.open("about:blank"); + const features = await browser.evalScript(this._script); + const image = await browser.captureViewportImage(); + + const { innerWidth, pixelRatio } = features; + const hasPixelRatio = Boolean(pixelRatio && pixelRatio > 1.0); + const imageFeatures = await this._analyzeImage(image, { calculateColorLength: hasPixelRatio }); + + if (!imageFeatures) { + throw new CoreError( + "Could not calibrate. This could be due to calibration page has failed to open properly", + ); + } + + const calibratedFeatures: CalibrationResult = { + ...features, + top: imageFeatures.viewportStart.y, + left: imageFeatures.viewportStart.x, + usePixelRatio: hasPixelRatio && imageFeatures.colorLength! > innerWidth, + }; + + this._cache[browser.id] = calibratedFeatures; + return calibratedFeatures; + } + + private async _analyzeImage( + image: Image, + params: { calculateColorLength?: boolean }, + ): Promise { + const imageHeight = (await image.getSize()).height; + + for (let y = 0; y < imageHeight; y++) { + const result = await analyzeRow(y, image, params); + if (result) { + return result; + } + } + + return null; + } +} + +async function analyzeRow( + row: number, + image: Image, + params: { calculateColorLength?: boolean } = {}, +): Promise { + const markerStart = await findMarkerInRow(row, image, DIRECTION.FORWARD); + + if (markerStart === -1) { + return null; + } + + const result: ImageAnalysisResult = { viewportStart: { x: markerStart, y: row } }; + + if (!params.calculateColorLength) { + return result; + } + + const markerEnd = await findMarkerInRow(row, image, DIRECTION.REVERSE); + const colorLength = markerEnd - markerStart + 1; + + return { ...result, colorLength }; +} + +async function findMarkerInRow(row: number, image: Image, searchDirection: "forward" | "reverse"): Promise { + const imageWidth = (await image.getSize()).width; + const searchColor = { R: 148, G: 250, B: 0 }; + + if (searchDirection === DIRECTION.REVERSE) { + return searchReverse_(); + } else { + return searchForward_(); + } + + async function searchForward_(): Promise { + for (let x = 0; x < imageWidth; x++) { + if (await compare_(x)) { + return x; + } + } + return -1; + } + + async function searchReverse_(): Promise { + for (let x = imageWidth - 1; x >= 0; x--) { + if (await compare_(x)) { + return x; + } + } + return -1; + } + + async function compare_(x: number): Promise { + const pixel = await image.getRGBA(x, row); + const color = pickRGB(pixel); + return looksSame.colors(color, searchColor); + } +} + +function pickRGB(rgba: RGBA): { R: number; G: number; B: number } { + return { R: rgba.r, G: rgba.g, B: rgba.b }; +} diff --git a/src/browser/camera/index.js b/src/browser/camera/index.ts similarity index 57% rename from src/browser/camera/index.js rename to src/browser/camera/index.ts index 8ff819795..ccb494489 100644 --- a/src/browser/camera/index.js +++ b/src/browser/camera/index.ts @@ -1,30 +1,52 @@ -"use strict"; - -const Image = require("../../image"); -const _ = require("lodash"); -const utils = require("./utils"); - -module.exports = class Camera { - static create(screenshotMode, takeScreenshot) { +import _ from "lodash"; +import { Image } from "../../image"; +import * as utils from "./utils"; + +export interface ImageArea { + left: number; + top: number; + width: number; + height: number; +} + +export type ScreenshotMode = "fullpage" | "viewport" | "auto"; + +export interface PageMeta { + viewport: ImageArea; + documentHeight: number; + documentWidth: number; +} + +interface Calibration { + left: number; + top: number; +} + +export class Camera { + private _screenshotMode: ScreenshotMode; + private _takeScreenshot: () => Promise; + private _calibration: Calibration | null; + + static create(screenshotMode: ScreenshotMode, takeScreenshot: () => Promise): Camera { return new this(screenshotMode, takeScreenshot); } - constructor(screenshotMode, takeScreenshot) { + constructor(screenshotMode: ScreenshotMode, takeScreenshot: () => Promise) { this._screenshotMode = screenshotMode; this._takeScreenshot = takeScreenshot; this._calibration = null; } - calibrate(calibration) { + calibrate(calibration: Calibration): void { this._calibration = calibration; } - async captureViewportImage(page) { + async captureViewportImage(page?: PageMeta): Promise { const base64 = await this._takeScreenshot(); const image = Image.fromBase64(base64); const { width, height } = await image.getSize(); - const imageArea = { left: 0, top: 0, width, height }; + const imageArea: ImageArea = { left: 0, top: 0, width, height }; const calibratedArea = this._calibrateArea(imageArea); const viewportCroppedArea = this._cropAreaToViewport(calibratedArea, page); @@ -36,7 +58,7 @@ module.exports = class Camera { return image; } - _calibrateArea(imageArea) { + private _calibrateArea(imageArea: ImageArea): ImageArea { if (!this._calibration) { return imageArea; } @@ -46,7 +68,7 @@ module.exports = class Camera { return { left, top, width: imageArea.width - left, height: imageArea.height - top }; } - _cropAreaToViewport(imageArea, page) { + private _cropAreaToViewport(imageArea: ImageArea, page?: PageMeta): ImageArea { if (!page) { return imageArea; } @@ -65,4 +87,4 @@ module.exports = class Camera { height: Math.min(imageArea.height - cropArea.top, cropArea.height), }; } -}; +} diff --git a/src/browser/camera/utils.js b/src/browser/camera/utils.js deleted file mode 100644 index f3485a21d..000000000 --- a/src/browser/camera/utils.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; - -exports.isFullPage = (imageArea, page, screenshotMode) => { - switch (screenshotMode) { - case "fullpage": - return true; - case "viewport": - return false; - case "auto": - return compareDimensions(imageArea, page); - } -}; - -/** - * @param {Object} imageArea - area - * @param {number} imageArea.left - left offset - * @param {number} imageArea.top - top offset - * @param {number} imageArea.width - area width - * @param {number} imageArea.height - area height - * @param {Object} page - capture meta information object - * @returns {boolean} - * @private - */ -function compareDimensions(imageArea, page) { - return imageArea.height >= page.documentHeight && imageArea.width >= page.documentWidth; -} diff --git a/src/browser/camera/utils.ts b/src/browser/camera/utils.ts new file mode 100644 index 000000000..36fa31303 --- /dev/null +++ b/src/browser/camera/utils.ts @@ -0,0 +1,16 @@ +import { ImageArea, PageMeta, ScreenshotMode } from "."; + +export const isFullPage = (imageArea: ImageArea, page: PageMeta, screenshotMode: ScreenshotMode): boolean => { + switch (screenshotMode) { + case "fullpage": + return true; + case "viewport": + return false; + case "auto": + return compareDimensions(imageArea, page); + } +}; + +function compareDimensions(imageArea: ImageArea, page: PageMeta): boolean { + return imageArea.height >= page.documentHeight && imageArea.width >= page.documentWidth; +} diff --git a/src/browser/client-bridge/client-bridge.js b/src/browser/client-bridge/client-bridge.js deleted file mode 100644 index aafbffb2a..000000000 --- a/src/browser/client-bridge/client-bridge.js +++ /dev/null @@ -1,50 +0,0 @@ -"use strict"; - -const { ClientBridgeError } = require("./error"); - -module.exports = class ClientBridge { - static create(browser, script) { - return new ClientBridge(browser, script); - } - - constructor(browser, script) { - this._browser = browser; - this._script = script; - } - - call(name, args = []) { - return this._callCommand(this._clientMethodCommand(name, args), true); - } - - _callCommand(command, injectAllowed) { - return this._browser - .evalScript(command) - .then(result => { - if (!result || !result.isClientScriptNotInjected) { - return Promise.resolve(result); - } - - if (injectAllowed) { - return this._inject().then(() => this._callCommand(command, false)); - } - - return Promise.reject(new ClientBridgeError("Unable to inject client script")); - }) - .catch(e => Promise.reject(new ClientBridgeError(e.message))); - } - - _clientMethodCommand(name, args) { - const params = args.map(JSON.stringify).join(", "); - const call = `__geminiCore.${name}(${params})`; - - return this._guardClientCall(call); - } - - _guardClientCall(call) { - return `typeof __geminiCore !== "undefined" ? ${call} : {isClientScriptNotInjected: true}`; - } - - _inject() { - return this._browser.injectScript(this._script); - } -}; diff --git a/src/browser/client-bridge/client-bridge.ts b/src/browser/client-bridge/client-bridge.ts new file mode 100644 index 000000000..5b9febd26 --- /dev/null +++ b/src/browser/client-bridge/client-bridge.ts @@ -0,0 +1,53 @@ +import { ClientBridgeError } from "./error"; +import { ExistingBrowser } from "../existing-browser"; + +export class ClientBridge { + private _browser: ExistingBrowser; + private _script: string; + + static create(browser: ExistingBrowser, script: string): ClientBridge { + return new ClientBridge(browser, script); + } + + constructor(browser: ExistingBrowser, script: string) { + this._browser = browser; + this._script = script; + } + + async call(name: string, args: unknown[] = []): Promise { + return this._callCommand(this._clientMethodCommand(name, args), true); + } + + private async _callCommand(command: string, injectAllowed: boolean): Promise { + try { + const result = await this._browser.evalScript<{ isClientScriptNotInjected?: boolean }>(command); + + if (!result || !result.isClientScriptNotInjected) { + return result as T; + } + + if (injectAllowed) { + await this._inject(); + return this._callCommand(command, false); + } + + throw new ClientBridgeError("Unable to inject client script"); + } catch (e) { + throw new ClientBridgeError((e as Error).message); + } + } + + private _clientMethodCommand(name: string, args: unknown[]): string { + const params = args.map(arg => JSON.stringify(arg)).join(", "); + const call = `__geminiCore.${name}(${params})`; + return this._guardClientCall(call); + } + + private _guardClientCall(call: string): string { + return `typeof __geminiCore !== "undefined" ? ${call} : {isClientScriptNotInjected: true}`; + } + + private async _inject(): Promise { + await this._browser.injectScript(this._script); + } +} diff --git a/src/browser/client-bridge/index.js b/src/browser/client-bridge/index.ts similarity index 51% rename from src/browser/client-bridge/index.js rename to src/browser/client-bridge/index.ts index dffe53459..d89d29ae8 100644 --- a/src/browser/client-bridge/index.js +++ b/src/browser/client-bridge/index.ts @@ -1,16 +1,17 @@ -"use strict"; +import path from "path"; +import fs from "fs"; +import { ClientBridge } from "./client-bridge"; +import { ExistingBrowser } from "../existing-browser"; -const path = require("path"); -const fs = require("fs"); -const ClientBridge = require("./client-bridge"); +const bundlesCache: Record = {}; -const bundlesCache = {}; - -exports.ClientBridge = ClientBridge; - -exports.build = async (browser, opts = {}) => { - const needsCompatLib = opts.calibration && opts.calibration.needsCompatLib; +export { ClientBridge }; +export const build = async ( + browser: ExistingBrowser, + opts: { calibration?: { needsCompatLib?: boolean } } = {}, +): Promise => { + const needsCompatLib = opts.calibration?.needsCompatLib ?? false; const scriptFileName = needsCompatLib ? "bundle.compat.js" : "bundle.native.js"; if (bundlesCache[scriptFileName]) { diff --git a/src/browser/commands/assert-view/errors/image-diff-error.ts b/src/browser/commands/assert-view/errors/image-diff-error.ts index 82635ba1d..5d75a8719 100644 --- a/src/browser/commands/assert-view/errors/image-diff-error.ts +++ b/src/browser/commands/assert-view/errors/image-diff-error.ts @@ -1,6 +1,6 @@ import { ImageInfo, RefImageInfo } from "../../../../types"; -import Image from "../../../../image"; +import { Image } from "../../../../image"; import { BaseStateError } from "./base-state-error"; import type { LooksSameOptions, LooksSameResult } from "looks-same"; diff --git a/src/browser/commands/assert-view/index.js b/src/browser/commands/assert-view/index.js index 086dee051..808d083d1 100644 --- a/src/browser/commands/assert-view/index.js +++ b/src/browser/commands/assert-view/index.js @@ -5,7 +5,7 @@ const path = require("path"); const _ = require("lodash"); const Promise = require("bluebird"); const { pngValidator: validatePng } = require("png-validator"); -const Image = require("../../../image"); +const { Image } = require("../../../image"); const ScreenShooter = require("../../screen-shooter"); const temp = require("../../../temp"); const { getCaptureProcessors } = require("./capture-processors"); diff --git a/src/browser/commands/clearSession.ts b/src/browser/commands/clearSession.ts index d880f1c18..b4ff88eb6 100644 --- a/src/browser/commands/clearSession.ts +++ b/src/browser/commands/clearSession.ts @@ -1,5 +1,5 @@ import type { Browser } from "../types"; -import logger from "../../utils/logger"; +import * as logger from "../../utils/logger"; export default (browser: Browser): void => { const { publicAPI: session } = browser; diff --git a/src/browser/commands/index.js b/src/browser/commands/index.ts similarity index 81% rename from src/browser/commands/index.js rename to src/browser/commands/index.ts index 36d2a5d8a..605b1ae57 100644 --- a/src/browser/commands/index.js +++ b/src/browser/commands/index.ts @@ -1,6 +1,4 @@ -"use strict"; - -module.exports = [ +export const customCommandFileNames = [ "assert-view", "clearSession", "getConfig", diff --git a/src/browser/commands/switchToRepl.ts b/src/browser/commands/switchToRepl.ts index 242f385ee..2333324f4 100644 --- a/src/browser/commands/switchToRepl.ts +++ b/src/browser/commands/switchToRepl.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { getEventListeners } from "node:events"; import chalk from "chalk"; import RuntimeConfig from "../../config/runtime-config"; -import logger from "../../utils/logger"; +import * as logger from "../../utils/logger"; import type { Browser } from "../types"; const REPL_LINE_EVENT = "line"; diff --git a/src/browser/existing-browser.js b/src/browser/existing-browser.ts similarity index 52% rename from src/browser/existing-browser.js rename to src/browser/existing-browser.ts index 8a2e95f10..0b81e1a2e 100644 --- a/src/browser/existing-browser.js +++ b/src/browser/existing-browser.ts @@ -1,39 +1,81 @@ -/* global window, document */ -"use strict"; - -const url = require("url"); -const Promise = require("bluebird"); -const _ = require("lodash"); -const webdriverio = require("webdriverio"); -const { sessionEnvironmentDetector } = require("../bundle/@wdio-utils"); -const { Browser } = require("./browser"); -const commandsList = require("./commands"); -const Camera = require("./camera"); -const clientBridge = require("./client-bridge"); -const history = require("./history"); -const logger = require("../utils/logger"); -const { WEBDRIVER_PROTOCOL } = require("../constants/config"); -const { MIN_CHROME_VERSION_SUPPORT_ISOLATION } = require("../constants/browser"); -const { isSupportIsolation } = require("../utils/browser"); -const { isRunInNodeJsEnv } = require("../utils/config"); +import url from "url"; +import _ from "lodash"; +import type { AttachOptions, ChainablePromiseArray, ElementArray } from "webdriverio"; +import { attach } from "webdriverio"; +import { sessionEnvironmentDetector } from "../bundle/@wdio-utils"; +import { Browser, BrowserOpts } from "./browser"; +import { customCommandFileNames } from "./commands"; +import { Camera, PageMeta } from "./camera"; +import { type ClientBridge, build as buildClientBridge } from "./client-bridge"; +import * as history from "./history"; +import * as logger from "../utils/logger"; +import { WEBDRIVER_PROTOCOL } from "../constants/config"; +import { MIN_CHROME_VERSION_SUPPORT_ISOLATION } from "../constants/browser"; +import { isSupportIsolation } from "../utils/browser"; +import { isRunInNodeJsEnv } from "../utils/config"; +import { Config } from "../config"; +import { Image, Rect } from "../image"; +import type { CalibrationResult, Calibrator } from "./calibrator"; +import { NEW_ISSUE_LINK } from "../constants/help"; +import type { Options } from "@wdio/types"; const OPTIONAL_SESSION_OPTS = ["transformRequest", "transformResponse"]; -module.exports = class ExistingBrowser extends Browser { - static create(config, opts) { - return new this(config, opts); +interface SessionOptions { + sessionId: string; + sessionCaps?: WebdriverIO.Capabilities; + sessionOpts?: Options.WebdriverIO; +} + +interface PrepareScreenshotOpts { + disableAnimation?: boolean; + // TODO: specify the rest of the options +} + +interface ClientBridgeErrorData { + error: string; + message: string; +} + +interface ScrollByParams { + x: number; + y: number; + selector?: string; +} + +const BROWSER_SESSION_HINT = "browser session"; +const CLIENT_BRIDGE_HINT = "client bridge"; + +function ensure(value: T | undefined | null, hint?: string): asserts value is T { + 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 ${NEW_ISSUE_LINK}, we will try to fix it as soon as possible.`, + ); } +} + +const isClientBridgeErrorData = (data: unknown): data is ClientBridgeErrorData => { + return Boolean(data && (data as ClientBridgeErrorData).error && (data as ClientBridgeErrorData).message); +}; - constructor(config, opts) { +export class ExistingBrowser extends Browser { + protected _camera: Camera; + protected _meta: Record; + protected _calibration?: CalibrationResult; + protected _clientBridge?: ClientBridge; + + constructor(config: Config, opts: BrowserOpts) { super(config, opts); - this._emitter = opts.emitter; this._camera = Camera.create(this._config.screenshotMode, () => this._takeScreenshot()); this._meta = this._initMeta(); } - async init({ sessionId, sessionCaps, sessionOpts } = {}, calibrator) { + async init({ sessionId, sessionCaps, sessionOpts }: SessionOptions, calibrator: Calibrator): Promise { this._session = await this._attachSession({ sessionId, sessionCaps, sessionOpts }); if (!isRunInNodeJsEnv(this._config)) { @@ -46,13 +88,12 @@ module.exports = class ExistingBrowser extends Browser { await history.runGroup(this._callstackHistory, "testplane: init browser", async () => { this._addCommands(); - await this._performIsolation({ sessionCaps, sessionOpts }); try { this.config.prepareBrowser && this.config.prepareBrowser(this.publicAPI); - } catch (e) { - logger.warn(`WARN: couldn't prepare browser ${this.id}\n`, e.stack); + } catch (e: unknown) { + logger.warn(`WARN: couldn't prepare browser ${this.id}\n`, (e as Error)?.stack); } await this._prepareSession(); @@ -63,7 +104,7 @@ module.exports = class ExistingBrowser extends Browser { return this; } - markAsBroken() { + markAsBroken(): void { if (this.state.isBroken) { return; } @@ -73,17 +114,18 @@ module.exports = class ExistingBrowser extends Browser { this._stubCommands(); } - quit() { + quit(): void { this._meta = this._initMeta(); } - async prepareScreenshot(selectors, opts = {}) { + async prepareScreenshot(selectors: string[] | Rect[], opts: PrepareScreenshotOpts = {}): Promise { opts = _.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 (result.error) { + if (isClientBridgeErrorData(result)) { throw new Error( `Prepare screenshot failed with error type '${result.error}' and error message: ${result.message}`, ); @@ -97,34 +139,43 @@ module.exports = class ExistingBrowser extends Browser { return result; } - async cleanupScreenshot(opts = {}) { + async cleanupScreenshot(opts: { disableAnimation?: boolean } = {}): Promise { if (opts.disableAnimation) { await this._cleanupPageAnimations(); } } - open(url) { + open(url: string): Promise { + ensure(this._session, BROWSER_SESSION_HINT); + return this._session.url(url); } - evalScript(script) { + evalScript(script: string): Promise { + ensure(this._session, BROWSER_SESSION_HINT); + return this._session.execute(`return ${script}`); } - injectScript(script) { + injectScript(script: string): Promise { + ensure(this._session, BROWSER_SESSION_HINT); + return this._session.execute(script); } - async captureViewportImage(page, screenshotDelay) { + async captureViewportImage(page?: PageMeta, screenshotDelay?: number): Promise { if (screenshotDelay) { - await Promise.delay(screenshotDelay); + await new Promise(resolve => setTimeout(resolve, screenshotDelay)); } return this._camera.captureViewportImage(page); } - scrollBy(params) { + scrollBy(params: ScrollByParams): Promise { + 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) { @@ -150,13 +201,17 @@ module.exports = class ExistingBrowser extends Browser { }, params); } - _attachSession({ sessionId, sessionCaps, sessionOpts = {} }) { + protected async _attachSession({ + sessionId, + sessionCaps, + sessionOpts = { capabilities: {} }, + }: SessionOptions): Promise { const detectedSessionEnvFlags = sessionEnvironmentDetector({ - capabilities: sessionCaps, + capabilities: sessionCaps!, requestedCapabilities: sessionOpts.capabilities, }); - const opts = { + const opts: AttachOptions = { sessionId, ...sessionOpts, ...this._getSessionOptsFromConfig(OPTIONAL_SESSION_OPTS), @@ -167,10 +222,10 @@ module.exports = class ExistingBrowser extends Browser { requestedCapabilities: sessionOpts.capabilities, }; - return webdriverio.attach(opts); + return attach(opts); } - _initMeta() { + protected _initMeta(): Record { return { pid: process.pid, browserVersion: this.version, @@ -180,53 +235,51 @@ module.exports = class ExistingBrowser extends Browser { }; } - _takeScreenshot() { + protected _takeScreenshot(): Promise { + ensure(this._session, BROWSER_SESSION_HINT); return this._session.takeScreenshot(); } - _addCommands() { + protected _addCommands(): void { + 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); - commandsList.forEach(command => require(`./commands/${command}`).default(this)); + // eslint-disable-next-line @typescript-eslint/no-var-requires + customCommandFileNames.forEach(command => require(`./commands/${command}`).default(this)); super._addCommands(); } - _overrideGetElementsList(session) { - 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; - return arr; - }); - 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; - return arr; - }, - true, - ); + protected _overrideGetElementsList(session: WebdriverIO.Browser): void { + // 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): ChainablePromiseArray => { + const arr: WebdriverIO.Element[] & { parent?: unknown; foundWith?: unknown; selector?: unknown } = + []; + 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; + + return arr as unknown as ChainablePromiseArray; + }, + attachToElement, + ); + } } - _addMetaAccessCommands(session) { + protected _addMetaAccessCommands(session: WebdriverIO.Browser): void { session.addCommand("setMeta", (key, value) => (this._meta[key] = value)); session.addCommand("getMeta", key => (key ? this._meta[key] : this._meta)); } - _decorateUrlMethod(session) { + protected _decorateUrlMethod(session: WebdriverIO.Browser): void { session.overwriteCommand("url", async (origUrlFn, uri) => { if (!uri) { return session.getUrl(); @@ -253,19 +306,25 @@ module.exports = class ExistingBrowser extends Browser { }); } - _resolveUrl(uri) { + protected _resolveUrl(uri: string): string { return this._config.baseUrl ? url.resolve(this._config.baseUrl, uri) : uri; } - async _performIsolation({ sessionCaps, sessionOpts }) { + protected async _performIsolation({ + sessionCaps, + sessionOpts, + }: Pick): Promise { + ensure(this._session, BROWSER_SESSION_HINT); if (!this._config.isolation) { return; } - const { browserName, browserVersion = "", version = "" } = sessionCaps; - const { automationProtocol } = sessionOpts; - - if (!isSupportIsolation(browserName, browserVersion)) { + const { + browserName, + browserVersion = "", + version = "", + } = (sessionCaps as SessionOptions["sessionCaps"] & { version?: string }) || {}; + if (!isSupportIsolation(browserName!, browserVersion)) { logger.warn( `WARN: test isolation works only with chrome@${MIN_CHROME_VERSION_SUPPORT_ISOLATION} and higher, ` + `but got ${browserName}@${browserVersion || version}`, @@ -279,11 +338,11 @@ module.exports = class ExistingBrowser extends Browser { const incognitoCtx = await puppeteer.createIncognitoBrowserContext(); const page = await incognitoCtx.newPage(); - if (automationProtocol === WEBDRIVER_PROTOCOL) { + if (sessionOpts?.automationProtocol === WEBDRIVER_PROTOCOL) { const windowIds = await this._session.getWindowHandles(); const incognitoWindowId = windowIds.find(id => id.includes(page.target()._targetId)); - await this._session.switchToWindow(incognitoWindowId); + await this._session.switchToWindow(incognitoWindowId!); } for (const ctx of browserCtxs) { @@ -298,24 +357,26 @@ module.exports = class ExistingBrowser extends Browser { } } - async _prepareSession() { + protected async _prepareSession(): Promise { await this._setOrientation(this.config.orientation); await this._setWindowSize(this.config.windowSize); } - async _setOrientation(orientation) { + protected async _setOrientation(orientation: string | null): Promise { if (orientation) { + ensure(this._session, BROWSER_SESSION_HINT); await this._session.setOrientation(orientation); } } - async _setWindowSize(size) { + protected async _setWindowSize(size: { width: number; height: number } | null): Promise { if (size) { + ensure(this._session, BROWSER_SESSION_HINT); await this._session.setWindowSize(size.width, size.height); } } - _performCalibration(calibrator) { + protected async _performCalibration(calibrator: Calibrator): Promise { if (!this.config.calibrate || this._calibration) { return Promise.resolve(); } @@ -326,13 +387,14 @@ module.exports = class ExistingBrowser extends Browser { }); } - _buildClientScripts() { - return clientBridge - .build(this, { calibration: this._calibration }) - .then(clientBridge => (this._clientBridge = clientBridge)); + protected async _buildClientScripts(): Promise { + return buildClientBridge(this, { calibration: this._calibration }).then( + clientBridge => (this._clientBridge = clientBridge), + ); } - async _runInEachIframe(cb) { + protected async _runInEachIframe(cb: (...args: unknown[]) => unknown): Promise { + ensure(this._session, BROWSER_SESSION_HINT); const iframes = await this._session.findElements("css selector", "iframe"); try { @@ -348,10 +410,11 @@ module.exports = class ExistingBrowser extends Browser { } } - async _disableFrameAnimations() { - const result = await this._clientBridge.call("disableFrameAnimations"); + protected async _disableFrameAnimations(): Promise { + ensure(this._clientBridge, CLIENT_BRIDGE_HINT); + const result = await this._clientBridge.call("disableFrameAnimations"); - if (result && result.error) { + if (isClientBridgeErrorData(result)) { throw new Error( `Disable animations failed with error type '${result.error}' and error message: ${result.message}`, ); @@ -360,19 +423,21 @@ module.exports = class ExistingBrowser extends Browser { return result; } - async _disableIframeAnimations() { + protected async _disableIframeAnimations(): Promise { await this._runInEachIframe(() => this._disableFrameAnimations()); } - async _cleanupFrameAnimations() { + protected async _cleanupFrameAnimations(): Promise { + ensure(this._clientBridge, CLIENT_BRIDGE_HINT); + return this._clientBridge.call("cleanupFrameAnimations"); } - async _cleanupIframeAnimations() { + protected async _cleanupIframeAnimations(): Promise { await this._runInEachIframe(() => this._cleanupFrameAnimations()); } - async _cleanupPageAnimations() { + protected async _cleanupPageAnimations(): Promise { await this._cleanupFrameAnimations(); if (this._config.automationProtocol === WEBDRIVER_PROTOCOL) { @@ -380,24 +445,23 @@ module.exports = class ExistingBrowser extends Browser { } } - _stubCommands() { - for (let commandName of this._session.commandList) { + _stubCommands(): void { + if (!this._session) { + return; + } + + for (const commandName of this._session.commandList) { if (commandName === "deleteSession") { continue; } - if (_.isFunction(this._session[commandName])) { - // eslint-disable-next-line @typescript-eslint/no-empty-function - this._session.overwriteCommand(commandName, () => {}); + if (_.isFunction(this._session[commandName as keyof WebdriverIO.Browser])) { + this._session.overwriteCommand(commandName as WebdriverIO.BrowserCommand, () => {}); } } } - get meta() { + get meta(): Record { return this._meta; } - - get emitter() { - return this._emitter; - } -}; +} diff --git a/src/browser/history/callstack.js b/src/browser/history/callstack.ts similarity index 53% rename from src/browser/history/callstack.js rename to src/browser/history/callstack.ts index 87cd67376..4ec94434a 100644 --- a/src/browser/history/callstack.js +++ b/src/browser/history/callstack.ts @@ -1,15 +1,16 @@ -"use strict"; +import _ from "lodash"; +import { TestStepKey, TestStep } from "../../types"; -const _ = require("lodash"); -const { TestStepKey } = require("../../types"); +export class Callstack { + private _history: TestStep[]; + private _stack: TestStep[]; -module.exports = class Callstack { constructor() { this._history = []; this._stack = []; } - enter(data) { + enter(data: Omit): void { this._stack.push({ ...data, [TestStepKey.TimeStart]: Date.now(), @@ -17,7 +18,7 @@ module.exports = class Callstack { }); } - leave(key) { + leave(key: symbol): void { const currentNodeIndex = _.findLastIndex(this._stack, node => node[TestStepKey.Key] === key); const wasRemovedByParent = currentNodeIndex === -1; @@ -26,31 +27,35 @@ module.exports = class Callstack { } const removedNodes = this._stack.splice(currentNodeIndex); - const currentNode = _.first(removedNodes); - const parentNode = _.last(this._stack); + const currentNode = _.first(removedNodes) as TestStep; + const parentNode = _.last(this._stack) as TestStep | undefined; const isCurrentNodeRoot = this._stack.length === 0; currentNode[TestStepKey.TimeEnd] = Date.now(); currentNode[TestStepKey.Duration] = currentNode[TestStepKey.TimeEnd] - currentNode[TestStepKey.TimeStart]; - isCurrentNodeRoot ? this._history.push(currentNode) : parentNode[TestStepKey.Children].push(currentNode); + if (isCurrentNodeRoot) { + this._history.push(currentNode); + } else { + parentNode![TestStepKey.Children].push(currentNode); + } } - markError(shouldPropagateFn) { - let parentNode = null; - let currentNode = _.first(this._stack); + markError(shouldPropagateFn: (parentNode: TestStep, currentNode: TestStep) => boolean): void { + let parentNode: TestStep | null = null; + let currentNode: TestStep | undefined = _.first(this._stack); let shouldContinue = Boolean(currentNode); - while (shouldContinue) { + while (shouldContinue && currentNode) { currentNode[TestStepKey.IsFailed] = true; parentNode = currentNode; currentNode = _.last(currentNode[TestStepKey.Children]); - shouldContinue = currentNode && shouldPropagateFn(parentNode, currentNode); + shouldContinue = Boolean(currentNode && shouldPropagateFn(parentNode, currentNode)); } } - release() { + release(): TestStep[] { const history = this._history; this._stack = []; @@ -58,4 +63,4 @@ module.exports = class Callstack { return history; } -}; +} diff --git a/src/browser/history/index.js b/src/browser/history/index.js deleted file mode 100644 index 424ac9060..000000000 --- a/src/browser/history/index.js +++ /dev/null @@ -1,133 +0,0 @@ -"use strict"; - -const Callstack = require("./callstack"); -const cmds = require("./commands"); -const { runWithHooks, normalizeCommandArgs, isGroup } = require("./utils"); -const { TestStepKey } = require("./../../types"); - -const shouldNotWrapCommand = commandName => - ["addCommand", "overwriteCommand", "extendOptions", "setMeta", "getMeta", "runStep"].includes(commandName); - -const mkHistoryNode = ({ name, args, elementScope, key, overwrite, isGroup }) => { - const map = { - [TestStepKey.Name]: name, - [TestStepKey.Args]: normalizeCommandArgs(name, args), - [TestStepKey.Scope]: cmds.createScope(elementScope), - [TestStepKey.Key]: key, - }; - - if (overwrite) { - map[TestStepKey.IsOverwritten] = Number(overwrite); - } - - if (isGroup) { - map[TestStepKey.IsGroup] = true; - } - - return map; -}; - -const runWithHistoryHooks = ({ callstack, nodeData, fn }) => { - nodeData.key = nodeData.key || Symbol(); - - return runWithHooks({ - before: () => callstack.enter(mkHistoryNode(nodeData)), - fn, - after: () => callstack.leave(nodeData.key), - error: () => callstack.markError(exports.shouldPropagateFn), - }); -}; - -const overwriteAddCommand = (session, callstack) => - session.overwriteCommand("addCommand", (origCommand, name, wrapper, elementScope) => { - if (shouldNotWrapCommand(name)) { - return origCommand(name, wrapper, elementScope); - } - - function decoratedWrapper(...args) { - return runWithHistoryHooks({ - callstack, - nodeData: { name, args, elementScope, overwrite: false }, - fn: () => wrapper.apply(this, args), - }); - } - - return origCommand(name, decoratedWrapper, elementScope); - }); - -const overwriteOverwriteCommand = (session, callstack) => - session.overwriteCommand("overwriteCommand", (origCommand, name, wrapper, elementScope) => { - if (shouldNotWrapCommand(name)) { - return origCommand(name, wrapper, elementScope); - } - - function decoratedWrapper(origFn, ...args) { - return runWithHistoryHooks({ - callstack, - nodeData: { name, args, elementScope, overwrite: true }, - fn: () => wrapper.apply(this, [origFn, ...args]), - }); - } - - return origCommand(name, decoratedWrapper, elementScope); - }); - -const overwriteCommands = ({ session, callstack, commands, elementScope }) => - commands.forEach(name => { - function decoratedWrapper(origFn, ...args) { - return runWithHistoryHooks({ - callstack, - nodeData: { name, args, elementScope, overwrite: false }, - fn: () => origFn(...args), - }); - } - - session.overwriteCommand(name, decoratedWrapper, elementScope); - }); - -const overwriteBrowserCommands = (session, callstack) => - overwriteCommands({ - session, - callstack, - commands: cmds.getBrowserCommands().filter(cmd => !shouldNotWrapCommand(cmd)), - elementScope: false, - }); - -const overwriteElementCommands = (session, callstack) => - overwriteCommands({ - session, - callstack, - commands: cmds.getElementCommands(), - elementScope: true, - }); - -const overwriteRunStepCommand = (session, callstack) => - session.overwriteCommand("runStep", (origCommand, stepName, stepCb) => { - return exports.runGroup(callstack, stepName, () => origCommand(stepName, stepCb)); - }); - -exports.initCommandHistory = session => { - const callstack = new Callstack(); - - overwriteAddCommand(session, callstack); - overwriteBrowserCommands(session, callstack); - overwriteElementCommands(session, callstack); - overwriteOverwriteCommand(session, callstack); - overwriteRunStepCommand(session, callstack); - - return callstack; -}; - -exports.runGroup = (callstack, name, fn) => { - if (!callstack) { - return fn(); - } - - return runWithHistoryHooks({ - callstack, - nodeData: { name, isGroup: true }, - fn, - }); -}; - -exports.shouldPropagateFn = (parentNode, currentNode) => isGroup(parentNode) || isGroup(currentNode); diff --git a/src/browser/history/index.ts b/src/browser/history/index.ts new file mode 100644 index 000000000..b643c213c --- /dev/null +++ b/src/browser/history/index.ts @@ -0,0 +1,164 @@ +import { Callstack } from "./callstack"; +import * as cmds from "./commands"; +import { runWithHooks, normalizeCommandArgs, isGroup } from "./utils"; +import { TestStepKey, TestStep } from "../../types"; + +interface NodeData { + name: string; + args: unknown[]; + elementScope?: boolean; + isGroup?: boolean; + key?: symbol; + overwrite?: boolean; +} + +const shouldNotWrapCommand = (commandName: string): boolean => + ["addCommand", "overwriteCommand", "extendOptions", "setMeta", "getMeta", "runStep"].includes(commandName); + +export const shouldPropagateFn = (parentNode: TestStep, currentNode: TestStep): boolean => + isGroup(parentNode) || isGroup(currentNode); + +const mkHistoryNode = ({ name, args, elementScope, key, overwrite, isGroup }: NodeData): TestStep => { + const map: Partial = { + [TestStepKey.Name]: name!, + [TestStepKey.Args]: normalizeCommandArgs(name!, args), + [TestStepKey.Scope]: cmds.createScope(elementScope!), + [TestStepKey.Key]: key ?? Symbol(), + }; + + if (overwrite) { + map[TestStepKey.IsOverwritten] = Boolean(overwrite); + } + + if (isGroup) { + map[TestStepKey.IsGroup] = true; + } + + return map as TestStep; +}; + +interface RunWithHistoryHooksData { + callstack: Callstack; + nodeData: NodeData; + fn: () => T; +} + +const runWithHistoryHooks = ({ callstack, nodeData, fn }: RunWithHistoryHooksData): T => { + nodeData.key = nodeData.key ?? Symbol(); + + return runWithHooks({ + before: () => callstack.enter(mkHistoryNode(nodeData)), + fn, + after: () => callstack.leave(nodeData.key!), + error: () => callstack.markError(shouldPropagateFn), + }); +}; + +const overwriteAddCommand = (session: WebdriverIO.Browser, callstack: Callstack): void => { + session.overwriteCommand("addCommand", (origCommand, name, wrapper, elementScope) => { + if (shouldNotWrapCommand(name)) { + return origCommand(name, wrapper, elementScope); + } + + function decoratedWrapper(this: WebdriverIO.Browser, ...args: unknown[]): unknown { + return runWithHistoryHooks({ + callstack, + nodeData: { name, args, elementScope, overwrite: false }, + fn: () => wrapper.apply(this, args), + }); + } + + return origCommand(name, decoratedWrapper, elementScope); + }); +}; + +const overwriteOverwriteCommand = (session: WebdriverIO.Browser, callstack: Callstack): void => { + session.overwriteCommand("overwriteCommand", (origCommand, name, wrapper, elementScope) => { + if (shouldNotWrapCommand(name)) { + return origCommand(name, wrapper, elementScope); + } + + function decoratedWrapper( + this: WebdriverIO.Browser, + origFn: (...args: unknown[]) => unknown, + ...args: unknown[] + ): unknown { + return runWithHistoryHooks({ + callstack, + nodeData: { name, args, elementScope, overwrite: true }, + fn: () => (wrapper as (...args: unknown[]) => unknown).apply(this, [origFn, ...args]), + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return origCommand(name, decoratedWrapper as any, elementScope); + }); +}; + +interface OverwriteCommandsData { + session: WebdriverIO.Browser; + callstack: Callstack; + commands: string[]; + elementScope: boolean; +} + +const overwriteCommands = ({ session, callstack, commands, elementScope }: OverwriteCommandsData): void => { + commands.forEach(name => { + function decoratedWrapper(origFn: (...args: unknown[]) => unknown, ...args: unknown[]): unknown { + return runWithHistoryHooks({ + callstack, + nodeData: { name, args, elementScope, overwrite: false }, + fn: () => origFn(...args), + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + session.overwriteCommand(name as any, decoratedWrapper as any, elementScope as any); + }); +}; + +const overwriteBrowserCommands = (session: WebdriverIO.Browser, callstack: Callstack): void => + overwriteCommands({ + session, + callstack, + commands: cmds.getBrowserCommands().filter(cmd => !shouldNotWrapCommand(cmd)), + elementScope: false, + }); + +const overwriteElementCommands = (session: WebdriverIO.Browser, callstack: Callstack): void => + overwriteCommands({ + session, + callstack, + commands: cmds.getElementCommands(), + elementScope: true, + }); + +export const runGroup = (callstack: Callstack | null, name: string, fn: () => T): T => { + if (!callstack) { + return fn(); + } + + return runWithHistoryHooks({ + callstack, + nodeData: { name, args: [], isGroup: true }, + fn, + }); +}; + +const overwriteRunStepCommand = (session: WebdriverIO.Browser, callstack: Callstack): void => { + session.overwriteCommand("runStep", (origCommand, stepName: string, stepCb) => { + return runGroup(callstack, stepName, () => origCommand(stepName, stepCb)); + }); +}; + +export const initCommandHistory = (session: WebdriverIO.Browser): Callstack => { + const callstack = new Callstack(); + + overwriteAddCommand(session, callstack); + overwriteBrowserCommands(session, callstack); + overwriteElementCommands(session, callstack); + overwriteOverwriteCommand(session, callstack); + overwriteRunStepCommand(session, callstack); + + return callstack; +}; diff --git a/src/browser/history/utils.js b/src/browser/history/utils.js deleted file mode 100644 index 83399a683..000000000 --- a/src/browser/history/utils.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; - -const _ = require("lodash"); -const { TestStepKey } = require("../../types"); - -const MAX_STRING_LENGTH = 50; - -exports.normalizeCommandArgs = (name, args = []) => { - if (name === "execute") { - return ["code"]; - } - - return args.map(arg => { - if (_.isString(arg)) { - return _.truncate(arg, { length: MAX_STRING_LENGTH }); - } - - if (_.isPlainObject(arg)) { - return "obj"; - } - - return arg; - }); -}; - -const isPromise = val => typeof _.get(val, "then") === "function"; - -exports.isGroup = node => Boolean(node && node[TestStepKey.IsGroup]); - -exports.runWithHooks = ({ fn, before, after, error }) => { - let isReturnedValuePromise = false; - - before(); - - try { - const value = fn(); - - if (isPromise(value)) { - isReturnedValuePromise = true; - - return value - .catch(err => { - error(err); - - throw err; - }) - .finally(after); - } - - return value; - } catch (err) { - if (!isReturnedValuePromise) { - error(err); - - throw err; - } - } finally { - if (!isReturnedValuePromise) { - after(); - } - } -}; diff --git a/src/browser/history/utils.ts b/src/browser/history/utils.ts new file mode 100644 index 000000000..c0ec6260a --- /dev/null +++ b/src/browser/history/utils.ts @@ -0,0 +1,68 @@ +import _ from "lodash"; +import { TestStep, TestStepKey } from "../../types"; + +const MAX_STRING_LENGTH = 50; + +type HookFunctions = { + fn: () => T; + before: () => void; + after: () => void; + error: (err: unknown) => unknown; +}; + +export const normalizeCommandArgs = (commandName: string, args: unknown[] = []): string[] => { + if (commandName === "execute") { + return ["code"]; + } + + return args.map(arg => { + if (typeof arg === "string") { + return _.truncate(arg, { length: MAX_STRING_LENGTH }); + } + + if (_.isPlainObject(arg)) { + return "obj"; + } + + return String(arg); + }); +}; + +const isPromise = (val: unknown): val is Promise => typeof _.get(val, "then") === "function"; + +export const isGroup = (node: TestStep): boolean => Boolean(node && node[TestStepKey.IsGroup]); + +export const runWithHooks = ({ fn, before, after, error }: HookFunctions): T => { + let isReturnedValuePromise = false; + + before(); + + try { + const value = fn(); + + if (isPromise(value)) { + isReturnedValuePromise = true; + + return value + .catch((err: unknown) => { + error(err); + + throw err; + }) + .finally(after) + .then(() => value) as T; // It's valid to convert Promise to T since value is already Promise here + } + + return value; + } catch (err) { + if (!isReturnedValuePromise) { + error(err); + } + + throw err; + } finally { + if (!isReturnedValuePromise) { + after(); + } + } +}; diff --git a/src/browser/stacktrace/index.ts b/src/browser/stacktrace/index.ts index fdbfbefa8..1689653df 100644 --- a/src/browser/stacktrace/index.ts +++ b/src/browser/stacktrace/index.ts @@ -25,7 +25,7 @@ export const runWithStacktraceHooks = ({ before: () => stackFrames.enter(key, frames), fn, after: () => stackFrames.leave(key), - error: (err: Error) => applyStackTraceIfBetter(err, frames), + error: err => applyStackTraceIfBetter(err, frames), }); }; diff --git a/src/browser/stacktrace/utils.ts b/src/browser/stacktrace/utils.ts index a906ecb9f..4641a8170 100644 --- a/src/browser/stacktrace/utils.ts +++ b/src/browser/stacktrace/utils.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import ErrorStackParser from "error-stack-parser"; import type { SetRequired } from "type-fest"; -import logger from "../../utils/logger"; +import * as logger from "../../utils/logger"; import { softFileURLToPath } from "../../utils/fs"; import { STACK_FRAME_REG_EXP, WDIO_IGNORED_STACK_FUNCTIONS, WDIO_STACK_TRACE_LIMIT } from "./constants"; @@ -131,8 +131,8 @@ const applyStackTrace = (error: Error, stack: RawStackFrames): Error => { return error; }; -export const applyStackTraceIfBetter = (error: Error, stack: RawStackFrames): Error => { - if (!error || !error.message) { +export const applyStackTraceIfBetter = (error: T, stack: RawStackFrames): T => { + if (!error || !(error instanceof Error) || !error.message) { return error; } diff --git a/src/browser/types.ts b/src/browser/types.ts index 6fd8cdb97..1c925d18a 100644 --- a/src/browser/types.ts +++ b/src/browser/types.ts @@ -5,7 +5,7 @@ import type { BrowserConfig } from "./../config/browser-config"; import type { ExecutionThreadCtx, ExecutionThreadToolCtx } from "../types"; import { MoveCursorToCommand } from "./commands/moveCursorTo"; import { OpenAndWaitCommand } from "./commands/openAndWait"; -import Callstack from "./history/callstack"; +import type { Callstack } from "./history/callstack"; import { Test, Hook } from "../test-reader/test-object"; export const BrowserName = { diff --git a/src/bundle/@wdio-utils.ts b/src/bundle/@wdio-utils.ts index 9e834faa0..8e5574214 100644 --- a/src/bundle/@wdio-utils.ts +++ b/src/bundle/@wdio-utils.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const bundle = require("./cjs"); -export const sessionEnvironmentDetector: typeof import("@wdio/utils-cjs").sessionEnvironmentDetector = +export const sessionEnvironmentDetector: typeof import("@wdio/utils").sessionEnvironmentDetector = bundle.wdioUtils.sessionEnvironmentDetector; diff --git a/src/cli/commands/config/index.ts b/src/cli/commands/config/index.ts index b4ec36a18..8e0734970 100644 --- a/src/cli/commands/config/index.ts +++ b/src/cli/commands/config/index.ts @@ -1,6 +1,6 @@ import { Testplane } from "../../../testplane"; import { CliCommands } from "../../constants"; -import logger from "../../../utils/logger"; +import * as logger from "../../../utils/logger"; const { CONFIG: commandName } = CliCommands; diff --git a/src/cli/commands/install-deps/index.ts b/src/cli/commands/install-deps/index.ts index 3d74898db..7b1fb9c66 100644 --- a/src/cli/commands/install-deps/index.ts +++ b/src/cli/commands/install-deps/index.ts @@ -1,6 +1,6 @@ import { Testplane } from "../../../testplane"; import { CliCommands } from "../../constants"; -import logger from "../../../utils/logger"; +import * as logger from "../../../utils/logger"; import { installBrowsersWithDrivers, BrowserInstallStatus } from "../../../browser-installer"; const { INSTALL_DEPS: commandName } = CliCommands; diff --git a/src/cli/commands/list-browsers/index.ts b/src/cli/commands/list-browsers/index.ts index 9fcc433c8..d5e56b938 100644 --- a/src/cli/commands/list-browsers/index.ts +++ b/src/cli/commands/list-browsers/index.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import { Testplane } from "../../../testplane"; import { CliCommands } from "../../constants"; -import logger from "../../../utils/logger"; +import * as logger from "../../../utils/logger"; const { LIST_BROWSERS: commandName } = CliCommands; diff --git a/src/cli/commands/list-tests/index.ts b/src/cli/commands/list-tests/index.ts index 232ac8b89..b77c29f23 100644 --- a/src/cli/commands/list-tests/index.ts +++ b/src/cli/commands/list-tests/index.ts @@ -2,10 +2,11 @@ import path from "node:path"; import fs from "fs-extra"; import { Testplane } from "../../../testplane"; -import { Formatters, validateFormatter } from "../../../test-collection"; +import { Formatters } from "../../../test-collection/constants"; +import { validateFormatter } from "../../../test-collection"; import { CliCommands } from "../../constants"; import { withCommonCliOptions, collectCliValues, handleRequires, type CommonCmdOpts } from "../../../utils/cli"; -import logger from "../../../utils/logger"; +import * as logger from "../../../utils/logger"; import type { ValueOf } from "../../../types/helpers"; diff --git a/src/cli/index.ts b/src/cli/index.ts index 895c9a255..a446bfe00 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -5,7 +5,7 @@ import defaults from "../config/defaults"; import { configOverriding } from "./info"; import { Testplane } from "../testplane"; import pkg from "../../package.json"; -import logger from "../utils/logger"; +import * as logger from "../utils/logger"; import { shouldIgnoreUnhandledRejection } from "../utils/errors"; import { utilInspectSafe } from "../utils/secret-replacer"; import { withCommonCliOptions, collectCliValues, handleRequires } from "../utils/cli"; diff --git a/src/config/index.ts b/src/config/index.ts index 50734394e..c8db72911 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -3,7 +3,7 @@ import * as _ from "lodash"; import defaults from "./defaults"; import { BrowserConfig } from "./browser-config"; import parseOptions from "./options"; -import logger from "../utils/logger"; +import * as logger from "../utils/logger"; import { ConfigInput, ConfigParsed } from "./types"; import { addUserAgentToArgs } from "./utils"; diff --git a/src/config/types.ts b/src/config/types.ts index 129d23d3a..9b3f5cc89 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -293,7 +293,7 @@ export interface CommonConfig { assertViewOpts: AssertViewOpts; expectOpts: ExpectOptsConfig; meta: { [name: string]: unknown }; - windowSize: string | { width: number; height: number } | null; + windowSize: { width: number; height: number } | null; orientation: "landscape" | "portrait" | null; resetCursor: boolean; headers: Record | null; diff --git a/src/constants/help.ts b/src/constants/help.ts new file mode 100644 index 000000000..47b19b82e --- /dev/null +++ b/src/constants/help.ts @@ -0,0 +1 @@ +export const NEW_ISSUE_LINK = "https://github.com/gemini-testing/testplane/issues"; diff --git a/src/dev-server/index.ts b/src/dev-server/index.ts index 532e2d88b..8fc4423be 100644 --- a/src/dev-server/index.ts +++ b/src/dev-server/index.ts @@ -3,7 +3,7 @@ import { spawn } from "child_process"; import debug from "debug"; import { Config } from "../config"; import { findCwd, pipeLogsWithPrefix, waitDevServerReady } from "./utils"; -import logger = require("../utils/logger"); +import * as logger from "../utils/logger"; import type { Testplane } from "../testplane"; export type DevServerOpts = { testplane: Testplane; devServerConfig: Config["devServer"]; configPath: string }; diff --git a/src/dev-server/utils.ts b/src/dev-server/utils.ts index f4d70b8f2..a0246a8cd 100644 --- a/src/dev-server/utils.ts +++ b/src/dev-server/utils.ts @@ -3,7 +3,7 @@ import path from "path"; import fs from "fs"; import chalk from "chalk"; import type { ChildProcess, ChildProcessWithoutNullStreams } from "child_process"; -import logger from "../utils/logger"; +import * as logger from "../utils/logger"; import type { Config } from "../config"; export const findCwd = (configPath: string): string => { diff --git a/src/error-snippets/frames.ts b/src/error-snippets/frames.ts index 12e6efa0c..f3355e816 100644 --- a/src/error-snippets/frames.ts +++ b/src/error-snippets/frames.ts @@ -1,5 +1,5 @@ import ErrorStackParser from "error-stack-parser"; -import logger from "../utils/logger"; +import * as logger from "../utils/logger"; import { getFrameRelevance } from "../browser/stacktrace/utils"; import type { ResolvedFrame, SufficientStackFrame } from "./types"; import { softFileURLToPath } from "../utils/fs"; diff --git a/src/image.js b/src/image.js deleted file mode 100644 index 721ab96e5..000000000 --- a/src/image.js +++ /dev/null @@ -1,178 +0,0 @@ -"use strict"; - -const Promise = require("bluebird"); -const looksSame = require("looks-same"); -const sharp = require("sharp"); - -module.exports = class Image { - static create(buffer) { - return new this(buffer); - } - - constructor(buffer) { - this._img = sharp(buffer); - this._imageData = null; - this._ignoreData = []; - this._composeImages = []; - } - - async getSize() { - const imgSizes = await Promise.map([this].concat(this._composeImages), img => img._img.metadata()); - - return imgSizes.reduce( - (totalSize, img) => { - return { - width: Math.max(totalSize.width, img.width), - height: totalSize.height + img.height, - }; - }, - { width: 0, height: 0 }, - ); - } - - async crop(rect) { - const { height, width } = await this._img.metadata(); - - this._img.extract({ - left: rect.left, - top: rect.top, - width: Math.min(width, rect.left + rect.width) - rect.left, - height: Math.min(height, rect.top + rect.height) - rect.top, - }); - - await this._forceRefreshImageData(); - } - - addJoin(attachedImages) { - this._composeImages = this._composeImages.concat(attachedImages); - } - - async applyJoin() { - if (!this._composeImages.length) { - return; - } - - const { height, width } = await this._img.metadata(); - const imagesData = await Promise.all(this._composeImages.map(img => img._getImageData())); - const compositeData = []; - - let newHeight = height; - - for (const { data, info } of imagesData) { - compositeData.push({ - input: data, - left: 0, - top: newHeight, - raw: { - width: info.width, - height: info.height, - channels: info.channels, - }, - }); - - newHeight += info.height; - } - - this._img.resize({ - width, - height: newHeight, - fit: "contain", - position: "top", - }); - - this._img.composite(compositeData); - } - - async addClear({ width, height, left, top }) { - const { channels } = await this._img.metadata(); - - this._ignoreData.push({ - input: { - create: { - channels, - background: { r: 0, g: 0, b: 0, alpha: 1 }, - width, - height, - }, - }, - left, - top, - }); - } - - applyClear() { - this._img.composite(this._ignoreData); - } - - async _getImageData() { - if (!this._imageData) { - this._imageData = await this._img.raw().toBuffer({ resolveWithObject: true }); - } - - return this._imageData; - } - - async _forceRefreshImageData() { - this._imageData = await this._img.raw().toBuffer({ resolveWithObject: true }); - this._img = sharp(this._imageData.data, { - raw: { - width: this._imageData.info.width, - height: this._imageData.info.height, - channels: this._imageData.info.channels, - }, - }); - - this._composeImages = []; - this._ignoreData = []; - } - - async getRGBA(x, y) { - const { data, info } = await this._getImageData(); - const idx = (info.width * y + x) * info.channels; - - return { - r: data[idx], - g: data[idx + 1], - b: data[idx + 2], - a: info.channels === 4 ? data[idx + 3] : 1, - }; - } - - async save(file) { - await this._img.png().toFile(file); - } - - static fromBase64(base64) { - return new this(Buffer.from(base64, "base64")); - } - - async toPngBuffer(opts = { resolveWithObject: true }) { - const imgData = await this._img.png().toBuffer(opts); - - return opts.resolveWithObject - ? { data: imgData.data, size: { height: imgData.info.height, width: imgData.info.width } } - : imgData; - } - - static compare(path1, path2, opts = {}) { - const compareOptions = { - ignoreCaret: opts.canHaveCaret, - pixelRatio: opts.pixelRatio, - ...opts.compareOpts, - createDiffImage: true, - }; - ["tolerance", "antialiasingTolerance"].forEach(option => { - if (option in opts) { - compareOptions[option] = opts[option]; - } - }); - return looksSame(path1, path2, compareOptions); - } - - static buildDiff(opts) { - const { diffColor: highlightColor, ...otherOpts } = opts; - const diffOptions = { highlightColor, ...otherOpts }; - - return looksSame.createDiff(diffOptions); - } -}; diff --git a/src/image.ts b/src/image.ts new file mode 100644 index 000000000..fee1584e9 --- /dev/null +++ b/src/image.ts @@ -0,0 +1,215 @@ +import sharp from "sharp"; +import looksSame from "looks-same"; +import { DiffOptions, ImageSize } from "./types"; + +interface SharpImageData { + data: Buffer; + info: sharp.OutputInfo; +} + +interface PngImageData { + data: Buffer; + size: ImageSize; +} + +export interface Rect { + width: number; + height: number; + top: number; + left: number; +} + +export interface RGBA { + r: number; + g: number; + b: number; + a: number; +} + +interface CompareOptions { + canHaveCaret?: boolean; + pixelRatio?: number; + compareOpts?: looksSame.LooksSameOptions; + tolerance?: number; + antialiasingTolerance?: number; +} + +export class Image { + private _img: sharp.Sharp; + private _imageData: SharpImageData | null = null; + private _ignoreData: sharp.OverlayOptions[] = []; + // eslint-disable-next-line no-use-before-define + private _composeImages: Image[] = []; + + static create(buffer: Buffer): Image { + return new this(buffer); + } + + constructor(buffer: Buffer) { + this._img = sharp(buffer); + } + + async getSize(): Promise { + const imgSizes = await Promise.all([this, ...this._composeImages].map(img => img._img.metadata())); + + return imgSizes.reduce( + (totalSize, img) => { + return { + width: Math.max(totalSize.width, img.width!), + height: totalSize.height + img.height!, + }; + }, + { width: 0, height: 0 }, + ); + } + + async crop(rect: Rect): Promise { + const { height, width } = await this._img.metadata(); + + this._img.extract({ + left: rect.left, + top: rect.top, + width: Math.min(width!, rect.left + rect.width) - rect.left, + height: Math.min(height!, rect.top + rect.height) - rect.top, + }); + + await this._forceRefreshImageData(); + } + + addJoin(attachedImages: Image[]): void { + this._composeImages = this._composeImages.concat(attachedImages); + } + + async applyJoin(): Promise { + if (!this._composeImages.length) return; + + const { height, width } = await this._img.metadata(); + const imagesData = await Promise.all(this._composeImages.map(img => img._getImageData())); + const compositeData = []; + + let newHeight = height!; + + for (const { data, info } of imagesData) { + compositeData.push({ + input: data, + left: 0, + top: newHeight, + raw: { + width: info.width, + height: info.height, + channels: info.channels, + }, + }); + + newHeight += info.height; + } + + this._img.resize({ + width, + height: newHeight, + fit: "contain", + position: "top", + }); + + this._img.composite(compositeData); + } + + async addClear({ width, height, left, top }: Rect): Promise { + const { channels } = await this._img.metadata(); + + this._ignoreData.push({ + input: { + create: { + channels: channels!, + background: { r: 0, g: 0, b: 0, alpha: 1 }, + width, + height, + }, + }, + left, + top, + }); + } + + applyClear(): void { + this._img.composite(this._ignoreData); + } + + private async _getImageData(): Promise { + if (!this._imageData) { + this._imageData = await this._img.raw().toBuffer({ resolveWithObject: true }); + } + return this._imageData; + } + + private async _forceRefreshImageData(): Promise { + this._imageData = await this._img.raw().toBuffer({ resolveWithObject: true }); + this._img = sharp(this._imageData.data, { + raw: { + width: this._imageData.info.width, + height: this._imageData.info.height, + channels: this._imageData.info.channels, + }, + }); + + this._composeImages = []; + this._ignoreData = []; + } + + async getRGBA(x: number, y: number): Promise { + const { data, info } = await this._getImageData(); + const idx = (info.width * y + x) * info.channels; + + return { + r: data[idx], + g: data[idx + 1], + b: data[idx + 2], + a: info.channels === 4 ? data[idx + 3] : 1, + }; + } + + async save(file: string): Promise { + await this._img.png().toFile(file); + } + + static fromBase64(base64: string): Image { + return new this(Buffer.from(base64, "base64")); + } + + async toPngBuffer(opts: { resolveWithObject: true }): Promise; + async toPngBuffer(opts: { resolveWithObject?: false }): Promise; + async toPngBuffer( + opts: { resolveWithObject?: boolean } = { resolveWithObject: true }, + ): Promise { + if (opts.resolveWithObject) { + const imgData = await this._img.png().toBuffer({ resolveWithObject: true }); + + return { data: imgData.data, size: { height: imgData.info.height, width: imgData.info.width } }; + } + + return await this._img.png().toBuffer({ resolveWithObject: false }); + } + + static compare(path1: string, path2: string, opts: CompareOptions = {}): Promise { + const compareOptions: looksSame.LooksSameOptions = { + ignoreCaret: opts.canHaveCaret, + pixelRatio: opts.pixelRatio, + ...opts.compareOpts, + }; + if (opts.tolerance) { + compareOptions.tolerance = opts.tolerance; + } + if (opts.antialiasingTolerance) { + compareOptions.antialiasingTolerance = opts.antialiasingTolerance; + } + + return looksSame(path1, path2, { ...compareOptions, createDiffImage: true }); + } + + static buildDiff(opts: DiffOptions): Promise { + const { diffColor: highlightColor, ...otherOpts } = opts; + const diffOptions = { highlightColor, ...otherOpts }; + + return looksSame.createDiff(diffOptions); + } +} diff --git a/src/runner/browser-env/vite/plugins/generate-index-html.ts b/src/runner/browser-env/vite/plugins/generate-index-html.ts index b9fec6285..d1c72a9cf 100644 --- a/src/runner/browser-env/vite/plugins/generate-index-html.ts +++ b/src/runner/browser-env/vite/plugins/generate-index-html.ts @@ -6,7 +6,7 @@ import createDebug from "debug"; import { MODULE_NAMES, MOCK_MODULE_NAME } from "../constants"; import { getNodeModulePath, getImportMetaUrl, getTestInfoFromViteRequest } from "../utils"; import { polyfillPath } from "../polyfill"; -import logger from "../../../../utils/logger"; +import * as logger from "../../../../utils/logger"; import type { WorkerInitializePayload } from "../browser-modules/types"; import type { Plugin, Rollup } from "vite"; diff --git a/src/runner/browser-env/vite/plugins/mock.ts b/src/runner/browser-env/vite/plugins/mock.ts index c39ff1a19..644d63f9e 100644 --- a/src/runner/browser-env/vite/plugins/mock.ts +++ b/src/runner/browser-env/vite/plugins/mock.ts @@ -4,7 +4,7 @@ import createDebug from "debug"; import { parse, print, visit, types } from "recast"; import { ManualMock } from "../manual-mock"; -import logger from "../../../../utils/logger"; +import * as logger from "../../../../utils/logger"; import { MOCK_MODULE_NAME } from "../constants"; import { getTestInfoFromViteRequest, getPathWithoutExtName } from "../utils"; diff --git a/src/runner/browser-env/vite/server.ts b/src/runner/browser-env/vite/server.ts index 9b738d5f3..562830d58 100644 --- a/src/runner/browser-env/vite/server.ts +++ b/src/runner/browser-env/vite/server.ts @@ -7,7 +7,7 @@ import _ from "lodash"; import getPort from "get-port"; import chalk from "chalk"; -import logger from "../../../utils/logger"; +import * as logger from "../../../utils/logger"; import { createSocketServer } from "./socket"; import { plugin as generateIndexHtml } from "./plugins/generate-index-html"; import { plugin as mockPlugin } from "./plugins/mock"; diff --git a/src/test-reader/test-parser.ts b/src/test-reader/test-parser.ts index cf484caa9..2b54d6c49 100644 --- a/src/test-reader/test-parser.ts +++ b/src/test-reader/test-parser.ts @@ -15,7 +15,7 @@ import _ from "lodash"; import clearRequire from "clear-require"; import path from "path"; import fs from "fs-extra"; -import logger from "../utils/logger"; +import * as logger from "../utils/logger"; import { getShortMD5 } from "../utils/crypto"; import { Test } from "./test-object"; import { Config } from "../config"; diff --git a/src/testplane.ts b/src/testplane.ts index 18560e048..137b1b0ec 100644 --- a/src/testplane.ts +++ b/src/testplane.ts @@ -13,7 +13,7 @@ import { TestReader } from "./test-reader"; import { TestCollection } from "./test-collection"; import { validateUnknownBrowsers } from "./validators"; import { initReporters } from "./reporters"; -import logger from "./utils/logger"; +import * as logger from "./utils/logger"; import { isRunInNodeJsEnv } from "./utils/config"; import { initDevServer } from "./dev-server"; import { ConfigInput } from "./config/types"; diff --git a/src/types/index.ts b/src/types/index.ts index 0ea500801..c1ec5846e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -13,7 +13,7 @@ import { AlsoController } from "../test-reader/controllers/also-controller"; import { BrowserVersionController } from "../test-reader/controllers/browser-version-controller"; import { WorkerProcess } from "../utils/worker-process"; import { BaseTestplane } from "../base-testplane"; -import Callstack from "../browser/history/callstack"; +import type { Callstack } from "../browser/history/callstack"; import { CoordBounds, LooksSameOptions } from "looks-same"; export type { Browser as WdioBrowser } from "webdriverio"; @@ -67,6 +67,7 @@ export interface RefImageInfo extends ImageInfo { } export interface DiffOptions extends LooksSameOptions { + diff: string; current: string; reference: string; diffColor: string; diff --git a/src/utils/cli.ts b/src/utils/cli.ts index cfe87f51e..8bff3943b 100644 --- a/src/utils/cli.ts +++ b/src/utils/cli.ts @@ -1,6 +1,6 @@ import _ from "lodash"; import type { Command } from "@gemini-testing/commander"; -import logger from "./logger"; +import * as logger from "./logger"; import { requireModule } from "./module"; export const collectCliValues = (newValue: unknown, array = [] as unknown[]): unknown[] => { diff --git a/src/utils/logger.js b/src/utils/logger.js deleted file mode 100644 index 3a86c3ef6..000000000 --- a/src/utils/logger.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; - -const format = require("strftime"); - -const withTimestampPrefix = - logFnName => - (...args) => { - const timestamp = format("%H:%M:%S %z"); - console[logFnName](`[${timestamp}]`, ...args); - }; - -module.exports = { - log: withTimestampPrefix("log"), - warn: withTimestampPrefix("warn"), - error: withTimestampPrefix("error"), -}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 000000000..a39604b62 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,12 @@ +import format from "strftime"; + +const withTimestampPrefix = + (logFnName: "log" | "warn" | "error") => + (...args: unknown[]): void => { + const timestamp = format("%H:%M:%S %z"); + console[logFnName](`[${timestamp}]`, ...args); + }; + +export const log = withTimestampPrefix("log"); +export const warn = withTimestampPrefix("warn"); +export const error = withTimestampPrefix("error"); diff --git a/src/utils/page-loader.ts b/src/utils/page-loader.ts index 31628359f..b08ed5c99 100644 --- a/src/utils/page-loader.ts +++ b/src/utils/page-loader.ts @@ -1,6 +1,6 @@ import EventEmitter from "events"; import type { Matches, Mock } from "webdriverio"; -import logger from "./logger"; +import * as logger from "./logger"; export interface PageLoaderOpts { selectors: string[]; diff --git a/src/utils/typescript.ts b/src/utils/typescript.ts index a0fddc733..4848969e2 100644 --- a/src/utils/typescript.ts +++ b/src/utils/typescript.ts @@ -1,6 +1,6 @@ import _ from "lodash"; import debug from "debug"; -import logger from "./logger"; +import * as logger from "./logger"; const swcDebugNamespace = "testplane:swc"; const swcDebugLog = debug(swcDebugNamespace); diff --git a/src/validators.ts b/src/validators.ts index 93b7d5c02..b6a58004f 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -1,7 +1,7 @@ import { format } from "util"; import chalk from "chalk"; import _ from "lodash"; -import logger from "./utils/logger"; +import * as logger from "./utils/logger"; export const validateUnknownBrowsers = (browsers: string[], configBrowsers: string[]): void => { const unknownBrowsers = getUnknownBrowsers(browsers, configBrowsers); diff --git a/src/worker/browser-env/runner/test-runner/index.ts b/src/worker/browser-env/runner/test-runner/index.ts index 21651f9a6..a3e9bb90c 100644 --- a/src/worker/browser-env/runner/test-runner/index.ts +++ b/src/worker/browser-env/runner/test-runner/index.ts @@ -14,7 +14,7 @@ import { BRO_INIT_INTERVAL_ON_RECONNECT, } from "./constants"; import { VITE_RUN_UUID_ROUTE } from "../../../../runner/browser-env/vite/constants"; -import logger from "../../../../utils/logger"; +import * as logger from "../../../../utils/logger"; import RuntimeConfig from "../../../../config/runtime-config"; import { AbortOnReconnectError } from "../../../../errors/abort-on-reconnect-error"; diff --git a/src/worker/runner/browser-agent.ts b/src/worker/runner/browser-agent.ts index d0b1cc750..d40b4cca1 100644 --- a/src/worker/runner/browser-agent.ts +++ b/src/worker/runner/browser-agent.ts @@ -1,4 +1,4 @@ -import ExistingBrowser from "../../browser/existing-browser"; +import { ExistingBrowser } from "../../browser/existing-browser"; import { WdioBrowser } from "../../types"; import BrowserPool from "./browser-pool"; diff --git a/src/worker/runner/browser-pool.js b/src/worker/runner/browser-pool.js index 6565ae744..e6165510f 100644 --- a/src/worker/runner/browser-pool.js +++ b/src/worker/runner/browser-pool.js @@ -1,7 +1,7 @@ "use strict"; -const Browser = require("../../browser/existing-browser"); -const Calibrator = require("../../browser/calibrator"); +const { ExistingBrowser } = require("../../browser/existing-browser"); +const { Calibrator } = require("../../browser/calibrator"); const { WorkerEvents } = require("../../events"); const ipc = require("../../utils/ipc"); @@ -17,7 +17,7 @@ module.exports = class BrowserPool { } async getBrowser({ browserId, browserVersion, sessionId, sessionCaps, sessionOpts, state }) { - const browser = Browser.create(this._config, { + const browser = ExistingBrowser.create(this._config, { id: browserId, version: browserVersion, state, diff --git a/src/worker/runner/test-runner/one-time-screenshooter.js b/src/worker/runner/test-runner/one-time-screenshooter.js index ea7f36365..4338a4180 100644 --- a/src/worker/runner/test-runner/one-time-screenshooter.js +++ b/src/worker/runner/test-runner/one-time-screenshooter.js @@ -2,7 +2,7 @@ const Promise = require("bluebird"); -const Image = require("../../../image"); +const { Image } = require("../../../image"); const ScreenShooter = require("../../../browser/screen-shooter"); const logger = require("../../../utils/logger"); diff --git a/test/src/browser/calibrator.js b/test/src/browser/calibrator.js index 0ca8f00c1..34206eba4 100644 --- a/test/src/browser/calibrator.js +++ b/test/src/browser/calibrator.js @@ -3,8 +3,8 @@ const path = require("path"); const fs = require("fs"); const Promise = require("bluebird"); -const Image = require("src/image"); -const Calibrator = require("src/browser/calibrator"); +const { Image } = require("src/image"); +const { Calibrator } = require("src/browser/calibrator"); const { CoreError } = require("src/browser/core-error"); describe("calibrator", () => { diff --git a/test/src/browser/camera/index.js b/test/src/browser/camera/index.js index cfa6c557b..9225c29c2 100644 --- a/test/src/browser/camera/index.js +++ b/test/src/browser/camera/index.js @@ -1,14 +1,24 @@ "use strict"; -const Camera = require("src/browser/camera"); -const Image = require("src/image"); -const utils = require("src/browser/camera/utils"); +// const {Camera} = require("src/browser/camera"); +const { Image } = require("src/image"); +const proxyquire = require("proxyquire"); +// const utils = require("src/browser/camera/utils"); describe("browser/camera", () => { const sandbox = sinon.createSandbox(); + let Camera; + let isFullPageStub; let image; beforeEach(() => { + isFullPageStub = sinon.stub(); + Camera = proxyquire("src/browser/camera", { + "./utils": { + isFullPage: isFullPageStub, + }, + }).Camera; + image = sinon.createStubInstance(Image); image.getSize.resolves({ width: 100500, height: 500100 }); image.crop.resolves(); @@ -55,8 +65,6 @@ describe("browser/camera", () => { }; beforeEach(() => { - sandbox.stub(utils, "isFullPage"); - page = { viewport: { left: 1, @@ -74,7 +82,7 @@ describe("browser/camera", () => { }); it("should crop fullPage image with viewport value if page disposition was set", async () => { - utils.isFullPage.returns(true); + isFullPageStub.returns(true); await mkCamera_({ screenshotMode: "fullPage" }).captureViewportImage(page); @@ -82,7 +90,7 @@ describe("browser/camera", () => { }); it("should crop not fullPage image to the left and right", async () => { - utils.isFullPage.returns(false); + isFullPageStub.returns(false); await mkCamera_({ screenshotMode: "viewport" }).captureViewportImage(page); diff --git a/test/src/browser/client-bridge/client-bridge.js b/test/src/browser/client-bridge/client-bridge.js index be7a1d7b7..d014d57b6 100644 --- a/test/src/browser/client-bridge/client-bridge.js +++ b/test/src/browser/client-bridge/client-bridge.js @@ -1,6 +1,6 @@ "use strict"; const Promise = require("bluebird"); -const ClientBridge = require("src/browser/client-bridge/client-bridge"); +const { ClientBridge } = require("src/browser/client-bridge/client-bridge"); const { ClientBridgeError } = require("src/browser/client-bridge/error"); const CALL = '__geminiCore.example(1, "two")'; diff --git a/test/src/browser/client-bridge/index.ts b/test/src/browser/client-bridge/index.ts index 8eaf3e6a0..8ea7c939d 100644 --- a/test/src/browser/client-bridge/index.ts +++ b/test/src/browser/client-bridge/index.ts @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import sinon, { type SinonStub } from "sinon"; -import ClientBridge from "src/browser/client-bridge/client-bridge"; +import { ClientBridge } from "src/browser/client-bridge/client-bridge"; import { build as buildClientBridge } from "src/browser/client-bridge"; describe("clientBridge", () => { @@ -23,7 +23,7 @@ describe("clientBridge", () => { readFileStub.withArgs(sinon.match.string, { encoding: "utf8" }).resolves("foo bar native script"); ClientBridgeCreateStub.withArgs("browser", "foo bar native script").returns({ clientBridge: "native" }); - const result = await buildClientBridge("browser"); + const result = await buildClientBridge("browser" as any); const fileName = path.basename(readFileStub.firstCall.args[0]); @@ -35,7 +35,7 @@ describe("clientBridge", () => { readFileStub.withArgs(sinon.match.string, { encoding: "utf8" }).resolves("foo bar compat script"); ClientBridgeCreateStub.withArgs("browser", "foo bar compat script").returns({ clientBridge: "compat" }); - const result = await buildClientBridge("browser", { calibration: { needsCompatLib: true } }); + const result = await buildClientBridge("browser" as any, { calibration: { needsCompatLib: true } }); const fileName = path.basename(readFileStub.firstCall.args[0]); diff --git a/test/src/browser/commands/assert-view/errors/image-diff-error.js b/test/src/browser/commands/assert-view/errors/image-diff-error.js index 0402a98a1..facb14457 100644 --- a/test/src/browser/commands/assert-view/errors/image-diff-error.js +++ b/test/src/browser/commands/assert-view/errors/image-diff-error.js @@ -3,7 +3,7 @@ const _ = require("lodash"); const { BaseStateError } = require("src/browser/commands/assert-view/errors/base-state-error"); const { ImageDiffError } = require("src/browser/commands/assert-view/errors/image-diff-error"); -const Image = require("src/image"); +const { Image } = require("src/image"); const mkImageDiffError = (opts = {}) => { const { stateName, currImg, refImg, diffOpts } = _.defaults(opts, { diff --git a/test/src/browser/commands/assert-view/index.js b/test/src/browser/commands/assert-view/index.js index 07393b140..bc3719fc1 100644 --- a/test/src/browser/commands/assert-view/index.js +++ b/test/src/browser/commands/assert-view/index.js @@ -4,8 +4,8 @@ const { EventEmitter } = require("events"); const _ = require("lodash"); const fs = require("fs-extra"); const webdriverio = require("webdriverio"); -const clientBridge = require("src/browser/client-bridge"); -const Image = require("src/image"); +// const clientBridge = require("src/browser/client-bridge"); +const { Image } = require("src/image"); const ScreenShooter = require("src/browser/screen-shooter"); const temp = require("src/temp"); const validator = require("png-validator"); @@ -16,9 +16,11 @@ const RuntimeConfig = require("src/config/runtime-config"); const updateRefs = require("src/browser/commands/assert-view/capture-processors/update-refs"); const { mkExistingBrowser_: mkBrowser_, mkSessionStub_ } = require("../../utils"); const { InvalidRefImageError } = require("src/browser/commands/assert-view/errors/invalid-ref-image-error"); +const proxyquire = require("proxyquire"); describe("assertView command", () => { const sandbox = sinon.createSandbox(); + let ExistingBrowser; const assertViewBrowser = async (browser, state = "plain", selector = ".selector", opts = {}) => { return browser.publicAPI.assertView(state, selector, opts); @@ -55,9 +57,7 @@ describe("assertView command", () => { }; const stubBrowser_ = config => { - sandbox.stub(clientBridge, "build").resolves(); - - const browser = mkBrowser_(config); + const browser = mkBrowser_(config, undefined, ExistingBrowser); sandbox.stub(browser, "prepareScreenshot").resolves({}); sandbox.stub(browser, "captureViewportImage").resolves(stubImage_()); sandbox.stub(browser, "emitter").get(() => new EventEmitter()); @@ -73,6 +73,12 @@ describe("assertView command", () => { }; beforeEach(() => { + ExistingBrowser = proxyquire("src/browser/existing-browser", { + "./client-bridge": { + build: sinon.stub().resolves(), + }, + }).ExistingBrowser; + sandbox.stub(Image, "create").returns(Object.create(Image.prototype)); sandbox.stub(Image, "compare").resolves({ diffImage: { createBuffer: sandbox.stub() } }); sandbox.stub(Image.prototype, "getSize"); diff --git a/test/src/browser/commands/clearSession.ts b/test/src/browser/commands/clearSession.ts index ff82a815d..54f0da457 100644 --- a/test/src/browser/commands/clearSession.ts +++ b/test/src/browser/commands/clearSession.ts @@ -1,25 +1,43 @@ import * as webdriverio from "webdriverio"; import sinon, { SinonStub } from "sinon"; -import clientBridge from "src/browser/client-bridge"; -import logger from "src/utils/logger"; import { mkExistingBrowser_ as mkBrowser_, mkSessionStub_ } from "../utils"; -import type ExistingBrowser from "src/browser/existing-browser"; +import type { ExistingBrowser as ExistingBrowserOriginal } from "src/browser/existing-browser"; +import { Calibrator } from "src/browser/calibrator"; +import proxyquire from "proxyquire"; describe('"clearSession" command', () => { const sandbox = sinon.createSandbox(); + let ExistingBrowser: typeof ExistingBrowserOriginal; + let loggerWarnStub: SinonStub; - const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}): Promise => { + const initBrowser_ = ({ + browser = mkBrowser_(undefined, undefined, ExistingBrowser), + session = mkSessionStub_(), + } = {}): Promise => { (webdriverio.attach as SinonStub).resolves(session); - return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities, sessionOpts: {} }); + return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities }, {} as Calibrator); }; beforeEach(() => { + loggerWarnStub = sandbox.stub(); + ExistingBrowser = proxyquire("src/browser/existing-browser", { + "./client-bridge": { + build: sandbox.stub().resolves(), + }, + "../utils/logger": { + warn: loggerWarnStub, + }, + "./commands/clearSession": proxyquire("src/browser/commands/clearSession", { + "../../utils/logger": { + warn: loggerWarnStub, + }, + }), + }).ExistingBrowser; + sandbox.stub(webdriverio, "attach"); - sandbox.stub(clientBridge, "build").resolves(); - sandbox.stub(logger, "warn"); global.window = { localStorage: { clear: sinon.stub() } as unknown as Storage, @@ -73,7 +91,7 @@ describe('"clearSession" command', () => { await initBrowser_({ session }); await assert.isFulfilled(session.clearSession()); - assert.calledOnceWith(logger.warn, `Couldn't clear ${storageName}: ${err.message}`); + assert.calledOnceWith(loggerWarnStub, `Couldn't clear ${storageName}: ${err.message}`); }); it("should throw if clear storage fails with not handled error", async () => { diff --git a/test/src/browser/commands/getConfig.js b/test/src/browser/commands/getConfig.js index 7c90c9f22..9e9be028c 100644 --- a/test/src/browser/commands/getConfig.js +++ b/test/src/browser/commands/getConfig.js @@ -1,21 +1,35 @@ "use strict"; -const webdriverio = require("webdriverio"); -const clientBridge = require("src/browser/client-bridge"); const { mkExistingBrowser_: mkBrowser_, mkSessionStub_ } = require("../utils"); +const proxyquire = require("proxyquire"); describe('"getConfig" command', () => { const sandbox = sinon.createSandbox(); + let ExistingBrowser; + let webdriverioAttachStub; + let clientBridgeBuildStub; beforeEach(() => { - sandbox.stub(webdriverio, "attach"); - sandbox.stub(clientBridge, "build").resolves(); + webdriverioAttachStub = sandbox.stub(); + clientBridgeBuildStub = sandbox.stub().resolves(); + + ExistingBrowser = proxyquire("src/browser/existing-browser", { + webdriverio: { + attach: webdriverioAttachStub, + }, + "./client-bridge": { + build: clientBridgeBuildStub, + }, + }).ExistingBrowser; }); afterEach(() => sandbox.restore()); - const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}) => { - webdriverio.attach.resolves(session); + const initBrowser_ = ({ + browser = mkBrowser_(undefined, undefined, ExistingBrowser), + session = mkSessionStub_(), + } = {}) => { + webdriverioAttachStub.resolves(session); return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities }); }; @@ -32,7 +46,7 @@ describe('"getConfig" command', () => { it("should have defined baseUrl", async () => { const session = mkSessionStub_(); - const browser = mkBrowser_({ baseUrl: "http://custom_base_url" }); + const browser = mkBrowser_({ baseUrl: "http://custom_base_url" }, undefined, ExistingBrowser); await initBrowser_({ browser, session }); diff --git a/test/src/browser/commands/getPuppeteer.js b/test/src/browser/commands/getPuppeteer.js index 2d2e20d7d..514497f9c 100644 --- a/test/src/browser/commands/getPuppeteer.js +++ b/test/src/browser/commands/getPuppeteer.js @@ -1,22 +1,36 @@ "use strict"; const _ = require("lodash"); -const webdriverio = require("webdriverio"); -const clientBridge = require("src/browser/client-bridge"); const { mkExistingBrowser_: mkBrowser_, mkSessionStub_ } = require("../utils"); +const proxyquire = require("proxyquire"); describe('"getPuppeteer" command', () => { const sandbox = sinon.createSandbox(); + let ExistingBrowser; + let webdriverioAttachStub; + let clientBridgeBuildStub; beforeEach(() => { - sandbox.stub(webdriverio, "attach"); - sandbox.stub(clientBridge, "build").resolves(); + webdriverioAttachStub = sandbox.stub(); + clientBridgeBuildStub = sandbox.stub().resolves(); + + ExistingBrowser = proxyquire("src/browser/existing-browser", { + webdriverio: { + attach: webdriverioAttachStub, + }, + "./client-bridge": { + build: clientBridgeBuildStub, + }, + }).ExistingBrowser; }); afterEach(() => sandbox.restore()); - const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}) => { - webdriverio.attach.resolves(session); + const initBrowser_ = ({ + browser = mkBrowser_(undefined, undefined, ExistingBrowser), + session = mkSessionStub_(), + } = {}) => { + webdriverioAttachStub.resolves(session); return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities }); }; @@ -40,7 +54,7 @@ describe('"getPuppeteer" command', () => { it("should overwrite command", async () => { const session = mkSessionStub_(); - const browser = mkBrowser_({ browserWSEndpoint: "ws://foo.bar/devtools" }); + const browser = mkBrowser_({ browserWSEndpoint: "ws://foo.bar/devtools" }, undefined, ExistingBrowser); await initBrowser_({ browser, session }); @@ -76,7 +90,11 @@ describe('"getPuppeteer" command', () => { return Promise.resolve(); }); - const browser = mkBrowser_({ browserWSEndpoint: "ws://new.endpoint/devtools" }); + const browser = mkBrowser_( + { browserWSEndpoint: "ws://new.endpoint/devtools" }, + undefined, + ExistingBrowser, + ); await initBrowser_({ browser, session }); await session.getPuppeteer(); @@ -105,7 +123,11 @@ describe('"getPuppeteer" command', () => { const session = mkSessionStub_(); session.capabilities = capabilities; session.sessionId = "100500"; - const browser = mkBrowser_({ browserWSEndpoint: "ws://new.endpoint/devtools" }); + const browser = mkBrowser_( + { browserWSEndpoint: "ws://new.endpoint/devtools" }, + undefined, + ExistingBrowser, + ); await initBrowser_({ browser, session }); await session.getPuppeteer(); diff --git a/test/src/browser/commands/openAndWait.ts b/test/src/browser/commands/openAndWait.ts index 0518d8dc1..11867f3a0 100644 --- a/test/src/browser/commands/openAndWait.ts +++ b/test/src/browser/commands/openAndWait.ts @@ -4,7 +4,8 @@ import FakeTimers from "@sinonjs/fake-timers"; import PageLoader from "src/utils/page-loader"; import { DEVTOOLS_PROTOCOL } from "src/constants/config"; import { mkSessionStub_ as mkSessionStubOrigin_, mkExistingBrowser_ } from "../utils"; -import type ExistingBrowser from "src/browser/existing-browser"; +import type { ExistingBrowser } from "src/browser/existing-browser"; +import { Calibrator } from "src/browser/calibrator"; type SessionOrigin = ReturnType; type Session = SessionOrigin & { openAndWait(uri: string, opts: Record): Promise }; @@ -83,7 +84,7 @@ describe('"openAndWait" command', () => { const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}): Promise => { wdioAttachStub.resolves(session); - return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities, sessionOpts: {} }); + return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities }, {} as Calibrator); }; it("should add command", async () => { diff --git a/test/src/browser/commands/runStep.js b/test/src/browser/commands/runStep.js index 496dbe515..3111e5911 100644 --- a/test/src/browser/commands/runStep.js +++ b/test/src/browser/commands/runStep.js @@ -1,21 +1,37 @@ "use strict"; -const webdriverio = require("webdriverio"); -const history = require("src/browser/history"); const { mkExistingBrowser_: mkBrowser_, mkSessionStub_ } = require("../utils"); +const proxyquire = require("proxyquire"); describe('"runStep" command', () => { const sandbox = sinon.createSandbox(); + let ExistingBrowser, webdriverioAttachStub; beforeEach(() => { - sandbox.stub(webdriverio, "attach"); - sandbox.stub(history, "initCommandHistory").returns(null); + webdriverioAttachStub = sandbox.stub(); + + ExistingBrowser = proxyquire("src/browser/existing-browser", { + webdriverio: { + attach: webdriverioAttachStub, + }, + "./browser": proxyquire("src/browser/browser", { + "./history": { + initCommandHistory: sandbox.stub().returns(null), + }, + }), + "./client-bridge": { + build: sandbox.stub().resolves(), + }, + }).ExistingBrowser; }); afterEach(() => sandbox.restore()); - const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}) => { - webdriverio.attach.resolves(session); + const initBrowser_ = ({ + browser = mkBrowser_(undefined, undefined, ExistingBrowser), + session = mkSessionStub_(), + } = {}) => { + webdriverioAttachStub.resolves(session); return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities }); }; diff --git a/test/src/browser/commands/setOrientation.js b/test/src/browser/commands/setOrientation.js index e2797f21c..8a1338cf0 100644 --- a/test/src/browser/commands/setOrientation.js +++ b/test/src/browser/commands/setOrientation.js @@ -1,21 +1,35 @@ "use strict"; -const webdriverio = require("webdriverio"); -const clientBridge = require("src/browser/client-bridge"); const { mkExistingBrowser_: mkBrowser_, mkSessionStub_ } = require("../utils"); +const proxyquire = require("proxyquire"); describe('"setOrientation" command', () => { const sandbox = sinon.createSandbox(); + let ExistingBrowser; + let webdriverioAttachStub; + let clientBridgeBuildStub; beforeEach(() => { - sandbox.stub(webdriverio, "attach"); - sandbox.stub(clientBridge, "build").resolves(); + webdriverioAttachStub = sandbox.stub(); + clientBridgeBuildStub = sandbox.stub().resolves(); + + ExistingBrowser = proxyquire("src/browser/existing-browser", { + webdriverio: { + attach: webdriverioAttachStub, + }, + "./client-bridge": { + build: clientBridgeBuildStub, + }, + }).ExistingBrowser; }); afterEach(() => sandbox.restore()); - const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}) => { - webdriverio.attach.resolves(session); + const initBrowser_ = ({ + browser = mkBrowser_(undefined, undefined, ExistingBrowser), + session = mkSessionStub_(), + } = {}) => { + webdriverioAttachStub.resolves(session); return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities }); }; @@ -94,7 +108,7 @@ describe('"setOrientation" command', () => { }); it("should not get initial body width", async () => { - const browser = mkBrowser_({ waitOrientationChange: false }); + const browser = mkBrowser_({ waitOrientationChange: false }, undefined, ExistingBrowser); await initBrowser_({ browser, session }); await session.setOrientation("portrait"); @@ -103,7 +117,7 @@ describe('"setOrientation" command', () => { }); it("should not wait for orientation change", async () => { - const browser = mkBrowser_({ waitOrientationChange: false }); + const browser = mkBrowser_({ waitOrientationChange: false }, undefined, ExistingBrowser); await initBrowser_({ browser, session }); await session.setOrientation("portrait"); @@ -125,7 +139,7 @@ describe('"setOrientation" command', () => { it("should wait for orientation change using a timeout from a browser config", async () => { const session = mkSessionStub_(); - const browser = mkBrowser_({ waitTimeout: 100500 }); + const browser = mkBrowser_({ waitTimeout: 100500 }, undefined, ExistingBrowser); await initBrowser_({ browser, session }); await session.setOrientation("portrait"); diff --git a/test/src/browser/commands/switchToRepl.ts b/test/src/browser/commands/switchToRepl.ts index 9d50a9bd0..d685fa01f 100644 --- a/test/src/browser/commands/switchToRepl.ts +++ b/test/src/browser/commands/switchToRepl.ts @@ -1,28 +1,33 @@ import repl, { type REPLServer } from "node:repl"; import { EventEmitter } from "node:events"; import proxyquire from "proxyquire"; -import * as webdriverio from "webdriverio"; import chalk from "chalk"; import sinon, { type SinonStub, type SinonSpy } from "sinon"; +import { mkExistingBrowser_ as mkBrowser_, mkSessionStub_ } from "../utils"; import RuntimeConfig from "src/config/runtime-config"; -import clientBridge from "src/browser/client-bridge"; -import type ExistingBrowser from "src/browser/existing-browser"; +import type { ExistingBrowser as ExistingBrowserOriginal } from "src/browser/existing-browser"; describe('"switchToRepl" command', () => { const sandbox = sinon.createSandbox(); - let mkBrowser_: SinonStub; - let mkSessionStub_: SinonStub; - + let ExistingBrowser: typeof ExistingBrowserOriginal; let logStub: SinonStub; let warnStub: SinonStub; + let webdriverioAttachStub: SinonStub; + let clientBridgeBuildStub; - const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}): Promise => { - (webdriverio.attach as SinonStub).resolves(session); + const initBrowser_ = ({ + browser = mkBrowser_(undefined, undefined, ExistingBrowser), + session = mkSessionStub_(), + } = {}): Promise => { + (webdriverioAttachStub as SinonStub).resolves(session); - return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities, sessionOpts: {} }); + return browser.init( + { sessionId: session.sessionId, sessionCaps: session.capabilities, sessionOpts: { capabilities: {} } }, + {} as any, + ); }; const mkReplServer_ = (): REPLServer => { @@ -46,25 +51,27 @@ describe('"switchToRepl" command', () => { }; beforeEach(() => { - mkBrowser_ = sandbox.stub(); - mkSessionStub_ = sandbox.stub(); - logStub = sandbox.stub(); warnStub = sandbox.stub(); - sandbox.stub(webdriverio, "attach"); - sandbox.stub(clientBridge, "build").resolves(); + webdriverioAttachStub = sandbox.stub(); + clientBridgeBuildStub = sandbox.stub().resolves(); + + ExistingBrowser = proxyquire("src/browser/existing-browser", { + webdriverio: { + attach: webdriverioAttachStub, + }, + "./client-bridge": { + build: clientBridgeBuildStub, + }, + "../utils/logger": { warn: warnStub, log: logStub }, + "./commands/switchToRepl": proxyquire("src/browser/commands/switchToRepl", { + "../../utils/logger": { warn: warnStub, log: logStub }, + }), + }).ExistingBrowser; + sandbox.stub(RuntimeConfig, "getInstance").returns({ replMode: { enabled: false }, extend: sinon.stub() }); sandbox.stub(process, "chdir"); - - ({ mkExistingBrowser_: mkBrowser_, mkSessionStub_ } = proxyquire("../utils", { - "src/browser/existing-browser": proxyquire("src/browser/existing-browser", { - "../utils/logger": { warn: warnStub, log: logStub }, - "./commands/switchToRepl": proxyquire("src/browser/commands/switchToRepl", { - "../../utils/logger": { warn: warnStub, log: logStub }, - }), - }), - })); }); afterEach(() => sandbox.restore()); diff --git a/test/src/browser/existing-browser.js b/test/src/browser/existing-browser.js index 6228eb5bc..2a4d401f0 100644 --- a/test/src/browser/existing-browser.js +++ b/test/src/browser/existing-browser.js @@ -3,14 +3,9 @@ const { EventEmitter } = require("events"); const crypto = require("crypto"); const _ = require("lodash"); -const Promise = require("bluebird"); -const webdriverio = require("webdriverio"); const jsdom = require("jsdom-global"); -const Browser = require("src/browser/existing-browser"); -const Calibrator = require("src/browser/calibrator"); -const Camera = require("src/browser/camera"); -const clientBridge = require("src/browser/client-bridge"); -const logger = require("src/utils/logger"); +const { Calibrator } = require("src/browser/calibrator"); +const { Camera } = require("src/browser/camera"); const history = require("src/browser/history"); const { SAVE_HISTORY_MODE, @@ -21,17 +16,24 @@ const { } = require("src/constants/config"); const { MIN_CHROME_VERSION_SUPPORT_ISOLATION, X_REQUEST_ID_DELIMITER } = require("src/constants/browser"); const { - mkExistingBrowser_: mkBrowser_, + mkExistingBrowser_, mkSessionStub_, mkCDPStub_, mkCDPBrowserCtx_, mkCDPPage_, mkCDPTarget_, } = require("./utils"); +const proxyquire = require("proxyquire"); describe("ExistingBrowser", () => { const sandbox = sinon.createSandbox(); let session; + let ExistingBrowser; + let webdriverioAttachStub, clientBridgeBuildStub, loggerWarnStub, initCommandHistoryStub, runGroupStub; + + const mkBrowser_ = (configOpts, opts) => { + return mkExistingBrowser_(configOpts, opts, ExistingBrowser); + }; const initBrowser_ = (browser = mkBrowser_(), sessionData = {}, calibrator) => { sessionData = _.defaults(sessionData, { @@ -45,16 +47,39 @@ describe("ExistingBrowser", () => { const stubClientBridge_ = () => { const bridge = { call: sandbox.stub().resolves({}) }; - clientBridge.build.resolves(bridge); + clientBridgeBuildStub.resolves(bridge); return bridge; }; beforeEach(() => { session = mkSessionStub_(); - sandbox.stub(webdriverio, "attach").resolves(session); - sandbox.stub(logger, "warn"); - sandbox.stub(clientBridge, "build").resolves(); + webdriverioAttachStub = sandbox.stub().resolves(session); + clientBridgeBuildStub = sandbox.stub().resolves(); + loggerWarnStub = sandbox.stub(); + initCommandHistoryStub = sandbox.stub(); + runGroupStub = sandbox.stub(); + + ExistingBrowser = proxyquire("src/browser/existing-browser", { + webdriverio: { + attach: webdriverioAttachStub, + }, + "./client-bridge": { + build: clientBridgeBuildStub, + }, + "../utils/logger": { + warn: loggerWarnStub, + }, + "./browser": proxyquire("src/browser/browser", { + "./history": { + initCommandHistory: initCommandHistoryStub.callsFake(history.initCommandHistory), + runGroup: runGroupStub.callsFake(history.runGroup), + }, + }), + "./history": { + runGroup: runGroupStub.callsFake(history.runGroup), + }, + }).ExistingBrowser; }); afterEach(() => sandbox.restore()); @@ -128,8 +153,8 @@ describe("ExistingBrowser", () => { }, }); - assert.calledOnce(webdriverio.attach); - assert.calledWithMatch(webdriverio.attach, { ...detectedSessionEnvFlags, isChrome: true }); + assert.calledOnce(webdriverioAttachStub); + assert.calledWithMatch(webdriverioAttachStub, { ...detectedSessionEnvFlags, isChrome: true }); }); it("should attach to browser with session environment flags from config", async () => { @@ -139,8 +164,8 @@ describe("ExistingBrowser", () => { await initBrowser_(browser); - assert.calledOnce(webdriverio.attach); - assert.calledWithMatch(webdriverio.attach, sessionEnvFlags); + assert.calledOnce(webdriverioAttachStub); + assert.calledWithMatch(webdriverioAttachStub, sessionEnvFlags); }); it("should attach to browser with options from master session", async () => { @@ -148,7 +173,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_(), { sessionOpts }); - assert.calledWithMatch(webdriverio.attach, { ...sessionOpts, options: sessionOpts }); + assert.calledWithMatch(webdriverioAttachStub, { ...sessionOpts, options: sessionOpts }); }); describe("collect custom command", () => { @@ -193,7 +218,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_({ transformRequest: transformRequestStub })); - const { transformRequest } = webdriverio.attach.lastCall.args[0]; + const { transformRequest } = webdriverioAttachStub.lastCall.args[0]; transformRequest(request); assert.calledOnceWith(transformRequestStub, request); @@ -208,7 +233,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_({ transformRequest: transformRequestStub })); - const { transformRequest } = webdriverio.attach.lastCall.args[0]; + const { transformRequest } = webdriverioAttachStub.lastCall.args[0]; transformRequest(request); assert.equal(request.headers["X-Request-ID"], "100500"); @@ -221,7 +246,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_({}, { state })); - const { transformRequest } = webdriverio.attach.lastCall.args[0]; + const { transformRequest } = webdriverioAttachStub.lastCall.args[0]; transformRequest(request); assert.equal(request.headers["X-Request-ID"], `12345${X_REQUEST_ID_DELIMITER}67890`); @@ -235,7 +260,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_({ transformResponse: transformResponseStub })); - const { transformResponse } = webdriverio.attach.lastCall.args[0]; + const { transformResponse } = webdriverioAttachStub.lastCall.args[0]; transformResponse(response); assert.calledOnceWith(transformResponseStub, response); @@ -248,7 +273,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_(), { sessionOpts }); - assert.calledOnceWith(webdriverio.attach, sinon.match.has("options", sessionOpts)); + assert.calledOnceWith(webdriverioAttachStub, sinon.match.has("options", sessionOpts)); }); it("should attach to browser with caps merged from master session opts and caps", async () => { @@ -257,7 +282,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_(), { sessionCaps, sessionOpts: { capabilities } }); - assert.calledWithMatch(webdriverio.attach, { + assert.calledWithMatch(webdriverioAttachStub, { capabilities: { ...capabilities, ...sessionCaps }, }); }); @@ -269,21 +294,17 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_(), { sessionOpts: { capabilities } }); - assert.calledWithMatch(webdriverio.attach, { requestedCapabilities: capabilities }); + assert.calledWithMatch(webdriverioAttachStub, { requestedCapabilities: capabilities }); }); }); describe("commands-history", () => { - beforeEach(() => { - sandbox.spy(history, "initCommandHistory"); - }); - it("should NOT init commands-history if it is off", async () => { const browser = mkBrowser_({ saveHistoryMode: SAVE_HISTORY_MODE.NONE }); await initBrowser_(browser); - assert.notCalled(history.initCommandHistory); + assert.notCalled(initCommandHistoryStub); }); describe("should save history of executed commands", () => { @@ -292,7 +313,7 @@ describe("ExistingBrowser", () => { await initBrowser_(browser); - assert.calledOnceWith(history.initCommandHistory, session); + assert.calledOnceWith(initCommandHistoryStub, session); }); it("if it is enabled for failed tests only", async () => { @@ -300,7 +321,7 @@ describe("ExistingBrowser", () => { await initBrowser_(browser); - assert.calledOnceWith(history.initCommandHistory, session); + assert.calledOnceWith(initCommandHistoryStub, session); }); }); @@ -309,7 +330,7 @@ describe("ExistingBrowser", () => { await initBrowser_(browser); - assert.calledOnceWith(history.initCommandHistory, session); + assert.calledOnceWith(initCommandHistoryStub, session); }); it("should init commands-history before any commands have added", async () => { @@ -317,16 +338,15 @@ describe("ExistingBrowser", () => { await initBrowser_(browser); - assert.callOrder(history.initCommandHistory, session.addCommand); + assert.callOrder(initCommandHistoryStub, session.addCommand); }); it('should log "init" to history if "saveHistory" is set', async () => { const browser = mkBrowser_({ saveHistoryMode: SAVE_HISTORY_MODE.ALL }); - sandbox.stub(history, "runGroup"); await initBrowser_(browser, {}); - assert.calledOnceWith(history.runGroup, sinon.match.any, "testplane: init browser", sinon.match.func); + assert.calledOnceWith(runGroupStub, sinon.match.any, "testplane: init browser", sinon.match.func); }); }); @@ -516,7 +536,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_({ isolation: false })); assert.notCalled(session.getPuppeteer); - assert.notCalled(logger.warn); + assert.notCalled(loggerWarnStub); }); it("test wasn't run in chrome", async () => { @@ -543,7 +563,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_({ isolation: true }), { sessionCaps }); assert.calledOnceWith( - logger.warn, + loggerWarnStub, `WARN: test isolation works only with chrome@${MIN_CHROME_VERSION_SUPPORT_ISOLATION} and higher, ` + "but got chrome@90.0", ); @@ -555,7 +575,7 @@ describe("ExistingBrowser", () => { await initBrowser_(mkBrowser_({ isolation: true }), { sessionCaps }); assert.calledOnceWith( - logger.warn, + loggerWarnStub, `WARN: test isolation works only with chrome@${MIN_CHROME_VERSION_SUPPORT_ISOLATION} and higher, ` + "but got chrome@70.0", ); @@ -658,7 +678,7 @@ describe("ExistingBrowser", () => { await initBrowser_(browser); - assert.calledOnce(logger.warn); + assert.calledOnce(loggerWarnStub); }); describe("set browser orientation", () => { @@ -705,7 +725,7 @@ describe("ExistingBrowser", () => { }); it("should perform calibration if `calibrate` is turn on", async () => { - calibrator.calibrate.withArgs(sinon.match.instanceOf(Browser)).resolves({ foo: "bar" }); + calibrator.calibrate.withArgs(sinon.match.instanceOf(ExistingBrowser)).resolves({ foo: "bar" }); const browser = mkBrowser_({ calibrate: true }); await initBrowser_(browser, {}, calibrator); @@ -738,7 +758,7 @@ describe("ExistingBrowser", () => { await initBrowser_(browser, {}, calibrator); - assert.calledOnceWith(clientBridge.build, browser, { calibration: { foo: "bar" } }); + assert.calledOnceWith(clientBridgeBuildStub, browser, { calibration: { foo: "bar" } }); }); }); @@ -969,7 +989,7 @@ describe("ExistingBrowser", () => { describe("captureViewportImage", () => { beforeEach(() => { sandbox.stub(Camera.prototype, "captureViewportImage"); - sandbox.stub(Promise, "delay").returns(Promise.resolve()); + sandbox.stub(global, "setTimeout").callsFake(fn => fn()); }); it("should delay capturing on the passed time", () => { @@ -978,8 +998,8 @@ describe("ExistingBrowser", () => { return mkBrowser_({ screenshotDelay: 100500 }) .captureViewportImage({ foo: "bar" }, 2000) .then(() => { - assert.calledOnceWith(Promise.delay, 2000); - assert.callOrder(Promise.delay, Camera.prototype.captureViewportImage); + assert.calledOnceWith(global.setTimeout, sinon.match.any, 2000); + assert.callOrder(global.setTimeout, Camera.prototype.captureViewportImage); }); }); diff --git a/test/src/browser/history/callstack.js b/test/src/browser/history/callstack.js index 45fcb32ac..5f810af66 100644 --- a/test/src/browser/history/callstack.js +++ b/test/src/browser/history/callstack.js @@ -1,6 +1,6 @@ "use strict"; -const Callstack = require("src/browser/history/callstack"); +const { Callstack } = require("src/browser/history/callstack"); const { TestStepKey } = require("src/types"); describe("commands-history", () => { diff --git a/test/src/browser/history/index.js b/test/src/browser/history/index.js index 1a314e270..38a79d6b6 100644 --- a/test/src/browser/history/index.js +++ b/test/src/browser/history/index.js @@ -3,7 +3,7 @@ const P = require("bluebird"); const webdriverio = require("webdriverio"); const proxyquire = require("proxyquire"); -const Callstack = require("../../../../src/browser/history/callstack"); +const { Callstack } = require("../../../../src/browser/history/callstack"); const { mkNewBrowser_, mkExistingBrowser_, mkSessionStub_ } = require("../utils"); describe("commands-history", () => { @@ -59,7 +59,7 @@ describe("commands-history", () => { assert.propertyVal(node, "n", "url"); assert.propertyVal(node, "s", "b"); - assert.propertyVal(node, "o", 1); + assert.propertyVal(node, "o", true); assert.deepPropertyVal(node, "a", ["site.com"]); }); diff --git a/test/src/browser/history/utils.js b/test/src/browser/history/utils.js index 5cf1bc661..bc057fa77 100644 --- a/test/src/browser/history/utils.js +++ b/test/src/browser/history/utils.js @@ -23,8 +23,8 @@ describe("commands-history", () => { ]); }); - it("should not modify an argument if it is not string or object", () => { - assert.deepEqual(normalizeCommandArgs("click", [false, null, 100]), [false, null, 100]); + it("should convert argument to string if it is not string or object", () => { + assert.deepEqual(normalizeCommandArgs("click", [false, null, 100]), ["false", "null", "100"]); }); }); diff --git a/test/src/browser/new-browser.js b/test/src/browser/new-browser.js index c7e253209..22df115e4 100644 --- a/test/src/browser/new-browser.js +++ b/test/src/browser/new-browser.js @@ -1,36 +1,49 @@ "use strict"; const crypto = require("crypto"); -const webdriverio = require("webdriverio"); const proxyquire = require("proxyquire"); const signalHandler = require("src/signal-handler"); const history = require("src/browser/history"); const { WEBDRIVER_PROTOCOL, DEVTOOLS_PROTOCOL, SAVE_HISTORY_MODE } = require("src/constants/config"); const { X_REQUEST_ID_DELIMITER } = require("src/constants/browser"); const RuntimeConfig = require("src/config/runtime-config"); +const { mkNewBrowser_, mkSessionStub_, mkWdPool_ } = require("./utils"); describe("NewBrowser", () => { const sandbox = sinon.createSandbox(); let session; - let mkBrowser_, mkSessionStub_, mkWdPool_, installBrowserStub, warnStub; + let NewBrowser, webdriverioRemoteStub, runGroupStub, initCommandHistoryStub, installBrowserStub, warnStub; + + const mkBrowser_ = (configOpts, opts) => { + return mkNewBrowser_(configOpts, opts, NewBrowser); + }; beforeEach(() => { + session = mkSessionStub_(); installBrowserStub = sandbox.stub().resolves("/browser/path"); warnStub = sandbox.stub(); - - ({ - mkNewBrowser_: mkBrowser_, - mkSessionStub_, - mkWdPool_, - } = proxyquire("./utils", { - "src/browser/new-browser": proxyquire("src/browser/new-browser", { - "../browser-installer": { installBrowser: installBrowserStub }, - "../utils/logger": { warn: warnStub }, + webdriverioRemoteStub = sandbox.stub().resolves(session); + runGroupStub = sandbox.stub().callsFake(history.runGroup); + initCommandHistoryStub = sandbox.stub(); + + NewBrowser = proxyquire("src/browser/new-browser", { + webdriverio: { + remote: webdriverioRemoteStub, + }, + "../browser-installer": { installBrowser: installBrowserStub }, + "../utils/logger": { warn: warnStub }, + "./history": { + runGroup: runGroupStub, + }, + "./browser": proxyquire("src/browser/browser", { + "./history": { + runGroup: runGroupStub, + initCommandHistory: initCommandHistoryStub, + }, }), - })); + }).NewBrowser; - session = mkSessionStub_(); - sandbox.stub(webdriverio, "remote").resolves(session); + // sandbox.stub(webdriverio, "remote").resolves(session); sandbox.stub(RuntimeConfig, "getInstance").returns({ devtools: undefined, local: undefined }); }); @@ -41,7 +54,7 @@ describe("NewBrowser", () => { it("should create session with properties from browser config", async () => { await mkBrowser_().init(); - assert.calledOnceWith(webdriverio.remote, { + assert.calledOnceWith(webdriverioRemoteStub, { protocol: "http", hostname: "test_host", port: 4444, @@ -63,13 +76,13 @@ describe("NewBrowser", () => { await mkBrowser_().init(); - assert.calledWithMatch(webdriverio.remote, { automationProtocol: DEVTOOLS_PROTOCOL }); + assert.calledWithMatch(webdriverioRemoteStub, { automationProtocol: DEVTOOLS_PROTOCOL }); }); it("should pass default port if it is not specified in grid url", async () => { await mkBrowser_({ gridUrl: "http://some-host/some-path" }).init(); - assert.calledWithMatch(webdriverio.remote, { port: 4444 }); + assert.calledWithMatch(webdriverioRemoteStub, { port: 4444 }); }); describe("headless setting", () => { @@ -80,7 +93,7 @@ describe("NewBrowser", () => { desiredCapabilities: { browserName: "chrome" }, }).init(); - assert.calledWithMatch(webdriverio.remote, { + assert.calledWithMatch(webdriverioRemoteStub, { capabilities: { browserName: "chrome", "goog:chromeOptions": { args: ["headless", "disable-gpu"] }, @@ -94,7 +107,7 @@ describe("NewBrowser", () => { desiredCapabilities: { browserName: "chrome" }, }).init(); - assert.calledWithMatch(webdriverio.remote, { + assert.calledWithMatch(webdriverioRemoteStub, { capabilities: { browserName: "chrome", "goog:chromeOptions": { args: ["headless=new", "disable-gpu"] }, @@ -109,7 +122,7 @@ describe("NewBrowser", () => { desiredCapabilities: { browserName: "firefox" }, }).init(); - assert.calledWithMatch(webdriverio.remote, { + assert.calledWithMatch(webdriverioRemoteStub, { capabilities: { browserName: "firefox", "moz:firefoxOptions": { args: ["-headless"] }, @@ -123,7 +136,7 @@ describe("NewBrowser", () => { desiredCapabilities: { browserName: "msedge" }, }).init(); - assert.calledWithMatch(webdriverio.remote, { + assert.calledWithMatch(webdriverioRemoteStub, { capabilities: { browserName: "msedge", "ms:edgeOptions": { args: ["--headless"] } }, }); }); @@ -137,7 +150,7 @@ describe("NewBrowser", () => { }, }).init(); - assert.calledWithMatch(webdriverio.remote, { + assert.calledWithMatch(webdriverioRemoteStub, { capabilities: { browserName: "chrome", "goog:chromeOptions": { args: ["my", "custom", "flags", "headless", "disable-gpu"] }, @@ -162,7 +175,7 @@ describe("NewBrowser", () => { { id: "browser", version: "2.0" }, ).init(); - assert.calledWithMatch(webdriverio.remote, { + assert.calledWithMatch(webdriverioRemoteStub, { capabilities: { browserName: "browser", browserVersion: "2.0" }, }); }); @@ -170,7 +183,7 @@ describe("NewBrowser", () => { it("w3c protocol is used", async () => { await mkBrowser_({ sessionEnvFlags: { isW3C: true } }, { id: "browser", version: "2.0" }).init(); - assert.calledWithMatch(webdriverio.remote, { + assert.calledWithMatch(webdriverioRemoteStub, { capabilities: { browserName: "browser", browserVersion: "2.0" }, }); }); @@ -202,19 +215,19 @@ describe("NewBrowser", () => { it("should use session request timeout for create a session", async () => { await mkBrowser_({ sessionRequestTimeout: 100500, httpTimeout: 500100 }).init(); - assert.calledWithMatch(webdriverio.remote, { connectionRetryTimeout: 100500 }); + assert.calledWithMatch(webdriverioRemoteStub, { connectionRetryTimeout: 100500 }); }); it("should use http timeout for create a session if session request timeout not set", async () => { await mkBrowser_({ sessionRequestTimeout: null, httpTimeout: 500100 }).init(); - assert.calledWithMatch(webdriverio.remote, { connectionRetryTimeout: 500100 }); + assert.calledWithMatch(webdriverioRemoteStub, { connectionRetryTimeout: 500100 }); }); it("should reset options to default after create a session", async () => { await mkBrowser_().init(); - assert.callOrder(webdriverio.remote, session.extendOptions); + assert.callOrder(webdriverioRemoteStub, session.extendOptions); }); it("should reset http timeout to default after create a session", async () => { @@ -241,7 +254,7 @@ describe("NewBrowser", () => { await mkBrowser_({ transformRequest: transformRequestStub }).init(); - const { transformRequest } = webdriverio.remote.lastCall.args[0]; + const { transformRequest } = webdriverioRemoteStub.lastCall.args[0]; transformRequest(request); assert.calledOnceWith(transformRequestStub, request); @@ -256,7 +269,7 @@ describe("NewBrowser", () => { await mkBrowser_({ transformRequest: transformRequestStub }).init(); - const { transformRequest } = webdriverio.remote.lastCall.args[0]; + const { transformRequest } = webdriverioRemoteStub.lastCall.args[0]; transformRequest(request); assert.equal(request.headers["X-Request-ID"], "100500"); @@ -269,7 +282,7 @@ describe("NewBrowser", () => { await mkBrowser_({}, { state }).init(); - const { transformRequest } = webdriverio.remote.lastCall.args[0]; + const { transformRequest } = webdriverioRemoteStub.lastCall.args[0]; transformRequest(request); assert.equal(request.headers["X-Request-ID"], `12345${X_REQUEST_ID_DELIMITER}67890`); @@ -283,7 +296,7 @@ describe("NewBrowser", () => { await mkBrowser_({ transformResponse: transformResponseStub }).init(); - const { transformResponse } = webdriverio.remote.lastCall.args[0]; + const { transformResponse } = webdriverioRemoteStub.lastCall.args[0]; transformResponse(response); assert.calledOnceWith(transformResponseStub, response); @@ -291,40 +304,35 @@ describe("NewBrowser", () => { }); describe("commands-history", () => { - beforeEach(() => { - sandbox.spy(history, "initCommandHistory"); - }); - it("should NOT init commands-history if it is off", async () => { await mkBrowser_({ saveHistoryMode: SAVE_HISTORY_MODE.NONE }).init(); - assert.notCalled(history.initCommandHistory); + assert.notCalled(initCommandHistoryStub); }); it("should save history of executed commands if it is enabled", async () => { await mkBrowser_({ saveHistoryMode: SAVE_HISTORY_MODE.ALL }).init(); - assert.calledOnceWith(history.initCommandHistory, session); + assert.calledOnceWith(initCommandHistoryStub, session); }); it("should save history of executed commands if it is enabled on fails", async () => { await mkBrowser_({ saveHistoryMode: "onlyFailed" }).init(); - assert.calledOnceWith(history.initCommandHistory, session); + assert.calledOnceWith(initCommandHistoryStub, session); }); it("should init commands-history before any commands have added", async () => { await mkBrowser_({ saveHistoryMode: SAVE_HISTORY_MODE.ALL }).init(); - assert.callOrder(history.initCommandHistory, session.addCommand); + assert.callOrder(initCommandHistoryStub, session.addCommand); }); it('should log "init" to history if "saveHistoryMode" and "pageLoadTimeout" are set', async () => { const browser = mkBrowser_({ saveHistoryMode: SAVE_HISTORY_MODE.ALL, pageLoadTimeout: 500100 }); - sandbox.stub(history, "runGroup"); await browser.init(); - assert.calledOnceWith(history.runGroup, sinon.match.any, "testplane: init browser", sinon.match.func); + assert.calledOnceWith(runGroupStub, sinon.match.any, "testplane: init browser", sinon.match.func); }); }); @@ -385,7 +393,7 @@ describe("NewBrowser", () => { await browser.init(); - assert.calledWithMatch(webdriverio.remote, { + assert.calledWithMatch(webdriverioRemoteStub, { protocol: "http", hostname: "localhost", port: 12345, @@ -418,7 +426,7 @@ describe("NewBrowser", () => { await browser.init(); - assert.calledWithMatch(webdriverio.remote, { + assert.calledWithMatch(webdriverioRemoteStub, { protocol: "http", hostname: "localhost", port: 12345, @@ -454,7 +462,7 @@ describe("NewBrowser", () => { await browser.init(); - assert.deepEqual(webdriverio.remote.lastCall.args[0].capabilities, { + assert.deepEqual(webdriverioRemoteStub.lastCall.args[0].capabilities, { browserName: "chrome", browserVersion: "115.0", "goog:chromeOptions": { diff --git a/test/src/browser/screen-shooter/index.js b/test/src/browser/screen-shooter/index.js index 5b62eb5c0..facdaa754 100644 --- a/test/src/browser/screen-shooter/index.js +++ b/test/src/browser/screen-shooter/index.js @@ -1,6 +1,6 @@ "use strict"; -const Image = require("src/image"); +const { Image } = require("src/image"); const ScreenShooter = require("src/browser/screen-shooter"); const Viewport = require("src/browser/screen-shooter/viewport"); diff --git a/test/src/browser/screen-shooter/viewport/index.js b/test/src/browser/screen-shooter/viewport/index.js index 834227ed2..a9e7a2470 100644 --- a/test/src/browser/screen-shooter/viewport/index.js +++ b/test/src/browser/screen-shooter/viewport/index.js @@ -2,7 +2,7 @@ const _ = require("lodash"); -const Image = require("src/image"); +const { Image } = require("src/image"); const Viewport = require("src/browser/screen-shooter/viewport"); const CoordValidator = require("src/browser/screen-shooter/viewport/coord-validator"); diff --git a/test/src/browser/utils.js b/test/src/browser/utils.js index b6719143d..e45b139ad 100644 --- a/test/src/browser/utils.js +++ b/test/src/browser/utils.js @@ -3,7 +3,7 @@ const _ = require("lodash"); const EventEmitter = require("events"); const { NewBrowser } = require("src/browser/new-browser"); -const ExistingBrowser = require("src/browser/existing-browser"); +const { ExistingBrowser } = require("src/browser/existing-browser"); const { WEBDRIVER_PROTOCOL } = require("src/constants/config"); function createBrowserConfig_(opts = {}) { @@ -68,12 +68,17 @@ exports.mkNewBrowser_ = ( state: {}, wdPool: exports.mkWdPool_(), }, + BrowserClass = NewBrowser, ) => { - return NewBrowser.create(createBrowserConfig_(configOpts), opts); + return BrowserClass.create(createBrowserConfig_(configOpts), opts); }; -exports.mkExistingBrowser_ = (configOpts, opts = { id: "browser", version: "1.0", state: {}, emitter: "emitter" }) => { - return ExistingBrowser.create(createBrowserConfig_(configOpts), opts); +exports.mkExistingBrowser_ = ( + configOpts, + opts = { id: "browser", version: "1.0", state: {}, emitter: "emitter" }, + BrowserClass = ExistingBrowser, +) => { + return BrowserClass.create(createBrowserConfig_(configOpts), opts); }; exports.mkMockStub_ = () => { diff --git a/test/src/cli/commands/list-browsers/index.ts b/test/src/cli/commands/list-browsers/index.ts index 9b84eeaae..833eca3ac 100644 --- a/test/src/cli/commands/list-browsers/index.ts +++ b/test/src/cli/commands/list-browsers/index.ts @@ -1,13 +1,15 @@ import { Command } from "@gemini-testing/commander"; import sinon, { SinonStub } from "sinon"; -import logger from "../../../../../src/utils/logger"; import { Testplane } from "../../../../../src/testplane"; -import * as testplaneCli from "../../../../../src/cli"; +import * as testplaneCliOriginal from "../../../../../src/cli"; +import proxyquire from "proxyquire"; +import path from "node:path"; describe("cli/commands/list-browsers", () => { const sandbox = sinon.createSandbox(); + let testplaneCli: typeof testplaneCliOriginal; let testplaneStub: Testplane; let loggerErrorStub: SinonStub; let consoleInfoStub: SinonStub; @@ -20,6 +22,27 @@ describe("cli/commands/list-browsers", () => { }; beforeEach(() => { + loggerErrorStub = sandbox.stub(); + + testplaneCli = proxyquire("src/cli", { + [path.resolve(__dirname, "../../../../../src/cli/commands/list-browsers")]: proxyquire.noCallThru()( + "../../../../../src/cli/commands/list-browsers", + { + "../../../utils/logger": { + error: loggerErrorStub, + }, + }, + ), + "../utils/cli": proxyquire("src/utils/cli", { + "./logger": { + error: loggerErrorStub, + }, + }), + "../utils/logger": { + error: loggerErrorStub, + }, + }); + testplaneStub = Object.create(Testplane.prototype); Object.defineProperty(testplaneStub, "config", { @@ -35,7 +58,6 @@ describe("cli/commands/list-browsers", () => { sandbox.stub(Testplane, "create").returns(testplaneStub); - loggerErrorStub = sandbox.stub(logger, "error"); consoleInfoStub = sandbox.stub(console, "info"); sandbox.stub(process, "exit"); diff --git a/test/src/cli/commands/list-tests/index.ts b/test/src/cli/commands/list-tests/index.ts index 8bed5bf1a..6dd5d5fb1 100644 --- a/test/src/cli/commands/list-tests/index.ts +++ b/test/src/cli/commands/list-tests/index.ts @@ -5,13 +5,13 @@ import sinon, { SinonStub } from "sinon"; import proxyquire from "proxyquire"; import { Formatters } from "../../../../../src/test-collection"; -import logger from "../../../../../src/utils/logger"; import { Testplane } from "../../../../../src/testplane"; -import * as testplaneCli from "../../../../../src/cli"; +import * as testplaneCliOriginal from "../../../../../src/cli"; import { TestCollection } from "../../../../../src/test-collection"; describe("cli/commands/list-tests", () => { const sandbox = sinon.createSandbox(); + let testplaneCli: typeof testplaneCliOriginal; const listTests_ = async (argv: string = "", cli: { run: VoidFunction } = testplaneCli): Promise => { process.argv = ["foo/bar/node", "foo/bar/script", "list-tests", ...argv.split(" ")].filter(Boolean); @@ -21,13 +21,27 @@ describe("cli/commands/list-tests", () => { }; beforeEach(() => { + testplaneCli = proxyquire("src/cli", { + "../utils/logger": { + warn: sinon.stub(), + error: sinon.stub(), + }, + [path.resolve(__dirname, "../../../../../src/cli/commands/list-tests")]: proxyquire.noCallThru()( + "../../../../../src/cli/commands/list-tests", + { + "../../../utils/logger": { + warn: sinon.stub(), + error: sinon.stub(), + }, + }, + ), + }); sandbox.stub(Testplane, "create").returns(Object.create(Testplane.prototype)); sandbox.stub(Testplane.prototype, "readTests").resolves(TestCollection.create({})); sandbox.stub(fs, "ensureDir").resolves(); sandbox.stub(fs, "writeJson").resolves(); - sandbox.stub(logger, "error"); sandbox.stub(console, "info"); sandbox.stub(process, "exit"); @@ -45,6 +59,10 @@ describe("cli/commands/list-tests", () => { "../../../test-collection": { validateFormatter: validateFormatterStub, }, + "../../../utils/logger": { + warn: sinon.stub(), + error: sinon.stub(), + }, }, ), }); diff --git a/test/src/cli/index.js b/test/src/cli/index.js index 2b4555981..4ac8b2978 100644 --- a/test/src/cli/index.js +++ b/test/src/cli/index.js @@ -2,17 +2,17 @@ const { Command } = require("@gemini-testing/commander"); const proxyquire = require("proxyquire").noCallThru(); -const testplaneCli = require("src/cli"); const { configOverriding } = require("src/cli/info"); const defaults = require("src/config/defaults"); const { Testplane } = require("src/testplane"); -const logger = require("src/utils/logger"); const { collectCliValues, withCommonCliOptions } = require("src/utils/cli"); const any = sinon.match.any; describe("cli", () => { const sandbox = sinon.createSandbox(); + let testplaneCli; + let loggerLogStub, loggerWarnStub, loggerErrorStub; const run_ = async (argv = "", cli) => { process.argv = ["foo/bar/node", "foo/bar/script", ...argv.split(" ")]; @@ -24,14 +24,29 @@ describe("cli", () => { }; beforeEach(() => { + loggerLogStub = sandbox.stub(); + loggerWarnStub = sandbox.stub(); + loggerErrorStub = sandbox.stub(); + + testplaneCli = proxyquire("src/cli", { + "../utils/cli": proxyquire("src/utils/cli", { + "./logger": { + log: loggerLogStub, + warn: loggerWarnStub, + error: loggerErrorStub, + }, + }), + "../utils/logger": { + log: loggerLogStub, + warn: loggerWarnStub, + error: loggerErrorStub, + }, + }); + sandbox.stub(Testplane, "create").returns(Object.create(Testplane.prototype)); sandbox.stub(Testplane.prototype, "run").resolves(); sandbox.stub(Testplane.prototype, "extendCli"); - sandbox.stub(logger, "log"); - sandbox.stub(logger, "error"); - sandbox.stub(logger, "warn"); - sandbox.stub(process, "exit"); sandbox.spy(Command.prototype, "action"); @@ -163,7 +178,7 @@ describe("cli", () => { it("should warn about invalid regex", async () => { await run_("--grep (foo|bar"); - assert.calledOnceWith(logger.warn, sinon.match("(foo|bar")); + assert.calledOnceWith(loggerWarnStub, sinon.match("(foo|bar")); }); }); @@ -225,7 +240,7 @@ describe("cli", () => { await run_(); - assert.calledWith(logger.error, "some-stack"); + assert.calledWith(loggerErrorStub, "some-stack"); }); it("should log an error on reject if stack does not exist", async () => { @@ -235,7 +250,7 @@ describe("cli", () => { await run_(); - assert.calledWithMatch(logger.error, err); + assert.calledWithMatch(loggerErrorStub, err); }); it("should turn on debug mode from cli", async () => { diff --git a/test/src/image.js b/test/src/image.js index 8b575c3fe..8c95d821c 100644 --- a/test/src/image.js +++ b/test/src/image.js @@ -47,7 +47,7 @@ describe("Image", () => { Image = proxyquire("src/image", { "looks-same": looksSameStub, sharp: mkSharpInstance, - }); + }).Image; image = Image.create("imgBuffer"); }); diff --git a/test/src/reporters/informers/console.js b/test/src/reporters/informers/console.js index 612c9cff9..ee922fd89 100644 --- a/test/src/reporters/informers/console.js +++ b/test/src/reporters/informers/console.js @@ -1,13 +1,22 @@ -const logger = require("src/utils/logger"); -const ConsoleInformer = require("src/reporters/informers/console"); +const proxyquire = require("proxyquire"); describe("reporter/informers/console", () => { const sandbox = sinon.createSandbox(); + let ConsoleInformer; + let loggerLogStub, loggerWarnStub, loggerErrorStub; beforeEach(() => { - sandbox.stub(logger, "log"); - sandbox.stub(logger, "warn"); - sandbox.stub(logger, "error"); + loggerLogStub = sandbox.stub(); + loggerWarnStub = sandbox.stub(); + loggerErrorStub = sandbox.stub(); + + ConsoleInformer = proxyquire("src/reporters/informers/console", { + "../../utils/logger": { + log: loggerLogStub, + warn: loggerWarnStub, + error: loggerErrorStub, + }, + }); }); afterEach(() => sandbox.restore()); @@ -16,7 +25,7 @@ describe("reporter/informers/console", () => { it("should send log message to console", () => { ConsoleInformer.create().log("message"); - assert.calledOnceWith(logger.log, "message"); + assert.calledOnceWith(loggerLogStub, "message"); }); }); @@ -24,7 +33,7 @@ describe("reporter/informers/console", () => { it("should send warn message to console", () => { ConsoleInformer.create().warn("message"); - assert.calledOnceWith(logger.warn, "message"); + assert.calledOnceWith(loggerWarnStub, "message"); }); }); @@ -32,7 +41,7 @@ describe("reporter/informers/console", () => { it("should send error message to console", () => { ConsoleInformer.create().error("message"); - assert.calledOnceWith(logger.error, "message"); + assert.calledOnceWith(loggerErrorStub, "message"); }); }); @@ -40,13 +49,13 @@ describe("reporter/informers/console", () => { it("should do nothing if message is not passed", () => { ConsoleInformer.create().end(); - assert.notCalled(logger.log); + assert.notCalled(loggerLogStub); }); it("should send end message to console", () => { ConsoleInformer.create().end("message"); - assert.calledOnceWith(logger.log, "message"); + assert.calledOnceWith(loggerLogStub, "message"); }); }); }); diff --git a/test/src/reporters/informers/file.js b/test/src/reporters/informers/file.js index 7ef3dbea6..b5beeae1b 100644 --- a/test/src/reporters/informers/file.js +++ b/test/src/reporters/informers/file.js @@ -1,17 +1,23 @@ const fs = require("fs"); const chalk = require("chalk"); -const logger = require("src/utils/logger"); -const FileInformer = require("src/reporters/informers/file"); +const proxyquire = require("proxyquire"); describe("reporter/informers/file", () => { const sandbox = sinon.createSandbox(); let fsStream; + let FileInformer; + let loggerLogStub; beforeEach(() => { fsStream = { write: sandbox.stub(), end: sandbox.stub() }; sandbox.stub(fs, "createWriteStream").returns(fsStream); + loggerLogStub = sandbox.stub(); - sandbox.stub(logger, "log"); + FileInformer = proxyquire("src/reporters/informers/file", { + "../../utils/logger": { + log: loggerLogStub, + }, + }); }); afterEach(() => sandbox.restore()); @@ -28,7 +34,7 @@ describe("reporter/informers/file", () => { FileInformer.create(opts); assert.calledOnceWith( - logger.log, + loggerLogStub, `Information with test results for report: "${opts.type}" will be saved to a file: "${opts.path}"`, ); }); diff --git a/test/src/runner/browser-env/vite/server.ts b/test/src/runner/browser-env/vite/server.ts index e35faba8b..93fc19016 100644 --- a/test/src/runner/browser-env/vite/server.ts +++ b/test/src/runner/browser-env/vite/server.ts @@ -6,7 +6,6 @@ import chalk from "chalk"; import { ViteServer } from "../../../../../src/runner/browser-env/vite/server"; import { ManualMock } from "../../../../../src/runner/browser-env/vite/manual-mock"; -import logger from "../../../../../src/utils/logger"; import { makeConfigStub } from "../../../../utils"; import { BROWSER_TEST_RUN_ENV } from "../../../../../src/constants/config"; @@ -21,6 +20,7 @@ describe("runner/browser-env/vite/server", () => { let getNodeModulePathStub: SinonStub; let generateIndexHtmlPlugin: () => Vite.Plugin[]; let mockPlugin: () => Vite.Plugin[]; + let loggerLogStub: SinonStub; const mkViteServer_ = (opts: Partial = {}): Vite.ViteDevServer => ({ @@ -45,8 +45,8 @@ describe("runner/browser-env/vite/server", () => { beforeEach(() => { sandbox.stub(Vite, "createServer").resolves(mkViteServer_()); - sandbox.stub(logger, "log"); + loggerLogStub = sandbox.stub(); createSocketServer = sandbox.stub(); getPortStub = sandbox.stub().resolves(12345); getNodeModulePathStub = sandbox.stub().resolves("file:///default-cwd"); @@ -60,6 +60,9 @@ describe("runner/browser-env/vite/server", () => { "./plugins/generate-index-html": { plugin: generateIndexHtmlPlugin }, "./plugins/mock": { plugin: mockPlugin }, "./utils": { getNodeModulePath: getNodeModulePathStub }, + "../../../utils/logger": { + log: loggerLogStub, + }, })); }); @@ -230,7 +233,7 @@ describe("runner/browser-env/vite/server", () => { await ViteServerStub.create(mkConfig_()).start(); - assert.calledOnceWith(logger.log, chalk.green("Vite server started on http://localhost:4444")); + assert.calledOnceWith(loggerLogStub, chalk.green("Vite server started on http://localhost:4444")); }); }); diff --git a/test/src/runner/index.js b/test/src/runner/index.js index 1e85473fe..fd0a2c13e 100644 --- a/test/src/runner/index.js +++ b/test/src/runner/index.js @@ -7,7 +7,6 @@ const temp = require("src/temp"); const RuntimeConfig = require("src/config/runtime-config"); const { Stats: RunnerStats } = require("src/stats"); const { MasterEvents: RunnerEvents, RunnerSyncEvents } = require("src/events"); -const logger = require("src/utils/logger"); const WorkersRegistry = require("src/utils/workers-registry"); const { BrowserRunner } = require("src/runner/browser-runner"); const { TestCollection } = require("src/test-collection"); @@ -54,7 +53,6 @@ describe("NodejsEnvRunner", () => { sandbox.stub(temp, "init"); sandbox.stub(temp, "serialize"); - sandbox.stub(logger, "warn"); sandbox.stub(RuntimeConfig, "getInstance").returns({ extend: () => {} }); sandbox.stub(TestCollection.prototype); @@ -65,6 +63,9 @@ describe("NodejsEnvRunner", () => { Runner = proxyquire("src/runner", { "../browser-pool": BrowserPool, + "../utils/logger": { + warn: sandbox.stub(), + }, }).MainRunner; }); diff --git a/test/src/runner/test-runner/regular-test-runner.js b/test/src/runner/test-runner/regular-test-runner.js index 8cdd35698..11e990cfe 100644 --- a/test/src/runner/test-runner/regular-test-runner.js +++ b/test/src/runner/test-runner/regular-test-runner.js @@ -4,17 +4,17 @@ const crypto = require("crypto"); const _ = require("lodash"); const BrowserAgent = require("src/runner/browser-agent"); -const RegularTestRunner = require("src/runner/test-runner/regular-test-runner"); const WorkersRegistry = require("src/utils/workers-registry"); -const logger = require("src/utils/logger"); const { MasterEvents: Events } = require("src/events"); const AssertViewResults = require("src/browser/commands/assert-view/assert-view-results"); const { Test } = require("src/test-reader/test-object"); const Promise = require("bluebird"); const { EventEmitter } = require("events"); +const proxyquire = require("proxyquire"); describe("runner/test-runner/regular-test-runner", () => { const sandbox = sinon.createSandbox(); + let RegularTestRunner; const stubTestResult_ = (opts = {}) => { return _.defaults(opts, { @@ -65,6 +65,12 @@ describe("runner/test-runner/regular-test-runner", () => { }; beforeEach(() => { + RegularTestRunner = proxyquire("src/runner/test-runner/regular-test-runner", { + "../../utils/logger": { + warn: sandbox.stub(), + }, + }); + sandbox.stub(BrowserAgent.prototype, "getBrowser").resolves(stubBrowser_()); sandbox.stub(BrowserAgent.prototype, "freeBrowser").resolves(); @@ -73,7 +79,6 @@ describe("runner/test-runner/regular-test-runner", () => { sandbox.stub(AssertViewResults, "fromRawObject").returns(Object.create(AssertViewResults.prototype)); sandbox.stub(AssertViewResults.prototype, "get").returns({}); - sandbox.stub(logger, "warn"); sandbox.stub(crypto, "randomUUID").returns(""); sandbox.stub(crypto, "randomBytes").callsFake(size => { return Buffer.from("11".repeat(size), "hex"); diff --git a/test/src/signal-handler.js b/test/src/signal-handler.js index 8cba64b8d..b0a5dd063 100644 --- a/test/src/signal-handler.js +++ b/test/src/signal-handler.js @@ -1,8 +1,8 @@ "use strict"; -const logger = require("src/utils/logger"); const clearRequire = require("clear-require"); const Promise = require("bluebird"); +const proxyquire = require("proxyquire"); describe("src/signal-handler", () => { const sandbox = sinon.createSandbox(); @@ -20,10 +20,13 @@ describe("src/signal-handler", () => { beforeEach(() => { sandbox.stub(process, "on"); sandbox.stub(process, "exit"); - sandbox.stub(logger, "log"); clearRequire("src/signal-handler"); - signalHandler = require("src/signal-handler"); + signalHandler = proxyquire("src/signal-handler", { + "./utils/logger": { + log: sandbox.stub(), + }, + }); }); afterEach(() => sandbox.restore()); diff --git a/test/src/testplane.js b/test/src/testplane.js index 3602f4c11..d9e213b93 100644 --- a/test/src/testplane.js +++ b/test/src/testplane.js @@ -18,12 +18,11 @@ const { TestCollection } = require("src/test-collection"); const { MasterEvents: RunnerEvents, CommonSyncEvents, MasterAsyncEvents, MasterSyncEvents } = require("src/events"); const { MainRunner: NodejsEnvRunner } = require("src/runner"); const { MainRunner: BrowserEnvRunner } = require("src/runner/browser-env"); -const logger = require("src/utils/logger"); const { makeConfigStub } = require("../utils"); describe("testplane", () => { const sandbox = sinon.createSandbox(); - let Testplane, initReporters, signalHandler; + let Testplane, initReporters, signalHandler, loggerWarnStub, loggerErrorStub; const mkTestplane_ = config => { Config.create.returns(config || makeConfigStub()); @@ -44,7 +43,6 @@ describe("testplane", () => { const mkNodejsEnvRunner_ = runFn => mkRunnerStubHelper_(NodejsEnvRunner, runFn); beforeEach(() => { - sandbox.stub(logger, "warn"); sandbox.stub(Config, "create").returns(makeConfigStub()); sandbox.stub(pluginsLoader, "load").returns([]); sandbox.stub(RuntimeConfig, "getInstance").returns({ extend: sandbox.stub() }); @@ -56,9 +54,22 @@ describe("testplane", () => { initReporters = sandbox.stub().resolves(); signalHandler = new AsyncEmitter(); + loggerWarnStub = sandbox.stub(); + loggerErrorStub = sandbox.stub(); + Testplane = proxyquire("src/testplane", { "./reporters": { initReporters }, "./signal-handler": signalHandler, + "./utils/logger": { + warn: loggerWarnStub, + error: loggerErrorStub, + }, + "./validators": proxyquire("src/validators", { + "./utils/logger": { + warn: loggerWarnStub, + error: loggerErrorStub, + }, + }), }).Testplane; }); @@ -187,7 +198,7 @@ describe("testplane", () => { mkNodejsEnvRunner_(); return runTestplane([], { browsers: ["bro3"] }).then(() => - assert.calledWithMatch(logger.warn, /Unknown browser ids: bro3/), + assert.calledWithMatch(loggerWarnStub, /Unknown browser ids: bro3/), ); }); @@ -844,7 +855,6 @@ describe("testplane", () => { beforeEach(() => { testplane = mkTestplane_(); - sandbox.stub(logger, "error"); sandbox.stub(process, "exit"); sandbox .stub(NodejsEnvRunner.prototype, "run") @@ -860,7 +870,7 @@ describe("testplane", () => { }); return testplane.run().finally(() => { - assert.calledOnceWith(logger.error, "Terminating on critical error:", err); + assert.calledOnceWith(loggerErrorStub, "Terminating on critical error:", err); }); }); @@ -911,7 +921,7 @@ describe("testplane", () => { .run() .finally(() => Promise.delay(300)) .then(() => { - assert.calledWithMatch(logger.error, /Forcing shutdown.../); + assert.calledWithMatch(loggerErrorStub, /Forcing shutdown.../); assert.calledOnceWith(process.exit, 1); }); }); diff --git a/test/src/utils/workers-registry.js b/test/src/utils/workers-registry.js index fc149c310..bac241442 100644 --- a/test/src/utils/workers-registry.js +++ b/test/src/utils/workers-registry.js @@ -6,7 +6,6 @@ const _ = require("lodash"); const RuntimeConfig = require("src/config/runtime-config"); const { MasterEvents: Events } = require("src/events"); const { WorkerProcess } = require("src/utils/worker-process"); -const logger = require("src/utils/logger"); const { MASTER_INIT, MASTER_SYNC_CONFIG, @@ -18,14 +17,20 @@ const { describe("WorkersRegistry", () => { const sandbox = sinon.createSandbox(); - let workersImpl, workerFarm; + let workersImpl, workerFarm, loggerErrorStub; const mkWorkersRegistry_ = (config = {}) => { config = _.defaults(config, { system: {}, }); + loggerErrorStub = sandbox.stub(); - const WorkersRegistry = proxyquire("../../../src/utils/workers-registry", { "worker-farm": workerFarm }); + const WorkersRegistry = proxyquire("../../../src/utils/workers-registry", { + "worker-farm": workerFarm, + "../utils/logger": { + error: loggerErrorStub, + }, + }); const workersRegistry = WorkersRegistry.create(config); workersRegistry.init(); @@ -49,7 +54,6 @@ describe("WorkersRegistry", () => { workerFarm.end = sandbox.stub().yieldsRight(); sandbox.stub(RuntimeConfig, "getInstance"); - sandbox.stub(logger, "error"); }); afterEach(() => sandbox.restore()); @@ -251,7 +255,7 @@ describe("WorkersRegistry", () => { child.emit("exit", 0, null); - assert.notCalled(logger.error); + assert.notCalled(loggerErrorStub); }); describe("should inform about incorrect ends of child process with", () => { @@ -263,7 +267,7 @@ describe("WorkersRegistry", () => { child.emit("exit", 1, null); assert.calledOnceWith( - logger.error, + loggerErrorStub, `testplane:worker:${child.pid} terminated unexpectedly with exit code: 1`, ); }); @@ -276,7 +280,7 @@ describe("WorkersRegistry", () => { child.emit("exit", null, "SIGINT"); assert.calledOnceWith( - logger.error, + loggerErrorStub, `testplane:worker:${child.pid} terminated unexpectedly with signal: SIGINT`, ); }); diff --git a/test/src/validators.js b/test/src/validators.js index 82f32d5e1..6c4b5e3bf 100644 --- a/test/src/validators.js +++ b/test/src/validators.js @@ -1,25 +1,32 @@ "use strict"; -const { validateUnknownBrowsers } = require("src/validators"); -const logger = require("src/utils/logger"); +const proxyquire = require("proxyquire"); describe("validators", () => { const sandbox = sinon.createSandbox(); + let validateUnknownBrowsers; + let loggerWarnStub; + + beforeEach(() => { + loggerWarnStub = sandbox.stub(); + + validateUnknownBrowsers = proxyquire("src/validators", { + "./utils/logger": { + warn: loggerWarnStub, + }, + }).validateUnknownBrowsers; + }); afterEach(() => { sandbox.restore(); }); describe("validateUnknownBrowsers", () => { - beforeEach(() => { - sandbox.stub(logger, "warn"); - }); - it("should warn about unknown browsers", () => { validateUnknownBrowsers(["foo"], ["bar", "baz"]); assert.calledWith( - logger.warn, + loggerWarnStub, sinon.match(/Unknown browser ids: foo(.+) specified in the config file: bar, baz/), ); }); @@ -27,13 +34,13 @@ describe("validators", () => { it("should not warn if all browsers are known", () => { validateUnknownBrowsers(["foo"], ["foo", "bar"]); - assert.notCalled(logger.warn); + assert.notCalled(loggerWarnStub); }); it("should warn only about unknown browsers", () => { validateUnknownBrowsers(["foo", "bar", "baz"], ["baz", "qux"]); - assert.calledWith(logger.warn, sinon.match(/Unknown browser ids: foo, bar\./)); + assert.calledWith(loggerWarnStub, sinon.match(/Unknown browser ids: foo, bar\./)); }); }); }); diff --git a/test/src/worker/browser-env/runner/test-runner/index.ts b/test/src/worker/browser-env/runner/test-runner/index.ts index b136f4b17..fba951490 100644 --- a/test/src/worker/browser-env/runner/test-runner/index.ts +++ b/test/src/worker/browser-env/runner/test-runner/index.ts @@ -6,7 +6,7 @@ import P from "bluebird"; import sinon, { SinonStub, SinonFakeTimers } from "sinon"; import proxyquire from "proxyquire"; -import NodejsEnvRunner from "../../../../../../src/worker/runner/test-runner"; +import type NodejsEnvRunnerOriginal from "../../../../../../src/worker/runner/test-runner"; import { TestRunner as BrowserEnvRunner } from "../../../../../../src/worker/browser-env/runner/test-runner"; import { wrapExecutionThread } from "../../../../../../src/worker/browser-env/runner/test-runner/execution-thread"; import { @@ -18,8 +18,7 @@ import { VITE_RUN_UUID_ROUTE } from "../../../../../../src/runner/browser-env/vi import { makeBrowserConfigStub } from "../../../../../utils"; import { Test, Suite } from "../../../../../../src/test-reader/test-object"; import { BrowserAgent } from "../../../../../../src/worker/runner/browser-agent"; -import history from "../../../../../../src/browser/history"; -import logger from "../../../../../../src/utils/logger"; +import * as history from "../../../../../../src/browser/history"; import OneTimeScreenshooter from "../../../../../../src/worker/runner/test-runner/one-time-screenshooter"; import RuntimeConfig from "../../../../../../src/config/runtime-config"; @@ -42,7 +41,7 @@ import type { Test as TestType } from "../../../../../../src/test-reader/test-ob import type { BrowserConfig } from "../../../../../../src/config/browser-config"; import type { WorkerRunTestResult } from "../../../../../../src/worker/testplane"; import { AbortOnReconnectError } from "../../../../../../src/errors/abort-on-reconnect-error"; -import ExistingBrowser from "../../../../../../src/browser/existing-browser"; +import { ExistingBrowser } from "../../../../../../src/browser/existing-browser"; interface TestOpts { title: string; @@ -56,9 +55,12 @@ interface RunOpts extends WorkerTestRunnerRunOpts { describe("worker/browser-env/runner/test-runner", () => { const sandbox = sinon.createSandbox(); + let loggerWarnStub: SinonStub; let BrowserEnvRunnerStub: typeof BrowserEnvRunner; let socketClientStub: SinonStub; let wrapExecutionThreadStub: SinonStub; + let historyRunGroupStub: SinonStub; + let NodejsEnvRunner: typeof NodejsEnvRunnerOriginal; const mkTest_ = (opts?: Partial): TestType => { const test = Test.create({ @@ -162,15 +164,26 @@ describe("worker/browser-env/runner/test-runner", () => { const initBrowserEnvRunner_ = ( opts: { expectMatchers: Record } = { expectMatchers: {} }, ): typeof BrowserEnvRunner => { + loggerWarnStub = sandbox.stub(); socketClientStub = sandbox.stub().returns(mkSocket_()); wrapExecutionThreadStub = sandbox .stub() .callsFake((socket, throwIfAborted) => wrapExecutionThread(socket, throwIfAborted)); + historyRunGroupStub = sandbox.stub().callsFake(history.runGroup); + NodejsEnvRunner = proxyquire("../../../../../../src/worker/runner/test-runner", { + "../../../browser/history": { + runGroup: historyRunGroupStub, + }, + }); return proxyquire.noCallThru()("../../../../../../src/worker/browser-env/runner/test-runner", { "socket.io-client": { io: socketClientStub }, "./execution-thread": { wrapExecutionThread: wrapExecutionThreadStub }, "expect-webdriverio/lib/matchers": opts.expectMatchers, + "../../../runner/test-runner": NodejsEnvRunner, + "../../../../utils/logger": { + warn: loggerWarnStub, + }, }).TestRunner; }; @@ -183,7 +196,6 @@ describe("worker/browser-env/runner/test-runner", () => { sandbox.stub(crypto, "randomUUID").returns("0-0-0-0-0"); sandbox.stub(process, "pid").value(11111); - sandbox.stub(logger, "warn"); sandbox.stub(RuntimeConfig, "getInstance").returns({ viteBaseUrl: "http://default" }); @@ -258,7 +270,7 @@ describe("worker/browser-env/runner/test-runner", () => { socket.emit("connect_error", error); assert.calledOnceWith( - logger.warn, + loggerWarnStub, "Worker with pid=77777 and runUuid=12345 was disconnected from the Vite server:", error, ); @@ -267,10 +279,6 @@ describe("worker/browser-env/runner/test-runner", () => { }); describe("run", () => { - beforeEach(() => { - sandbox.spy(history, "runGroup"); - }); - it("should call execution thread wrapper with socket and abort function", async () => { const socket = mkSocket_(); socketClientStub.returns(socket); @@ -950,7 +958,7 @@ describe("worker/browser-env/runner/test-runner", () => { await runWithEmitBrowserInit(socket); - assert.calledWith(history.runGroup as SinonStub, browser.callstackHistory, "openVite", sinon.match.func); + assert.calledWith(historyRunGroupStub as SinonStub, browser.callstackHistory, "openVite", sinon.match.func); }); it(`should open vite server url with "/${VITE_RUN_UUID_ROUTE}/:uuid" format`, async () => { diff --git a/test/src/worker/runner/browser-pool.js b/test/src/worker/runner/browser-pool.js index b944522de..8107da7e1 100644 --- a/test/src/worker/runner/browser-pool.js +++ b/test/src/worker/runner/browser-pool.js @@ -2,11 +2,10 @@ const EventEmitter = require("events").EventEmitter; const _ = require("lodash"); -const Browser = require("src/browser/existing-browser"); +const { ExistingBrowser } = require("src/browser/existing-browser"); const BrowserPool = require("src/worker/runner/browser-pool"); -const Calibrator = require("src/browser/calibrator"); +const { Calibrator } = require("src/browser/calibrator"); const { WorkerEvents: RunnerEvents } = require("src/events"); -const logger = require("src/utils/logger"); const ipc = require("src/utils/ipc"); describe("worker/browser-pool", () => { @@ -40,8 +39,7 @@ describe("worker/browser-pool", () => { }; beforeEach(() => { - sandbox.stub(logger, "warn"); - sandbox.stub(Browser, "create"); + sandbox.stub(ExistingBrowser, "create"); sandbox.stub(ipc, "emit"); }); @@ -52,11 +50,11 @@ describe("worker/browser-pool", () => { const config = stubConfig(); const emitter = new EventEmitter(); const browserPool = createPool({ config, emitter }); - Browser.create.returns(stubBrowser({ browserId: "bro-id" })); + ExistingBrowser.create.returns(stubBrowser({ browserId: "bro-id" })); await browserPool.getBrowser({ browserId: "bro-id", browserVersion: "1.0", state: {} }); - assert.calledOnceWith(Browser.create, config, { + assert.calledOnceWith(ExistingBrowser.create, config, { id: "bro-id", version: "1.0", state: {}, @@ -66,7 +64,7 @@ describe("worker/browser-pool", () => { it("should init a new created browser ", async () => { const browser = stubBrowser({ browserId: "bro-id" }); - Browser.create.returns(browser); + ExistingBrowser.create.returns(browser); await createPool().getBrowser({ browserId: "bro-id", @@ -89,7 +87,7 @@ describe("worker/browser-pool", () => { emitter.on(RunnerEvents.NEW_BROWSER, onNewBrowser); - Browser.create.returns(stubBrowser({ id: "bro-id", publicAPI: { some: "api" } })); + ExistingBrowser.create.returns(stubBrowser({ id: "bro-id", publicAPI: { some: "api" } })); await browserPool.getBrowser({ browserId: "bro-id", browserVersion: "10.1" }); @@ -100,7 +98,7 @@ describe("worker/browser-pool", () => { const config = stubConfig(); const browserPool = createPool({ config }); const browser = stubBrowser({ browserId: "bro-id" }); - Browser.create.returns(browser); + ExistingBrowser.create.returns(browser); return assert.becomes(browserPool.getBrowser({ browserId: "bro-id" }), browser); }); @@ -111,7 +109,7 @@ describe("worker/browser-pool", () => { }); it("should be rejected if instance of browser was not created", () => { - Browser.create.throws(new Error("foo bar")); + ExistingBrowser.create.throws(new Error("foo bar")); return assert.isRejected(createPool().getBrowser({}), /foo bar/); }); @@ -119,7 +117,7 @@ describe("worker/browser-pool", () => { describe("init fails", () => { const stubBrowserWhichRejectsOnInit = (params = {}) => { const browser = stubBrowser(params); - Browser.create.returns(browser); + ExistingBrowser.create.returns(browser); browser.init.rejects(); @@ -178,7 +176,7 @@ describe("worker/browser-pool", () => { it("should quit from browser", async () => { const browserPool = createPool(); - Browser.create.returns(stubBrowser()); + ExistingBrowser.create.returns(stubBrowser()); const browser = await browserPool.getBrowser({ browserId: "bro-id" }); browserPool.freeBrowser(browser); diff --git a/test/src/worker/runner/test-runner/execution-thread.js b/test/src/worker/runner/test-runner/execution-thread.js index 84161a7ad..a2daf1339 100644 --- a/test/src/worker/runner/test-runner/execution-thread.js +++ b/test/src/worker/runner/test-runner/execution-thread.js @@ -4,15 +4,16 @@ const Promise = require("bluebird"); const _ = require("lodash"); const AssertViewResults = require("src/browser/commands/assert-view/assert-view-results"); -const ExecutionThread = require("src/worker/runner/test-runner/execution-thread"); const OneTimeScreenshooter = require("src/worker/runner/test-runner/one-time-screenshooter"); const { Test } = require("src/test-reader/test-object"); const RuntimeConfig = require("src/config/runtime-config"); -const logger = require("src/utils/logger"); const { AbortOnReconnectError } = require("src/errors/abort-on-reconnect-error"); +const proxyquire = require("proxyquire"); describe("worker/runner/test-runner/execution-thread", () => { const sandbox = sinon.createSandbox(); + let loggerLogStub; + let ExecutionThread; const mkTest_ = (opts = {}) => { opts.fn = opts.fn || sinon.spy(); @@ -49,10 +50,15 @@ describe("worker/runner/test-runner/execution-thread", () => { }; beforeEach(() => { + loggerLogStub = sinon.stub(); + ExecutionThread = proxyquire("src/worker/runner/test-runner/execution-thread", { + "../../../utils/logger": { + log: loggerLogStub, + }, + }); sandbox.stub(OneTimeScreenshooter.prototype, "extendWithScreenshot").callsFake(e => Promise.resolve(e)); sandbox.stub(OneTimeScreenshooter.prototype, "captureScreenshotOnAssertViewFail").resolves(); sandbox.stub(RuntimeConfig, "getInstance").returns({ replMode: { onFail: false } }); - sandbox.stub(logger, "log"); }); afterEach(() => sandbox.restore()); @@ -427,7 +433,7 @@ describe("worker/runner/test-runner/execution-thread", () => { .catch(() => {}); await assert.callOrder( - logger.log.withArgs("Caught error:", err), + loggerLogStub.withArgs("Caught error:", err), browser.publicAPI.switchToRepl, ); }); diff --git a/test/src/worker/runner/test-runner/index.js b/test/src/worker/runner/test-runner/index.js index c0c5c9db2..da0e6c6d3 100644 --- a/test/src/worker/runner/test-runner/index.js +++ b/test/src/worker/runner/test-runner/index.js @@ -2,7 +2,6 @@ const _ = require("lodash"); const Promise = require("bluebird"); -const TestRunner = require("src/worker/runner/test-runner"); const HookRunner = require("src/worker/runner/test-runner/hook-runner"); const ExecutionThread = require("src/worker/runner/test-runner/execution-thread"); const OneTimeScreenshooter = require("src/worker/runner/test-runner/one-time-screenshooter"); @@ -13,9 +12,12 @@ const { Suite, Test } = require("src/test-reader/test-object"); const history = require("src/browser/history"); const { SAVE_HISTORY_MODE } = require("src/constants/config"); const { makeConfigStub } = require("../../../../utils"); +const proxyquire = require("proxyquire"); describe("worker/runner/test-runner", () => { const sandbox = sinon.createSandbox(); + let historyRunGroupStub; + let TestRunner; const mkTest_ = (opts = {}) => { opts.fn = opts.fn || sinon.spy(); @@ -82,6 +84,13 @@ describe("worker/runner/test-runner", () => { }; beforeEach(() => { + historyRunGroupStub = sandbox.stub().callsFake(history.runGroup); + TestRunner = proxyquire("src/worker/runner/test-runner", { + "../../../browser/history": { + runGroup: historyRunGroupStub, + }, + }); + sandbox.stub(BrowserAgent.prototype, "getBrowser").resolves(mkBrowser_()); sandbox.stub(BrowserAgent.prototype, "freeBrowser"); @@ -404,9 +413,9 @@ describe("worker/runner/test-runner", () => { }); describe("beforeEach hooks", () => { - beforeEach(() => { - sandbox.spy(history, "runGroup"); - }); + // beforeEach(() => { + // sandbox.spy(history, "runGroup"); + // }); it("should be called before test hook", async () => { await run_(); @@ -446,7 +455,7 @@ describe("worker/runner/test-runner", () => { await run_(); - assert.calledWith(history.runGroup, sinon.match.any, "resetCursor", sinon.match.func); + assert.calledWith(historyRunGroupStub, sinon.match.any, "resetCursor", sinon.match.func); }); it('should log "beforeEach" in history if beforeEach hooks exist', async () => { @@ -456,7 +465,7 @@ describe("worker/runner/test-runner", () => { await run_(); - assert.calledWith(history.runGroup, sinon.match.any, "beforeEach", sinon.match.func); + assert.calledWith(historyRunGroupStub, sinon.match.any, "beforeEach", sinon.match.func); }); it('should not log "beforeEach" in history if beforeEach hooks do not exist', async () => { @@ -466,14 +475,14 @@ describe("worker/runner/test-runner", () => { await run_(); - assert.neverCalledWith(history.runGroup, sinon.match.any, "beforeEach", sinon.match.func); + assert.neverCalledWith(historyRunGroupStub, sinon.match.any, "beforeEach", sinon.match.func); }); }); describe("afterEach hooks", () => { - beforeEach(() => { - sandbox.spy(history, "runGroup"); - }); + // beforeEach(() => { + // sandbox.spy(history, "runGroup"); + // }); it("should be called if beforeEach hook failed", async () => { HookRunner.prototype.hasBeforeEachHooks.returns(true); @@ -519,7 +528,7 @@ describe("worker/runner/test-runner", () => { await run_(); - assert.calledWith(history.runGroup, sinon.match.any, "afterEach", sinon.match.func); + assert.calledWith(historyRunGroupStub, sinon.match.any, "afterEach", sinon.match.func); }); it('should not log "afterEach" in history if afterEach hooks do not exist', async () => { @@ -529,7 +538,7 @@ describe("worker/runner/test-runner", () => { await run_(); - assert.neverCalledWith(history.runGroup, sinon.match.any, "afterEach", sinon.match.func); + assert.neverCalledWith(historyRunGroupStub, sinon.match.any, "afterEach", sinon.match.func); }); }); diff --git a/test/src/worker/runner/test-runner/one-time-screenshooter.js b/test/src/worker/runner/test-runner/one-time-screenshooter.js index 16e8d17be..ae93b12dc 100644 --- a/test/src/worker/runner/test-runner/one-time-screenshooter.js +++ b/test/src/worker/runner/test-runner/one-time-screenshooter.js @@ -2,14 +2,15 @@ const _ = require("lodash"); const Promise = require("bluebird"); -const Image = require("src/image"); +const { Image } = require("src/image"); const ScreenShooter = require("src/browser/screen-shooter"); -const OneTimeScreenshooter = require("src/worker/runner/test-runner/one-time-screenshooter"); -const logger = require("src/utils/logger"); const { mkSessionStub_ } = require("../../../browser/utils"); +const proxyquire = require("proxyquire"); describe("worker/runner/test-runner/one-time-screenshooter", () => { const sandbox = sinon.createSandbox(); + let OneTimeScreenshooter; + let logger; const mkBrowser_ = (opts = {}) => { const session = mkSessionStub_(); @@ -53,9 +54,16 @@ describe("worker/runner/test-runner/one-time-screenshooter", () => { }; beforeEach(() => { + logger = { + warn: sinon.stub(), + }; + OneTimeScreenshooter = proxyquire("src/worker/runner/test-runner/one-time-screenshooter", { + "../../../utils/logger": logger, + }); + sandbox.stub(ScreenShooter.prototype, "capture").resolves(stubImage_()); sandbox.stub(Image, "fromBase64").returns(stubImage_()); - sandbox.stub(logger, "warn"); + // sandbox.stub(logger, "warn"); }); afterEach(() => sandbox.restore());