Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
32807ed
add percy support for wdio v7
amaanbs Dec 11, 2023
f14e3bc
ts-lint fixes
amaanbs Dec 11, 2023
b3d6b89
minor fixes
amaanbs Dec 12, 2023
63a0001
minor fixes 2
amaanbs Dec 12, 2023
cf5d47c
PR review fixes
amaanbs Dec 13, 2023
29daea0
add unit tests
amaanbs Dec 14, 2023
883d46e
remove unused imports
amaanbs Dec 14, 2023
2d6ebe8
minor fixes + add package-lock.json
amaanbs Dec 14, 2023
0af2198
ts-lint fixes
amaanbs Dec 14, 2023
6e084d4
Merge branch 'v7' of github.com:amaanbs/webdriverio into wdio_percy_s…
amaanbs Dec 19, 2023
8d99528
Stabilization + PR review fixes
amaanbs Dec 19, 2023
f7fff5e
master merge and conflict resolution
rev-doshi Jan 8, 2024
d648b98
fixes related to types, return early and private fields
rev-doshi Jan 8, 2024
ed84de3
master merge
rev-doshi Jan 9, 2024
d6dc372
Merge branch 'v7' of github.com:amaanbs/webdriverio into wdio_percy_s…
rev-doshi Jan 9, 2024
0121dc1
master merge
rev-doshi Jan 9, 2024
6f7e838
fixes
rev-doshi Jan 9, 2024
b5a2b16
resolved dependency check
rev-doshi Jan 9, 2024
de8c057
review changes
rev-doshi Jan 15, 2024
d25d9fa
review changes
rev-doshi Jan 15, 2024
ff064d1
test fixes
rev-doshi Jan 15, 2024
83767e4
test fixes
rev-doshi Jan 15, 2024
ec06ee8
Merge pull request #6 from rev-doshi/wdio_percy_support_v7_review_fixes
rev-doshi Jan 15, 2024
fdb0e15
added percy logger tests
rev-doshi Jan 17, 2024
845dc69
added percy logger tests
rev-doshi Jan 17, 2024
cce2d27
added linting fix
rev-doshi Jan 17, 2024
b3ff1fc
percy handler tests
rev-doshi Jan 17, 2024
48dc775
linting fix
rev-doshi Jan 18, 2024
9272da3
percy capture map tests
rev-doshi Jan 18, 2024
d70cef6
linting fix
rev-doshi Jan 18, 2024
13d78ca
linting fix
rev-doshi Jan 18, 2024
c14b1f1
percy test UTs added
rev-doshi Jan 18, 2024
60229e2
tests and minor fixes
rev-doshi Jan 22, 2024
8d3ac0e
linting fix
rev-doshi Jan 22, 2024
5c28b50
percy test fixes
rev-doshi Jan 22, 2024
9dbc50f
percy test fixes
rev-doshi Jan 22, 2024
86b507e
percy test added
rev-doshi Jan 22, 2024
1ca8aa1
tests for utils and launcher
rev-doshi Jan 22, 2024
07d143f
tests fix for launcher
rev-doshi Jan 22, 2024
dec59da
tests fix
rev-doshi Jan 23, 2024
cf64e96
tests fix
rev-doshi Jan 23, 2024
d77dd45
tests fix
rev-doshi Jan 23, 2024
f56cadc
tests fix
rev-doshi Jan 23, 2024
26ac1f5
coverage threshold change
rev-doshi Feb 7, 2024
de7b507
Merge branch 'v7' of github.com:amaanbs/webdriverio into wdio_percy_s…
rev-doshi Feb 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion packages/wdio-browserstack-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import type { Browser } from 'webdriverio'

declare interface BrowserAsync extends Browser<'async'> {
getAccessibilityResultsSummary: () => Promise<{ [key: string]: any; }>,
getAccessibilityResults: () => Promise<Array<{ [key: string]: any; }>>
getAccessibilityResults: () => Promise<Array<{ [key: string]: any; }>>,
percyCaptureMap: any
}
98 changes: 98 additions & 0 deletions packages/wdio-browserstack-service/src/Percy/Percy-Handler.ts
Original file line number Diff line number Diff line change
@@ -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<void>((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
176 changes: 176 additions & 0 deletions packages/wdio-browserstack-service/src/Percy/Percy.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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
Loading