From 32807ed91493e0040d705cda502c072caf59a2c6 Mon Sep 17 00:00:00 2001 From: amaanbs Date: Tue, 12 Dec 2023 02:53:12 +0530 Subject: [PATCH 01/38] add percy support for wdio v7 --- package.json | 2 + .../wdio-browserstack-service/package.json | 6 +- .../src/@types/bstack-service-types.d.ts | 3 +- .../src/Percy/Percy-Handler.ts | 98 ++++++++ .../src/Percy/Percy.ts | 176 +++++++++++++++ .../src/Percy/PercyBinary.ts | 211 ++++++++++++++++++ .../src/Percy/PercyCaptureMap.ts | 38 ++++ .../src/Percy/PercyHelper.ts | 74 ++++++ .../src/Percy/PercyLogger.ts | 76 +++++++ .../src/Percy/PercySDK.ts | 40 ++++ .../src/constants.ts | 2 + .../wdio-browserstack-service/src/index.ts | 4 + .../wdio-browserstack-service/src/launcher.ts | 75 ++++++- .../wdio-browserstack-service/src/service.ts | 86 +++++-- .../wdio-browserstack-service/src/types.ts | 15 ++ .../wdio-browserstack-service/src/util.ts | 21 ++ 16 files changed, 905 insertions(+), 22 deletions(-) create mode 100644 packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts create mode 100644 packages/wdio-browserstack-service/src/Percy/Percy.ts create mode 100644 packages/wdio-browserstack-service/src/Percy/PercyBinary.ts create mode 100644 packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts create mode 100644 packages/wdio-browserstack-service/src/Percy/PercyHelper.ts create mode 100644 packages/wdio-browserstack-service/src/Percy/PercyLogger.ts create mode 100644 packages/wdio-browserstack-service/src/Percy/PercySDK.ts diff --git a/package.json b/package.json index 6c5ad5b4e8e..1e60948dcef 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/archiver": "^6.0.0", "@types/cheerio": "^0.22.31", "@types/eslint": "^8.4.2", + "@types/follow-redirects": "^1.14.4", "@types/fs-extra": "^11.0.1", "@types/jest": "^28.1.1", "@types/lodash.clonedeep": "^4.5.6", @@ -76,6 +77,7 @@ "@types/node": "^18.0.0", "@types/split2": "^4.2.0", "@types/uuid": "^9.0.0", + "@types/yauzl": "^2.10.3", "@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/utils": "^6.2.0", diff --git a/packages/wdio-browserstack-service/package.json b/packages/wdio-browserstack-service/package.json index 40c19b32bd4..d4ea07ec114 100644 --- a/packages/wdio-browserstack-service/package.json +++ b/packages/wdio-browserstack-service/package.json @@ -29,13 +29,17 @@ "@wdio/types": "7.33.0", "browserstack-local": "^1.4.5", "csv-writer": "^1.6.0", + "follow-redirects": "^1.15.3", "form-data": "^4.0.0", "git-repo-info": "^2.1.1", "gitconfiglocal": "^2.1.0", "got": "^11.0.2", "uuid": "^9.0.1", "webdriverio": "7.33.0", - "winston-transport": "^4.5.0" + "winston-transport": "^4.5.0", + "yauzl": "^2.10.0", + "@percy/appium-app": "^2.0.1", + "@percy/selenium-webdriver": "^2.0.2" }, "peerDependencies": { "@wdio/cli": "^5.0.0 || ^6.0.0 || ^7.0.0" diff --git a/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts b/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts index 4d50bf8c354..d9f7e2d1b55 100644 --- a/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts +++ b/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts @@ -2,5 +2,6 @@ import type { Browser } from 'webdriverio' declare interface BrowserAsync extends Browser<'async'> { getAccessibilityResultsSummary: () => Promise<{ [key: string]: any; }>, - getAccessibilityResults: () => Promise> + getAccessibilityResults: () => Promise>, + percyCaptureMap: any } diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts new file mode 100644 index 00000000000..8769bc53666 --- /dev/null +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -0,0 +1,98 @@ +import type { Capabilities } from '@wdio/types' +import type { BeforeCommandArgs, AfterCommandArgs } from '@wdio/reporter' +import type { Browser, MultiRemoteBrowser } from 'webdriverio' + +import { + o11yClassErrorHandler +} from '../util' +import PercyCaptureMap from './PercyCaptureMap' + +import * as PercySDK from './PercySDK' +import { PercyLogger } from './PercyLogger' +import { BrowserAsync } from 'src/@types/bstack-service-types' + +class _PercyHandler { + private _testMetadata: { [key: string]: any } = {} + private sessionName?: string + private _isAppAutomate?: boolean + public _percyScreenshotCounter: any = 0 + + constructor ( + private _percyAutoCaptureMode: string | undefined, + private _browser: Browser<'async'> | MultiRemoteBrowser<'async'>, + private _capabilities: Capabilities.RemoteCapability, + isAppAutomate?: boolean, + private _framework?: string + ) { + this._isAppAutomate = isAppAutomate + } + + _setSessionName(name: string) { + this.sessionName = name + } + + async teardown () { + await new Promise((resolve) => { + setInterval(() => { + if (this._percyScreenshotCounter === 0) { + resolve() + } + }, 1000) + }) + } + + async percyAutoCapture(eventName: string | null) { + try { + if (eventName) { + this._percyScreenshotCounter += 1 + this._isAppAutomate ? await PercySDK.screenshotApp(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : await PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)); + ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment((this.sessionName as string), eventName) + this._percyScreenshotCounter -= 1 + } + } catch (err: any) { + PercyLogger.error(`Error while trying to auto capture Percy screenshot ${err}`) + } + } + + async before () { + (this._browser as BrowserAsync).percyCaptureMap = new PercyCaptureMap() + } + + async browserCommand (args: BeforeCommandArgs & AfterCommandArgs) { + try { + if (args.endpoint && this._percyAutoCaptureMode) { + let eventName = null + if ((args.endpoint as string).includes('click') && ['click', 'auto'].includes(this._percyAutoCaptureMode as string)) { + eventName = 'click' + } else if ((args.endpoint as string).includes('screenshot') && ['screenshot', 'auto'].includes(this._percyAutoCaptureMode as string)) { + eventName = 'screenshot' + } else if ((args.endpoint as string).includes('actions') && ['auto'].includes(this._percyAutoCaptureMode as string)) { + if (args.body && args.body.actions && Array.isArray(args.body.actions) && args.body.actions.length && args.body.actions[0].type === 'key') { + eventName = 'keys' + } + } + await this.percyAutoCapture(eventName) + } + } catch (err: any) { + PercyLogger.error(`Error while trying to calculate auto capture parameters ${err}`) + } + } + + async afterTest () { + if (this._percyAutoCaptureMode && this._percyAutoCaptureMode === 'testcase') { + await this.percyAutoCapture('testcase') + } + } + + async afterScenario () { + if (this._percyAutoCaptureMode && this._percyAutoCaptureMode === 'testcase') { + await this.percyAutoCapture('testcase') + } + } +} + +// https://github.com/microsoft/TypeScript/issues/6543 +const PercyHandler: typeof _PercyHandler = o11yClassErrorHandler(_PercyHandler) +type PercyHandler = _PercyHandler + +export default PercyHandler diff --git a/packages/wdio-browserstack-service/src/Percy/Percy.ts b/packages/wdio-browserstack-service/src/Percy/Percy.ts new file mode 100644 index 00000000000..deb1ec7f916 --- /dev/null +++ b/packages/wdio-browserstack-service/src/Percy/Percy.ts @@ -0,0 +1,176 @@ +import fs from 'node:fs' +import path from 'node:path' +import os from 'node:os' + +import { spawn } from 'node:child_process' + +import { nodeRequest, getBrowserStackUser, getBrowserStackKey } from '../util' +import { PercyLogger } from './PercyLogger' + +import PercyBinary from './PercyBinary' + +import type { BrowserstackConfig, UserConfig } from '../types' +import type { Options } from '@wdio/types' + +const logDir = 'logs' + +class Percy { + #logfile: string = path.join(logDir, 'percy.log') + #address: string = process.env.PERCY_SERVER_ADDRESS || 'http://localhost:5338' + + #binaryPath: string | any = null + #options: BrowserstackConfig & Options.Testrunner + #config: Options.Testrunner + #proc: any = null + #isApp: boolean = false + #projectName: string | undefined = undefined + + isProcessRunning = false + + constructor(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig) { + this.#options = options + this.#config = config + if (options.app) { + this.#isApp = true + } + this.#projectName = bsConfig.projectName + } + + async #getBinaryPath(): Promise { + if (!this.#binaryPath) { + const pb = new PercyBinary() + this.#binaryPath = await pb.getBinaryPath(this.#config) + } + return this.#binaryPath + } + + async #sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) + } + + async healthcheck() { + try { + const resp = await nodeRequest('GET', 'percy/healthcheck', null, this.#address) + if (resp) { + return true + } + } catch (err) { + return false + } + } + + async start() { + const binaryPath: string = await this.#getBinaryPath() + const logStream = fs.createWriteStream(this.#logfile, { flags: 'a' }) + const token = await this.fetchPercyToken() + const configPath = await this.createPercyConfig() + + if (!token) { + return false + } + + const commandArgs = [`${this.#isApp ? 'app:exec' : 'exec'}:start`] + + if (configPath) { + commandArgs.push('-c', configPath as string) + } + + this.#proc = spawn( + binaryPath, + commandArgs, + { env: Object.assign(process.env, { PERCY_TOKEN: token }) } + ) + + this.#proc.stdout.pipe(logStream) + this.#proc.stderr.pipe(logStream) + this.isProcessRunning = true + const that = this + + /* eslint-disable @typescript-eslint/no-unused-vars */ + this.#proc.on('close', function (code: any) { + that.isProcessRunning = false + }) + + do { + const healthcheck = await this.healthcheck() + if (healthcheck) { + PercyLogger.debug('Percy healthcheck successful') + return true + } + + await this.#sleep(1000) + } while (this.isProcessRunning) + + return false + } + + async stop() { + const binaryPath = await this.#getBinaryPath() + return new Promise( (resolve) => { + const proc = spawn(binaryPath, ['exec:stop']) + proc.on('close', (code: any) => { + this.isProcessRunning = false + resolve(code) + }) + }) + } + + isRunning() { + return this.isProcessRunning + } + + async fetchPercyToken() { + const projectName = this.#projectName + + try { + const type = this.#isApp ? 'app' : 'automate' + const response: any = await nodeRequest( + 'GET', + `api/app_percy/get_project_token?name=${projectName}&type=${type}`, + { + username: getBrowserStackUser(this.#config), + password: getBrowserStackKey(this.#config) + }, + 'https://api.browserstack.com' + ) + PercyLogger.debug('Percy fetch token success : ' + response.token) + return response.token + } catch (err: any) { + PercyLogger.error(`Percy unable to fetch project token: ${err}`) + return null + } + } + + async createPercyConfig() { + if (!this.#options.percyOptions) { + return null + } + + const configPath = path.join(os.tmpdir(), 'percy.json') + const percyOptions = this.#options.percyOptions + + if (!percyOptions.version) { + percyOptions.version = '2' + } + + return new Promise((resolve) => { + fs.writeFile( + configPath, + JSON.stringify( + percyOptions + ), + (err: any) => { + if (err) { + PercyLogger.error(`Error creating percy config: ${err}`) + resolve(null) + } + + PercyLogger.debug('Percy config created at ' + configPath) + resolve(configPath) + } + ) + }) + } +} + +export default Percy diff --git a/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts new file mode 100644 index 00000000000..2079061eff4 --- /dev/null +++ b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts @@ -0,0 +1,211 @@ +import url from 'node:url' +import yauzl from 'yauzl' + +const fs = require('node:fs') +import { https } from 'follow-redirects' + +import path from 'node:path' +import os from 'node:os' +import { spawn } from 'node:child_process' +import { PercyLogger } from './PercyLogger' + +class PercyBinary { + #hostOS = process.platform + #httpPath: any = null + #binaryName = 'percy' + + #orderedPaths = [ + path.join(this.#homedir(), '.browserstack'), + process.cwd(), + os.tmpdir() + ] + + constructor() { + const base = 'https://github.com/percy/cli/releases/latest/download' + if (this.#hostOS.match(/darwin|mac os/i)) { + this.#httpPath = base + '/percy-osx.zip' + } else if (this.#hostOS.match(/mswin|msys|mingw|cygwin|bccwin|wince|emc|win32/i)) { + this.#httpPath = base + '/percy-win.zip' + this.#binaryName = 'percy.exe' + } else { + this.#httpPath = base + '/percy-linux.zip' + } + } + + #homedir(): any { + if (typeof os.homedir === 'function') { + return os.homedir() + } + + const env = process.env + const home = env.HOME + const user = env.LOGNAME || env.USER || env.LNAME || env.USERNAME + + if (process.platform === 'win32') { + return env.USERPROFILE || (env.HOMEDRIVE || 'null') + env.HOMEPATH || home || null + } + + if (process.platform === 'darwin') { + return home || (user ? '/Users/' + user : null) + } + + if (process.platform === 'linux') { + return home || (process.getuid && process.getuid() === 0 ? '/root' : (user ? '/home/' + user : null)) + } + + return home || null + } + + #makePath(path: string) { + try { + if (!this.#checkPath(path)) { + fs.mkdirSync(path) + } + return true + } catch { + return false + } + } + + #checkPath(path: string, mode?: any) { + mode = mode || (fs.R_OK | fs.W_OK) + try { + fs.accessSync(path, mode) + return true + } catch (e) { + if (typeof fs.accessSync !== 'undefined') { + return false + } + + // node v0.10 + try { + fs.statSync(path) + return true + } catch (e) { + return false + } + } + } + + #getAvailableDirs() { + for (let i = 0; i < this.#orderedPaths.length; i++) { + const path = this.#orderedPaths[i] + if (this.#makePath(path)) { + return path + } + } + throw new Error('Error trying to download percy binary') + } + + async getBinaryPath(conf: any): Promise { + const destParentDir = this.#getAvailableDirs() + const binaryPath = path.join(destParentDir, this.#binaryName) + if (this.#checkPath(binaryPath, fs.X_OK)) { + return binaryPath + } + const downloadedBinaryPath: string = await this.download(conf, destParentDir) + const isValid = await this.validateBinary(downloadedBinaryPath) + if (!isValid) { + // retry once + PercyLogger.error('Corrupt percy binary, retrying') + return await this.download(conf, destParentDir) + } + return downloadedBinaryPath + } + + async validateBinary(binaryPath: string) { + const versionRegex = /^.*@percy\/cli \d.\d+.\d+/ + /* eslint-disable @typescript-eslint/no-unused-vars */ + return new Promise((resolve, reject) => { + const proc = spawn(binaryPath, ['--version']) + proc.stdout.on('data', (data) => { + if (versionRegex.test(data)) { + resolve(true) + } + }) + + proc.on('close', () => { + resolve(false) + }) + }) + } + + download(conf: any, destParentDir: any): Promise { + if (!this.#checkPath(destParentDir)){ + fs.mkdirSync(destParentDir) + } + + const binaryName = this.#binaryName + const zipFilePath = path.join(destParentDir, binaryName + '.zip') + const binaryPath = path.join(destParentDir, binaryName) + const downloadedFileStream = fs.createWriteStream(zipFilePath) + + const options: any = url.parse(this.#httpPath) + + return new Promise((resolve, reject) => { + https.get(options, function (response: any) { + response.pipe(downloadedFileStream) + response.on('error', function (err: any) { + PercyLogger.error('Got Error in percy binary download response : ' + err) + reject(err) + }) + downloadedFileStream.on('error', function (err: any) { + PercyLogger.error('Got Error while downloading percy binary file : ' + err) + reject(err) + }) + downloadedFileStream.on('close', function () { + yauzl.open(zipFilePath, { lazyEntries: true }, function (err: any, zipfile: any) { + if (err) { + return reject(err) + } + zipfile.readEntry() + zipfile.on('entry', (entry: any) => { + if (/\/$/.test(entry.fileName)) { + // Directory file names end with '/'. + zipfile.readEntry() + } else { + // file entry + const writeStream = fs.createWriteStream( + path.join(destParentDir, entry.fileName) + ) + zipfile.openReadStream(entry, function (zipErr: any, readStream: any) { + if (zipErr) { + reject(err) + } + readStream.on('end', function () { + writeStream.close() + zipfile.readEntry() + }) + readStream.pipe(writeStream) + }) + + if (entry.fileName === binaryName) { + zipfile.close() + } + } + }) + + zipfile.on('error', (zipErr: any) => { + reject(zipErr) + }) + + zipfile.once('end', () => { + fs.chmod(binaryPath, '0755', function (zipErr: any) { + if (zipErr) { + reject(zipErr) + } + resolve(binaryPath) + }) + zipfile.close() + }) + }) + }) + }).on('error', function (err: any) { + PercyLogger.error('Got Error in percy binary downloading request : ' + err) + reject(err) + }) + }) + } +} + +export default PercyBinary diff --git a/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts b/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts new file mode 100644 index 00000000000..0389f2be7de --- /dev/null +++ b/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts @@ -0,0 +1,38 @@ +/* + * Maintains a counter for each driver to get consistent and + * unique screenshot names for percy + */ + +class PercyCaptureMap { + #map: any = {} + + increment(sessionName: string, eventName: string) { + if (!this.#map[sessionName]) { + this.#map[sessionName] = {} + } + + if (!this.#map[sessionName][eventName]) { + this.#map[sessionName][eventName] = 0 + } + + this.#map[sessionName][eventName]++ + } + + getName(sessionName: string, eventName: string) { + return `${sessionName}-${eventName}-${this.get(sessionName, eventName)}` + } + + get(sessionName: string, eventName: string) { + if (!this.#map[sessionName]) { + return 0 + } + + if (!this.#map[sessionName][eventName]) { + return 0 + } + + return this.#map[sessionName][eventName] + } +} + +export default PercyCaptureMap diff --git a/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts b/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts new file mode 100644 index 00000000000..9a17611cb5a --- /dev/null +++ b/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts @@ -0,0 +1,74 @@ +// ======= Percy helper methods start ======= + +import type { Capabilities } from '@wdio/types' +import type { BrowserstackConfig, UserConfig } from '../types' + +import type { Options } from '@wdio/types' + +import { PercyLogger } from './PercyLogger' +import Percy from './Percy' + +export const startPercy = async (options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig): Promise => { + PercyLogger.debug('Starting percy') + const percy = new Percy(options, config, bsConfig) + const response = await percy.start() + if (response) { + return percy + } + return ({} as Percy) +} + +export const stopPercy = async (percy: Percy) => { + PercyLogger.debug('Stopping percy') + return percy.stop() +} + +export const getBestPlatformForPercySnapshot = (capabilities?: Capabilities.RemoteCapabilities) : any => { + try { + const percyBrowserPreference: any = { 'chrome': 0, 'firefox': 1, 'edge': 2, 'safari': 3 } + + let bestPlatformCaps: any = null + let bestBrowser: any = null + + if (Array.isArray(capabilities)) { + capabilities + .flatMap((c: Capabilities.DesiredCapabilities | Capabilities.MultiRemoteCapabilities) => { + if (Object.values(c).length > 0 && Object.values(c).every(c => typeof c === 'object' && c.capabilities)) { + return Object.values(c).map((o: Options.WebdriverIO) => o.capabilities) + } + return c as (Capabilities.DesiredCapabilities) + }).forEach((capability: Capabilities.DesiredCapabilities) => { + let currBrowserName = capability.browserName + if (capability['bstack:options']) { + currBrowserName = capability['bstack:options'].browserName || currBrowserName + } + if (!bestBrowser || !bestPlatformCaps || (bestPlatformCaps.deviceName || bestPlatformCaps['bstack:options']?.deviceName)) { + bestBrowser = currBrowserName + bestPlatformCaps = capability + } else if (currBrowserName && percyBrowserPreference[currBrowserName.toLowerCase()] < percyBrowserPreference[bestBrowser.toLowerCase()]) { + bestBrowser = currBrowserName + bestPlatformCaps = capability + } + }) + return bestPlatformCaps + } else if (typeof capabilities === 'object') { + Object.entries(capabilities as Capabilities.MultiRemoteCapabilities).forEach(([, caps]) => { + let currBrowserName = (caps.capabilities as Capabilities.Capabilities).browserName + if ((caps.capabilities as Capabilities.Capabilities)['bstack:options']) { + // @ts-ignore: Object is possibly 'null'. + currBrowserName = (caps.capabilities as Capabilities.Capabilities)['bstack:options'].browserName || currBrowserName + } + if (!bestBrowser || !bestPlatformCaps || (bestPlatformCaps.deviceName || bestPlatformCaps['bstack:options']?.deviceName)) { + bestBrowser = currBrowserName + bestPlatformCaps = (caps.capabilities as Capabilities.Capabilities) + } else if (currBrowserName && percyBrowserPreference[currBrowserName.toLowerCase()] < percyBrowserPreference[bestBrowser.toLowerCase()]) { + bestBrowser = currBrowserName + bestPlatformCaps = (caps.capabilities as Capabilities.Capabilities) + } + }) + } + } catch (err: any) { + PercyLogger.error(`Error while trying to determine best platform for Percy snapshot ${err}`) + return null + } +} diff --git a/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts b/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts new file mode 100644 index 00000000000..8f227e49be9 --- /dev/null +++ b/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts @@ -0,0 +1,76 @@ +import path from 'node:path' +import fs from 'node:fs' + +import logger from '@wdio/logger' + +import { PERCY_LOGS_FILE } from '../constants' + +const log = logger('@wdio/browserstack-service') + +export abstract class PercyLogger { + public static logFilePath = path.join(process.cwd(), PERCY_LOGS_FILE) + private static logFolderPath = path.join(process.cwd(), 'logs') + private static logFileStream: fs.WriteStream | null + + static logToFile(logMessage: string, logLevel: string) { + try { + if (!this.logFileStream) { + if (!fs.existsSync(this.logFolderPath)){ + fs.mkdirSync(this.logFolderPath) + } + this.logFileStream = fs.createWriteStream(this.logFilePath, { flags: 'a' }) + } + if (this.logFileStream && this.logFileStream.writable) { + this.logFileStream.write(this.formatLog(logMessage, logLevel)) + } + } catch (error) { + log.debug(`Failed to log to file. Error ${error}`) + } + } + + private static formatLog(logMessage: string, level: string) { + return `${new Date().toISOString()} ${level.toUpperCase()} @wdio/browserstack-service ${logMessage}\n` + } + + public static info(message: string) { + this.logToFile(message, 'info') + log.info(message) + } + + public static error(message: string) { + this.logToFile(message, 'error') + log.error(message) + } + + public static debug(message: string, param?: any) { + this.logToFile(message, 'debug') + if (param) { + log.debug(message, param) + } else { + log.debug(message) + } + } + + public static warn(message: string) { + this.logToFile(message, 'warn') + log.warn(message) + } + + public static trace(message: string) { + this.logToFile(message, 'trace') + log.trace(message) + } + + public static clearLogger() { + if (this.logFileStream) { + this.logFileStream.end() + } + this.logFileStream = null + } + + public static clearLogFile() { + if (fs.existsSync(this.logFilePath)) { + fs.truncateSync(this.logFilePath) + } + } +} diff --git a/packages/wdio-browserstack-service/src/Percy/PercySDK.ts b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts new file mode 100644 index 00000000000..b57b0f7c517 --- /dev/null +++ b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts @@ -0,0 +1,40 @@ +const tryRequire = function (pkg: string, fallback: any) { + try { + return require(pkg) + } catch { + return fallback + } +} + +import type { Browser, MultiRemoteBrowser } from 'webdriverio' + +// const percySnapshot = tryRequire('@percy/webdriverio', null); +const percySnapshot = tryRequire('@percy/selenium-webdriver', null) +const { percyScreenshot } = tryRequire('@percy/selenium-webdriver', {}) +const percyAppScreenshot = require('@percy/appium-app') + +import { PercyLogger } from './PercyLogger' + +/* eslint-disable @typescript-eslint/no-unused-vars */ +let snapshotHandler = (...args: any[]) => { + PercyLogger.error('Unsupported driver for percy') +} +if (percySnapshot) { + snapshotHandler = (browser: Browser<'async'> | MultiRemoteBrowser<'async'>, name: string) => { + if (process.env.PERCY_SNAPSHOT === 'true') { + return percySnapshot(browser, name) + } + } +} +export const snapshot = snapshotHandler + +/* eslint-disable @typescript-eslint/no-unused-vars */ +let screenshotHandler = async (...args: any[]) => { + PercyLogger.error('Unsupported driver for percy') +} +if (percyScreenshot) { + screenshotHandler = percyScreenshot +} +export const screenshot = screenshotHandler + +export const screenshotApp = percyAppScreenshot diff --git a/packages/wdio-browserstack-service/src/constants.ts b/packages/wdio-browserstack-service/src/constants.ts index 2c72b297a9d..8d3b309e8fe 100644 --- a/packages/wdio-browserstack-service/src/constants.ts +++ b/packages/wdio-browserstack-service/src/constants.ts @@ -37,3 +37,5 @@ export const DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS = 100 // 100ms export const ACCESSIBILITY_API_URL = 'https://accessibility.browserstack.com/api' export const NOT_ALLOWED_KEYS_IN_CAPS = ['includeTagsInTestingScope', 'excludeTagsInTestingScope'] + +export const PERCY_LOGS_FILE = 'logs/percy.log' diff --git a/packages/wdio-browserstack-service/src/index.ts b/packages/wdio-browserstack-service/src/index.ts index 69cc9a3e4ad..675d5ad6091 100644 --- a/packages/wdio-browserstack-service/src/index.ts +++ b/packages/wdio-browserstack-service/src/index.ts @@ -10,6 +10,10 @@ export default BrowserstackService export const launcher = BrowserstackLauncher export const log4jsAppender = { configure } export const BStackTestOpsLogger = logReportingAPI + +import * as Percy from './Percy/PercySDK' +export const PercySDK = Percy + export * from './types' declare global { diff --git a/packages/wdio-browserstack-service/src/launcher.ts b/packages/wdio-browserstack-service/src/launcher.ts index 3395de8c1fb..5ca962ea93c 100644 --- a/packages/wdio-browserstack-service/src/launcher.ts +++ b/packages/wdio-browserstack-service/src/launcher.ts @@ -14,7 +14,8 @@ import type { Capabilities, Services, Options } from '@wdio/types' // @ts-ignore import { version as bstackServiceVersion } from '../package.json' import CrashReporter from './crash-reporter' -import type { App, AppConfig, AppUploadResponse, BrowserstackConfig } from './types' +import { startPercy, stopPercy, getBestPlatformForPercySnapshot } from './Percy/PercyHelper' +import type { App, AppConfig, AppUploadResponse, BrowserstackConfig, UserConfig } from './types' import { VALID_APP_EXTENSION, NOT_ALLOWED_KEYS_IN_CAPS } from './constants' import { launchTestSession, @@ -26,9 +27,12 @@ import { isUndefined, isAccessibilityAutomationSession, stopAccessibilityTestRun, + ObjectsAreEqual, isTrue } from './util' import PerformanceTester from './performance-tester' +import { PercyLogger } from './Percy/PercyLogger.js' +import type Percy from './Percy/Percy.js' const log = logger('@wdio/browserstack-service') @@ -44,12 +48,15 @@ export default class BrowserstackLauncherService implements Services.ServiceInst private _buildTag?: string private _buildIdentifier?: string private _accessibilityAutomation?: boolean + private _percy?: Percy + private _percyBestPlatformCaps?: any constructor ( private _options: BrowserstackConfig & Options.Testrunner, capabilities: Capabilities.RemoteCapability, private _config: Options.Testrunner ) { + PercyLogger.clearLogFile() // added to maintain backward compatibility with webdriverIO v5 this._config || (this._config = _options) if (Array.isArray(capabilities)) { @@ -142,6 +149,21 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } } + /* eslint-disable @typescript-eslint/no-unused-vars */ + async onWorkerStart (cid: any, caps: any, specs: any, args: any, execArgv: any) { + try { + if (this._options.percy && this._percyBestPlatformCaps) { + const isThisBestPercyPlatform = ObjectsAreEqual(caps, this._percyBestPlatformCaps) + if (isThisBestPercyPlatform) { + process.env.BEST_PLATFORM_CID = cid + '' + } + caps['wdio:cid'] = cid + } + } catch (err: any) { + PercyLogger.error(`Error while setting best platform for Percy snapshot at worker start ${err}`) + } + } + async onPrepare (config?: Options.Testrunner, capabilities?: Capabilities.RemoteCapabilities) { /** * Upload app to BrowserStack if valid file path to app is given. @@ -235,6 +257,18 @@ export default class BrowserstackLauncherService implements Services.ServiceInst }) } + if (this._options.percy) { + try { + const bestPlatformPercyCaps = getBestPlatformForPercySnapshot(capabilities) + this._percyBestPlatformCaps = bestPlatformPercyCaps + await this.setupPercy(this._options, this._config, { + projectName: this._projectName + }) + } catch (err: any) { + PercyLogger.error(`Error while setting up Percy ${err}`) + } + } + if (!this._options.browserstackLocal) { return log.info('browserstackLocal is not enabled - skipping...') } @@ -308,6 +342,12 @@ export default class BrowserstackLauncherService implements Services.ServiceInst log.info(`Total duration is ${duration / 1000 } s`) } + if (this._options.percy) { + await this.stopPercy() + } + + PercyLogger.clearLogger() + if (!this.browserstackLocal || !this.browserstackLocal.isRunning()) { return } @@ -342,6 +382,39 @@ export default class BrowserstackLauncherService implements Services.ServiceInst }) } + async setupPercy(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig) { + if (!this._percy || !this._percy.isRunning()) { + try { + this._percy = await startPercy(options, config, bsConfig) + if (!this._percy) { + throw new Error('Could not start percy, check percy logs for info.') + } + PercyLogger.info('Percy started successfully') + let signal = 0 + const handler = async () => { + signal++ + signal === 1 && await this.stopPercy() + } + process.on('beforeExit', handler) + process.on('SIGINT', handler) + process.on('SIGTERM', handler) + } catch (err: any) { + PercyLogger.debug(`Error in percy setup ${err}`) + } + } + } + + async stopPercy() { + if (this._percy && this._percy.isRunning()) { + try { + await stopPercy(this._percy) + PercyLogger.info('Percy stopped') + } catch (err) { + PercyLogger.error('Error occured while stopping percy : ' + err) + } + } + } + async _uploadApp(app:App): Promise { log.info(`uploading app ${app.app} ${app.customId? `and custom_id: ${app.customId}` : ''} to browserstack`) diff --git a/packages/wdio-browserstack-service/src/service.ts b/packages/wdio-browserstack-service/src/service.ts index 0d52981c8f8..4d8c9320433 100644 --- a/packages/wdio-browserstack-service/src/service.ts +++ b/packages/wdio-browserstack-service/src/service.ts @@ -20,6 +20,7 @@ import { import TestReporter from './reporter' import PerformanceTester from './performance-tester' import AccessibilityHandler from './accessibility-handler' +import PercyHandler from './Percy/Percy-Handler.js' const log = logger('@wdio/browserstack-service') @@ -39,6 +40,8 @@ export default class BrowserstackService implements Services.ServiceInstance { private _accessibility private _accessibilityHandler?: AccessibilityHandler private _turboScale + private _percy + private _percyHandler?: PercyHandler constructor ( options: BrowserstackConfig & Options.Testrunner, @@ -50,6 +53,7 @@ export default class BrowserstackService implements Services.ServiceInstance { this._config || (this._config = this._options) this._observability = this._options.testObservability this._accessibility = this._options.accessibility + this._percy = this._options.percy this._turboScale = this._options.turboScale if (this._observability) { @@ -69,6 +73,10 @@ export default class BrowserstackService implements Services.ServiceInstance { if (strict) { this._failureStatuses.push('pending') } + + if (((_caps as any)['wdio:cid'] as string) === process.env.BEST_PLATFORM_CID) { + process.env.PERCY_SNAPSHOT = 'true' + } } _updateCaps (fn: (caps: Capabilities.Capabilities | Capabilities.DesiredCapabilities) => void) { @@ -78,6 +86,8 @@ export default class BrowserstackService implements Services.ServiceInstance { return Object.entries(multiRemoteCap).forEach(([, caps]) => fn(caps.capabilities as Capabilities.Capabilities)) } + // if(this._caps && (this._caps as Capabilities.DesiredCapabilities)['bstack:options']) delete (this._caps as Capabilities.DesiredCapabilities)['bstack:options']['bestPlatform']; + return fn(this._caps as Capabilities.Capabilities) } @@ -113,31 +123,55 @@ export default class BrowserstackService implements Services.ServiceInstance { this._scenariosThatRan = [] - if (this._observability && this._browser) { + if (this._browser) { + if (this._percy) { + this._percyHandler = new PercyHandler( + this._options.percyCaptureMode, + this._browser, + this._caps, + this._isAppAutomate(), + this._config.framework + ) + this._percyHandler.before() + } try { - patchConsoleLogs() - this._insightsHandler = new InsightsHandler(this._browser, this._browser.capabilities as Capabilities.Capabilities, this._isAppAutomate(), this._browser.sessionId as string, this._config.framework) - await this._insightsHandler.before() - - /** - * register command event - */ - this._browser.on('command', async (command) => await this._insightsHandler?.browserCommand( - 'client:beforeCommand', - Object.assign(command, { sessionId: this._browser?.sessionId }), - this._currentTest - )) + if (this._observability) { + patchConsoleLogs() + this._insightsHandler = new InsightsHandler(this._browser, this._browser.capabilities as Capabilities.Capabilities, this._isAppAutomate(), this._browser.sessionId as string, this._config.framework) + await this._insightsHandler.before() + + /** + * register command event + */ + this._browser.on('command', async (command) => await this._insightsHandler?.browserCommand( + 'client:beforeCommand', + Object.assign(command, { sessionId: this._browser?.sessionId }), + this._currentTest + )) + } + /** * register result event */ - this._browser.on('result', async (result) => await this._insightsHandler?.browserCommand( - 'client:afterCommand', - Object.assign(result, { sessionId: this._browser?.sessionId }), - this._currentTest - )) + this._browser.on('result', async (result) => { + if (this._observability) { + await this._insightsHandler?.browserCommand( + 'client:afterCommand', + Object.assign(result, { sessionId: this._browser?.sessionId }), + this._currentTest + ) + } + if (this._percy) { + this._percyHandler?.browserCommand( + Object.assign(result, { sessionId: this._browser?.sessionId }), + ) + } + }) } catch (err) { log.error(`Error in service class before function: ${err}`) - CrashReporter.uploadCrashReport(`Error in service class before function: ${err}`, err && (err as any).stack) + if (this._observability) { + CrashReporter.uploadCrashReport(`Error in service class before function: ${err}`, err && (err as any).stack) + } } } @@ -212,6 +246,9 @@ export default class BrowserstackService implements Services.ServiceInstance { } await this._insightsHandler?.afterTest(test, results) + if (this._percy) { + await this._percyHandler?.afterTest() + } await this._accessibilityHandler?.afterTest(this._suiteTitle, test) } @@ -235,6 +272,10 @@ export default class BrowserstackService implements Services.ServiceInstance { await this._insightsHandler?.uploadPending() await this._insightsHandler?.teardown() + if (this._percy) { + await this._percyHandler?.teardown() + } + if (process.env.BROWSERSTACK_O11Y_PERF_MEASUREMENT) { await PerformanceTester.stopAndGenerate('performance-service.html') PerformanceTester.calculateTimes([ @@ -289,6 +330,9 @@ export default class BrowserstackService implements Services.ServiceInstance { } await this._insightsHandler?.afterScenario(world) + if (this._percy) { + await this._percyHandler?.afterScenario() + } await this._accessibilityHandler?.afterScenario(world) } @@ -445,6 +489,10 @@ export default class BrowserstackService implements Services.ServiceInstance { name = `${pre}${test.parent}${post}` } + if (this._percy && this._percyHandler) { + this._percyHandler._setSessionName(name) + } + if (name !== this._fullTitle) { this._fullTitle = name await this._updateJob({ name }) diff --git a/packages/wdio-browserstack-service/src/types.ts b/packages/wdio-browserstack-service/src/types.ts index 41abd442aae..7974c039f65 100644 --- a/packages/wdio-browserstack-service/src/types.ts +++ b/packages/wdio-browserstack-service/src/types.ts @@ -60,6 +60,21 @@ export interface BrowserstackConfig { * For e.g. buildName, projectName, BrowserStack access credentials, etc. */ testObservabilityOptions?: TestObservabilityOptions; + /** + * Set this to true to enable BrowserStack Percy which will take screenshots + * and snapshots for your tests run on Browserstack + * @default false + */ + percy?: boolean; + /** + * Accepts mode as a string to auto capture screenshots at different execution points + * Accepted values are auto, click, testcase, screenshot & manual + */ + percyCaptureMode?: string; + /** + * Set the Percy related config options under this key. + */ + percyOptions?: any; /** * Set this to true to enable BrowserStack Accessibility Automation which will * automically conduct accessibility testing on your pre-existing test builds diff --git a/packages/wdio-browserstack-service/src/util.ts b/packages/wdio-browserstack-service/src/util.ts index c0054ce4706..5e4f2e7bb61 100644 --- a/packages/wdio-browserstack-service/src/util.ts +++ b/packages/wdio-browserstack-service/src/util.ts @@ -1112,3 +1112,24 @@ export function isUndefined(value: any) { export function isTrue(value?: any) { return (value + '').toLowerCase() === 'true' } + +export const isObject = (object: any) => { + return object !== null && typeof object === 'object' +} + +export const ObjectsAreEqual = (object1: any, object2: any) => { + const objectKeys1 = Object.keys(object1) + const objectKeys2 = Object.keys(object2) + if (objectKeys1.length !== objectKeys2.length) { + return false + } + for (const key of objectKeys1) { + const value1 = object1[key] + const value2 = object2[key] + const isBothAreObjects = isObject(value1) && isObject(value2) + if ((isBothAreObjects && !ObjectsAreEqual(value1, value2)) || (!isBothAreObjects && value1 !== value2)) { + return false + } + } + return true +} From f14e3bc6112f0791c1e9d915d1fd62d15d3c56e1 Mon Sep 17 00:00:00 2001 From: amaanbs Date: Tue, 12 Dec 2023 02:56:48 +0530 Subject: [PATCH 02/38] ts-lint fixes --- .../src/Percy/PercyCaptureMap.ts | 44 +++++++++---------- .../wdio-browserstack-service/src/service.ts | 10 ++--- .../wdio-browserstack-service/src/util.ts | 30 ++++++------- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts b/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts index 0389f2be7de..eb720d3e096 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts @@ -4,35 +4,35 @@ */ class PercyCaptureMap { - #map: any = {} + #map: any = {} - increment(sessionName: string, eventName: string) { - if (!this.#map[sessionName]) { - this.#map[sessionName] = {} - } + increment(sessionName: string, eventName: string) { + if (!this.#map[sessionName]) { + this.#map[sessionName] = {} + } - if (!this.#map[sessionName][eventName]) { - this.#map[sessionName][eventName] = 0 - } + if (!this.#map[sessionName][eventName]) { + this.#map[sessionName][eventName] = 0 + } - this.#map[sessionName][eventName]++ - } + this.#map[sessionName][eventName]++ + } - getName(sessionName: string, eventName: string) { - return `${sessionName}-${eventName}-${this.get(sessionName, eventName)}` - } + getName(sessionName: string, eventName: string) { + return `${sessionName}-${eventName}-${this.get(sessionName, eventName)}` + } - get(sessionName: string, eventName: string) { - if (!this.#map[sessionName]) { - return 0 - } + get(sessionName: string, eventName: string) { + if (!this.#map[sessionName]) { + return 0 + } - if (!this.#map[sessionName][eventName]) { - return 0 - } + if (!this.#map[sessionName][eventName]) { + return 0 + } - return this.#map[sessionName][eventName] - } + return this.#map[sessionName][eventName] + } } export default PercyCaptureMap diff --git a/packages/wdio-browserstack-service/src/service.ts b/packages/wdio-browserstack-service/src/service.ts index 4d8c9320433..716f14cd104 100644 --- a/packages/wdio-browserstack-service/src/service.ts +++ b/packages/wdio-browserstack-service/src/service.ts @@ -139,7 +139,7 @@ export default class BrowserstackService implements Services.ServiceInstance { patchConsoleLogs() this._insightsHandler = new InsightsHandler(this._browser, this._browser.capabilities as Capabilities.Capabilities, this._isAppAutomate(), this._browser.sessionId as string, this._config.framework) await this._insightsHandler.before() - + /** * register command event */ @@ -149,16 +149,16 @@ export default class BrowserstackService implements Services.ServiceInstance { this._currentTest )) } - + /** * register result event */ this._browser.on('result', async (result) => { if (this._observability) { await this._insightsHandler?.browserCommand( - 'client:afterCommand', - Object.assign(result, { sessionId: this._browser?.sessionId }), - this._currentTest + 'client:afterCommand', + Object.assign(result, { sessionId: this._browser?.sessionId }), + this._currentTest ) } if (this._percy) { diff --git a/packages/wdio-browserstack-service/src/util.ts b/packages/wdio-browserstack-service/src/util.ts index 5e4f2e7bb61..734e30ad383 100644 --- a/packages/wdio-browserstack-service/src/util.ts +++ b/packages/wdio-browserstack-service/src/util.ts @@ -1114,22 +1114,22 @@ export function isTrue(value?: any) { } export const isObject = (object: any) => { - return object !== null && typeof object === 'object' + return object !== null && typeof object === 'object' } export const ObjectsAreEqual = (object1: any, object2: any) => { - const objectKeys1 = Object.keys(object1) - const objectKeys2 = Object.keys(object2) - if (objectKeys1.length !== objectKeys2.length) { - return false - } - for (const key of objectKeys1) { - const value1 = object1[key] - const value2 = object2[key] - const isBothAreObjects = isObject(value1) && isObject(value2) - if ((isBothAreObjects && !ObjectsAreEqual(value1, value2)) || (!isBothAreObjects && value1 !== value2)) { - return false - } - } - return true + const objectKeys1 = Object.keys(object1) + const objectKeys2 = Object.keys(object2) + if (objectKeys1.length !== objectKeys2.length) { + return false + } + for (const key of objectKeys1) { + const value1 = object1[key] + const value2 = object2[key] + const isBothAreObjects = isObject(value1) && isObject(value2) + if ((isBothAreObjects && !ObjectsAreEqual(value1, value2)) || (!isBothAreObjects && value1 !== value2)) { + return false + } + } + return true } From b3d6b89d268e5944e0342cb6f2cbb659e64f250a Mon Sep 17 00:00:00 2001 From: amaanbs Date: Tue, 12 Dec 2023 17:31:13 +0530 Subject: [PATCH 03/38] minor fixes --- .../src/Percy/Percy-Handler.ts | 2 +- .../wdio-browserstack-service/src/Percy/PercySDK.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index 8769bc53666..4a69d0110dc 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -45,7 +45,7 @@ class _PercyHandler { try { if (eventName) { this._percyScreenshotCounter += 1 - this._isAppAutomate ? await PercySDK.screenshotApp(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : await PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)); + await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName))); ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment((this.sessionName as string), eventName) this._percyScreenshotCounter -= 1 } diff --git a/packages/wdio-browserstack-service/src/Percy/PercySDK.ts b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts index b57b0f7c517..4725d720f81 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercySDK.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts @@ -11,7 +11,7 @@ import type { Browser, MultiRemoteBrowser } from 'webdriverio' // const percySnapshot = tryRequire('@percy/webdriverio', null); const percySnapshot = tryRequire('@percy/selenium-webdriver', null) const { percyScreenshot } = tryRequire('@percy/selenium-webdriver', {}) -const percyAppScreenshot = require('@percy/appium-app') +const percyAppScreenshot = tryRequire('@percy/appium-app', {}) import { PercyLogger } from './PercyLogger' @@ -37,4 +37,11 @@ if (percyScreenshot) { } export const screenshot = screenshotHandler -export const screenshotApp = percyAppScreenshot +/* eslint-disable @typescript-eslint/no-unused-vars */ +let screenshotAppHandler = async (...args: any[]) => { + PercyLogger.error('Unsupported driver for percy') +} +if (percyAppScreenshot) { + screenshotAppHandler = percyAppScreenshot +} +export const screenshotApp = screenshotAppHandler From 63a0001de9f56bc3701b37e40da4cbb80f849a56 Mon Sep 17 00:00:00 2001 From: amaanbs Date: Tue, 12 Dec 2023 17:32:32 +0530 Subject: [PATCH 04/38] minor fixes 2 --- packages/wdio-browserstack-service/src/Percy/PercySDK.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/PercySDK.ts b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts index 4725d720f81..dd9fd60573e 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercySDK.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts @@ -39,9 +39,9 @@ export const screenshot = screenshotHandler /* eslint-disable @typescript-eslint/no-unused-vars */ let screenshotAppHandler = async (...args: any[]) => { - PercyLogger.error('Unsupported driver for percy') + PercyLogger.error('Unsupported driver for percy') } if (percyAppScreenshot) { - screenshotAppHandler = percyAppScreenshot + screenshotAppHandler = percyAppScreenshot } export const screenshotApp = screenshotAppHandler From cf5d47c7b37518ad86c81d77ea399c285187900c Mon Sep 17 00:00:00 2001 From: amaanbs Date: Wed, 13 Dec 2023 19:58:37 +0530 Subject: [PATCH 05/38] PR review fixes --- .../src/Percy/Percy-Handler.ts | 1 + .../src/Percy/PercyHelper.ts | 1 + .../wdio-browserstack-service/src/service.ts | 26 +++++-------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index 4a69d0110dc..e5eacfe0f8c 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -44,6 +44,7 @@ class _PercyHandler { async percyAutoCapture(eventName: string | null) { try { if (eventName) { + /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ this._percyScreenshotCounter += 1 await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName))); ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment((this.sessionName as string), eventName) diff --git a/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts b/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts index 9a17611cb5a..fc9964c293a 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts @@ -66,6 +66,7 @@ export const getBestPlatformForPercySnapshot = (capabilities?: Capabilities.Remo bestPlatformCaps = (caps.capabilities as Capabilities.Capabilities) } }) + return bestPlatformCaps } } catch (err: any) { PercyLogger.error(`Error while trying to determine best platform for Percy snapshot ${err}`) diff --git a/packages/wdio-browserstack-service/src/service.ts b/packages/wdio-browserstack-service/src/service.ts index 716f14cd104..b058735c418 100644 --- a/packages/wdio-browserstack-service/src/service.ts +++ b/packages/wdio-browserstack-service/src/service.ts @@ -86,8 +86,6 @@ export default class BrowserstackService implements Services.ServiceInstance { return Object.entries(multiRemoteCap).forEach(([, caps]) => fn(caps.capabilities as Capabilities.Capabilities)) } - // if(this._caps && (this._caps as Capabilities.DesiredCapabilities)['bstack:options']) delete (this._caps as Capabilities.DesiredCapabilities)['bstack:options']['bestPlatform']; - return fn(this._caps as Capabilities.Capabilities) } @@ -161,11 +159,9 @@ export default class BrowserstackService implements Services.ServiceInstance { this._currentTest ) } - if (this._percy) { - this._percyHandler?.browserCommand( - Object.assign(result, { sessionId: this._browser?.sessionId }), - ) - } + this._percyHandler?.browserCommand( + Object.assign(result, { sessionId: this._browser?.sessionId }), + ) }) } catch (err) { log.error(`Error in service class before function: ${err}`) @@ -246,9 +242,7 @@ export default class BrowserstackService implements Services.ServiceInstance { } await this._insightsHandler?.afterTest(test, results) - if (this._percy) { - await this._percyHandler?.afterTest() - } + await this._percyHandler?.afterTest() await this._accessibilityHandler?.afterTest(this._suiteTitle, test) } @@ -272,9 +266,7 @@ export default class BrowserstackService implements Services.ServiceInstance { await this._insightsHandler?.uploadPending() await this._insightsHandler?.teardown() - if (this._percy) { - await this._percyHandler?.teardown() - } + await this._percyHandler?.teardown() if (process.env.BROWSERSTACK_O11Y_PERF_MEASUREMENT) { await PerformanceTester.stopAndGenerate('performance-service.html') @@ -330,9 +322,7 @@ export default class BrowserstackService implements Services.ServiceInstance { } await this._insightsHandler?.afterScenario(world) - if (this._percy) { - await this._percyHandler?.afterScenario() - } + await this._percyHandler?.afterScenario() await this._accessibilityHandler?.afterScenario(world) } @@ -489,9 +479,7 @@ export default class BrowserstackService implements Services.ServiceInstance { name = `${pre}${test.parent}${post}` } - if (this._percy && this._percyHandler) { - this._percyHandler._setSessionName(name) - } + this._percyHandler?._setSessionName(name) if (name !== this._fullTitle) { this._fullTitle = name From 29daea06ea177478ed4c48de12436e9fb1673deb Mon Sep 17 00:00:00 2001 From: amaanbs Date: Thu, 14 Dec 2023 17:59:46 +0530 Subject: [PATCH 06/38] add unit tests --- .../src/Percy/PercyLogger.ts | 8 +- .../wdio-browserstack-service/src/launcher.ts | 4 +- .../wdio-browserstack-service/src/service.ts | 2 +- .../tests/Percy-Handler.test.ts | 287 ++++++++++++++++++ .../tests/PercyHelper.test.ts | 213 +++++++++++++ .../tests/PercyLogger.test.ts | 77 +++++ 6 files changed, 586 insertions(+), 5 deletions(-) create mode 100644 packages/wdio-browserstack-service/tests/Percy-Handler.test.ts create mode 100644 packages/wdio-browserstack-service/tests/PercyHelper.test.ts create mode 100644 packages/wdio-browserstack-service/tests/PercyLogger.test.ts diff --git a/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts b/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts index 8f227e49be9..5f7aecd0fdb 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts @@ -69,8 +69,12 @@ export abstract class PercyLogger { } public static clearLogFile() { - if (fs.existsSync(this.logFilePath)) { - fs.truncateSync(this.logFilePath) + try { + if (fs.existsSync(this.logFilePath)) { + fs.truncateSync(this.logFilePath) + } + } catch (err: any) { + log.error(`Failed to clear percy.log file. Error ${err}`) } } } diff --git a/packages/wdio-browserstack-service/src/launcher.ts b/packages/wdio-browserstack-service/src/launcher.ts index 5ca962ea93c..1fa5573f346 100644 --- a/packages/wdio-browserstack-service/src/launcher.ts +++ b/packages/wdio-browserstack-service/src/launcher.ts @@ -31,8 +31,8 @@ import { isTrue } from './util' import PerformanceTester from './performance-tester' -import { PercyLogger } from './Percy/PercyLogger.js' -import type Percy from './Percy/Percy.js' +import { PercyLogger } from './Percy/PercyLogger' +import type Percy from './Percy/Percy' const log = logger('@wdio/browserstack-service') diff --git a/packages/wdio-browserstack-service/src/service.ts b/packages/wdio-browserstack-service/src/service.ts index b058735c418..e2aa6550c3a 100644 --- a/packages/wdio-browserstack-service/src/service.ts +++ b/packages/wdio-browserstack-service/src/service.ts @@ -20,7 +20,7 @@ import { import TestReporter from './reporter' import PerformanceTester from './performance-tester' import AccessibilityHandler from './accessibility-handler' -import PercyHandler from './Percy/Percy-Handler.js' +import PercyHandler from './Percy/Percy-Handler' const log = logger('@wdio/browserstack-service') diff --git a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts new file mode 100644 index 00000000000..111960ea5c0 --- /dev/null +++ b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts @@ -0,0 +1,287 @@ +/// + +import got from 'got' +import logger from '@wdio/logger' + +import PercyHandler from '../src/percy/Percy-Handler' +// import * as PercyCaptureMapExport from '../src/Percy/PercyCaptureMap' +import PercyCaptureMap from '../src/Percy/PercyCaptureMap' +import * as PercySDK from '../src/Percy/PercySDK' +import type { Capabilities } from '@wdio/types' +import { Browser, MultiRemoteBrowser } from 'webdriverio' +import * as PercyLogger from '../src/Percy/PercyLogger' + +const log = logger('test') +let percyHandler: PercyHandler +let browser: Browser<'async'> | MultiRemoteBrowser<'async'> +let caps: Capabilities.RemoteCapability + +jest.mock('got') +// jest.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) +jest.useFakeTimers().setSystemTime(new Date('2020-01-01')) +jest.mock('uuid', () => ({ v4: () => '123456789' })) + +const PercyLoggerSpy = jest.spyOn(PercyLogger.PercyLogger, 'logToFile') +PercyLoggerSpy.mockImplementation(() => {}) + +beforeEach(() => { + jest.mocked(log.info).mockClear() + jest.mocked(got).mockClear() + jest.mocked(got.put).mockClear() + jest.mocked(got).mockResolvedValue({ + body: { + automation_session: { + browser_url: 'https://www.browserstack.com/automate/builds/1/sessions/2' + } + } + }) + jest.mocked(got.put).mockResolvedValue({}) + + browser = { + sessionId: 'session123', + config: {}, + capabilities: { + device: '', + os: 'OS X', + os_version: 'Catalina', + browserName: 'chrome' + }, + instances: ['browserA', 'browserB'], + isMultiremote: false, + browserA: { + sessionId: 'session456', + capabilities: { 'bstack:options': { + device: '', + os: 'Windows', + osVersion: 10, + browserName: 'chrome' + } } + }, + getInstance: jest.fn().mockImplementation((browserName: string) => browser[browserName]), + browserB: {}, + execute: jest.fn(), + executeAsync: async () => { 'done' }, + getUrl: () => { return 'https://www.google.com/'}, + on: jest.fn(), + } as any as Browser<'async'> | MultiRemoteBrowser<'async'> + caps = { + browserName: 'chrome', + 'bstack:options': { + os: 'OS X', + osVersion: 'Catalina', + accessibility: true + } } as Capabilities.RemoteCapability + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') +}) + +it('should initialize correctly', () => { + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') + expect(percyHandler['_isAppAutomate']).toEqual(false) + expect(percyHandler['_capabilities']).toEqual(caps) + expect(percyHandler['_framework']).toEqual('framework') + expect(percyHandler['_percyScreenshotCounter']).toEqual(0) +}) + +describe('before', () => { + // const percyCaptureMapSpy = jest.spyOn(PercyCaptureMapExport, 'PercyCaptureMap' as any) + beforeEach(() => { + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') + // percyCaptureMapSpy.mockClear() + }) + it('assigns PercyCaptureMap to browser', async () => { + await percyHandler.before() + expect(browser).toHaveProperty('percyCaptureMap') + }) +}) + +describe('_setSessionName', () => { + beforeEach(() => { + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') + }) + it('sets sessionName property', async () => { + percyHandler._setSessionName('1234') + expect(percyHandler['sessionName']).toEqual('1234') + }) +}) + +describe('teardown', () => { + beforeEach(() => { + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') + }) + it('resolves promise if _percyScreenshotCounter is 0', async () => { + percyHandler.teardown().then(() => { + expect(percyHandler['_percyScreenshotCounter']).toEqual(0) + /* eslint-disable @typescript-eslint/no-unused-vars */ + }).catch((err: any) => { + expect(percyHandler['_percyScreenshotCounter']).not.equal(0) + }) + }) +}) + +describe('browserCommand', () => { + let percyAutoCaptureSpy: any + let percyHandler: PercyHandler + + beforeEach(() => { + percyHandler = new PercyHandler('auto', browser, caps, false, 'framework') + percyHandler.before() + percyAutoCaptureSpy = jest.spyOn(PercyHandler.prototype, 'percyAutoCapture') + }) + + it('should not call percyAutoCapture if no browser endpoint', async () => { + const args = {} + await percyHandler.browserCommand(args) + expect(percyAutoCaptureSpy).not.toBeCalled() + }) + + it('should call percyAutoCapture for event type keys', async () => { + const args = { + endpoint: 'actions', + body: { + actions: [{ + type: 'key' + }] + } + } + await percyHandler.browserCommand(args) + expect(percyAutoCaptureSpy).toBeCalledTimes(1) + }) + + it('should call percyAutoCapture for event type click', async () => { + const args = { + endpoint: 'click' + } + await percyHandler.browserCommand(args) + expect(percyAutoCaptureSpy).toBeCalledTimes(1) + }) + + it('should call percyAutoCapture for event type screenshot', async () => { + const args = { + endpoint: 'screenshot' + } + await percyHandler.browserCommand(args) + expect(percyAutoCaptureSpy).toBeCalledTimes(1) + }) + + afterEach(() => { + percyAutoCaptureSpy.mockClear() + }) +}) + +describe('afterScenario', () => { + let percyAutoCaptureSpy: any + let percyHandler: PercyHandler + + beforeEach(() => { + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') + percyHandler.before() + percyAutoCaptureSpy = jest.spyOn(PercyHandler.prototype, 'percyAutoCapture') + }) + + it('should not call percyAutoCapture', async () => { + await percyHandler.afterScenario() + expect(percyAutoCaptureSpy).not.toBeCalled() + }) + + it('should call percyAutoCapture', async () => { + percyHandler['_percyAutoCaptureMode'] = 'testcase' + await percyHandler.afterScenario() + expect(percyAutoCaptureSpy).toBeCalledTimes(1) + }) + + afterEach(() => { + percyAutoCaptureSpy.mockClear() + }) +}) + +describe('afterTest', () => { + let percyAutoCaptureSpy: any + let percyHandler: PercyHandler + + beforeEach(() => { + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') + percyHandler.before() + percyAutoCaptureSpy = jest.spyOn(PercyHandler.prototype, 'percyAutoCapture') + }) + + // it('should not call percyAutoCapture', async () => { + // await percyHandler.afterTest() + // expect(percyAutoCaptureSpy).not.toBeCalled() + // }) + + it('should call percyAutoCapture', async () => { + percyHandler['_percyAutoCaptureMode'] = 'testcase' + await percyHandler.afterTest() + expect(percyAutoCaptureSpy).toBeCalledTimes(1) + }) + + afterEach(() => { + percyAutoCaptureSpy.mockClear() + jest.clearAllMocks() + }) +}) + +describe('percyAutoCapture', () => { + let percyScreenshotSpy: any + let percyScreenshotAppSpy: any + + beforeEach(() => { + percyHandler = new PercyHandler('auto', browser, caps, false, 'framework') + percyHandler._setSessionName('1234') + percyHandler.before() + + percyScreenshotSpy = jest.spyOn(PercySDK, 'screenshot').mockImplementation(() => Promise.resolve()) + percyScreenshotAppSpy = jest.spyOn(PercySDK, 'screenshotApp').mockImplementation(() => Promise.resolve()) + }) + + it('does not call Percy Selenium Screenshot', async () => { + await percyHandler.percyAutoCapture(null) + expect(percyScreenshotSpy).not.toBeCalled() + }) + + it('calls Percy Selenium Screenshot', async () => { + await percyHandler.percyAutoCapture('keys') + expect(percyScreenshotSpy).toBeCalledTimes(1) + }) + + it('calls Percy Appium Screenshot', async () => { + percyHandler = new PercyHandler('auto', browser, caps, true, 'framework') + await percyHandler.percyAutoCapture('keys') + expect(percyScreenshotAppSpy).toBeCalledTimes(1) + }) + + afterEach(() => { + percyScreenshotSpy.mockClear() + percyScreenshotAppSpy.mockClear() + }) +}) + +describe('percyCaptureMap', () => { + let percyAutoCaptureMapGetNameSpy: any + let percyAutoCaptureMapIncrementSpy: any + let percyHandler: PercyHandler + + beforeEach(() => { + percyHandler = new PercyHandler('auto', browser, caps, false, 'framework') + percyHandler.before() + percyAutoCaptureMapGetNameSpy = jest.spyOn(PercyCaptureMap.prototype, 'getName') + percyAutoCaptureMapIncrementSpy = jest.spyOn(PercyCaptureMap.prototype, 'increment') + }) + + it('should call getName method of PercyCaptureMap', async () => { + await percyHandler.percyAutoCapture('keys') + await percyHandler.percyAutoCapture('keys') + expect(percyAutoCaptureMapGetNameSpy).toBeCalledTimes(2) + }) + + it('should call getName method of PercyCaptureMap', async () => { + await percyHandler.percyAutoCapture('click') + await percyHandler.percyAutoCapture('click') + expect(percyAutoCaptureMapIncrementSpy).toBeCalledTimes(2) + }) + + afterEach(() => { + percyAutoCaptureMapGetNameSpy.mockClear() + percyAutoCaptureMapIncrementSpy.mockClear() + }) +}) diff --git a/packages/wdio-browserstack-service/tests/PercyHelper.test.ts b/packages/wdio-browserstack-service/tests/PercyHelper.test.ts new file mode 100644 index 00000000000..e466e67a16f --- /dev/null +++ b/packages/wdio-browserstack-service/tests/PercyHelper.test.ts @@ -0,0 +1,213 @@ +/// +import path from 'node:path' + +import got from 'got' +import logger from '@wdio/logger' + +import * as PercyHelper from '../src/Percy/PercyHelper' +import Percy from '../src/Percy/Percy' +import * as PercyLogger from '../src/Percy/PercyLogger' + +import { Browser, MultiRemoteBrowser } from 'webdriverio' + +const log = logger('test') +let browser: Browser<'async'> | MultiRemoteBrowser<'async'> + +jest.mock('got') +// jest.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) +jest.useFakeTimers().setSystemTime(new Date('2020-01-01')) +jest.mock('uuid', () => ({ v4: () => '123456789' })) + +const PercyLoggerSpy = jest.spyOn(PercyLogger.PercyLogger, 'logToFile') +PercyLoggerSpy.mockImplementation(() => {}) + +beforeEach(() => { + jest.mocked(log.info).mockClear() + jest.mocked(got).mockClear() + jest.mocked(got.put).mockClear() + jest.mocked(got).mockResolvedValue({ + body: { + automation_session: { + browser_url: 'https://www.browserstack.com/automate/builds/1/sessions/2' + } + } + }) + jest.mocked(got.put).mockResolvedValue({}) + + browser = { + sessionId: 'session123', + config: {}, + capabilities: { + device: '', + os: 'OS X', + os_version: 'Catalina', + browserName: 'chrome' + }, + instances: ['browserA', 'browserB'], + isMultiremote: false, + browserA: { + sessionId: 'session456', + capabilities: { 'bstack:options': { + device: '', + os: 'Windows', + osVersion: 10, + browserName: 'chrome' + } } + }, + getInstance: jest.fn().mockImplementation((browserName: string) => browser[browserName]), + browserB: {}, + execute: jest.fn(), + executeAsync: async () => { 'done' }, + getUrl: () => { return 'https://www.google.com/'}, + on: jest.fn(), + } as any as Browser<'async'> | MultiRemoteBrowser<'async'> +}) + +describe('startPercy', () => { + let percyStartSpy: any + + beforeEach(() => { + percyStartSpy = jest.spyOn(Percy.prototype, 'start').mockImplementationOnce(async () => { + return true + }) + }) + + it('should call start method of Percy', async () => { + await PercyHelper.startPercy({}, {}, {}) + expect(percyStartSpy).toBeCalledTimes(1) + }) + + afterEach(() => { + percyStartSpy.mockClear() + }) +}) + +describe('stopPercy', () => { + let percyStopSpy: any + + beforeEach(() => { + percyStopSpy = jest.spyOn(Percy.prototype, 'stop').mockImplementationOnce(async () => { + return {} + }) + }) + + it('should call stop method of Percy', async () => { + const percy = new Percy({}, {}, {}) + await PercyHelper.stopPercy(percy) + expect(percyStopSpy).toBeCalledTimes(1) + }) + + afterEach(() => { + percyStopSpy.mockClear() + }) +}) + +describe('getBestPlatformForPercySnapshot', () => { + const capsArr: any = [ + { + maxInstances: 5, + browserName: 'edge', + browserVersion: 'latest', + platformName: 'Windows 10', + 'goog:chromeOptions': {}, + 'bstack:options': { + 'projectName': 'Project Name', + 'browserName': 'edge' + } + }, + { + maxInstances: 5, + browserName: 'chrome', + browserVersion: 'latest', + platformName: 'Windows 10', + 'goog:chromeOptions': {}, + 'bstack:options': { + 'projectName': 'Project Name', + 'browserName': 'chrome' + } + }, + { + maxInstances: 5, + browserName: 'firefox', + browserVersion: 'latest', + platformName: 'Windows 10', + 'moz:firefoxOptions': {}, + 'bstack:options': { + 'projectName': 'Project Name', + 'browserName': 'firefox' + } + } + ] + + const capsObj: any = { + 'key-1': { + capabilities: { + maxInstances: 5, + browserName: 'edge', + browserVersion: 'latest', + platformName: 'Windows 10', + 'goog:chromeOptions': {}, + 'bstack:options': { + 'projectName': 'Project Name', + 'browserName': 'edge' + } + } + }, + 'key-2': { + capabilities: { + maxInstances: 5, + browserName: 'chrome', + browserVersion: 'latest', + platformName: 'Windows 10', + 'goog:chromeOptions': {}, + 'bstack:options': { + 'projectName': 'Project Name', + 'browserName': 'chrome' + } + } + }, + 'key-3': { + capabilities: { + maxInstances: 5, + browserName: 'firefox', + browserVersion: 'latest', + platformName: 'Windows 10', + 'moz:firefoxOptions': {}, + 'bstack:options': { + 'projectName': 'Project Name', + 'browserName': 'firefox' + } + } + }, + } + + it('should return correct caps for best platform - Array', () => { + const bestPlatformCaps = PercyHelper.getBestPlatformForPercySnapshot(capsArr) + expect(bestPlatformCaps).toEqual({ + maxInstances: 5, + browserName: 'chrome', + browserVersion: 'latest', + platformName: 'Windows 10', + 'goog:chromeOptions': {}, + 'bstack:options': { + 'projectName': 'Project Name', + 'browserName': 'chrome' + } + }) + }) + + it('should return correct caps for best platform - Object', () => { + const bestPlatformCaps = PercyHelper.getBestPlatformForPercySnapshot(capsObj) + expect(bestPlatformCaps).toEqual({ + maxInstances: 5, + browserName: 'chrome', + browserVersion: 'latest', + platformName: 'Windows 10', + 'goog:chromeOptions': {}, + 'bstack:options': { + 'projectName': 'Project Name', + 'browserName': 'chrome' + } + }) + }) +}) diff --git a/packages/wdio-browserstack-service/tests/PercyLogger.test.ts b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts new file mode 100644 index 00000000000..badb2502061 --- /dev/null +++ b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts @@ -0,0 +1,77 @@ +import path from 'node:path' +import fs from 'node:fs' + +import logger from '@wdio/logger' +import { PercyLogger } from '../src/Percy/PercyLogger' + +const log = logger('test') + +// jest.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) +jest.mock('node:fs/promises', () => ({ + default: { + createReadStream: jest.fn().mockReturnValue({ pipe: jest.fn() }), + createWriteStream: jest.fn().mockReturnValue( + { + pipe: jest.fn(), + write: jest.fn() + }), + stat: jest.fn().mockReturnValue(Promise.resolve({ size: 123 })), + } +})) +jest.mock('node:fs', () => ({ + default: { + readFileSync: jest.fn().mockReturnValue('1234\nsomepath'), + existsSync: jest.fn(), + // existsSync: fs.existsSync, + truncateSync: jest.fn(), + mkdirSync: jest.fn() + } +})) + +describe('PercyLogger Log methods', () => { + let logToFileSpy: any + beforeEach(() => { + logToFileSpy = jest.spyOn(PercyLogger, 'logToFile') + }) + + it('should write to file and console - info', () => { + const logInfoMock = jest.spyOn(log, 'info') + + PercyLogger.info('This is the test for log.info') + expect(logToFileSpy).toBeCalled() + expect(logInfoMock).toBeCalled() + }) + + it('should write to file and console - warn', () => { + const logWarnMock = jest.spyOn(log, 'warn') + + PercyLogger.warn('This is the test for log.warn') + expect(logToFileSpy).toBeCalled() + expect(logWarnMock).toBeCalled() + }) + + it('should write to file and console - trace', () => { + const logTraceMock = jest.spyOn(log, 'trace') + + PercyLogger.trace('This is the test for log.trace') + expect(logToFileSpy).toBeCalled() + expect(logTraceMock).toBeCalled() + }) + + it('should write to file and console - debug', () => { + const logDebugMock = jest.spyOn(log, 'debug') + + PercyLogger.debug('This is the test for log.debug') + expect(logToFileSpy).toBeCalled() + expect(logDebugMock).toBeCalled() + }) + + it('should write to file and console - error', () => { + const logDebugMock = jest.spyOn(log, 'error') + + PercyLogger.error('This is the test for log.error') + expect(logToFileSpy).toBeCalled() + expect(logDebugMock).toBeCalled() + }) +}) + From 883d46e607f5c452823cb73049cc0ede0f1a045f Mon Sep 17 00:00:00 2001 From: amaanbs Date: Thu, 14 Dec 2023 18:00:57 +0530 Subject: [PATCH 07/38] remove unused imports --- packages/wdio-browserstack-service/tests/PercyHelper.test.ts | 2 -- packages/wdio-browserstack-service/tests/PercyLogger.test.ts | 3 --- 2 files changed, 5 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/PercyHelper.test.ts b/packages/wdio-browserstack-service/tests/PercyHelper.test.ts index e466e67a16f..8921f5133fe 100644 --- a/packages/wdio-browserstack-service/tests/PercyHelper.test.ts +++ b/packages/wdio-browserstack-service/tests/PercyHelper.test.ts @@ -1,6 +1,4 @@ /// -import path from 'node:path' - import got from 'got' import logger from '@wdio/logger' diff --git a/packages/wdio-browserstack-service/tests/PercyLogger.test.ts b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts index badb2502061..e0b01e44980 100644 --- a/packages/wdio-browserstack-service/tests/PercyLogger.test.ts +++ b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts @@ -1,6 +1,3 @@ -import path from 'node:path' -import fs from 'node:fs' - import logger from '@wdio/logger' import { PercyLogger } from '../src/Percy/PercyLogger' From 2d6ebe8f8978f7b581f5e4895a995a6a5b4dc812 Mon Sep 17 00:00:00 2001 From: amaanbs Date: Thu, 14 Dec 2023 18:28:40 +0530 Subject: [PATCH 08/38] minor fixes + add package-lock.json --- package-lock.json | 38 +++++++++++++++++++ .../src/Percy/Percy-Handler.ts | 5 ++- .../src/Percy/PercyCaptureMap.ts | 10 ++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56c24ceedb8..2cea520516d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/archiver": "^6.0.0", "@types/cheerio": "^0.22.31", "@types/eslint": "^8.4.2", + "@types/follow-redirects": "^1.14.4", "@types/fs-extra": "^11.0.1", "@types/jest": "^28.1.1", "@types/lodash.clonedeep": "^4.5.6", @@ -23,6 +24,7 @@ "@types/node": "^18.0.0", "@types/split2": "^4.2.0", "@types/uuid": "^9.0.0", + "@types/yauzl": "^2.10.3", "@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/utils": "^6.2.0", @@ -4332,6 +4334,15 @@ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, + "node_modules/@types/follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -4555,6 +4566,15 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.61.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz", @@ -22540,6 +22560,15 @@ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, + "@types/follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -22763,6 +22792,15 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, + "@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "5.61.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz", diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index e5eacfe0f8c..c60613c5bc6 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -45,12 +45,13 @@ class _PercyHandler { try { if (eventName) { /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ - this._percyScreenshotCounter += 1 - await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName))); + this._percyScreenshotCounter += 1; ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment((this.sessionName as string), eventName) + await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName))); this._percyScreenshotCounter -= 1 } } catch (err: any) { + ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).decrement((this.sessionName as string), eventName as string) PercyLogger.error(`Error while trying to auto capture Percy screenshot ${err}`) } } diff --git a/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts b/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts index eb720d3e096..9ec2e8e9bb4 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyCaptureMap.ts @@ -18,6 +18,14 @@ class PercyCaptureMap { this.#map[sessionName][eventName]++ } + decrement(sessionName: string, eventName: string) { + if (!this.#map[sessionName] || !this.#map[sessionName][eventName]) { + return + } + + this.#map[sessionName][eventName]-- + } + getName(sessionName: string, eventName: string) { return `${sessionName}-${eventName}-${this.get(sessionName, eventName)}` } @@ -31,7 +39,7 @@ class PercyCaptureMap { return 0 } - return this.#map[sessionName][eventName] + return this.#map[sessionName][eventName] - 1 } } From 0af2198e482b4e4a783c9fda71d47e4ffc67b49c Mon Sep 17 00:00:00 2001 From: amaanbs Date: Thu, 14 Dec 2023 18:36:10 +0530 Subject: [PATCH 09/38] ts-lint fixes --- packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index c60613c5bc6..22eb4754bfe 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -47,7 +47,7 @@ class _PercyHandler { /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ this._percyScreenshotCounter += 1; ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment((this.sessionName as string), eventName) - await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName))); + await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName))) this._percyScreenshotCounter -= 1 } } catch (err: any) { From 8d995288918784dcf35768e5d8c3bc993d2ca4a1 Mon Sep 17 00:00:00 2001 From: amaanbs Date: Tue, 19 Dec 2023 21:35:57 +0530 Subject: [PATCH 10/38] Stabilization + PR review fixes --- .../src/Percy/Percy-Handler.ts | 97 +++++++++++++++++-- .../src/Percy/Percy.ts | 2 +- .../src/Percy/PercySDK.ts | 6 +- .../src/constants.ts | 10 ++ .../wdio-browserstack-service/src/launcher.ts | 2 +- .../wdio-browserstack-service/src/service.ts | 27 ++++-- .../wdio-browserstack-service/src/util.ts | 2 +- .../tests/Percy-Handler.test.ts | 20 ++-- 8 files changed, 127 insertions(+), 39 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index 22eb4754bfe..e5938daef90 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -11,11 +11,16 @@ import * as PercySDK from './PercySDK' import { PercyLogger } from './PercyLogger' import { BrowserAsync } from 'src/@types/bstack-service-types' +import { PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS } from '../constants' + class _PercyHandler { private _testMetadata: { [key: string]: any } = {} private sessionName?: string private _isAppAutomate?: boolean + private isPercyCleanupProcessingUnderway?: boolean = false public _percyScreenshotCounter: any = 0 + private percyDeferredScreenshots: any = [] + private percyScreenshotInterval: any = null constructor ( private _percyAutoCaptureMode: string | undefined, @@ -25,6 +30,9 @@ class _PercyHandler { private _framework?: string ) { this._isAppAutomate = isAppAutomate + if (!_percyAutoCaptureMode || !['click', 'auto', 'screenshot', 'manual', 'testcase'].includes(_percyAutoCaptureMode as string)) { + this._percyAutoCaptureMode = 'auto' + } } _setSessionName(name: string) { @@ -41,17 +49,21 @@ class _PercyHandler { }) } - async percyAutoCapture(eventName: string | null) { + async percyAutoCapture(eventName: string | null, sessionName: string | null) { try { if (eventName) { - /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ - this._percyScreenshotCounter += 1; - ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment((this.sessionName as string), eventName) - await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName))) + if (!sessionName) { + /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ + this._percyScreenshotCounter += 1 + } + + ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment(sessionName ? sessionName : (this.sessionName as string), eventName) + await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName(sessionName ? sessionName : (this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName( sessionName ? sessionName : (this.sessionName as string), eventName))) this._percyScreenshotCounter -= 1 } } catch (err: any) { - ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).decrement((this.sessionName as string), eventName as string) + this._percyScreenshotCounter -= 1; + ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).decrement(sessionName ? sessionName : (this.sessionName as string), eventName as string) PercyLogger.error(`Error while trying to auto capture Percy screenshot ${err}`) } } @@ -60,7 +72,68 @@ class _PercyHandler { (this._browser as BrowserAsync).percyCaptureMap = new PercyCaptureMap() } - async browserCommand (args: BeforeCommandArgs & AfterCommandArgs) { + deferCapture(sessionName: string, eventName: string | null) { + /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ + this._percyScreenshotCounter += 1 + this.percyDeferredScreenshots.push({ sessionName, eventName }) + } + + isDOMChangingCommand(args: BeforeCommandArgs): boolean { + if ((args.method as string) === 'POST') { + if (PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS.includes((args.endpoint as string))) { + return true + } else if ((args.endpoint as string).includes('/session/:sessionId/element') && (args.endpoint as string).includes('click')) { + /* click element */ + return true + } else if ((args.endpoint as string).includes('/session/:sessionId/element') && (args.endpoint as string).includes('clear')) { + /* clear element */ + return true + } else if ((args.endpoint as string).includes('/session/:sessionId/execute') && args.body?.script) { + /* execute script sync / async */ + return true + } else if ((args.endpoint as string).includes('/session/:sessionId/touch')) { + /* Touch action for Appium */ + return true + } + } else if ((args.method as string) === 'DELETE' && (args.endpoint as string) === '/session/:sessionId') { + return true + } + return false + } + + async cleanupDeferredScreenshots() { + this.isPercyCleanupProcessingUnderway = true + for await (const entry of this.percyDeferredScreenshots) { + await this.percyAutoCapture(entry.eventName, entry.sessionName) + } + this.percyDeferredScreenshots = [] + this.isPercyCleanupProcessingUnderway = false + } + + async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) + } + + async browserBeforeCommand (args: BeforeCommandArgs) { + try { + if (this.isDOMChangingCommand(args)) { + do { + await this.sleep(1000) + } while (this.percyScreenshotInterval) + this.percyScreenshotInterval = setInterval(async () => { + if (!this.isPercyCleanupProcessingUnderway) { + clearInterval(this.percyScreenshotInterval) + await this.cleanupDeferredScreenshots() + this.percyScreenshotInterval = null + } + }, 1000) + } + } catch (err: any) { + PercyLogger.error(`Error while trying to cleanup deferred screenshots ${err}`) + } + } + + async browserAfterCommand (args: BeforeCommandArgs & AfterCommandArgs) { try { if (args.endpoint && this._percyAutoCaptureMode) { let eventName = null @@ -72,8 +145,12 @@ class _PercyHandler { if (args.body && args.body.actions && Array.isArray(args.body.actions) && args.body.actions.length && args.body.actions[0].type === 'key') { eventName = 'keys' } + } else if ((args.endpoint as string).includes('/session/:sessionId/element') && (args.endpoint as string).includes('value') && ['auto'].includes(this._percyAutoCaptureMode as string)) { + eventName = 'keys' + } + if (eventName) { + this.deferCapture(this.sessionName as string, eventName) } - await this.percyAutoCapture(eventName) } } catch (err: any) { PercyLogger.error(`Error while trying to calculate auto capture parameters ${err}`) @@ -82,13 +159,13 @@ class _PercyHandler { async afterTest () { if (this._percyAutoCaptureMode && this._percyAutoCaptureMode === 'testcase') { - await this.percyAutoCapture('testcase') + await this.percyAutoCapture('testcase', null) } } async afterScenario () { if (this._percyAutoCaptureMode && this._percyAutoCaptureMode === 'testcase') { - await this.percyAutoCapture('testcase') + await this.percyAutoCapture('testcase', null) } } } diff --git a/packages/wdio-browserstack-service/src/Percy/Percy.ts b/packages/wdio-browserstack-service/src/Percy/Percy.ts index deb1ec7f916..6a8936378f7 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy.ts @@ -16,7 +16,7 @@ const logDir = 'logs' class Percy { #logfile: string = path.join(logDir, 'percy.log') - #address: string = process.env.PERCY_SERVER_ADDRESS || 'http://localhost:5338' + #address: string = process.env.PERCY_SERVER_ADDRESS || 'http://127.0.0.1:5338' #binaryPath: string | any = null #options: BrowserstackConfig & Options.Testrunner diff --git a/packages/wdio-browserstack-service/src/Percy/PercySDK.ts b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts index dd9fd60573e..a2511589375 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercySDK.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts @@ -8,9 +8,7 @@ const tryRequire = function (pkg: string, fallback: any) { import type { Browser, MultiRemoteBrowser } from 'webdriverio' -// const percySnapshot = tryRequire('@percy/webdriverio', null); const percySnapshot = tryRequire('@percy/selenium-webdriver', null) -const { percyScreenshot } = tryRequire('@percy/selenium-webdriver', {}) const percyAppScreenshot = tryRequire('@percy/appium-app', {}) import { PercyLogger } from './PercyLogger' @@ -32,8 +30,8 @@ export const snapshot = snapshotHandler let screenshotHandler = async (...args: any[]) => { PercyLogger.error('Unsupported driver for percy') } -if (percyScreenshot) { - screenshotHandler = percyScreenshot +if (percySnapshot && percySnapshot.percyScreenshot) { + screenshotHandler = percySnapshot.percyScreenshot } export const screenshot = screenshotHandler diff --git a/packages/wdio-browserstack-service/src/constants.ts b/packages/wdio-browserstack-service/src/constants.ts index 8d3b309e8fe..2df7a53fab0 100644 --- a/packages/wdio-browserstack-service/src/constants.ts +++ b/packages/wdio-browserstack-service/src/constants.ts @@ -39,3 +39,13 @@ export const ACCESSIBILITY_API_URL = 'https://accessibility.browserstack.com/api export const NOT_ALLOWED_KEYS_IN_CAPS = ['includeTagsInTestingScope', 'excludeTagsInTestingScope'] export const PERCY_LOGS_FILE = 'logs/percy.log' + +export const PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS = [ + '/session/:sessionId/url', + '/session/:sessionId/forward', + '/session/:sessionId/back', + '/session/:sessionId/refresh', + '/session/:sessionId/screenshot', + '/session/:sessionId/actions', + '/session/:sessionId/appium/device/shake' +] diff --git a/packages/wdio-browserstack-service/src/launcher.ts b/packages/wdio-browserstack-service/src/launcher.ts index 1fa5573f346..83a1cc0b986 100644 --- a/packages/wdio-browserstack-service/src/launcher.ts +++ b/packages/wdio-browserstack-service/src/launcher.ts @@ -155,7 +155,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst if (this._options.percy && this._percyBestPlatformCaps) { const isThisBestPercyPlatform = ObjectsAreEqual(caps, this._percyBestPlatformCaps) if (isThisBestPercyPlatform) { - process.env.BEST_PLATFORM_CID = cid + '' + process.env.BEST_PLATFORM_CID = cid } caps['wdio:cid'] = cid } diff --git a/packages/wdio-browserstack-service/src/service.ts b/packages/wdio-browserstack-service/src/service.ts index e2aa6550c3a..f156c2e37d3 100644 --- a/packages/wdio-browserstack-service/src/service.ts +++ b/packages/wdio-browserstack-service/src/service.ts @@ -137,17 +137,24 @@ export default class BrowserstackService implements Services.ServiceInstance { patchConsoleLogs() this._insightsHandler = new InsightsHandler(this._browser, this._browser.capabilities as Capabilities.Capabilities, this._isAppAutomate(), this._browser.sessionId as string, this._config.framework) await this._insightsHandler.before() - - /** - * register command event - */ - this._browser.on('command', async (command) => await this._insightsHandler?.browserCommand( - 'client:beforeCommand', - Object.assign(command, { sessionId: this._browser?.sessionId }), - this._currentTest - )) } + /** + * register command event + */ + this._browser.on('command', async (command) => { + if (this._observability) { + await this._insightsHandler?.browserCommand( + 'client:beforeCommand', + Object.assign(command, { sessionId: this._browser?.sessionId }), + this._currentTest + ) + } + await this._percyHandler?.browserBeforeCommand( + Object.assign(command, { sessionId: this._browser?.sessionId }) + ) + }) + /** * register result event */ @@ -159,7 +166,7 @@ export default class BrowserstackService implements Services.ServiceInstance { this._currentTest ) } - this._percyHandler?.browserCommand( + this._percyHandler?.browserAfterCommand( Object.assign(result, { sessionId: this._browser?.sessionId }), ) }) diff --git a/packages/wdio-browserstack-service/src/util.ts b/packages/wdio-browserstack-service/src/util.ts index 734e30ad383..e91d895bc2d 100644 --- a/packages/wdio-browserstack-service/src/util.ts +++ b/packages/wdio-browserstack-service/src/util.ts @@ -1114,7 +1114,7 @@ export function isTrue(value?: any) { } export const isObject = (object: any) => { - return object !== null && typeof object === 'object' + return object !== null && typeof object === 'object' && !Array.isArray(object) } export const ObjectsAreEqual = (object1: any, object2: any) => { diff --git a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts index 111960ea5c0..c48cc2bd23c 100644 --- a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts +++ b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts @@ -3,14 +3,15 @@ import got from 'got' import logger from '@wdio/logger' -import PercyHandler from '../src/percy/Percy-Handler' -// import * as PercyCaptureMapExport from '../src/Percy/PercyCaptureMap' +import PercyHandler from '../src/Percy/Percy-Handler' import PercyCaptureMap from '../src/Percy/PercyCaptureMap' import * as PercySDK from '../src/Percy/PercySDK' import type { Capabilities } from '@wdio/types' import { Browser, MultiRemoteBrowser } from 'webdriverio' import * as PercyLogger from '../src/Percy/PercyLogger' +import type { BeforeCommandArgs, AfterCommandArgs } from '@wdio/reporter' + const log = logger('test') let percyHandler: PercyHandler let browser: Browser<'async'> | MultiRemoteBrowser<'async'> @@ -125,12 +126,12 @@ describe('browserCommand', () => { beforeEach(() => { percyHandler = new PercyHandler('auto', browser, caps, false, 'framework') percyHandler.before() - percyAutoCaptureSpy = jest.spyOn(PercyHandler.prototype, 'percyAutoCapture') + percyAutoCaptureSpy = jest.spyOn(PercyHandler.prototype, 'deferCapture') }) it('should not call percyAutoCapture if no browser endpoint', async () => { const args = {} - await percyHandler.browserCommand(args) + await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) expect(percyAutoCaptureSpy).not.toBeCalled() }) @@ -143,7 +144,7 @@ describe('browserCommand', () => { }] } } - await percyHandler.browserCommand(args) + await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) expect(percyAutoCaptureSpy).toBeCalledTimes(1) }) @@ -151,7 +152,7 @@ describe('browserCommand', () => { const args = { endpoint: 'click' } - await percyHandler.browserCommand(args) + await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) expect(percyAutoCaptureSpy).toBeCalledTimes(1) }) @@ -159,7 +160,7 @@ describe('browserCommand', () => { const args = { endpoint: 'screenshot' } - await percyHandler.browserCommand(args) + await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) expect(percyAutoCaptureSpy).toBeCalledTimes(1) }) @@ -204,11 +205,6 @@ describe('afterTest', () => { percyAutoCaptureSpy = jest.spyOn(PercyHandler.prototype, 'percyAutoCapture') }) - // it('should not call percyAutoCapture', async () => { - // await percyHandler.afterTest() - // expect(percyAutoCaptureSpy).not.toBeCalled() - // }) - it('should call percyAutoCapture', async () => { percyHandler['_percyAutoCaptureMode'] = 'testcase' await percyHandler.afterTest() From d648b981f9fc027861516386fdb8ee97947f0658 Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 8 Jan 2024 10:04:51 +0530 Subject: [PATCH 11/38] fixes related to types, return early and private fields --- .../src/@types/bstack-service-types.d.ts | 3 +- .../src/Percy/Percy-Handler.ts | 62 +++++++++---------- .../src/Percy/PercyBinary.ts | 3 +- .../src/constants.ts | 2 + .../wdio-browserstack-service/src/launcher.ts | 60 +++++++++--------- .../wdio-browserstack-service/src/service.ts | 2 +- 6 files changed, 69 insertions(+), 63 deletions(-) diff --git a/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts b/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts index d9f7e2d1b55..afb0075ed25 100644 --- a/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts +++ b/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts @@ -1,7 +1,8 @@ import type { Browser } from 'webdriverio' +import PercyCaptureMap from '../Percy/PercyCaptureMap.js' declare interface BrowserAsync extends Browser<'async'> { getAccessibilityResultsSummary: () => Promise<{ [key: string]: any; }>, getAccessibilityResults: () => Promise>, - percyCaptureMap: any + percyCaptureMap: PercyCaptureMap } diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index e5938daef90..659e6b7b674 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -11,16 +11,16 @@ import * as PercySDK from './PercySDK' import { PercyLogger } from './PercyLogger' import { BrowserAsync } from 'src/@types/bstack-service-types' -import { PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS } from '../constants' +import { PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES } from '../constants.js' class _PercyHandler { - private _testMetadata: { [key: string]: any } = {} - private sessionName?: string - private _isAppAutomate?: boolean - private isPercyCleanupProcessingUnderway?: boolean = false - public _percyScreenshotCounter: any = 0 - private percyDeferredScreenshots: any = [] - private percyScreenshotInterval: any = null + #_testMetadata: { [key: string]: any } = {} + #sessionName?: string + #_isAppAutomate?: boolean + #isPercyCleanupProcessingUnderway?: boolean = false + #_percyScreenshotCounter: any = 0 + #percyDeferredScreenshots: any = [] + #percyScreenshotInterval: any = null constructor ( private _percyAutoCaptureMode: string | undefined, @@ -29,20 +29,20 @@ class _PercyHandler { isAppAutomate?: boolean, private _framework?: string ) { - this._isAppAutomate = isAppAutomate - if (!_percyAutoCaptureMode || !['click', 'auto', 'screenshot', 'manual', 'testcase'].includes(_percyAutoCaptureMode as string)) { + this.#_isAppAutomate = isAppAutomate + if (_percyAutoCaptureMode && !_percyAutoCaptureMode || !CAPTURE_MODES.includes(_percyAutoCaptureMode as string)) { this._percyAutoCaptureMode = 'auto' } } _setSessionName(name: string) { - this.sessionName = name + this.#sessionName = name } async teardown () { await new Promise((resolve) => { setInterval(() => { - if (this._percyScreenshotCounter === 0) { + if (this.#_percyScreenshotCounter === 0) { resolve() } }, 1000) @@ -54,16 +54,16 @@ class _PercyHandler { if (eventName) { if (!sessionName) { /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ - this._percyScreenshotCounter += 1 + this.#_percyScreenshotCounter += 1 } - ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment(sessionName ? sessionName : (this.sessionName as string), eventName) - await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName(sessionName ? sessionName : (this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName( sessionName ? sessionName : (this.sessionName as string), eventName))) - this._percyScreenshotCounter -= 1 + ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment(sessionName ? sessionName : (this.#sessionName as string), eventName) + await (this.#_isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName(sessionName ? sessionName : (this.#sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName( sessionName ? sessionName : (this.#sessionName as string), eventName))) + this.#_percyScreenshotCounter -= 1 } } catch (err: any) { - this._percyScreenshotCounter -= 1; - ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).decrement(sessionName ? sessionName : (this.sessionName as string), eventName as string) + this.#_percyScreenshotCounter -= 1; + ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).decrement(sessionName ? sessionName : (this.#sessionName as string), eventName as string) PercyLogger.error(`Error while trying to auto capture Percy screenshot ${err}`) } } @@ -74,8 +74,8 @@ class _PercyHandler { deferCapture(sessionName: string, eventName: string | null) { /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ - this._percyScreenshotCounter += 1 - this.percyDeferredScreenshots.push({ sessionName, eventName }) + this.#_percyScreenshotCounter += 1 + this.#percyDeferredScreenshots.push({ sessionName, eventName }) } isDOMChangingCommand(args: BeforeCommandArgs): boolean { @@ -102,12 +102,12 @@ class _PercyHandler { } async cleanupDeferredScreenshots() { - this.isPercyCleanupProcessingUnderway = true - for await (const entry of this.percyDeferredScreenshots) { - await this.percyAutoCapture(entry.eventName, entry.sessionName) + this.#isPercyCleanupProcessingUnderway = true + for await (const entry of this.#percyDeferredScreenshots) { + await this.percyAutoCapture(entry.eventName, entry.#sessionName) } - this.percyDeferredScreenshots = [] - this.isPercyCleanupProcessingUnderway = false + this.#percyDeferredScreenshots = [] + this.#isPercyCleanupProcessingUnderway = false } async sleep(ms: number) { @@ -119,12 +119,12 @@ class _PercyHandler { if (this.isDOMChangingCommand(args)) { do { await this.sleep(1000) - } while (this.percyScreenshotInterval) - this.percyScreenshotInterval = setInterval(async () => { - if (!this.isPercyCleanupProcessingUnderway) { - clearInterval(this.percyScreenshotInterval) + } while (this.#percyScreenshotInterval) + this.#percyScreenshotInterval = setInterval(async () => { + if (!this.#isPercyCleanupProcessingUnderway) { + clearInterval(this.#percyScreenshotInterval) await this.cleanupDeferredScreenshots() - this.percyScreenshotInterval = null + this.#percyScreenshotInterval = null } }, 1000) } @@ -149,7 +149,7 @@ class _PercyHandler { eventName = 'keys' } if (eventName) { - this.deferCapture(this.sessionName as string, eventName) + this.deferCapture(this.#sessionName as string, eventName) } } } catch (err: any) { diff --git a/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts index 2079061eff4..10ea77cd17b 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts @@ -8,6 +8,7 @@ import path from 'node:path' import os from 'node:os' import { spawn } from 'node:child_process' import { PercyLogger } from './PercyLogger' +import type { Options } from '@wdio/types' class PercyBinary { #hostOS = process.platform @@ -97,7 +98,7 @@ class PercyBinary { throw new Error('Error trying to download percy binary') } - async getBinaryPath(conf: any): Promise { + async getBinaryPath(conf: Options.Testrunner): Promise { const destParentDir = this.#getAvailableDirs() const binaryPath = path.join(destParentDir, this.#binaryName) if (this.#checkPath(binaryPath, fs.X_OK)) { diff --git a/packages/wdio-browserstack-service/src/constants.ts b/packages/wdio-browserstack-service/src/constants.ts index 2df7a53fab0..d5e4ebf80aa 100644 --- a/packages/wdio-browserstack-service/src/constants.ts +++ b/packages/wdio-browserstack-service/src/constants.ts @@ -49,3 +49,5 @@ export const PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS = [ '/session/:sessionId/actions', '/session/:sessionId/appium/device/shake' ] + +export const CAPTURE_MODES = ['click', 'auto', 'screenshot', 'manual', 'testcase'] diff --git a/packages/wdio-browserstack-service/src/launcher.ts b/packages/wdio-browserstack-service/src/launcher.ts index c91ab76aaa5..e4104430963 100644 --- a/packages/wdio-browserstack-service/src/launcher.ts +++ b/packages/wdio-browserstack-service/src/launcher.ts @@ -39,7 +39,7 @@ const log = logger('@wdio/browserstack-service') type BrowserstackLocal = BrowserstackLocalLauncher.Local & { pid?: number; - stop(callback: (err?: any) => void): void; + stop(callback: (err?: Error) => void): void; } export default class BrowserstackLauncherService implements Services.ServiceInstance { @@ -51,7 +51,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst private _accessibilityAutomation?: boolean public _testOpsBuildStopped?: boolean private _percy?: Percy - private _percyBestPlatformCaps?: any + private _percyBestPlatformCaps?: Capabilities.DesiredCapabilities constructor ( private _options: BrowserstackConfig & Options.Testrunner, @@ -153,7 +153,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } /* eslint-disable @typescript-eslint/no-unused-vars */ - async onWorkerStart (cid: any, caps: any, specs: any, args: any, execArgv: any) { + async onWorkerStart (cid: any, caps: any) { try { if (this._options.percy && this._percyBestPlatformCaps) { const isThisBestPercyPlatform = ObjectsAreEqual(caps, this._percyBestPlatformCaps) @@ -162,7 +162,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } caps['wdio:cid'] = cid } - } catch (err: any) { + } catch (err: unknown) { PercyLogger.error(`Error while setting best platform for Percy snapshot at worker start ${err}`) } } @@ -277,7 +277,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst await this.setupPercy(this._options, this._config, { projectName: this._projectName }) - } catch (err: any) { + } catch (err: unknown) { PercyLogger.error(`Error while setting up Percy ${err}`) } } @@ -397,35 +397,37 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } async setupPercy(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig) { - if (!this._percy || !this._percy.isRunning()) { - try { - this._percy = await startPercy(options, config, bsConfig) - if (!this._percy) { - throw new Error('Could not start percy, check percy logs for info.') - } - PercyLogger.info('Percy started successfully') - let signal = 0 - const handler = async () => { - signal++ - signal === 1 && await this.stopPercy() - } - process.on('beforeExit', handler) - process.on('SIGINT', handler) - process.on('SIGTERM', handler) - } catch (err: any) { - PercyLogger.debug(`Error in percy setup ${err}`) + if (this._percy?.isRunning()) { + return + } + try { + this._percy = await startPercy(options, config, bsConfig) + if (!this._percy) { + throw new Error('Could not start percy, check percy logs for info.') } + PercyLogger.info('Percy started successfully') + let signal = 0 + const handler = async () => { + signal++ + signal === 1 && await this.stopPercy() + } + process.on('beforeExit', handler) + process.on('SIGINT', handler) + process.on('SIGTERM', handler) + } catch (err: any) { + PercyLogger.debug(`Error in percy setup ${err}`) } } async stopPercy() { - if (this._percy && this._percy.isRunning()) { - try { - await stopPercy(this._percy) - PercyLogger.info('Percy stopped') - } catch (err) { - PercyLogger.error('Error occured while stopping percy : ' + err) - } + if (!this._percy || !this._percy.isRunning()) { + return + } + try { + await stopPercy(this._percy) + PercyLogger.info('Percy stopped') + } catch (err) { + PercyLogger.error('Error occured while stopping percy : ' + err) } } diff --git a/packages/wdio-browserstack-service/src/service.ts b/packages/wdio-browserstack-service/src/service.ts index f156c2e37d3..f7303b3bdf5 100644 --- a/packages/wdio-browserstack-service/src/service.ts +++ b/packages/wdio-browserstack-service/src/service.ts @@ -74,7 +74,7 @@ export default class BrowserstackService implements Services.ServiceInstance { this._failureStatuses.push('pending') } - if (((_caps as any)['wdio:cid'] as string) === process.env.BEST_PLATFORM_CID) { + if (process.env.WDIO_WORKER_ID === process.env.BEST_PLATFORM_CID) { process.env.PERCY_SNAPSHOT = 'true' } } From 6f7e8386c899086265867abffa671052f81a32e7 Mon Sep 17 00:00:00 2001 From: riya Date: Tue, 9 Jan 2024 22:21:15 +0530 Subject: [PATCH 12/38] fixes --- .../src/Percy/Percy-Handler.ts | 62 +++++++++---------- .../wdio-browserstack-service/src/util.ts | 2 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index 659e6b7b674..a5ed188516f 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -11,16 +11,16 @@ import * as PercySDK from './PercySDK' import { PercyLogger } from './PercyLogger' import { BrowserAsync } from 'src/@types/bstack-service-types' -import { PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES } from '../constants.js' +import { PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES } from '../constants' class _PercyHandler { - #_testMetadata: { [key: string]: any } = {} - #sessionName?: string - #_isAppAutomate?: boolean - #isPercyCleanupProcessingUnderway?: boolean = false - #_percyScreenshotCounter: any = 0 - #percyDeferredScreenshots: any = [] - #percyScreenshotInterval: any = null + private _testMetadata: { [key: string]: any } = {} + private sessionName?: string + private _isAppAutomate?: boolean + private isPercyCleanupProcessingUnderway?: boolean = false + public _percyScreenshotCounter: any = 0 + private percyDeferredScreenshots: any = [] + private percyScreenshotInterval: any = null constructor ( private _percyAutoCaptureMode: string | undefined, @@ -29,20 +29,20 @@ class _PercyHandler { isAppAutomate?: boolean, private _framework?: string ) { - this.#_isAppAutomate = isAppAutomate - if (_percyAutoCaptureMode && !_percyAutoCaptureMode || !CAPTURE_MODES.includes(_percyAutoCaptureMode as string)) { + this._isAppAutomate = isAppAutomate + if (!_percyAutoCaptureMode || !CAPTURE_MODES.includes(_percyAutoCaptureMode as string)) { this._percyAutoCaptureMode = 'auto' } } _setSessionName(name: string) { - this.#sessionName = name + this.sessionName = name } async teardown () { await new Promise((resolve) => { setInterval(() => { - if (this.#_percyScreenshotCounter === 0) { + if (this._percyScreenshotCounter === 0) { resolve() } }, 1000) @@ -54,16 +54,16 @@ class _PercyHandler { if (eventName) { if (!sessionName) { /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ - this.#_percyScreenshotCounter += 1 + this._percyScreenshotCounter += 1 } - ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment(sessionName ? sessionName : (this.#sessionName as string), eventName) - await (this.#_isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName(sessionName ? sessionName : (this.#sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName( sessionName ? sessionName : (this.#sessionName as string), eventName))) - this.#_percyScreenshotCounter -= 1 + ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment(sessionName ? sessionName : (this.sessionName as string), eventName) + await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName(sessionName ? sessionName : (this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName( sessionName ? sessionName : (this.sessionName as string), eventName))) + this._percyScreenshotCounter -= 1 } } catch (err: any) { - this.#_percyScreenshotCounter -= 1; - ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).decrement(sessionName ? sessionName : (this.#sessionName as string), eventName as string) + this._percyScreenshotCounter -= 1; + ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).decrement(sessionName ? sessionName : (this.sessionName as string), eventName as string) PercyLogger.error(`Error while trying to auto capture Percy screenshot ${err}`) } } @@ -74,8 +74,8 @@ class _PercyHandler { deferCapture(sessionName: string, eventName: string | null) { /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ - this.#_percyScreenshotCounter += 1 - this.#percyDeferredScreenshots.push({ sessionName, eventName }) + this._percyScreenshotCounter += 1 + this.percyDeferredScreenshots.push({ sessionName, eventName }) } isDOMChangingCommand(args: BeforeCommandArgs): boolean { @@ -102,12 +102,12 @@ class _PercyHandler { } async cleanupDeferredScreenshots() { - this.#isPercyCleanupProcessingUnderway = true - for await (const entry of this.#percyDeferredScreenshots) { - await this.percyAutoCapture(entry.eventName, entry.#sessionName) + this.isPercyCleanupProcessingUnderway = true + for await (const entry of this.percyDeferredScreenshots) { + await this.percyAutoCapture(entry.eventName, entry.sessionName) } - this.#percyDeferredScreenshots = [] - this.#isPercyCleanupProcessingUnderway = false + this.percyDeferredScreenshots = [] + this.isPercyCleanupProcessingUnderway = false } async sleep(ms: number) { @@ -119,12 +119,12 @@ class _PercyHandler { if (this.isDOMChangingCommand(args)) { do { await this.sleep(1000) - } while (this.#percyScreenshotInterval) - this.#percyScreenshotInterval = setInterval(async () => { - if (!this.#isPercyCleanupProcessingUnderway) { - clearInterval(this.#percyScreenshotInterval) + } while (this.percyScreenshotInterval) + this.percyScreenshotInterval = setInterval(async () => { + if (!this.isPercyCleanupProcessingUnderway) { + clearInterval(this.percyScreenshotInterval) await this.cleanupDeferredScreenshots() - this.#percyScreenshotInterval = null + this.percyScreenshotInterval = null } }, 1000) } @@ -149,7 +149,7 @@ class _PercyHandler { eventName = 'keys' } if (eventName) { - this.deferCapture(this.#sessionName as string, eventName) + this.deferCapture(this.sessionName as string, eventName) } } } catch (err: any) { diff --git a/packages/wdio-browserstack-service/src/util.ts b/packages/wdio-browserstack-service/src/util.ts index e91d895bc2d..1b6d5bd3ddd 100644 --- a/packages/wdio-browserstack-service/src/util.ts +++ b/packages/wdio-browserstack-service/src/util.ts @@ -546,7 +546,7 @@ export async function nodeRequest(requestType: Method, apiEndpoint: string, opti } throw error } else { - log.error(`Failed to fire api request due to ${error} - ${error.stack}`) + log.debug(`Failed to fire api request due to ${error} - ${error.stack}`) throw error } } From b5a2b16b5851779102a7f223b85e6a85fdfa53ac Mon Sep 17 00:00:00 2001 From: riya Date: Wed, 10 Jan 2024 04:19:02 +0530 Subject: [PATCH 13/38] resolved dependency check --- packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index a5ed188516f..38b99822fef 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -9,7 +9,7 @@ import PercyCaptureMap from './PercyCaptureMap' import * as PercySDK from './PercySDK' import { PercyLogger } from './PercyLogger' -import { BrowserAsync } from 'src/@types/bstack-service-types' +import { BrowserAsync } from '../@types/bstack-service-types' import { PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES } from '../constants' From de8c057a98860864f6cad4b467cd0626872a9f3e Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 15 Jan 2024 18:05:23 +0530 Subject: [PATCH 14/38] review changes --- package-lock.json | 5 +- .../wdio-browserstack-service/package.json | 1 - .../src/@types/bstack-service-types.d.ts | 3 - .../src/Percy/Percy-Handler.ts | 43 +++---- .../src/Percy/Percy.ts | 11 +- .../src/Percy/PercyBinary.ts | 106 +++++------------- .../src/Percy/PercyHelper.ts | 5 +- .../src/Percy/PercyLogger.ts | 2 +- .../wdio-browserstack-service/src/launcher.ts | 7 +- 9 files changed, 63 insertions(+), 120 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41f1329b710..7ff133da6e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24915,8 +24915,7 @@ "dev": true }, "dox": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/dox/-/dox-0.9.1.tgz", + "version": "https://registry.npmjs.org/dox/-/dox-0.9.1.tgz", "integrity": "sha512-3bC8QeBn1xYWU628qfW7jlA0ssd7PL/x3ndYdT3tq52arRKFHW5zpVHGgkZPahBCZHU60O+TiJossR+RZZW15w==", "dev": true, "requires": { @@ -29562,7 +29561,7 @@ "async": ">= 0.1.18", "coffee-script": ">= 1.3.3", "commander": ">= 0.6.0", - "dox": "0.9.1", + "dox": "https://github.com/visionmedia/dox/tarball/master", "ejs": ">= 0.7.2 <2.0.0", "iced-coffee-script": ">= 1.3.3d", "underscore": ">= 1.3.3" diff --git a/packages/wdio-browserstack-service/package.json b/packages/wdio-browserstack-service/package.json index 469e29f49a2..4bf3e1d7c1d 100644 --- a/packages/wdio-browserstack-service/package.json +++ b/packages/wdio-browserstack-service/package.json @@ -29,7 +29,6 @@ "@wdio/types": "7.33.0", "browserstack-local": "^1.4.5", "csv-writer": "^1.6.0", - "follow-redirects": "^1.15.3", "form-data": "^4.0.0", "git-repo-info": "^2.1.1", "gitconfiglocal": "^2.1.0", diff --git a/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts b/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts index afb0075ed25..4c7d153a2e9 100644 --- a/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts +++ b/packages/wdio-browserstack-service/src/@types/bstack-service-types.d.ts @@ -1,8 +1,5 @@ import type { Browser } from 'webdriverio' -import PercyCaptureMap from '../Percy/PercyCaptureMap.js' - declare interface BrowserAsync extends Browser<'async'> { getAccessibilityResultsSummary: () => Promise<{ [key: string]: any; }>, getAccessibilityResults: () => Promise>, - percyCaptureMap: PercyCaptureMap } diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index 38b99822fef..b414edd456e 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -15,12 +15,13 @@ import { PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES } from '../constan class _PercyHandler { private _testMetadata: { [key: string]: any } = {} - private sessionName?: string + private _sessionName?: string private _isAppAutomate?: boolean - private isPercyCleanupProcessingUnderway?: boolean = false - public _percyScreenshotCounter: any = 0 - private percyDeferredScreenshots: any = [] - private percyScreenshotInterval: any = null + private _isPercyCleanupProcessingUnderway?: boolean = false + private _percyScreenshotCounter: any = 0 + private _percyDeferredScreenshots: any = [] + private _percyScreenshotInterval: any = null + private _percyCaptureMap?: PercyCaptureMap constructor ( private _percyAutoCaptureMode: string | undefined, @@ -36,7 +37,7 @@ class _PercyHandler { } _setSessionName(name: string) { - this.sessionName = name + this._sessionName = name } async teardown () { @@ -57,25 +58,25 @@ class _PercyHandler { this._percyScreenshotCounter += 1 } - ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).increment(sessionName ? sessionName : (this.sessionName as string), eventName) - await (this._isAppAutomate ? PercySDK.screenshotApp(((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName(sessionName ? sessionName : (this.sessionName as string), eventName)) : PercySDK.screenshot(this._browser, ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).getName( sessionName ? sessionName : (this.sessionName as string), eventName))) + this._percyCaptureMap?.increment(sessionName ? sessionName : (this._sessionName as string), eventName) + await (this._isAppAutomate ? PercySDK.screenshotApp(this._percyCaptureMap?.getName(sessionName ? sessionName : (this._sessionName as string), eventName)) : PercySDK.screenshot(this._browser, this._percyCaptureMap?.getName( sessionName ? sessionName : (this._sessionName as string), eventName))) this._percyScreenshotCounter -= 1 } } catch (err: any) { this._percyScreenshotCounter -= 1; - ((this._browser as BrowserAsync).percyCaptureMap as PercyCaptureMap).decrement(sessionName ? sessionName : (this.sessionName as string), eventName as string) + this._percyCaptureMap?.decrement(sessionName ? sessionName : (this._sessionName as string), eventName as string) PercyLogger.error(`Error while trying to auto capture Percy screenshot ${err}`) } } async before () { - (this._browser as BrowserAsync).percyCaptureMap = new PercyCaptureMap() + this._percyCaptureMap = new PercyCaptureMap() } deferCapture(sessionName: string, eventName: string | null) { /* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */ this._percyScreenshotCounter += 1 - this.percyDeferredScreenshots.push({ sessionName, eventName }) + this._percyDeferredScreenshots.push({ sessionName, eventName }) } isDOMChangingCommand(args: BeforeCommandArgs): boolean { @@ -102,12 +103,12 @@ class _PercyHandler { } async cleanupDeferredScreenshots() { - this.isPercyCleanupProcessingUnderway = true - for await (const entry of this.percyDeferredScreenshots) { + this._isPercyCleanupProcessingUnderway = true + for await (const entry of this._percyDeferredScreenshots) { await this.percyAutoCapture(entry.eventName, entry.sessionName) } - this.percyDeferredScreenshots = [] - this.isPercyCleanupProcessingUnderway = false + this._percyDeferredScreenshots = [] + this._isPercyCleanupProcessingUnderway = false } async sleep(ms: number) { @@ -119,12 +120,12 @@ class _PercyHandler { if (this.isDOMChangingCommand(args)) { do { await this.sleep(1000) - } while (this.percyScreenshotInterval) - this.percyScreenshotInterval = setInterval(async () => { - if (!this.isPercyCleanupProcessingUnderway) { - clearInterval(this.percyScreenshotInterval) + } while (this._percyScreenshotInterval) + this._percyScreenshotInterval = setInterval(async () => { + if (!this._isPercyCleanupProcessingUnderway) { + clearInterval(this._percyScreenshotInterval) await this.cleanupDeferredScreenshots() - this.percyScreenshotInterval = null + this._percyScreenshotInterval = null } }, 1000) } @@ -149,7 +150,7 @@ class _PercyHandler { eventName = 'keys' } if (eventName) { - this.deferCapture(this.sessionName as string, eventName) + this.deferCapture(this._sessionName as string, eventName) } } } catch (err: any) { diff --git a/packages/wdio-browserstack-service/src/Percy/Percy.ts b/packages/wdio-browserstack-service/src/Percy/Percy.ts index 6a8936378f7..80beeff70f3 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy.ts @@ -22,7 +22,7 @@ class Percy { #options: BrowserstackConfig & Options.Testrunner #config: Options.Testrunner #proc: any = null - #isApp: boolean = false + #isApp: boolean #projectName: string | undefined = undefined isProcessRunning = false @@ -30,9 +30,7 @@ class Percy { constructor(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig) { this.#options = options this.#config = config - if (options.app) { - this.#isApp = true - } + this.#isApp = Boolean(options.app) this.#projectName = bsConfig.projectName } @@ -78,7 +76,7 @@ class Percy { this.#proc = spawn( binaryPath, commandArgs, - { env: Object.assign(process.env, { PERCY_TOKEN: token }) } + { env: { ...process.env, PERCY_TOKEN: token } } ) this.#proc.stdout.pipe(logStream) @@ -86,8 +84,7 @@ class Percy { this.isProcessRunning = true const that = this - /* eslint-disable @typescript-eslint/no-unused-vars */ - this.#proc.on('close', function (code: any) { + this.#proc.on('close', function () { that.isProcessRunning = false }) diff --git a/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts index 10ea77cd17b..1ada7b21b0c 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts @@ -1,11 +1,10 @@ -import url from 'node:url' import yauzl from 'yauzl' const fs = require('node:fs') -import { https } from 'follow-redirects' - +import got from 'got' import path from 'node:path' import os from 'node:os' +import fsp from 'node:fs/promises' import { spawn } from 'node:child_process' import { PercyLogger } from './PercyLogger' import type { Options } from '@wdio/types' @@ -16,7 +15,7 @@ class PercyBinary { #binaryName = 'percy' #orderedPaths = [ - path.join(this.#homedir(), '.browserstack'), + path.join(os.homedir(), '.browserstack'), process.cwd(), os.tmpdir() ] @@ -33,65 +32,28 @@ class PercyBinary { } } - #homedir(): any { - if (typeof os.homedir === 'function') { - return os.homedir() - } - - const env = process.env - const home = env.HOME - const user = env.LOGNAME || env.USER || env.LNAME || env.USERNAME - - if (process.platform === 'win32') { - return env.USERPROFILE || (env.HOMEDRIVE || 'null') + env.HOMEPATH || home || null - } - - if (process.platform === 'darwin') { - return home || (user ? '/Users/' + user : null) - } - - if (process.platform === 'linux') { - return home || (process.getuid && process.getuid() === 0 ? '/root' : (user ? '/home/' + user : null)) - } - - return home || null - } - - #makePath(path: string) { - try { - if (!this.#checkPath(path)) { - fs.mkdirSync(path) - } + async #makePath(path: string) { + if (await this.#checkPath(path)) { return true - } catch { - return false } + return fsp.mkdir(path).then(() => true).catch(() => false) } - #checkPath(path: string, mode?: any) { - mode = mode || (fs.R_OK | fs.W_OK) + async #checkPath(path: string) { try { - fs.accessSync(path, mode) - return true - } catch (e) { - if (typeof fs.accessSync !== 'undefined') { - return false - } - - // node v0.10 - try { - fs.statSync(path) + const hasDir = await fsp.access(path).then(() => true, () => false) + if (hasDir) { return true - } catch (e) { - return false } + } catch (err) { + return false } } - #getAvailableDirs() { + async #getAvailableDirs() { for (let i = 0; i < this.#orderedPaths.length; i++) { const path = this.#orderedPaths[i] - if (this.#makePath(path)) { + if (await this.#makePath(path)) { return path } } @@ -99,9 +61,9 @@ class PercyBinary { } async getBinaryPath(conf: Options.Testrunner): Promise { - const destParentDir = this.#getAvailableDirs() + const destParentDir = await this.#getAvailableDirs() const binaryPath = path.join(destParentDir, this.#binaryName) - if (this.#checkPath(binaryPath, fs.X_OK)) { + if (await this.#checkPath(binaryPath)) { return binaryPath } const downloadedBinaryPath: string = await this.download(conf, destParentDir) @@ -131,9 +93,9 @@ class PercyBinary { }) } - download(conf: any, destParentDir: any): Promise { - if (!this.#checkPath(destParentDir)){ - fs.mkdirSync(destParentDir) + async download(conf: any, destParentDir: any): Promise { + if (!await this.#checkPath(destParentDir)){ + await fsp.mkdir(destParentDir) } const binaryName = this.#binaryName @@ -141,26 +103,20 @@ class PercyBinary { const binaryPath = path.join(destParentDir, binaryName) const downloadedFileStream = fs.createWriteStream(zipFilePath) - const options: any = url.parse(this.#httpPath) - return new Promise((resolve, reject) => { - https.get(options, function (response: any) { - response.pipe(downloadedFileStream) - response.on('error', function (err: any) { - PercyLogger.error('Got Error in percy binary download response : ' + err) - reject(err) - }) - downloadedFileStream.on('error', function (err: any) { - PercyLogger.error('Got Error while downloading percy binary file : ' + err) - reject(err) - }) - downloadedFileStream.on('close', function () { - yauzl.open(zipFilePath, { lazyEntries: true }, function (err: any, zipfile: any) { + const stream = got.extend({ followRedirect: true }).get(this.#httpPath, { isStream: true }) + stream.on('error', (err) => { + PercyLogger.error(`Got Error in percy binary download response: ${err}`) + }) + + stream.pipe(downloadedFileStream) + .on('finish', () => { + yauzl.open(zipFilePath, { lazyEntries: true }, function (err, zipfile) { if (err) { return reject(err) } zipfile.readEntry() - zipfile.on('entry', (entry: any) => { + zipfile.on('entry', (entry) => { if (/\/$/.test(entry.fileName)) { // Directory file names end with '/'. zipfile.readEntry() @@ -169,7 +125,7 @@ class PercyBinary { const writeStream = fs.createWriteStream( path.join(destParentDir, entry.fileName) ) - zipfile.openReadStream(entry, function (zipErr: any, readStream: any) { + zipfile.openReadStream(entry, function (zipErr, readStream) { if (zipErr) { reject(err) } @@ -186,7 +142,7 @@ class PercyBinary { } }) - zipfile.on('error', (zipErr: any) => { + zipfile.on('error', (zipErr) => { reject(zipErr) }) @@ -201,10 +157,6 @@ class PercyBinary { }) }) }) - }).on('error', function (err: any) { - PercyLogger.error('Got Error in percy binary downloading request : ' + err) - reject(err) - }) }) } } diff --git a/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts b/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts index fc9964c293a..0a40c40c1bc 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyHelper.ts @@ -55,8 +55,7 @@ export const getBestPlatformForPercySnapshot = (capabilities?: Capabilities.Remo Object.entries(capabilities as Capabilities.MultiRemoteCapabilities).forEach(([, caps]) => { let currBrowserName = (caps.capabilities as Capabilities.Capabilities).browserName if ((caps.capabilities as Capabilities.Capabilities)['bstack:options']) { - // @ts-ignore: Object is possibly 'null'. - currBrowserName = (caps.capabilities as Capabilities.Capabilities)['bstack:options'].browserName || currBrowserName + currBrowserName = (caps.capabilities as Capabilities.Capabilities)['bstack:options']?.browserName || currBrowserName } if (!bestBrowser || !bestPlatformCaps || (bestPlatformCaps.deviceName || bestPlatformCaps['bstack:options']?.deviceName)) { bestBrowser = currBrowserName @@ -68,7 +67,7 @@ export const getBestPlatformForPercySnapshot = (capabilities?: Capabilities.Remo }) return bestPlatformCaps } - } catch (err: any) { + } catch (err: unknown) { PercyLogger.error(`Error while trying to determine best platform for Percy snapshot ${err}`) return null } diff --git a/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts b/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts index 5f7aecd0fdb..99418370a46 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts @@ -7,7 +7,7 @@ import { PERCY_LOGS_FILE } from '../constants' const log = logger('@wdio/browserstack-service') -export abstract class PercyLogger { +export class PercyLogger { public static logFilePath = path.join(process.cwd(), PERCY_LOGS_FILE) private static logFolderPath = path.join(process.cwd(), 'logs') private static logFileStream: fs.WriteStream | null diff --git a/packages/wdio-browserstack-service/src/launcher.ts b/packages/wdio-browserstack-service/src/launcher.ts index e46fc853b62..e1693cb9c9e 100644 --- a/packages/wdio-browserstack-service/src/launcher.ts +++ b/packages/wdio-browserstack-service/src/launcher.ts @@ -50,7 +50,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst private _buildIdentifier?: string private _accessibilityAutomation?: boolean private _percy?: Percy - private _percyBestPlatformCaps?: any + private _percyBestPlatformCaps?: Capabilities.DesiredCapabilities public _testOpsBuildStopped?: boolean constructor ( @@ -160,7 +160,6 @@ export default class BrowserstackLauncherService implements Services.ServiceInst if (isThisBestPercyPlatform) { process.env.BEST_PLATFORM_CID = cid } - caps['wdio:cid'] = cid } } catch (err: unknown) { PercyLogger.error(`Error while setting best platform for Percy snapshot at worker start ${err}`) @@ -414,7 +413,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst process.on('beforeExit', handler) process.on('SIGINT', handler) process.on('SIGTERM', handler) - } catch (err: any) { + } catch (err: unknown) { PercyLogger.debug(`Error in percy setup ${err}`) } } @@ -427,7 +426,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst await stopPercy(this._percy) PercyLogger.info('Percy stopped') } catch (err) { - PercyLogger.error('Error occured while stopping percy : ' + err) + PercyLogger.error(`Error occured while stopping percy : ${err}`) } } From d25d9fa9ae961a9ef177163d9cbdfb9520ec5331 Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 15 Jan 2024 18:19:15 +0530 Subject: [PATCH 15/38] review changes --- .../wdio-browserstack-service/src/Percy/Percy-Handler.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index b414edd456e..02194263ddc 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -9,7 +9,6 @@ import PercyCaptureMap from './PercyCaptureMap' import * as PercySDK from './PercySDK' import { PercyLogger } from './PercyLogger' -import { BrowserAsync } from '../@types/bstack-service-types' import { PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES } from '../constants' @@ -62,8 +61,8 @@ class _PercyHandler { await (this._isAppAutomate ? PercySDK.screenshotApp(this._percyCaptureMap?.getName(sessionName ? sessionName : (this._sessionName as string), eventName)) : PercySDK.screenshot(this._browser, this._percyCaptureMap?.getName( sessionName ? sessionName : (this._sessionName as string), eventName))) this._percyScreenshotCounter -= 1 } - } catch (err: any) { - this._percyScreenshotCounter -= 1; + } catch (err: unknown) { + this._percyScreenshotCounter -= 1 this._percyCaptureMap?.decrement(sessionName ? sessionName : (this._sessionName as string), eventName as string) PercyLogger.error(`Error while trying to auto capture Percy screenshot ${err}`) } From ff064d1abf62d94bc57742098e36cad8ad3dc060 Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 15 Jan 2024 18:30:06 +0530 Subject: [PATCH 16/38] test fixes --- .../wdio-browserstack-service/tests/Percy-Handler.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts index c48cc2bd23c..57091b613f6 100644 --- a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts +++ b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts @@ -89,10 +89,6 @@ describe('before', () => { percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') // percyCaptureMapSpy.mockClear() }) - it('assigns PercyCaptureMap to browser', async () => { - await percyHandler.before() - expect(browser).toHaveProperty('percyCaptureMap') - }) }) describe('_setSessionName', () => { @@ -101,7 +97,7 @@ describe('_setSessionName', () => { }) it('sets sessionName property', async () => { percyHandler._setSessionName('1234') - expect(percyHandler['sessionName']).toEqual('1234') + expect(percyHandler['_sessionName']).toEqual('1234') }) }) From 83767e4a2a4acededf08b72451788398d59e6d58 Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 15 Jan 2024 18:36:50 +0530 Subject: [PATCH 17/38] test fixes --- .../wdio-browserstack-service/tests/Percy-Handler.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts index 57091b613f6..077b319cc21 100644 --- a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts +++ b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts @@ -83,14 +83,6 @@ it('should initialize correctly', () => { expect(percyHandler['_percyScreenshotCounter']).toEqual(0) }) -describe('before', () => { - // const percyCaptureMapSpy = jest.spyOn(PercyCaptureMapExport, 'PercyCaptureMap' as any) - beforeEach(() => { - percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') - // percyCaptureMapSpy.mockClear() - }) -}) - describe('_setSessionName', () => { beforeEach(() => { percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') From fdb0e1504f1c7de6661c0ff675c21a79735db8e1 Mon Sep 17 00:00:00 2001 From: riya Date: Wed, 17 Jan 2024 09:19:04 +0530 Subject: [PATCH 18/38] added percy logger tests --- .../src/Percy/PercyLogger.ts | 2 +- .../tests/PercyLogger.test.ts | 68 ++++++++++++++++--- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts b/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts index 99418370a46..b062ab96092 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyLogger.ts @@ -73,7 +73,7 @@ export class PercyLogger { if (fs.existsSync(this.logFilePath)) { fs.truncateSync(this.logFilePath) } - } catch (err: any) { + } catch (err: unknown) { log.error(`Failed to clear percy.log file. Error ${err}`) } } diff --git a/packages/wdio-browserstack-service/tests/PercyLogger.test.ts b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts index e0b01e44980..d9ac363856d 100644 --- a/packages/wdio-browserstack-service/tests/PercyLogger.test.ts +++ b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts @@ -1,5 +1,6 @@ import logger from '@wdio/logger' import { PercyLogger } from '../src/Percy/PercyLogger' +import fs from 'node:fs' const log = logger('test') @@ -15,15 +16,6 @@ jest.mock('node:fs/promises', () => ({ stat: jest.fn().mockReturnValue(Promise.resolve({ size: 123 })), } })) -jest.mock('node:fs', () => ({ - default: { - readFileSync: jest.fn().mockReturnValue('1234\nsomepath'), - existsSync: jest.fn(), - // existsSync: fs.existsSync, - truncateSync: jest.fn(), - mkdirSync: jest.fn() - } -})) describe('PercyLogger Log methods', () => { let logToFileSpy: any @@ -70,5 +62,63 @@ describe('PercyLogger Log methods', () => { expect(logToFileSpy).toBeCalled() expect(logDebugMock).toBeCalled() }) + afterEach(() => { + jest.clearAllMocks() + }) }) + +describe('PercyLogger clearLogger method', () => { + let clearLoggerSpy: any + beforeEach(() => { + clearLoggerSpy = jest.spyOn(PercyLogger, 'clearLogger') + }) + + it('should do nothing if logFileStream is null', () => { + + PercyLogger.clearLogger() + expect(clearLoggerSpy).toBeCalled() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + }) + + describe('PercyLogger clearLogFile method', () => { + let clearLogFileSpy: any + beforeEach(() => { + clearLogFileSpy = jest.spyOn(PercyLogger, 'clearLogFile') + }) + + it('should do nothing if logFileStream is null', () => { + + PercyLogger.clearLogFile() + expect(clearLogFileSpy).toBeCalled() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + }) + + describe('PercyLogger logToFile method', () => { + let logToFileSpy: any + + beforeEach(() => { + logToFileSpy = jest.spyOn(PercyLogger, 'logToFile') + }) + + it('should do nothing if logFileStream is null', () => { + + jest.spyOn(fs, 'existsSync' ).mockReturnValue(false) + jest.spyOn(fs, 'mkdirSync' ).mockReturnValue(true) + + jest.spyOn(fs, 'createWriteStream' ).mockReturnValue("filepath") + PercyLogger.logToFile("message", "info") + expect(logToFileSpy).toBeCalled() + }) + afterEach(() => { + jest.clearAllMocks() + }) + }) From 845dc693f7a840f9d8a3e466a6605823a654723c Mon Sep 17 00:00:00 2001 From: riya Date: Wed, 17 Jan 2024 09:23:14 +0530 Subject: [PATCH 19/38] added percy logger tests --- .../tests/PercyLogger.test.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/PercyLogger.test.ts b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts index d9ac363856d..7036c11e6eb 100644 --- a/packages/wdio-browserstack-service/tests/PercyLogger.test.ts +++ b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts @@ -67,7 +67,6 @@ describe('PercyLogger Log methods', () => { }) }) - describe('PercyLogger clearLogger method', () => { let clearLoggerSpy: any beforeEach(() => { @@ -75,7 +74,6 @@ describe('PercyLogger clearLogger method', () => { }) it('should do nothing if logFileStream is null', () => { - PercyLogger.clearLogger() expect(clearLoggerSpy).toBeCalled() }) @@ -85,12 +83,12 @@ describe('PercyLogger clearLogger method', () => { }) }) - describe('PercyLogger clearLogFile method', () => { +describe('PercyLogger clearLogFile method', () => { let clearLogFileSpy: any beforeEach(() => { clearLogFileSpy = jest.spyOn(PercyLogger, 'clearLogFile') }) - + it('should do nothing if logFileStream is null', () => { PercyLogger.clearLogFile() @@ -100,25 +98,25 @@ describe('PercyLogger clearLogger method', () => { afterEach(() => { jest.clearAllMocks() }) - }) +}) - describe('PercyLogger logToFile method', () => { +describe('PercyLogger logToFile method', () => { let logToFileSpy: any - + beforeEach(() => { logToFileSpy = jest.spyOn(PercyLogger, 'logToFile') }) - + it('should do nothing if logFileStream is null', () => { jest.spyOn(fs, 'existsSync' ).mockReturnValue(false) jest.spyOn(fs, 'mkdirSync' ).mockReturnValue(true) - jest.spyOn(fs, 'createWriteStream' ).mockReturnValue("filepath") - PercyLogger.logToFile("message", "info") + jest.spyOn(fs, 'createWriteStream' ).mockReturnValue('filepath') + PercyLogger.logToFile('message', 'info') expect(logToFileSpy).toBeCalled() }) afterEach(() => { jest.clearAllMocks() }) - }) +}) From cce2d2799d4ab92976ac594bfd34a62b508c8593 Mon Sep 17 00:00:00 2001 From: riya Date: Wed, 17 Jan 2024 09:24:28 +0530 Subject: [PATCH 20/38] added linting fix --- .../wdio-browserstack-service/tests/PercyLogger.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/PercyLogger.test.ts b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts index 7036c11e6eb..19cb64abe71 100644 --- a/packages/wdio-browserstack-service/tests/PercyLogger.test.ts +++ b/packages/wdio-browserstack-service/tests/PercyLogger.test.ts @@ -72,16 +72,16 @@ describe('PercyLogger clearLogger method', () => { beforeEach(() => { clearLoggerSpy = jest.spyOn(PercyLogger, 'clearLogger') }) - + it('should do nothing if logFileStream is null', () => { PercyLogger.clearLogger() expect(clearLoggerSpy).toBeCalled() }) - + afterEach(() => { jest.clearAllMocks() }) - }) +}) describe('PercyLogger clearLogFile method', () => { let clearLogFileSpy: any From b3ff1fc695064e41ad08f2b2ce01da09fb143524 Mon Sep 17 00:00:00 2001 From: riya Date: Wed, 17 Jan 2024 10:59:05 +0530 Subject: [PATCH 21/38] percy handler tests --- .../src/Percy/Percy-Handler.ts | 4 +- .../tests/Percy-Handler.test.ts | 172 ++++++++++++++++++ 2 files changed, 174 insertions(+), 2 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts index 02194263ddc..9d06f515ef5 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts @@ -128,7 +128,7 @@ class _PercyHandler { } }, 1000) } - } catch (err: any) { + } catch (err: unknown) { PercyLogger.error(`Error while trying to cleanup deferred screenshots ${err}`) } } @@ -152,7 +152,7 @@ class _PercyHandler { this.deferCapture(this._sessionName as string, eventName) } } - } catch (err: any) { + } catch (err: unknown) { PercyLogger.error(`Error while trying to calculate auto capture parameters ${err}`) } } diff --git a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts index 077b319cc21..2b0ff107310 100644 --- a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts +++ b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts @@ -93,6 +93,133 @@ describe('_setSessionName', () => { }) }) +describe('sleep', () => { + beforeEach(() => { + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') + }) + it('sets sleep', async () => { + percyHandler.sleep(234) + }) +}) + +describe('cleanupDeferredScreenshots', () => { + beforeEach(() => { + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') + }) + it('calls cleanupDeferredScreenshots', async () => { + percyHandler['_percyDeferredScreenshots'] = [] + percyHandler.cleanupDeferredScreenshots() + expect(percyHandler['_percyDeferredScreenshots']).toEqual([]) + expect(percyHandler['_isPercyCleanupProcessingUnderway']).toEqual(true) + }) +}) + +describe('isDOMChangingCommand', () => { + beforeEach(() => { + percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') + }) + it('should call isDOMChangingCommand', async () => { + const args = { + endpoint: 'actions', + body: { + actions: [{ + type: 'key' + }] + } + } + // await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) + let res = percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + expect(res).toEqual(false) + }) + it('should call isDOMChangingCommand with method: DELETE', async () => { + const args = { + method: 'DELETE', + endpoint: '/session/:sessionId', + body: { + actions: [{ + type: 'key' + }] + } + } + // await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) + let res = percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + expect(res).toEqual(true) + }) + it('should call isDOMChangingCommand with method: POST', async () => { + const args = { + method: 'POST', + endpoint: '/session/:sessionId/url', + body: { + actions: [{ + type: 'key' + }] + } + } + // await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) + percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + let res = percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + expect(res).toEqual(true) + }) + it('should call isDOMChangingCommand with method: POST and click', async () => { + const args = { + method: 'POST', + endpoint: '/session/:sessionId/element/click', + body: { + actions: [{ + type: 'key' + }] + } + } + // await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) + percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + let res = percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + expect(res).toEqual(true) + }) + it('should call isDOMChangingCommand with method: POST and clear', async () => { + const args = { + method: 'POST', + endpoint: '/session/:sessionId/element/clear', + body: { + actions: [{ + type: 'key' + }] + } + } + percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + let res = percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + expect(res).toEqual(true) + }) + it('should call isDOMChangingCommand with method: POST and command touch', async () => { + const args = { + method: 'POST', + endpoint: '/session/:sessionId/touch', + body: { + actions: [{ + type: 'key' + }] + } + } + percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + let res = percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + expect(res).toEqual(true) + }) + it('should call isDOMChangingCommand with method: POST and command execute', async () => { + const args = { + method: 'POST', + endpoint: '/session/:sessionId/execute', + body: { + script: 'script', + actions: [{ + type: 'key' + }] + } + } + percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + let res = percyHandler.isDOMChangingCommand(args as BeforeCommandArgs) + expect(res).toEqual(true) + }) +}) + describe('teardown', () => { beforeEach(() => { percyHandler = new PercyHandler('manual', browser, caps, false, 'framework') @@ -107,6 +234,33 @@ describe('teardown', () => { }) }) +describe('browserBeforeCommand', () => { + let isDOMChangingCommandSpy: any + let percyHandler: PercyHandler + + beforeEach(() => { + percyHandler = new PercyHandler('auto', browser, caps, false, 'framework') + // isDOMChangingCommandSpy = jest.spyOn(PercyHandler.prototype, 'isDOMChangingCommand').mockReturnValue(true) + }) + + it('should call browserBeforeCommand', async () => { + const args = { + endpoint: 'actions', + body: { + actions: [{ + type: 'key' + }] + } + } + await percyHandler.browserBeforeCommand(args as BeforeCommandArgs & AfterCommandArgs) + // expect(isDOMChangingCommandSpy).toBeCalledTimes(1) + }) + + afterEach(() => { + // isDOMChangingCommandSpy.mockClear() + }) +}) + describe('browserCommand', () => { let percyAutoCaptureSpy: any let percyHandler: PercyHandler @@ -152,6 +306,24 @@ describe('browserCommand', () => { expect(percyAutoCaptureSpy).toBeCalledTimes(1) }) + it('should call percyAutoCapture for event type element and capture mode auto', async () => { + const args = { + endpoint: '/session/:sessionId/element/value' + } + percyHandler['_percyAutoCaptureMode'] = 'auto' + + await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) + expect(percyAutoCaptureSpy).toBeCalledTimes(1) + }) + + it('should call percyAutoCapture for event type element and capture mode auto', async () => { + const args = {} + percyHandler['_percyAutoCaptureMode'] = 'auto' + + await percyHandler.browserAfterCommand(args as BeforeCommandArgs & AfterCommandArgs) + expect(percyAutoCaptureSpy).toBeCalledTimes(0) + }) + afterEach(() => { percyAutoCaptureSpy.mockClear() }) From 48dc7751123091bbfe75d9af7d8ff21a79e73f99 Mon Sep 17 00:00:00 2001 From: riya Date: Thu, 18 Jan 2024 11:31:53 +0530 Subject: [PATCH 22/38] linting fix --- .../src/Percy/Percy.ts | 66 ++++++------ .../tests/Percy.test.js | 101 ++++++++++++++++++ 2 files changed, 134 insertions(+), 33 deletions(-) create mode 100644 packages/wdio-browserstack-service/tests/Percy.test.js diff --git a/packages/wdio-browserstack-service/src/Percy/Percy.ts b/packages/wdio-browserstack-service/src/Percy/Percy.ts index 80beeff70f3..e9e7d9456d0 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy.ts @@ -15,40 +15,40 @@ import type { Options } from '@wdio/types' const logDir = 'logs' class Percy { - #logfile: string = path.join(logDir, 'percy.log') - #address: string = process.env.PERCY_SERVER_ADDRESS || 'http://127.0.0.1:5338' + _logfile: string = path.join(logDir, 'percy.log') + _address: string = process.env.PERCY_SERVER_ADDRESS || 'http://127.0.0.1:5338' - #binaryPath: string | any = null - #options: BrowserstackConfig & Options.Testrunner - #config: Options.Testrunner - #proc: any = null - #isApp: boolean - #projectName: string | undefined = undefined + _binaryPath: string | any = null + _options: BrowserstackConfig & Options.Testrunner + _config: Options.Testrunner + _proc: any = null + _isApp: boolean + _projectName: string | undefined = undefined isProcessRunning = false constructor(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig) { - this.#options = options - this.#config = config - this.#isApp = Boolean(options.app) - this.#projectName = bsConfig.projectName + this._options = options + this._config = config + this._isApp = Boolean(options.app) + this._projectName = bsConfig.projectName } - async #getBinaryPath(): Promise { - if (!this.#binaryPath) { + private async getBinaryPath(): Promise { + if (!this._binaryPath) { const pb = new PercyBinary() - this.#binaryPath = await pb.getBinaryPath(this.#config) + this._binaryPath = await pb.getBinaryPath(this._config) } - return this.#binaryPath + return this._binaryPath } - async #sleep(ms: number) { + private async sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) } async healthcheck() { try { - const resp = await nodeRequest('GET', 'percy/healthcheck', null, this.#address) + const resp = await nodeRequest('GET', 'percy/healthcheck', null, this._address) if (resp) { return true } @@ -58,8 +58,8 @@ class Percy { } async start() { - const binaryPath: string = await this.#getBinaryPath() - const logStream = fs.createWriteStream(this.#logfile, { flags: 'a' }) + const binaryPath: string = await this.getBinaryPath() + const logStream = fs.createWriteStream(this._logfile, { flags: 'a' }) const token = await this.fetchPercyToken() const configPath = await this.createPercyConfig() @@ -67,24 +67,24 @@ class Percy { return false } - const commandArgs = [`${this.#isApp ? 'app:exec' : 'exec'}:start`] + const commandArgs = [`${this._isApp ? 'app:exec' : 'exec'}:start`] if (configPath) { commandArgs.push('-c', configPath as string) } - this.#proc = spawn( + this._proc = spawn( binaryPath, commandArgs, { env: { ...process.env, PERCY_TOKEN: token } } ) - this.#proc.stdout.pipe(logStream) - this.#proc.stderr.pipe(logStream) + this._proc.stdout.pipe(logStream) + this._proc.stderr.pipe(logStream) this.isProcessRunning = true const that = this - this.#proc.on('close', function () { + this._proc.on('close', function () { that.isProcessRunning = false }) @@ -95,14 +95,14 @@ class Percy { return true } - await this.#sleep(1000) + await this.sleep(1000) } while (this.isProcessRunning) return false } async stop() { - const binaryPath = await this.#getBinaryPath() + const binaryPath = await this.getBinaryPath() return new Promise( (resolve) => { const proc = spawn(binaryPath, ['exec:stop']) proc.on('close', (code: any) => { @@ -117,16 +117,16 @@ class Percy { } async fetchPercyToken() { - const projectName = this.#projectName + const projectName = this._projectName try { - const type = this.#isApp ? 'app' : 'automate' + const type = this._isApp ? 'app' : 'automate' const response: any = await nodeRequest( 'GET', `api/app_percy/get_project_token?name=${projectName}&type=${type}`, { - username: getBrowserStackUser(this.#config), - password: getBrowserStackKey(this.#config) + username: getBrowserStackUser(this._config), + password: getBrowserStackKey(this._config) }, 'https://api.browserstack.com' ) @@ -139,12 +139,12 @@ class Percy { } async createPercyConfig() { - if (!this.#options.percyOptions) { + if (!this._options.percyOptions) { return null } const configPath = path.join(os.tmpdir(), 'percy.json') - const percyOptions = this.#options.percyOptions + const percyOptions = this._options.percyOptions if (!percyOptions.version) { percyOptions.version = '2' diff --git a/packages/wdio-browserstack-service/tests/Percy.test.js b/packages/wdio-browserstack-service/tests/Percy.test.js new file mode 100644 index 00000000000..3dd32259772 --- /dev/null +++ b/packages/wdio-browserstack-service/tests/Percy.test.js @@ -0,0 +1,101 @@ +import Percy from '../src/Percy/Percy' +import * as utils from '../src/util' + +jest.mock('node:fs') +jest.mock('node:path') +jest.mock('node:os') +jest.mock('node:child_process') +jest.mock('../src/util') +jest.mock('../src/Percy/PercyLogger') + +describe('Percy Class', () => { + let percyInstance + + beforeEach(() => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) + jest.clearAllMocks() + }) + + describe('Constructor', () => { + it('should initialize Percy instance', () => { + expect(percyInstance._options).toEqual({}) + expect(percyInstance._config).toEqual({}) + expect(percyInstance._isApp).toBe(false) + expect(percyInstance._projectName).toBe('testProject') + }) + }) + + describe('getBinaryPath method', () => { + it('should return binary path if already present', async () => { + percyInstance['_binaryPath'] = 'some_path' + + const result = await percyInstance.getBinaryPath() + expect(result).toBe('some_path') + }) + }) + + describe('running method', () => { + // const getBinaryPathSpy = jest.spyOn(Percy.prototype, 'getBinaryPath') + + it('should return true if running', async () => { + percyInstance['isProcessRunning'] = true + const res = await percyInstance.isRunning() + expect(res).toEqual(true) + }) + it('should return false if running', async () => { + percyInstance['isProcessRunning'] = false + const res = await percyInstance.isRunning() + expect(res).toEqual(false) + }) + }) + + describe('health check method', () => { + it('should return true if running', async () => { + const nodeRequestSpy = jest.spyOn(utils, 'nodeRequest').mockReturnValue(true) + const res = await percyInstance.healthcheck() + expect(nodeRequestSpy).toBeCalledTimes(1) + expect(res).toEqual(true) + }) + it('should return false if running', async () => { + const nodeRequestSpy = jest.spyOn(utils, 'nodeRequest').mockReturnValue(false) + const res = await percyInstance.healthcheck() + expect(nodeRequestSpy).toBeCalledTimes(1) + expect(res).toEqual(undefined) + }) + }) + + describe('fetchPercyToken method', () => { + it('should return false if running', async () => { + percyInstance['_projectName'] = 'project_name' + percyInstance['_isApp'] = true + const response = { + token: 'token' + } + const nodeRequestSpy = jest.spyOn(utils, 'nodeRequest').mockReturnValue(response) + const res = await percyInstance.fetchPercyToken() + expect(nodeRequestSpy).toBeCalledTimes(1) + expect(res).toEqual('token') + }) + }) + + // describe('stopPercy', () => { + // let percyStopSpy: any + + // beforeEach(() => { + // percyStopSpy = jest.spyOn(Percy.prototype, 'stop').mockImplementationOnce(async () => { + // return {} + // }) + // }) + + // it('should call stop method of Percy', async () => { + // const percy = new Percy({}, {}, {}) + // await PercyHelper.stopPercy(percy) + // expect(percyStopSpy).toBeCalledTimes(1) + // }) + + // afterEach(() => { + // percyStopSpy.mockClear() + // }) + // }) + +}) From 9272da32b4bdb2674c22bb4c457ec9c716676059 Mon Sep 17 00:00:00 2001 From: riya Date: Thu, 18 Jan 2024 15:22:55 +0530 Subject: [PATCH 23/38] percy capture map tests --- .../src/Percy/PercyBinary.ts | 46 +++++------ .../tests/PercyBinary.test.js | 78 +++++++++++++++++++ .../tests/PercyCaptureMap.test.ts | 40 ++++++++++ 3 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 packages/wdio-browserstack-service/tests/PercyBinary.test.js create mode 100644 packages/wdio-browserstack-service/tests/PercyCaptureMap.test.ts diff --git a/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts index 1ada7b21b0c..d2001b3a2b6 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts @@ -10,11 +10,11 @@ import { PercyLogger } from './PercyLogger' import type { Options } from '@wdio/types' class PercyBinary { - #hostOS = process.platform - #httpPath: any = null - #binaryName = 'percy' + _hostOS = process.platform + _httpPath: any = null + _binaryName = 'percy' - #orderedPaths = [ + _orderedPaths = [ path.join(os.homedir(), '.browserstack'), process.cwd(), os.tmpdir() @@ -22,24 +22,24 @@ class PercyBinary { constructor() { const base = 'https://github.com/percy/cli/releases/latest/download' - if (this.#hostOS.match(/darwin|mac os/i)) { - this.#httpPath = base + '/percy-osx.zip' - } else if (this.#hostOS.match(/mswin|msys|mingw|cygwin|bccwin|wince|emc|win32/i)) { - this.#httpPath = base + '/percy-win.zip' - this.#binaryName = 'percy.exe' + if (this._hostOS.match(/darwin|mac os/i)) { + this._httpPath = base + '/percy-osx.zip' + } else if (this._hostOS.match(/mswin|msys|mingw|cygwin|bccwin|wince|emc|win32/i)) { + this._httpPath = base + '/percy-win.zip' + this._binaryName = 'percy.exe' } else { - this.#httpPath = base + '/percy-linux.zip' + this._httpPath = base + '/percy-linux.zip' } } - async #makePath(path: string) { - if (await this.#checkPath(path)) { + private async makePath(path: string) { + if (await this.checkPath(path)) { return true } return fsp.mkdir(path).then(() => true).catch(() => false) } - async #checkPath(path: string) { + private async checkPath(path: string) { try { const hasDir = await fsp.access(path).then(() => true, () => false) if (hasDir) { @@ -50,10 +50,10 @@ class PercyBinary { } } - async #getAvailableDirs() { - for (let i = 0; i < this.#orderedPaths.length; i++) { - const path = this.#orderedPaths[i] - if (await this.#makePath(path)) { + private async _getAvailableDirs() { + for (let i = 0; i < this._orderedPaths.length; i++) { + const path = this._orderedPaths[i] + if (await this.makePath(path)) { return path } } @@ -61,9 +61,9 @@ class PercyBinary { } async getBinaryPath(conf: Options.Testrunner): Promise { - const destParentDir = await this.#getAvailableDirs() - const binaryPath = path.join(destParentDir, this.#binaryName) - if (await this.#checkPath(binaryPath)) { + const destParentDir = await this._getAvailableDirs() + const binaryPath = path.join(destParentDir, this._binaryName) + if (await this.checkPath(binaryPath)) { return binaryPath } const downloadedBinaryPath: string = await this.download(conf, destParentDir) @@ -94,17 +94,17 @@ class PercyBinary { } async download(conf: any, destParentDir: any): Promise { - if (!await this.#checkPath(destParentDir)){ + if (!await this.checkPath(destParentDir)){ await fsp.mkdir(destParentDir) } - const binaryName = this.#binaryName + const binaryName = this._binaryName const zipFilePath = path.join(destParentDir, binaryName + '.zip') const binaryPath = path.join(destParentDir, binaryName) const downloadedFileStream = fs.createWriteStream(zipFilePath) return new Promise((resolve, reject) => { - const stream = got.extend({ followRedirect: true }).get(this.#httpPath, { isStream: true }) + const stream = got.extend({ followRedirect: true }).get(this._httpPath, { isStream: true }) stream.on('error', (err) => { PercyLogger.error(`Got Error in percy binary download response: ${err}`) }) diff --git a/packages/wdio-browserstack-service/tests/PercyBinary.test.js b/packages/wdio-browserstack-service/tests/PercyBinary.test.js new file mode 100644 index 00000000000..a543ba4e609 --- /dev/null +++ b/packages/wdio-browserstack-service/tests/PercyBinary.test.js @@ -0,0 +1,78 @@ +import PercyBinary from '../src/Percy/PercyBinary' + + +// Mocking dependencies +jest.mock('got') +jest.mock('node:fs/promises', () => ({ + access: jest.fn(), + mkdir: jest.fn().mockResolvedValue(true), +})) +jest.mock('node:fs', () => ({ + createWriteStream: jest.fn(), + chmod: jest.fn().mockImplementation((_, __, callback) => callback()), +})) +jest.mock('yauzl') + +describe('PercyBinary', () => { + describe('makePath', () => { + it('should create a path if it does not exist', async () => { + const percyBinary = new PercyBinary() + const result = await percyBinary.makePath('some_path') + expect(result).toBe(true) + }) + }) + + describe('_getAvailableDirs', () => { + it('should _getAvailableDirs', async () => { + const percyBinary = new PercyBinary() + percyBinary['_orderedPaths'] = ['path1', 'path2', 'path3'] + const makePathSpy = jest.spyOn(percyBinary, 'makePath').mockReturnValue(true) + + const result = await percyBinary._getAvailableDirs() + expect(makePathSpy).toBeCalledTimes(1) + expect(result).toBe('path1') + }) + }) + + describe('getBinaryPath', () => { + it('should getBinaryPath', async () => { + const percyBinary = new PercyBinary() + const getAvailableDirsSpy = jest.spyOn(percyBinary, '_getAvailableDirs').mockReturnValue('some_path') + const checkPathSpy = jest.spyOn(percyBinary, 'checkPath').mockReturnValue(true) + + const result = await percyBinary.getBinaryPath() + expect(getAvailableDirsSpy).toBeCalledTimes(1) + expect(checkPathSpy).toBeCalledTimes(1) + expect(result).toBe('some_path/percy') + }) + it('should getBinaryPath from first download try', async () => { + const percyBinary = new PercyBinary() + const getAvailableDirsSpy = jest.spyOn(percyBinary, '_getAvailableDirs').mockReturnValue('some_path') + const checkPathSpy = jest.spyOn(percyBinary, 'checkPath').mockReturnValue(false) + const downloadSpy = jest.spyOn(percyBinary, 'download').mockReturnValue('download_path') + const validateSpy = jest.spyOn(percyBinary, 'validateBinary').mockReturnValue(true) + + const result = await percyBinary.getBinaryPath() + expect(getAvailableDirsSpy).toBeCalledTimes(1) + expect(checkPathSpy).toBeCalledTimes(1) + expect(downloadSpy).toBeCalledTimes(1) + expect(validateSpy).toBeCalledTimes(1) + expect(result).toBe('download_path') + }) + + it('should getBinaryPath from second download try', async () => { + const percyBinary = new PercyBinary() + const getAvailableDirsSpy = jest.spyOn(percyBinary, '_getAvailableDirs').mockReturnValue('some_path') + const checkPathSpy = jest.spyOn(percyBinary, 'checkPath').mockReturnValue(false) + const downloadSpy = jest.spyOn(percyBinary, 'download').mockReturnValue('download_path') + const validateSpy = jest.spyOn(percyBinary, 'validateBinary').mockReturnValue(false) + + const result = await percyBinary.getBinaryPath() + expect(getAvailableDirsSpy).toBeCalledTimes(1) + expect(checkPathSpy).toBeCalledTimes(1) + expect(downloadSpy).toBeCalledTimes(2) + expect(validateSpy).toBeCalledTimes(1) + expect(result).toBe('download_path') + }) + }) +}) diff --git a/packages/wdio-browserstack-service/tests/PercyCaptureMap.test.ts b/packages/wdio-browserstack-service/tests/PercyCaptureMap.test.ts new file mode 100644 index 00000000000..d875da24479 --- /dev/null +++ b/packages/wdio-browserstack-service/tests/PercyCaptureMap.test.ts @@ -0,0 +1,40 @@ +import PercyCaptureMap from '../src/Percy/PercyCaptureMap' + + +describe('PercyCaptureMap', () => { + let percyCaptureMap: PercyCaptureMap + + beforeEach(() => { + percyCaptureMap = new PercyCaptureMap() + }) + + it('increment method should increase the count', () => { + percyCaptureMap.increment('session1', 'event1') + expect(percyCaptureMap.get('session1', 'event1')).toBe(0) + }) + + it('decrement method should decrease the count', () => { + percyCaptureMap.increment('session1', 'event1') + percyCaptureMap.decrement('session1', 'event1') + expect(percyCaptureMap.get('session1', 'event1')).toBe(0) + }) + + it('getName method should return the correct name', () => { + percyCaptureMap.increment('session1', 'event1') + expect(percyCaptureMap.getName('session1', 'event1')).toBe('session1-event1-0') + }) + + it('get method should return the correct count', () => { + percyCaptureMap.increment('session1', 'event1') + expect(percyCaptureMap.get('session1', 'event1')).toBe(0) + }) + + it('get method should return 0 for non-existing session and event', () => { + expect(percyCaptureMap.get('nonexistentSession', 'nonexistentEvent')).toBe(0) + }) + + it('decrement method should not decrease count below 0', () => { + percyCaptureMap.decrement('session1', 'event1') + expect(percyCaptureMap.get('session1', 'event1')).toBe(0) + }) +}) From d70cef685ce8cd7aa11c9f0d3a79bc3878ef724d Mon Sep 17 00:00:00 2001 From: riya Date: Thu, 18 Jan 2024 15:23:44 +0530 Subject: [PATCH 24/38] linting fix --- packages/wdio-browserstack-service/tests/PercyBinary.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/wdio-browserstack-service/tests/PercyBinary.test.js b/packages/wdio-browserstack-service/tests/PercyBinary.test.js index a543ba4e609..e27377fcb76 100644 --- a/packages/wdio-browserstack-service/tests/PercyBinary.test.js +++ b/packages/wdio-browserstack-service/tests/PercyBinary.test.js @@ -1,6 +1,5 @@ import PercyBinary from '../src/Percy/PercyBinary' - // Mocking dependencies jest.mock('got') jest.mock('node:fs/promises', () => ({ From 13d78ca770b98670f3b6f73f1f7bf4c6bf5c11ab Mon Sep 17 00:00:00 2001 From: riya Date: Thu, 18 Jan 2024 15:24:34 +0530 Subject: [PATCH 25/38] linting fix --- packages/wdio-browserstack-service/tests/PercyCaptureMap.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/wdio-browserstack-service/tests/PercyCaptureMap.test.ts b/packages/wdio-browserstack-service/tests/PercyCaptureMap.test.ts index d875da24479..e46b152c370 100644 --- a/packages/wdio-browserstack-service/tests/PercyCaptureMap.test.ts +++ b/packages/wdio-browserstack-service/tests/PercyCaptureMap.test.ts @@ -1,6 +1,5 @@ import PercyCaptureMap from '../src/Percy/PercyCaptureMap' - describe('PercyCaptureMap', () => { let percyCaptureMap: PercyCaptureMap From c14b1f147677544470819ec327e477fab6c4a03a Mon Sep 17 00:00:00 2001 From: riya Date: Thu, 18 Jan 2024 18:35:58 +0530 Subject: [PATCH 26/38] percy test UTs added --- .../src/Percy/PercyBinary.ts | 3 +- .../tests/Percy.test.js | 158 +++++++++++++++--- 2 files changed, 133 insertions(+), 28 deletions(-) diff --git a/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts index d2001b3a2b6..917f0408ca9 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercyBinary.ts @@ -78,8 +78,7 @@ class PercyBinary { async validateBinary(binaryPath: string) { const versionRegex = /^.*@percy\/cli \d.\d+.\d+/ - /* eslint-disable @typescript-eslint/no-unused-vars */ - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const proc = spawn(binaryPath, ['--version']) proc.stdout.on('data', (data) => { if (versionRegex.test(data)) { diff --git a/packages/wdio-browserstack-service/tests/Percy.test.js b/packages/wdio-browserstack-service/tests/Percy.test.js index 3dd32259772..c67518d1a39 100644 --- a/packages/wdio-browserstack-service/tests/Percy.test.js +++ b/packages/wdio-browserstack-service/tests/Percy.test.js @@ -1,12 +1,9 @@ import Percy from '../src/Percy/Percy' -import * as utils from '../src/util' +import * as PercyLogger from '../src/Percy/PercyLogger' -jest.mock('node:fs') -jest.mock('node:path') -jest.mock('node:os') -jest.mock('node:child_process') -jest.mock('../src/util') -jest.mock('../src/Percy/PercyLogger') +import * as utils from '../src/util' +import fs from 'fs' +import { ChildProcess } from 'child_process' describe('Percy Class', () => { let percyInstance @@ -78,24 +75,133 @@ describe('Percy Class', () => { }) }) - // describe('stopPercy', () => { - // let percyStopSpy: any - - // beforeEach(() => { - // percyStopSpy = jest.spyOn(Percy.prototype, 'stop').mockImplementationOnce(async () => { - // return {} - // }) - // }) - - // it('should call stop method of Percy', async () => { - // const percy = new Percy({}, {}, {}) - // await PercyHelper.stopPercy(percy) - // expect(percyStopSpy).toBeCalledTimes(1) - // }) - - // afterEach(() => { - // percyStopSpy.mockClear() - // }) - // }) + describe('createPercyConfig method', () => { + afterEach(() => { + jest.clearAllMocks() + }) + it('should return early null if percyOptions is null', async () => { + percyInstance['_options'] = { + percyOptions: null + } + const res = await percyInstance.createPercyConfig() + expect(res).toEqual(null) + }) + + it('should return valid response', async () => { + percyInstance['_options'] = { + percyOptions: { + version: null + } + } + const PercyLoggerDebugSpy = jest.spyOn(PercyLogger.PercyLogger, 'debug') + PercyLoggerDebugSpy.mockImplementation(() => {}) + const writeFileSpy = jest.spyOn(fs, 'writeFile') + writeFileSpy.mockImplementation(() => {}) + + percyInstance.createPercyConfig().then(() => { + expect(writeFileSpy).toBeCalledTimes(1) + expect(PercyLoggerDebugSpy).toBeCalledTimes(1) + }).catch(() => { + }) + }) + it('should return valid response', async () => { + percyInstance['_options'] = { + percyOptions: { + version: null + } + } + const PercyLoggerErrorSpy = jest.spyOn(PercyLogger.PercyLogger, 'error') + PercyLoggerErrorSpy.mockImplementation(() => {}) + const PercyLoggerDebugSpy = jest.spyOn(PercyLogger.PercyLogger, 'debug') + PercyLoggerDebugSpy.mockImplementation(() => {}) + const writeFileSpy = jest.spyOn(fs, 'writeFile') + writeFileSpy.mockImplementation(() => {}) + + percyInstance.createPercyConfig().then(() => { + expect(writeFileSpy).toBeCalledTimes(1) + expect(PercyLoggerErrorSpy).toBeCalledTimes(1) + + expect(PercyLoggerDebugSpy).toBeCalledTimes(0) + }).catch(() => { + }) + }) + }) + + describe('stop method', () => { + + afterEach(() => { + jest.clearAllMocks() + }) + it('should stop', async () => { + const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') + const spawnSpy = jest.spyOn(ChildProcess.prototype, 'spawn') + + percyInstance.stop().then(() => { + expect(getBinaryPathSpy).toBeCalledTimes(1) + expect(percyInstance['isProcessRunning']).toEqual(false) + expect(spawnSpy).toBeCalledTimes(1) + }).catch(() => { + }) + }) + }) + + describe('start method', () => { + afterEach(() => { + jest.clearAllMocks() + }) + it('should return false when token is not there', async () => { + percyInstance['_logfile'] = 'log_file' + const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') + const logInfoSpy = jest.spyOn(fs, 'createWriteStream') + const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue(null) + + const res = await percyInstance.start() + expect(res).toEqual(false) + expect(getBinaryPathSpy).toBeCalledTimes(1) + expect(logInfoSpy).toBeCalledTimes(1) + expect(fetchPercyTokenSpy).toBeCalledTimes(1) + + }) + + it('should return false when token is health check false', async () => { + percyInstance['_logfile'] = 'log_file' + const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') + const logInfoSpy = jest.spyOn(fs, 'createWriteStream') + const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue('token') + const createPercyConfigSpy = jest.spyOn(percyInstance, 'createPercyConfig').mockReturnValue('config_path') + const healthcheckSpy = jest.spyOn(percyInstance, 'healthcheck').mockReturnValue(false) + const healthcheckSpy2 = jest.spyOn(percyInstance, 'healthcheck').mockReturnValue(true) + + percyInstance['isProcessRunning'] = true + + const res = await percyInstance.start() + expect(res).toEqual(true) + expect(getBinaryPathSpy).toBeCalledTimes(1) + expect(logInfoSpy).toBeCalledTimes(1) + expect(fetchPercyTokenSpy).toBeCalledTimes(1) + expect(createPercyConfigSpy).toBeCalledTimes(1) + expect(healthcheckSpy).toBeCalledTimes(1) + expect(healthcheckSpy2).toBeCalledTimes(1) + + }) + + it('should return true when token is there', async () => { + percyInstance['_logfile'] = 'log_file' + const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') + const logInfoSpy = jest.spyOn(fs, 'createWriteStream') + const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue('token') + const createPercyConfigSpy = jest.spyOn(percyInstance, 'createPercyConfig').mockReturnValue('config_path') + const sleepSpy = jest.spyOn(percyInstance, 'sleep') + + const res = await percyInstance.start() + expect(res).toEqual(true) + expect(getBinaryPathSpy).toBeCalledTimes(1) + expect(logInfoSpy).toBeCalledTimes(1) + expect(fetchPercyTokenSpy).toBeCalledTimes(1) + expect(createPercyConfigSpy).toBeCalledTimes(1) + expect(sleepSpy).toBeCalledTimes(0) + + }) + }) }) From 60229e2b45c6da507e685d76bd8a7ddaee4f0d59 Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 22 Jan 2024 11:14:37 +0530 Subject: [PATCH 27/38] tests and minor fixes --- .../wdio-browserstack-service/src/launcher.ts | 1 - .../tests/Percy-Handler.test.ts | 12 ----- ...ercyBinary.test.js => PercyBinary.test.ts} | 48 ++++++++++++++++++- .../tests/PercyHelper.test.ts | 12 ----- 4 files changed, 46 insertions(+), 27 deletions(-) rename packages/wdio-browserstack-service/tests/{PercyBinary.test.js => PercyBinary.test.ts} (73%) diff --git a/packages/wdio-browserstack-service/src/launcher.ts b/packages/wdio-browserstack-service/src/launcher.ts index e1693cb9c9e..d5a15659a1d 100644 --- a/packages/wdio-browserstack-service/src/launcher.ts +++ b/packages/wdio-browserstack-service/src/launcher.ts @@ -152,7 +152,6 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } } - /* eslint-disable @typescript-eslint/no-unused-vars */ async onWorkerStart (cid: any, caps: any) { try { if (this._options.percy && this._percyBestPlatformCaps) { diff --git a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts index 2b0ff107310..f333da6d5e9 100644 --- a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts +++ b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts @@ -1,6 +1,5 @@ /// -import got from 'got' import logger from '@wdio/logger' import PercyHandler from '../src/Percy/Percy-Handler' @@ -17,7 +16,6 @@ let percyHandler: PercyHandler let browser: Browser<'async'> | MultiRemoteBrowser<'async'> let caps: Capabilities.RemoteCapability -jest.mock('got') // jest.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) jest.useFakeTimers().setSystemTime(new Date('2020-01-01')) jest.mock('uuid', () => ({ v4: () => '123456789' })) @@ -27,16 +25,6 @@ PercyLoggerSpy.mockImplementation(() => {}) beforeEach(() => { jest.mocked(log.info).mockClear() - jest.mocked(got).mockClear() - jest.mocked(got.put).mockClear() - jest.mocked(got).mockResolvedValue({ - body: { - automation_session: { - browser_url: 'https://www.browserstack.com/automate/builds/1/sessions/2' - } - } - }) - jest.mocked(got.put).mockResolvedValue({}) browser = { sessionId: 'session123', diff --git a/packages/wdio-browserstack-service/tests/PercyBinary.test.js b/packages/wdio-browserstack-service/tests/PercyBinary.test.ts similarity index 73% rename from packages/wdio-browserstack-service/tests/PercyBinary.test.js rename to packages/wdio-browserstack-service/tests/PercyBinary.test.ts index e27377fcb76..f0871aa2416 100644 --- a/packages/wdio-browserstack-service/tests/PercyBinary.test.js +++ b/packages/wdio-browserstack-service/tests/PercyBinary.test.ts @@ -1,7 +1,17 @@ import PercyBinary from '../src/Percy/PercyBinary' +import got from 'got' +import yauzl from 'yauzl' + + +import path from 'path/win32' +import fs, { WriteStream } from 'fs' +import { resolve } from 'path' + +import childProcess from 'node:child_process' + // Mocking dependencies -jest.mock('got') + jest.mock('node:fs/promises', () => ({ access: jest.fn(), mkdir: jest.fn().mockResolvedValue(true), @@ -12,6 +22,14 @@ jest.mock('node:fs', () => ({ })) jest.mock('yauzl') +jest.mock('node:child_process', () => ({ + spawn: jest.fn(), +})) + +beforeEach(() => { + jest.clearAllMocks() +}) + describe('PercyBinary', () => { describe('makePath', () => { it('should create a path if it does not exist', async () => { @@ -42,7 +60,8 @@ describe('PercyBinary', () => { const result = await percyBinary.getBinaryPath() expect(getAvailableDirsSpy).toBeCalledTimes(1) expect(checkPathSpy).toBeCalledTimes(1) - expect(result).toBe('some_path/percy') + expect(result).toContain('some_path') + expect(result).toContain('percy') }) it('should getBinaryPath from first download try', async () => { const percyBinary = new PercyBinary() @@ -74,4 +93,29 @@ describe('PercyBinary', () => { expect(result).toBe('download_path') }) }) + + + describe('validateBinary', () => { + it('should resolve to true for a valid binary version', async () => { + const percyBinary = new PercyBinary() + const validVersionOutput = '@percy/cli 1.2.3' + const mockSpawn = { + stdout: { + on: jest.fn().mockImplementation((data, cb) => { + cb(validVersionOutput) + }) + }, + on: jest.fn().mockImplementation((close, cb) => { + cb(false) + }) + } + ;(childProcess.spawn as jest.Mock).mockClear() + ;(childProcess.spawn as jest.Mock).mockReturnValue(mockSpawn) + + percyBinary.validateBinary(validVersionOutput).then(() => { + }).catch(() => { + }) + + }) + }) }) diff --git a/packages/wdio-browserstack-service/tests/PercyHelper.test.ts b/packages/wdio-browserstack-service/tests/PercyHelper.test.ts index 8921f5133fe..5f5b64a57c5 100644 --- a/packages/wdio-browserstack-service/tests/PercyHelper.test.ts +++ b/packages/wdio-browserstack-service/tests/PercyHelper.test.ts @@ -1,5 +1,4 @@ /// -import got from 'got' import logger from '@wdio/logger' import * as PercyHelper from '../src/Percy/PercyHelper' @@ -11,7 +10,6 @@ import { Browser, MultiRemoteBrowser } from 'webdriverio' const log = logger('test') let browser: Browser<'async'> | MultiRemoteBrowser<'async'> -jest.mock('got') // jest.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) jest.useFakeTimers().setSystemTime(new Date('2020-01-01')) jest.mock('uuid', () => ({ v4: () => '123456789' })) @@ -21,16 +19,6 @@ PercyLoggerSpy.mockImplementation(() => {}) beforeEach(() => { jest.mocked(log.info).mockClear() - jest.mocked(got).mockClear() - jest.mocked(got.put).mockClear() - jest.mocked(got).mockResolvedValue({ - body: { - automation_session: { - browser_url: 'https://www.browserstack.com/automate/builds/1/sessions/2' - } - } - }) - jest.mocked(got.put).mockResolvedValue({}) browser = { sessionId: 'session123', From 8d3ac0ef2617c6824199741a5179644ea56f8a88 Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 22 Jan 2024 11:16:18 +0530 Subject: [PATCH 28/38] linting fix --- .../tests/PercyBinary.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/PercyBinary.test.ts b/packages/wdio-browserstack-service/tests/PercyBinary.test.ts index f0871aa2416..cbf87a70bf7 100644 --- a/packages/wdio-browserstack-service/tests/PercyBinary.test.ts +++ b/packages/wdio-browserstack-service/tests/PercyBinary.test.ts @@ -1,13 +1,4 @@ import PercyBinary from '../src/Percy/PercyBinary' - -import got from 'got' -import yauzl from 'yauzl' - - -import path from 'path/win32' -import fs, { WriteStream } from 'fs' -import { resolve } from 'path' - import childProcess from 'node:child_process' // Mocking dependencies @@ -94,7 +85,6 @@ describe('PercyBinary', () => { }) }) - describe('validateBinary', () => { it('should resolve to true for a valid binary version', async () => { const percyBinary = new PercyBinary() From 5c28b5018728305955fdfe4b24e82b6fe15eb8fa Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 22 Jan 2024 14:54:46 +0530 Subject: [PATCH 29/38] percy test fixes --- .../tests/{Percy.test.js => Percy.test.ts} | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) rename packages/wdio-browserstack-service/tests/{Percy.test.js => Percy.test.ts} (81%) diff --git a/packages/wdio-browserstack-service/tests/Percy.test.js b/packages/wdio-browserstack-service/tests/Percy.test.ts similarity index 81% rename from packages/wdio-browserstack-service/tests/Percy.test.js rename to packages/wdio-browserstack-service/tests/Percy.test.ts index c67518d1a39..0cbfdce76ba 100644 --- a/packages/wdio-browserstack-service/tests/Percy.test.js +++ b/packages/wdio-browserstack-service/tests/Percy.test.ts @@ -1,20 +1,22 @@ import Percy from '../src/Percy/Percy' import * as PercyLogger from '../src/Percy/PercyLogger' - +import childProcess from 'node:child_process' import * as utils from '../src/util' import fs from 'fs' -import { ChildProcess } from 'child_process' +jest.mock('node:child_process', () => ({ + spawn: jest.fn(), +})) describe('Percy Class', () => { let percyInstance beforeEach(() => { - percyInstance = new Percy({}, {}, { projectName: 'testProject' }) jest.clearAllMocks() }) describe('Constructor', () => { it('should initialize Percy instance', () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) expect(percyInstance._options).toEqual({}) expect(percyInstance._config).toEqual({}) expect(percyInstance._isApp).toBe(false) @@ -24,6 +26,7 @@ describe('Percy Class', () => { describe('getBinaryPath method', () => { it('should return binary path if already present', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_binaryPath'] = 'some_path' const result = await percyInstance.getBinaryPath() @@ -32,14 +35,15 @@ describe('Percy Class', () => { }) describe('running method', () => { - // const getBinaryPathSpy = jest.spyOn(Percy.prototype, 'getBinaryPath') it('should return true if running', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['isProcessRunning'] = true const res = await percyInstance.isRunning() expect(res).toEqual(true) }) it('should return false if running', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['isProcessRunning'] = false const res = await percyInstance.isRunning() expect(res).toEqual(false) @@ -48,12 +52,14 @@ describe('Percy Class', () => { describe('health check method', () => { it('should return true if running', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) const nodeRequestSpy = jest.spyOn(utils, 'nodeRequest').mockReturnValue(true) const res = await percyInstance.healthcheck() expect(nodeRequestSpy).toBeCalledTimes(1) expect(res).toEqual(true) }) it('should return false if running', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) const nodeRequestSpy = jest.spyOn(utils, 'nodeRequest').mockReturnValue(false) const res = await percyInstance.healthcheck() expect(nodeRequestSpy).toBeCalledTimes(1) @@ -63,6 +69,7 @@ describe('Percy Class', () => { describe('fetchPercyToken method', () => { it('should return false if running', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_projectName'] = 'project_name' percyInstance['_isApp'] = true const response = { @@ -79,7 +86,9 @@ describe('Percy Class', () => { afterEach(() => { jest.clearAllMocks() }) + it('should return early null if percyOptions is null', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_options'] = { percyOptions: null } @@ -88,6 +97,7 @@ describe('Percy Class', () => { }) it('should return valid response', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_options'] = { percyOptions: { version: null @@ -105,6 +115,7 @@ describe('Percy Class', () => { }) }) it('should return valid response', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_options'] = { percyOptions: { version: null @@ -133,13 +144,12 @@ describe('Percy Class', () => { jest.clearAllMocks() }) it('should stop', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') - const spawnSpy = jest.spyOn(ChildProcess.prototype, 'spawn') percyInstance.stop().then(() => { expect(getBinaryPathSpy).toBeCalledTimes(1) expect(percyInstance['isProcessRunning']).toEqual(false) - expect(spawnSpy).toBeCalledTimes(1) }).catch(() => { }) }) @@ -150,6 +160,7 @@ describe('Percy Class', () => { jest.clearAllMocks() }) it('should return false when token is not there', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_logfile'] = 'log_file' const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') const logInfoSpy = jest.spyOn(fs, 'createWriteStream') @@ -164,11 +175,27 @@ describe('Percy Class', () => { }) it('should return false when token is health check false', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_logfile'] = 'log_file' const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') const logInfoSpy = jest.spyOn(fs, 'createWriteStream') const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue('token') const createPercyConfigSpy = jest.spyOn(percyInstance, 'createPercyConfig').mockReturnValue('config_path') + + const mockSpawn = { + stdout: { + pipe: jest.fn() + }, + stderr: { + pipe: jest.fn() + }, + on: jest.fn().mockImplementation((close, cb) => { + cb() + }) + } + ;(childProcess.spawn as jest.Mock).mockClear() + ;(childProcess.spawn as jest.Mock).mockReturnValue(mockSpawn) + const healthcheckSpy = jest.spyOn(percyInstance, 'healthcheck').mockReturnValue(false) const healthcheckSpy2 = jest.spyOn(percyInstance, 'healthcheck').mockReturnValue(true) @@ -186,8 +213,11 @@ describe('Percy Class', () => { }) it('should return true when token is there', async () => { + percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_logfile'] = 'log_file' - const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') + const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath').mockReturnValue('getBinaryPath_path') + // const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') + const logInfoSpy = jest.spyOn(fs, 'createWriteStream') const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue('token') const createPercyConfigSpy = jest.spyOn(percyInstance, 'createPercyConfig').mockReturnValue('config_path') From 9dbc50f94abdd55249004a570a78831e646a62ba Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 22 Jan 2024 15:30:14 +0530 Subject: [PATCH 30/38] percy test fixes --- packages/wdio-browserstack-service/tests/Percy.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/Percy.test.ts b/packages/wdio-browserstack-service/tests/Percy.test.ts index 0cbfdce76ba..bd08b4045d5 100644 --- a/packages/wdio-browserstack-service/tests/Percy.test.ts +++ b/packages/wdio-browserstack-service/tests/Percy.test.ts @@ -145,7 +145,7 @@ describe('Percy Class', () => { }) it('should stop', async () => { percyInstance = new Percy({}, {}, { projectName: 'testProject' }) - const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') + const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath').mockReturnValue('mock_binary_path') percyInstance.stop().then(() => { expect(getBinaryPathSpy).toBeCalledTimes(1) @@ -162,7 +162,7 @@ describe('Percy Class', () => { it('should return false when token is not there', async () => { percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_logfile'] = 'log_file' - const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') + const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath').mockReturnValue('mock_binary_path') const logInfoSpy = jest.spyOn(fs, 'createWriteStream') const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue(null) @@ -177,7 +177,7 @@ describe('Percy Class', () => { it('should return false when token is health check false', async () => { percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_logfile'] = 'log_file' - const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') + const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath').mockReturnValue('mock_binary_path') const logInfoSpy = jest.spyOn(fs, 'createWriteStream') const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue('token') const createPercyConfigSpy = jest.spyOn(percyInstance, 'createPercyConfig').mockReturnValue('config_path') @@ -216,7 +216,6 @@ describe('Percy Class', () => { percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_logfile'] = 'log_file' const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath').mockReturnValue('getBinaryPath_path') - // const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath') const logInfoSpy = jest.spyOn(fs, 'createWriteStream') const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue('token') From 86b507e22a1d577d4df75a5c8a3977d61b3486ee Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 22 Jan 2024 18:38:36 +0530 Subject: [PATCH 31/38] percy test added --- .../tests/PercyBinary.test.ts | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/wdio-browserstack-service/tests/PercyBinary.test.ts b/packages/wdio-browserstack-service/tests/PercyBinary.test.ts index cbf87a70bf7..1d45267eea0 100644 --- a/packages/wdio-browserstack-service/tests/PercyBinary.test.ts +++ b/packages/wdio-browserstack-service/tests/PercyBinary.test.ts @@ -1,5 +1,8 @@ import PercyBinary from '../src/Percy/PercyBinary' import childProcess from 'node:child_process' +import yauzl from 'yauzl' +import got from 'got' +import path from 'node:path' // Mocking dependencies @@ -8,7 +11,11 @@ jest.mock('node:fs/promises', () => ({ mkdir: jest.fn().mockResolvedValue(true), })) jest.mock('node:fs', () => ({ - createWriteStream: jest.fn(), + createWriteStream: jest.fn().mockImplementation(() => { + return { + close: jest.fn() + } + }), chmod: jest.fn().mockImplementation((_, __, callback) => callback()), })) jest.mock('yauzl') @@ -105,7 +112,69 @@ describe('PercyBinary', () => { percyBinary.validateBinary(validVersionOutput).then(() => { }).catch(() => { }) + }) + }) + + describe('download', () => { + it('should download', async () => { + const percyBinary = new PercyBinary() + percyBinary['_binaryName'] = 'binary_name' + jest.spyOn(percyBinary, 'checkPath').mockReturnValue(true) + + const mockedGot = jest.mocked(got) + const mockReadStream = { + on: jest.fn().mockImplementation((event, entry) => { + entry({ fileName: 'filename' }) + }), + pipe: jest.fn() + } + + const mockZipFile = { + on: jest.fn().mockImplementation((event, entry) => { + if (event === 'entry'){ + entry({ + fileName: 'filename' + }) + } + }), + readEntry: jest.fn(), + openReadStream: jest.fn().mockImplementation((event, readStream) => { + readStream(null, mockReadStream) + }), + close: jest.fn(), + once: jest.fn().mockImplementation((event, end) => { + end() + }), + } + jest.spyOn(yauzl, 'open').mockImplementation((event, arg1, arg2) => { + arg2(null, mockZipFile) + }) + + const stream = { + on: jest.fn().mockImplementation((event, error) => { + error() + }), + pipe: jest.fn().mockReturnValue({ + on: jest.fn().mockImplementation((event, finish) => { + finish() + }), + }) + } + + mockedGot.get = jest.fn().mockReturnValue(stream) + + jest.mock('got', () => ({ + extend: jest.fn().mockImplementation(() => new Promise(() => { + resolve(mockedGot) + })) + })) + + mockedGot.extend = jest.fn().mockReturnValue({ + get: () => stream + }) + jest.spyOn(path, 'join') + await percyBinary.download('conf', 'dir_path') }) }) }) From 1ca8aa12db8f5f70490467efddeff9b9f794cfb7 Mon Sep 17 00:00:00 2001 From: riya Date: Mon, 22 Jan 2024 20:35:37 +0530 Subject: [PATCH 32/38] tests for utils and launcher --- .../tests/launcher.test.ts | 16 ++++++++++++++++ .../wdio-browserstack-service/tests/util.test.ts | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/wdio-browserstack-service/tests/launcher.test.ts b/packages/wdio-browserstack-service/tests/launcher.test.ts index f776af3a3d8..8b7cef70639 100644 --- a/packages/wdio-browserstack-service/tests/launcher.test.ts +++ b/packages/wdio-browserstack-service/tests/launcher.test.ts @@ -1239,3 +1239,19 @@ describe('_updateLocalBuildCache', () => { expect(writeFileSyncSpy).not.toHaveBeenCalled() }) }) + +describe('onWorkerStart', () => { + const options: BrowserstackConfig = { app: '/path/to/app.apk', percy: true } + const caps: any = [{}] + const config = { + user: 'foobaruser', + key: '12345', + capabilities: [] + } + + it('should upload the app and return app_url', async() => { + const service = new BrowserstackLauncher(options, caps, config) + service['_percyBestPlatformCaps'] = {} + await service.onWorkerStart() + }) +}) diff --git a/packages/wdio-browserstack-service/tests/util.test.ts b/packages/wdio-browserstack-service/tests/util.test.ts index 9192ed2eff8..752c9e89d61 100644 --- a/packages/wdio-browserstack-service/tests/util.test.ts +++ b/packages/wdio-browserstack-service/tests/util.test.ts @@ -1397,3 +1397,16 @@ describe('getBrowserStackKey', function () { }) }) +describe('ObjectsAreEqual', function () { + it('should return true for equal values ', function () { + expect(utils.ObjectsAreEqual({ 'a': true }, { 'a': true })).toEqual(true) + }) + + it('should return false for unequal lengths', function () { + expect(utils.ObjectsAreEqual({ 'a': true }, { 'a': true, 'b': false })).toEqual(false) + }) + + it('should return false for unequal values', function () { + expect(utils.ObjectsAreEqual({ 'a': true }, { 'b': false })).toEqual(false) + }) +}) From 07d143fd5364ed661f0d0a4485b5053db5015b88 Mon Sep 17 00:00:00 2001 From: riya Date: Tue, 23 Jan 2024 04:14:02 +0530 Subject: [PATCH 33/38] tests fix for launcher --- .../tests/launcher.test.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/launcher.test.ts b/packages/wdio-browserstack-service/tests/launcher.test.ts index 8b7cef70639..f776af3a3d8 100644 --- a/packages/wdio-browserstack-service/tests/launcher.test.ts +++ b/packages/wdio-browserstack-service/tests/launcher.test.ts @@ -1239,19 +1239,3 @@ describe('_updateLocalBuildCache', () => { expect(writeFileSyncSpy).not.toHaveBeenCalled() }) }) - -describe('onWorkerStart', () => { - const options: BrowserstackConfig = { app: '/path/to/app.apk', percy: true } - const caps: any = [{}] - const config = { - user: 'foobaruser', - key: '12345', - capabilities: [] - } - - it('should upload the app and return app_url', async() => { - const service = new BrowserstackLauncher(options, caps, config) - service['_percyBestPlatformCaps'] = {} - await service.onWorkerStart() - }) -}) From dec59dac93ffd6bc7a6db6caf45d08421184bc87 Mon Sep 17 00:00:00 2001 From: riya Date: Tue, 23 Jan 2024 06:40:25 +0530 Subject: [PATCH 34/38] tests fix --- package-lock.json | 19 ------ package.json | 1 - .../tests/Percy.test.ts | 13 ++-- .../tests/launcher.test.ts | 64 +++++++++++++++++++ 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ff133da6e5..a453c583aad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@types/archiver": "^6.0.0", "@types/cheerio": "^0.22.31", "@types/eslint": "^8.4.2", - "@types/follow-redirects": "^1.14.4", "@types/fs-extra": "^11.0.1", "@types/jest": "^28.1.1", "@types/lodash.clonedeep": "^4.5.6", @@ -4334,15 +4333,6 @@ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, - "node_modules/@types/follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -22585,15 +22575,6 @@ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, - "@types/follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", diff --git a/package.json b/package.json index 1e60948dcef..0f6a927670d 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "@types/archiver": "^6.0.0", "@types/cheerio": "^0.22.31", "@types/eslint": "^8.4.2", - "@types/follow-redirects": "^1.14.4", "@types/fs-extra": "^11.0.1", "@types/jest": "^28.1.1", "@types/lodash.clonedeep": "^4.5.6", diff --git a/packages/wdio-browserstack-service/tests/Percy.test.ts b/packages/wdio-browserstack-service/tests/Percy.test.ts index bd08b4045d5..c803b0b1d8d 100644 --- a/packages/wdio-browserstack-service/tests/Percy.test.ts +++ b/packages/wdio-browserstack-service/tests/Percy.test.ts @@ -6,6 +6,10 @@ import fs from 'fs' jest.mock('node:child_process', () => ({ spawn: jest.fn(), })) +jest.mock('fs', () => ({ + createWriteStream: jest.fn(), + writeFile: jest.fn(), +})) describe('Percy Class', () => { let percyInstance @@ -163,13 +167,11 @@ describe('Percy Class', () => { percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_logfile'] = 'log_file' const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath').mockReturnValue('mock_binary_path') - const logInfoSpy = jest.spyOn(fs, 'createWriteStream') const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue(null) const res = await percyInstance.start() expect(res).toEqual(false) expect(getBinaryPathSpy).toBeCalledTimes(1) - expect(logInfoSpy).toBeCalledTimes(1) expect(fetchPercyTokenSpy).toBeCalledTimes(1) }) @@ -178,7 +180,6 @@ describe('Percy Class', () => { percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_logfile'] = 'log_file' const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath').mockReturnValue('mock_binary_path') - const logInfoSpy = jest.spyOn(fs, 'createWriteStream') const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue('token') const createPercyConfigSpy = jest.spyOn(percyInstance, 'createPercyConfig').mockReturnValue('config_path') @@ -204,7 +205,6 @@ describe('Percy Class', () => { const res = await percyInstance.start() expect(res).toEqual(true) expect(getBinaryPathSpy).toBeCalledTimes(1) - expect(logInfoSpy).toBeCalledTimes(1) expect(fetchPercyTokenSpy).toBeCalledTimes(1) expect(createPercyConfigSpy).toBeCalledTimes(1) expect(healthcheckSpy).toBeCalledTimes(1) @@ -216,8 +216,6 @@ describe('Percy Class', () => { percyInstance = new Percy({}, {}, { projectName: 'testProject' }) percyInstance['_logfile'] = 'log_file' const getBinaryPathSpy = jest.spyOn(percyInstance, 'getBinaryPath').mockReturnValue('getBinaryPath_path') - - const logInfoSpy = jest.spyOn(fs, 'createWriteStream') const fetchPercyTokenSpy = jest.spyOn(percyInstance, 'fetchPercyToken').mockReturnValue('token') const createPercyConfigSpy = jest.spyOn(percyInstance, 'createPercyConfig').mockReturnValue('config_path') const sleepSpy = jest.spyOn(percyInstance, 'sleep') @@ -225,12 +223,9 @@ describe('Percy Class', () => { const res = await percyInstance.start() expect(res).toEqual(true) expect(getBinaryPathSpy).toBeCalledTimes(1) - expect(logInfoSpy).toBeCalledTimes(1) expect(fetchPercyTokenSpy).toBeCalledTimes(1) expect(createPercyConfigSpy).toBeCalledTimes(1) expect(sleepSpy).toBeCalledTimes(0) - }) }) - }) diff --git a/packages/wdio-browserstack-service/tests/launcher.test.ts b/packages/wdio-browserstack-service/tests/launcher.test.ts index f776af3a3d8..6085bdb9527 100644 --- a/packages/wdio-browserstack-service/tests/launcher.test.ts +++ b/packages/wdio-browserstack-service/tests/launcher.test.ts @@ -8,6 +8,10 @@ import BrowserstackLauncher from '../src/launcher' import { BrowserstackConfig } from '../src/types' import * as utils from '../src/util' +import * as PercyHelper from '../src/Percy/PercyHelper' +import * as PercyLogger from '../src/Percy/PercyLogger' +import Percy from '../src/Percy/Percy' + import fs from 'fs' // @ts-ignore @@ -15,6 +19,7 @@ import { version as bstackServiceVersion } from '../package.json' import { Testrunner } from '@wdio/types/build/Options' const expect = global.expect as unknown as jest.Expect +let _percy: Percy const log = logger('test') const error = new Error('I\'m an error!') @@ -1239,3 +1244,62 @@ describe('_updateLocalBuildCache', () => { expect(writeFileSyncSpy).not.toHaveBeenCalled() }) }) + +describe('setupPercy', () => { + const options: BrowserstackConfig & Testrunner = { capabilities: [] } + const config = { + user: 'foobaruser', + key: '12345', + capabilities: [] + } + const caps: any = [{ + 'bstack:options': { + buildName: 'browserstack wdio build test', + buildIdentifier: '#${BUILD_NUMBER}' + } + }] + const service = new BrowserstackLauncher({ percy: true }, caps, config) + + it('should return if percy is already running', async() => { + const PercyLoggerInfoSpy = jest.spyOn(PercyLogger.PercyLogger, 'info').mockImplementation((string) => string) + const start = jest.spyOn(PercyHelper, 'startPercy') + + await service.setupPercy(options, config, { + projectName: 'projectName' + }) + expect(start).toBeCalledTimes(1) + expect(PercyLoggerInfoSpy).toBeCalledTimes(1) + }) +}) + +describe('stopPercy', () => { + const options: BrowserstackConfig & Testrunner = { capabilities: [] } + const config = { + user: 'foobaruser', + key: '12345', + capabilities: [] + } + const caps: any = [{ + 'bstack:options': { + buildName: 'browserstack wdio build test', + buildIdentifier: '#${BUILD_NUMBER}' + } + }] + _percy = new Percy(options, config, {}) + const service = new BrowserstackLauncher({ percy: true }, caps, config) + + it('should return if percy is not defined', async() => { + service.stopPercy() + }) + it('should return if percy is already running', async() => { + service._percy = _percy + const mockPercyisRunning = jest.spyOn(_percy, 'isRunning').mockImplementation(() => (true)) + const PercyLoggerInfoSpy = jest.spyOn(PercyLogger.PercyLogger, 'info').mockImplementation((string) => string) + const mockPercyStart = jest.spyOn(PercyHelper, 'stopPercy') + + await service.stopPercy() + expect(mockPercyisRunning).toBeCalledTimes(1) + expect(mockPercyStart).toBeCalledTimes(1) + expect(PercyLoggerInfoSpy).toBeCalledTimes(1) + }) +}) \ No newline at end of file From cf64e96ab66f62e7744d8d30a94995db02b9ba95 Mon Sep 17 00:00:00 2001 From: riya Date: Tue, 23 Jan 2024 07:01:33 +0530 Subject: [PATCH 35/38] tests fix --- .../wdio-browserstack-service/tests/launcher.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/launcher.test.ts b/packages/wdio-browserstack-service/tests/launcher.test.ts index 6085bdb9527..07240052b2c 100644 --- a/packages/wdio-browserstack-service/tests/launcher.test.ts +++ b/packages/wdio-browserstack-service/tests/launcher.test.ts @@ -19,7 +19,6 @@ import { version as bstackServiceVersion } from '../package.json' import { Testrunner } from '@wdio/types/build/Options' const expect = global.expect as unknown as jest.Expect -let _percy: Percy const log = logger('test') const error = new Error('I\'m an error!') @@ -1246,6 +1245,9 @@ describe('_updateLocalBuildCache', () => { }) describe('setupPercy', () => { + beforeEach(() => { + jest.clearAllMocks() + }) const options: BrowserstackConfig & Testrunner = { capabilities: [] } const config = { user: 'foobaruser', @@ -1262,7 +1264,9 @@ describe('setupPercy', () => { it('should return if percy is already running', async() => { const PercyLoggerInfoSpy = jest.spyOn(PercyLogger.PercyLogger, 'info').mockImplementation((string) => string) - const start = jest.spyOn(PercyHelper, 'startPercy') + const mockPercy = new Percy(options, config, {}) + + const start = jest.spyOn(PercyHelper, 'startPercy').mockReturnValue(mockPercy) await service.setupPercy(options, config, { projectName: 'projectName' @@ -1273,6 +1277,7 @@ describe('setupPercy', () => { }) describe('stopPercy', () => { + let _percy: Percy const options: BrowserstackConfig & Testrunner = { capabilities: [] } const config = { user: 'foobaruser', @@ -1295,7 +1300,7 @@ describe('stopPercy', () => { service._percy = _percy const mockPercyisRunning = jest.spyOn(_percy, 'isRunning').mockImplementation(() => (true)) const PercyLoggerInfoSpy = jest.spyOn(PercyLogger.PercyLogger, 'info').mockImplementation((string) => string) - const mockPercyStart = jest.spyOn(PercyHelper, 'stopPercy') + const mockPercyStart = jest.spyOn(PercyHelper, 'stopPercy').mockImplementation() await service.stopPercy() expect(mockPercyisRunning).toBeCalledTimes(1) From d77dd450140ecc436cb0c3a09895bb97f6fa0a0a Mon Sep 17 00:00:00 2001 From: riya Date: Tue, 23 Jan 2024 08:58:04 +0530 Subject: [PATCH 36/38] tests fix --- .../tests/Percy-Handler.test.ts | 10 ++++------ .../tests/launcher.test.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts index f333da6d5e9..a63679b133c 100644 --- a/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts +++ b/packages/wdio-browserstack-service/tests/Percy-Handler.test.ts @@ -69,6 +69,9 @@ it('should initialize correctly', () => { expect(percyHandler['_capabilities']).toEqual(caps) expect(percyHandler['_framework']).toEqual('framework') expect(percyHandler['_percyScreenshotCounter']).toEqual(0) + percyHandler['_percyAutoCaptureMode'] = 'auto' + expect(percyHandler['_percyAutoCaptureMode']).toEqual('auto') + }) describe('_setSessionName', () => { @@ -215,20 +218,17 @@ describe('teardown', () => { it('resolves promise if _percyScreenshotCounter is 0', async () => { percyHandler.teardown().then(() => { expect(percyHandler['_percyScreenshotCounter']).toEqual(0) - /* eslint-disable @typescript-eslint/no-unused-vars */ - }).catch((err: any) => { + }).catch(() => { expect(percyHandler['_percyScreenshotCounter']).not.equal(0) }) }) }) describe('browserBeforeCommand', () => { - let isDOMChangingCommandSpy: any let percyHandler: PercyHandler beforeEach(() => { percyHandler = new PercyHandler('auto', browser, caps, false, 'framework') - // isDOMChangingCommandSpy = jest.spyOn(PercyHandler.prototype, 'isDOMChangingCommand').mockReturnValue(true) }) it('should call browserBeforeCommand', async () => { @@ -241,11 +241,9 @@ describe('browserBeforeCommand', () => { } } await percyHandler.browserBeforeCommand(args as BeforeCommandArgs & AfterCommandArgs) - // expect(isDOMChangingCommandSpy).toBeCalledTimes(1) }) afterEach(() => { - // isDOMChangingCommandSpy.mockClear() }) }) diff --git a/packages/wdio-browserstack-service/tests/launcher.test.ts b/packages/wdio-browserstack-service/tests/launcher.test.ts index 07240052b2c..256db9bfb69 100644 --- a/packages/wdio-browserstack-service/tests/launcher.test.ts +++ b/packages/wdio-browserstack-service/tests/launcher.test.ts @@ -1307,4 +1307,23 @@ describe('stopPercy', () => { expect(mockPercyStart).toBeCalledTimes(1) expect(PercyLoggerInfoSpy).toBeCalledTimes(1) }) +}) + +describe('onWorkerStart', () => { + const options: BrowserstackConfig = { app: '/path/to/app.apk', percy: true } + const caps: any = [{}] + const config = { + user: 'foobaruser', + key: '12345', + capabilities: [] + } + let _percy: Percy + _percy = new Percy(options, config, {}) + + it('should upload the app and return app_url', async() => { + const service = new BrowserstackLauncher(options, caps, config) + service._percy = _percy + service['_percyBestPlatformCaps'] = {} + await service.onWorkerStart() + }) }) \ No newline at end of file From f56cadc90172b8efab8a0280fe6a76517ef197ea Mon Sep 17 00:00:00 2001 From: riya Date: Tue, 23 Jan 2024 09:21:49 +0530 Subject: [PATCH 37/38] tests fix --- .../tests/launcher.test.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/wdio-browserstack-service/tests/launcher.test.ts b/packages/wdio-browserstack-service/tests/launcher.test.ts index 256db9bfb69..47be413ba16 100644 --- a/packages/wdio-browserstack-service/tests/launcher.test.ts +++ b/packages/wdio-browserstack-service/tests/launcher.test.ts @@ -1308,22 +1308,3 @@ describe('stopPercy', () => { expect(PercyLoggerInfoSpy).toBeCalledTimes(1) }) }) - -describe('onWorkerStart', () => { - const options: BrowserstackConfig = { app: '/path/to/app.apk', percy: true } - const caps: any = [{}] - const config = { - user: 'foobaruser', - key: '12345', - capabilities: [] - } - let _percy: Percy - _percy = new Percy(options, config, {}) - - it('should upload the app and return app_url', async() => { - const service = new BrowserstackLauncher(options, caps, config) - service._percy = _percy - service['_percyBestPlatformCaps'] = {} - await service.onWorkerStart() - }) -}) \ No newline at end of file From 26ac1f542362585e0612b0e06d338f00b6473d73 Mon Sep 17 00:00:00 2001 From: riya Date: Wed, 7 Feb 2024 08:15:24 +0530 Subject: [PATCH 38/38] coverage threshold change --- jest.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jest.config.js b/jest.config.js index d941994de19..90e4229f6ae 100644 --- a/jest.config.js +++ b/jest.config.js @@ -53,10 +53,10 @@ module.exports = { collectCoverage: true, coverageThreshold: { global: { - branches: 85.5, + branches: 85.15, functions: 92, - lines: 94.5, - statements: 94.5 + lines: 94.34, + statements: 94.19 } }, testEnvironment: 'node',