diff --git a/.changeset/pr-210.md b/.changeset/pr-210.md new file mode 100644 index 00000000..17993b5a --- /dev/null +++ b/.changeset/pr-210.md @@ -0,0 +1,8 @@ +--- +"@wdio/browserstack-service": patch +--- + +- Fixed Percy capture on WebdriverIO. Runs with `percy: true` logged "Unsupported driver for percy" and produced no screenshots, while the tests themselves continued to pass. +- Fixed Percy web snapshots, which previously captured nothing on WebdriverIO. +- Percy errors are now logged instead of failing the test. Set `PERCY_RAISE_ERROR=true` to fail the build on Percy errors instead. +- Added Percy documentation to the README, including how to use a Percy web project alongside this service. diff --git a/package-lock.json b/package-lock.json index ad1443cc..18f0a289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1763,18 +1763,42 @@ } }, "node_modules/@percy/selenium-webdriver": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@percy/selenium-webdriver/-/selenium-webdriver-2.2.6.tgz", - "integrity": "sha512-5aeJh3ncYQl1Ug8/eazae8Ux281cvUX87e5YvHTFnLKtELaqJEb2k0NoNZbnWpPgAdz7yxeZZgTEbDn9HTOSYA==", - "license": "MIT", + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@percy/selenium-webdriver/-/selenium-webdriver-2.2.8.tgz", + "integrity": "sha512-DGi23l8EnLqSwkluyqJCUlW58EZpOL5gFMKdlRDTp+0Ukg/v7JXZ6hcaOq//12Nio62Dt0Df+tH09e0U2xU69Q==", "dependencies": { - "@percy/sdk-utils": "^1.31.10", + "@percy/sdk-utils": "1.32.3-beta.1", "node-request-interceptor": "^0.6.3" }, "engines": { "node": ">=14" } }, + "node_modules/@percy/selenium-webdriver/node_modules/@percy/sdk-utils": { + "version": "1.32.3-beta.1", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.32.3-beta.1.tgz", + "integrity": "sha512-/oD+82slu1tuV3nFkwrdBWUIZ3JdmPuh2ajlcWbumTxu0lu2hhZLKmCTE72Fc5N6UXoHl9tQzJPrkT0/W/aigQ==", + "dependencies": { + "pac-proxy-agent": "^7.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/webdriverio": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@percy/webdriverio/-/webdriverio-3.3.2.tgz", + "integrity": "sha512-YpnrUv6tRvITqSo3fNz2jLh1qy1Rmd5QfblOPsthbXNPQ1quMScuZwM8HGPTGiiqouZXByBzkUCIss/mGgJlkQ==", + "dependencies": { + "@percy/sdk-utils": "^1.30.3" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "webdriverio": "~6 || ~7 || ~8 || ~ 9" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -12895,7 +12919,7 @@ }, "packages/browserstack-service": { "name": "@wdio/browserstack-service", - "version": "9.29.1", + "version": "9.36.2", "license": "MIT", "dependencies": { "@browserstack/ai-sdk-node": "1.5.17", @@ -12903,6 +12927,7 @@ "@grpc/grpc-js": "~1.13.5", "@percy/appium-app": "^2.0.9", "@percy/selenium-webdriver": "^2.2.2", + "@percy/webdriverio": ">=3.3.0 <3.3.3", "@types/gitconfiglocal": "^2.0.1", "@wdio/logger": "^9.0.0", "@wdio/reporter": "^9.0.0", diff --git a/packages/browserstack-service/README.md b/packages/browserstack-service/README.md index 17f32161..1c0f8d5e 100644 --- a/packages/browserstack-service/README.md +++ b/packages/browserstack-service/README.md @@ -224,6 +224,40 @@ Automatically set the BrowserStack Automate session status (passed/failed). Type: `Boolean`
Default: `true` +### percy + +Enable Percy visual testing. + +Type: `Boolean`
+Default: `false` — except for App Automate runs, where Percy is enabled automatically when `percy` is left unset and `app` is provided. + +This service runs Percy in **Percy on Automate** mode: it provisions the Percy project for you and captures screenshots server-side from the Automate session. Use `percyCaptureMode` to control when captures happen — no code changes are needed. + +**Percy web projects.** This service provisions a Percy on Automate project; it does not create web-type Percy projects. To use a Percy **web** project with WebdriverIO, leave `percy` unset in the service options and drive Percy yourself: + +```bash +PERCY_TOKEN= npx percy exec -- npx wdio run wdio.conf.js +``` + +calling [`@percy/webdriverio`](https://github.com/percy/percy-webdriverio)'s `percySnapshot` in your specs. BrowserStack Automate and Test Observability continue to work through this service alongside it. + +> Pin `@percy/webdriverio` below `3.3.3` — that release omits a file its entry point requires and fails to load. + +### percyCaptureMode + +When to capture Percy screenshots automatically. + +Type: `String`
+Default: `auto` + +* `auto` — capture on clicks, screenshots, actions and input changes +* `click` — capture on clicks only +* `screenshot` — capture on screenshot commands only +* `testcase` — capture once at the end of each test +* `manual` — never capture automatically + +Your Percy project's own capture-mode setting takes precedence over this option when one is configured. + ### buildIdentifier **buildIdentifier** is a unique id to differentiate every execution that gets appended to buildName. Choose your buildIdentifier format from the available expressions: diff --git a/packages/browserstack-service/package.json b/packages/browserstack-service/package.json index 8689a732..4b06d97c 100644 --- a/packages/browserstack-service/package.json +++ b/packages/browserstack-service/package.json @@ -58,7 +58,11 @@ "@grpc/grpc-js": "~1.13.5", "@percy/appium-app": "^2.0.9", "@percy/selenium-webdriver": "^2.2.2", + "@percy/webdriverio": ">=3.3.0 <3.3.3", "@types/gitconfiglocal": "^2.0.1", + "@wdio/logger": "^9.0.0", + "@wdio/reporter": "^9.0.0", + "@wdio/types": "^9.0.0", "browserstack-local": "^1.5.1", "chalk": "^5.3.0", "csv-writer": "^1.6.0", @@ -68,12 +72,9 @@ "tar": "^7.5.11", "undici": "^6.24.0", "uuid": "^11.1.0", + "webdriverio": "^9.0.0", "winston-transport": "^4.5.0", - "yauzl": "^3.4.0", - "@wdio/logger": "^9.0.0", - "@wdio/reporter": "^9.0.0", - "@wdio/types": "^9.0.0", - "webdriverio": "^9.0.0" + "yauzl": "^3.4.0" }, "peerDependencies": { "@wdio/cli": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" diff --git a/packages/browserstack-service/src/Percy/PercySDK.ts b/packages/browserstack-service/src/Percy/PercySDK.ts index 9f91013c..da5b51ed 100644 --- a/packages/browserstack-service/src/Percy/PercySDK.ts +++ b/packages/browserstack-service/src/Percy/PercySDK.ts @@ -13,21 +13,56 @@ const tryRequire = function (pkg: string, fallback: unknown) { return (mod as { default: unknown }).default } return mod - } catch { + } catch (err) { + PercyLogger.debug(`Percy: could not load ${pkg} - ${(err as Error)?.message}`) return fallback } } const percySnapshot = tryRequire('@percy/selenium-webdriver', null) +/* +Percy ships two disjoint web SDKs, and the correct one depends on the driver, not the +product. percySnapshot from @percy/selenium-webdriver drives the browser through Selenium +client APIs - executeScript(script) with a single argument, By, switchTo() - none of which +a WebdriverIO browser provides, so it captures nothing and swallows the failure. The +WebdriverIO-native port lives in @percy/webdriverio and is what `snapshot` binds to. + +percyScreenshot (Percy on Automate) deliberately stays on @percy/selenium-webdriver: it is +driver-agnostic - it reads session metadata and posts, capturing server-side - and carries +an explicit wdio branch in its DriverMetadata. +*/ +const percyWebdriverioSnapshot = tryRequire('@percy/webdriverio', null) + +const webSnapshot = percyWebdriverioSnapshot || percySnapshot + const percyAppScreenshot = tryRequire('@percy/appium-app', {}) +/* +Percy's SDKs raise their misuse guards - percySnapshot against a Percy-on-Automate build, +percyScreenshot against anything else - before their own try/catch, so those rejections +reach the caller. Every PercySDK entry point is publicly exported, so an unguarded one +fails the user's test rather than their visual coverage. PERCY_RAISE_ERROR is Percy's own +opt-in for the opposite behaviour and is honoured. +*/ +const runPercy = async (label: string, call: () => unknown) => { + try { + return await call() + } catch (err) { + if (process.env.PERCY_RAISE_ERROR === 'true') { + throw err + } + PercyLogger.error(`Percy ${label} failed: ${(err as Error)?.message}`) + } +} + /* eslint-disable @typescript-eslint/no-unused-vars */ -let snapshotHandler = (...args: unknown[]) => { +let snapshotHandler = async (...args: unknown[]): Promise => { PercyLogger.error('Unsupported driver for percy') + return undefined } -if (percySnapshot) { - snapshotHandler = (browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, snapshotName: string, options?: { [key: string]: unknown }) => { +if (webSnapshot) { + snapshotHandler = async (browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, snapshotName: string, options?: { [key: string]: unknown }) => { if (process.env.PERCY_SNAPSHOT === 'true') { let { name, uuid } = InsightsHandler.currentTest if (isUndefined(name)) { @@ -38,7 +73,7 @@ if (percySnapshot) { ...options, testCase: name || '' } - return percySnapshot(browser, snapshotName, options) + return await runPercy(`snapshot "${snapshotName}"`, () => webSnapshot(browser, snapshotName, options)) } } } @@ -75,23 +110,25 @@ const screenshotHelper = (type: string, driverOrName: WebdriverIO.Browser | Webd } /* eslint-disable @typescript-eslint/no-unused-vars */ -let screenshotHandler = async (...args: unknown[]) => { +let screenshotHandler = async (...args: unknown[]): Promise => { PercyLogger.error('Unsupported driver for percy') + return undefined } if (percySnapshot && percySnapshot.percyScreenshot) { - screenshotHandler = (browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser | string, screenshotName?: string | { [key: string]: unknown }, options?: { [key: string]: unknown }) => { - return screenshotHelper('web', browser, screenshotName, options) + screenshotHandler = async (browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser | string, screenshotName?: string | { [key: string]: unknown }, options?: { [key: string]: unknown }) => { + return await runPercy('screenshot', () => screenshotHelper('web', browser, screenshotName, options)) } } export const screenshot = screenshotHandler /* eslint-disable @typescript-eslint/no-unused-vars */ -let screenshotAppHandler = async (...args: unknown[]) => { +let screenshotAppHandler = async (...args: unknown[]): Promise => { PercyLogger.error('Unsupported driver for percy') + return undefined } if (percyAppScreenshot) { - screenshotAppHandler = (driverOrName: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser | string, nameOrOptions?: string | { [key: string]: unknown }, options?: { [key: string]: unknown }) => { - return screenshotHelper('app', driverOrName, nameOrOptions, options) + screenshotAppHandler = async (driverOrName: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser | string, nameOrOptions?: string | { [key: string]: unknown }, options?: { [key: string]: unknown }) => { + return await runPercy('app screenshot', () => screenshotHelper('app', driverOrName, nameOrOptions, options)) } } export const screenshotApp = screenshotAppHandler \ No newline at end of file