Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
933fcae
Add support for Percy
amaanbs Dec 7, 2023
a007216
es-lint fixes
amaanbs Dec 7, 2023
c85e2b1
binary download fix
amaanbs Dec 8, 2023
d1cba84
Merge branch 'main' of github.com:amaanbs/webdriverio into wdio_percy…
amaanbs Dec 8, 2023
02a7706
resolve merge conflicts
amaanbs Dec 8, 2023
f165eaf
add error handling
amaanbs Dec 10, 2023
4f6d8ca
best platform for caps as objects
amaanbs Dec 10, 2023
ffca74b
minor fix
amaanbs Dec 11, 2023
f7a9ca1
minor fixes
amaanbs Dec 12, 2023
abd6493
added unit tests
amaanbs Dec 13, 2023
3047eb2
ts-lint fixes
amaanbs Dec 13, 2023
b29658a
PR review fixes
amaanbs Dec 13, 2023
497c10f
add package-lock.json
amaanbs Dec 13, 2023
f6ee31b
minor fixes
amaanbs Dec 14, 2023
f067682
Screenshot stabilization + default mode = auto
amaanbs Dec 19, 2023
dafa563
PR review fixes
amaanbs Dec 19, 2023
a7a9397
Merge branch 'main' of github.com:amaanbs/webdriverio into wdio_percy…
amaanbs Dec 19, 2023
42b9e06
Fix test failure import
amaanbs Dec 19, 2023
b8e8755
update tests
amaanbs Dec 19, 2023
7c7b1ab
PR review fixes
amaanbs Dec 21, 2023
6819cf2
Merge branch 'main' of github.com:amaanbs/webdriverio into wdio_percy…
amaanbs Dec 21, 2023
907de46
Merge branch 'main' of github.com:amaanbs/webdriverio into wdio_percy…
rev-doshi Jan 8, 2024
1315406
Merge branch 'main' of github.com:amaanbs/webdriverio into wdio_percy…
rev-doshi Jan 8, 2024
e8d6b94
review changes
rev-doshi Jan 4, 2024
4ab0126
linting fix
rev-doshi Jan 4, 2024
e9279bb
import fix
rev-doshi Jan 4, 2024
7811083
import fix
rev-doshi Jan 4, 2024
08c222d
test fixes and using # prefix for private fields
rev-doshi Jan 8, 2024
52b38d9
Revert "test fixes and using # prefix for private fields"
rev-doshi Jan 8, 2024
a0b80e5
test fix
rev-doshi Jan 8, 2024
b438b1f
review comments fix
rev-doshi Jan 9, 2024
425e941
percy packages changes
rev-doshi Jan 9, 2024
7b07fa3
Merge pull request #3 from rev-doshi/wdio_percy_support_v8
rev-doshi Jan 9, 2024
5d5d033
conflict resolution
rev-doshi Jan 9, 2024
436e132
package-lock fix
rev-doshi Jan 9, 2024
463e3b4
review changes
rev-doshi Jan 10, 2024
7b4503c
review changes
rev-doshi Jan 10, 2024
68e13ce
review changes
rev-doshi Jan 11, 2024
fdfcf82
Merge branch 'main' of github.com:amaanbs/webdriverio into wdio_percy…
rev-doshi Jan 11, 2024
77bc0fe
minor fix
rev-doshi Jan 11, 2024
250ed1d
review changes
rev-doshi Jan 11, 2024
0f01b16
master merge and UT fix
rev-doshi Jan 12, 2024
a84203b
master merge
rev-doshi Jan 12, 2024
d39f702
minor fix
rev-doshi Jan 12, 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
7 changes: 6 additions & 1 deletion packages/wdio-browserstack-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,23 @@
"browserstack-local": "^1.5.1",
"chalk": "^5.3.0",
"csv-writer": "^1.6.0",
"follow-redirects": "^1.15.3",
"formdata-node": "5.0.1",
"git-repo-info": "^2.1.1",
"gitconfiglocal": "^2.1.0",
"got": "^12.6.1",
"uuid": "^9.0.0",
"webdriverio": "8.24.12",
"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 || ^8.0.0"
},
"devDependencies": {
"@types/follow-redirects": "^1.14.4",
"@types/node": "^20.1.0",
"@wdio/globals": "8.24.12"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
declare namespace WebdriverIO {
interface Browser {
getAccessibilityResultsSummary: () => Promise<{ [key: string]: any; }>,
getAccessibilityResults: () => Promise<Array<{ [key: string]: any; }>>
getAccessibilityResults: () => Promise<Array<{ [key: string]: any; }>>,
percyCaptureMap: any
}

interface MultiRemoteBrowser {
getAccessibilityResultsSummary: () => Promise<{ [key: string]: any; }>,
getAccessibilityResults: () => Promise<Array<{ [key: string]: any; }>>
getAccessibilityResults: () => Promise<Array<{ [key: string]: any; }>>,
percyCaptureMap: any
}
}
97 changes: 97 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,97 @@
import type { Capabilities } from '@wdio/types'
import type { BeforeCommandArgs, AfterCommandArgs } from '@wdio/reporter'

import {
o11yClassErrorHandler
} from '../util.js'
import PercyCaptureMap from './PercyCaptureMap.js'

import * as PercySDK from './PercySDK.js'
import { PercyLogger } from './PercyLogger.js'

class _PercyHandler {
private _testMetadata: { [key: string]: any } = {}
private sessionName?: string
private _isAppAutomate?: boolean
public _percyScreenshotCounter: any = 0

constructor (
private _percyAutoCaptureMode: string | undefined,
private _browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser,
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.percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName)) : await PercySDK.screenshot(this._browser, (this._browser.percyCaptureMap as PercyCaptureMap).getName((this.sessionName as string), eventName));
(this._browser.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.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

178 changes: 178 additions & 0 deletions packages/wdio-browserstack-service/src/Percy/Percy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import fs from 'node:fs'
import path from 'node:path'
import os from 'node:os'

import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const { spawn } = require('node:child_process')

import { nodeRequest, getBrowserStackUser, getBrowserStackKey } from '../util.js'
import { PercyLogger } from './PercyLogger.js'

import PercyBinary from './PercyBinary.js'

import type { BrowserstackConfig, UserConfig } from '../types.js'
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 = 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