fix(percy): restore Percy capture on WebdriverIO (screenshot + snapshot) by harshit-browserstack · Pull Request #210 · browserstack/wdio-browserstack-service · GitHub
Skip to content

fix(percy): restore Percy capture on WebdriverIO (screenshot + snapshot) - #210

Merged
rounak610 merged 5 commits into
release_9.37.0from
fix/sdk-7711-percy-capture
Sep 24, 2026
Merged

rounak610 merged 5 commits into
release_9.37.0from
fix/sdk-7711-percy-capture

Conversation

@harshit-browserstack

@harshit-browserstack harshit-browserstack commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

What is this about?

Percy produced no visual coverage at all on WebdriverIO. Two independent defects were stacked on top of each other, and both were silent — tests kept passing, the only symptom was a Unsupported driver for percy log line.

Defect A — screenshot (Percy on Automate). Percy's bug; Percy has fixed it.
@percy/selenium-webdriver 2.2.6 (published 2026-04-01) added a top-level require('selenium-webdriver') for a Selenium-only iframe helper, but ships that package as a devDependency only, so it is never present in a WebdriverIO project and the module throws MODULE_NOT_FOUND on load. Our tryRequire swallowed the throw and left every Percy entry point as a no-op stub. Our floating ^2.2.2 is what admitted it. Percy guarded the require in 2.2.8, so the range stays ^2.2.2 and the lockfile simply moves 2.2.6 → 2.2.8. Not v9-specific: the v8 line's ^2.0.3 floats into the same window.

Defect B — snapshot (Percy web). Ours, older, and it never worked.
snapshot was bound to percySnapshot from @percy/selenium-webdriver, which drives the browser through Selenium client APIs — single-argument executeScript(script), By, switchTo(). A WebdriverIO browser provides none of them, so it failed on the first driver call, the error was swallowed, and zero snapshots were produced. The WebdriverIO-native port is a different package, @percy/webdriverio, and snapshot now binds to it. screenshot deliberately stays on @percy/selenium-webdriver: Percy on Automate is driver-agnostic (it reads session metadata and captures server-side) and already carries an explicit wdio branch in its DriverMetadata.

Changes:

  • Add @percy/webdriverio and route snapshot to it; screenshot and screenshotApp are unchanged in behaviour.
  • Route all three Percy entry points through a shared runPercy helper that logs errors instead of propagating them into the user's test. This is load-bearing, not defensive tidying: now that 2.2.8 loads cleanly, Percy's misuse guards (percySnapshot against an Automate build, percyScreenshot against anything else) throw ahead of their own try/catch, and all three entry points are public exports — without the wrapper this PR would start failing user tests where they previously only logged. PERCY_RAISE_ERROR=true opts back into throwing.
  • tryRequire now logs the underlying load failure instead of discarding it, so a future packaging break surfaces as itself rather than as a misleading driver message.
  • Document Percy in the README for the first time — the percy and percyCaptureMode options, that this service runs Percy on Automate, and how to drive a Percy web project alongside it.

Notes for the reviewer:

  • @percy/webdriverio is pinned >=3.3.0 <3.3.3 deliberately, and the upper bound is load-bearing. 3.3.3 is unloadable as published: index.js:1 requires ./_iframe_shim, a file the published tarball does not contain (npm pack yields only LICENSE, README.md, index.js, package.json, types/index.d.ts). Through 3.3.2 and 3.3.3-beta.0 that line is require('@percy/sdk-utils'), an external package; the relative require appears from 3.3.3-beta.1 onward (beta.1, beta.2 and the stable cut are all broken), against a files list unchanged at ["index.js","types/index.d.ts"] throughout. It is still latest on npm, so a floating ^3.3.0 lands on it. Worse, a failed load here does not disable snapshot — webSnapshot = percyWebdriverioSnapshot || percySnapshot falls through to the Selenium SDK, silently restoring the exact defect this PR fixes. Verified by pinning 3.3.3 and rebuilding. Do not relax the upper bound until Percy republishes; raised with them separately.
  • @percy/selenium-webdriver@2.2.8 pins @percy/sdk-utils@1.32.3-beta.1 — a beta, and it is Percy's own pin inside their published package, not something set here. It appears in the lockfile as a nested dependency. Flagging it because a beta transitive arriving via a patch-range float is worth a conscious decision.
  • The package-lock.json diff is entirely Percy-scoped except one line: npm corrects the lock's stale 9.29.1 entry for this workspace to the actual 9.36.2. Any npm install on main does this; it is not introduced here.
  • The v8 branch needs the equivalent change and is not covered here. origin/v8 still has the bare catch {} and no wrapper, and the same snapshot misbinding.
  • The same snapshot misbinding exists in browserstack-node-sdk (src/helpers/percy/PercySDK.js), affecting its Playwright/Puppeteer/Jest/Cucumber users. Needs its own ticket.

Related Jira task/s

https://browserstack.atlassian.net/browse/SDK-7711

Release (mandatory for every PR — required for the ready-for-review label)

Version bump: (required — tick exactly one)

  • minor (backwards-compatible feature)
  • patch (bug fix or other small change)

Release notes type: (optional)

  • New Feature
  • Bug Fix
  • Other Improvement

Release notes (customer-facing): (optional but encouraged)

  • 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.

Release notes (internal): (required — engineer-facing; what actually changed / why)

  • snapshot was bound to percySnapshot from @percy/selenium-webdriver, which drives the browser via Selenium client APIs (1-arg executeScript, By, switchTo()) that a WebdriverIO browser does not have. It failed on the first driver call and the error was swallowed — 0 snapshots, at every version. Rebound to @percy/webdriverio, pinned >=3.3.0 <3.3.3 because the stable 3.3.3 cut added a require('./_iframe_shim') at index.js:1 without adding that file to files, so it is unloadable as published (the betas and 3.3.2 require the external @percy/sdk-utils instead).
  • @percy/selenium-webdriver 2.2.6/2.2.7 added an unguarded top-level require('selenium-webdriver') at index.js:14 while shipping that package as a devDependency only, so it threw MODULE_NOT_FOUND in any WebdriverIO-only install and stubbed out every Percy export. Percy guarded it in 2.2.8; range stays ^2.2.2, lock moves to 2.2.8. The v8 line's ^2.0.3 floats into the same window and still needs the fix.
  • Note: a lockfile pinned below 2.2.8 does not self-heal on the next npm install — npm keeps a locked resolution while it satisfies the declared range. Verified by controlled test (pin 2.2.7, widen range, reinstall → stays 2.2.7). Recovery needs an explicit npm update @percy/selenium-webdriver.
  • Added a runPercy helper wrapping snapshot, screenshot and screenshotApp. Percy's misuse guards throw ahead of their own try/catch, so with the module loading again those rejections would reach the caller — all three entry points are public exports. Honours PERCY_RAISE_ERROR.
  • tryRequire now logs the swallowed error. Collapsing MODULE_NOT_FOUND into "Unsupported driver for percy" is why Defect A went undiagnosed for ~5 months.
  • screenshotApp was never affected by either defect (separate @percy/appium-app dependency).

Checklist

  • Ready to review
  • Has it been tested locally?

Defect A — verified on Jenkins (BStackAutomation SDK-7711-percy-wdio-verify, selection -m sdk_7711). Run #275 against main, run #276 against this branch. Both desktop-Automate Percy cases go FAIL → PASS. The App Automate case was tagged as a control and failed in both runs — it is invariant across the change, so it is unaffected by this PR and is a separate pre-existing defect (raised separately).

Defect B — verified locally against a local Percy CLI stub, not percy.io. This session has no Percy web project token, so the snapshot path was driven against a local Percy CLI listening on :5338 and reporting type: "web". The DOM was captured from a real BrowserStack browser session, not a simulated one: session b1b9225d74fc7ae292c19665947cb92fad96f68f shows the Selenium SDK posting 0 snapshots (swallowing Wrong parameters applied for executeScript) against @percy/webdriverio posting 1 snapshot / 594-byte DOM; session 37443af39f547cd3ce0b35d97693593510b0c5b0 reproduces that through this service's own PercySDK.snapshot export (snapshotsPosted: 1, domBytes: 594). What is not covered: no snapshot has been rendered on a real percy.io web project. That needs a Percy web project token and a new BStackAutomation case — the existing -m sdk_7711 cases all run percy: true, which provisions an automate project where snapshot is misuse by design.

Build and eslint are clean. The full suite matches the pre-change baseline exactly (same 8 files / 75 failures, all pre-existing and network-dependent in this environment).

PR Validations

Run Tests: Comment RUN_TESTS to trigger sanity tests.

Percy capture has been silently disabled for any install resolving
@percy/selenium-webdriver 2.2.6 or newer (published 2026-04-01). Tests still
passed, so the only symptoms were a log line and missing visual coverage.

2.2.6 added a top-level require('selenium-webdriver') for a Selenium-only
iframe helper, but that package is shipped as a devDependency only and is never
present in a WebdriverIO project, so the module throws MODULE_NOT_FOUND on load.
tryRequire swallowed the throw and left the Percy entry points as no-op stubs
logging "Unsupported driver for percy" — a message about the driver for what was
actually a failed module load. Our floating "^2.2.2" range is what admitted the
broken version, on the v9 and v8 lines alike.

- Cap @percy/selenium-webdriver below 2.2.6.
- Log the underlying load failure instead of discarding it, so a future
  packaging break surfaces as itself.
- Route all three Percy entry points through a shared helper that logs errors
  rather than propagating them into the user's test. Percy raises its misuse
  guards before its own try/catch, so capping alone would have started throwing
  from snapshot/screenshot calls. PERCY_RAISE_ERROR is honoured for users who
  want failures to fail the build.
- Document Percy in the README for the first time: the percy and
  percyCaptureMode options, that this service runs Percy on Automate, and how to
  drive a Percy web project alongside it.

App Automate is unaffected — it uses @percy/appium-app, a separate package.

The v8 line declares the same floating range and needs the equivalent change on
the v8 branch; this covers v9 only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

github-actions Bot and others added 2 commits September 21, 2026 16:35
Percy released 2.2.8, which guards the require that broke module load, so the
cap added in the previous commit is no longer needed and the range returns to
^2.2.2. The lockfile is refreshed onto 2.2.8.

That leaves the second, older defect. The service routes its `snapshot` (Percy
web) export to @percy/selenium-webdriver's percySnapshot, which drives the
browser through Selenium client APIs — executeScript(script) with a single
argument, By, switchTo(). A WebdriverIO browser provides none of those, so the
call fails on the first driver interaction, the SDK swallows the failure, and
zero snapshots are posted. That path has never worked on WebdriverIO at any
version. @percy/webdriverio is the WebdriverIO-native port and is what `snapshot`
now binds to.

percyScreenshot (Percy on Automate) deliberately stays on
@percy/selenium-webdriver: it is driver-agnostic — it reads session metadata and
posts, with capture happening server-side — and carries an explicit wdio branch
in its DriverMetadata.

@percy/webdriverio is pinned below 3.3.3; that release omits a file its own entry
point requires and fails to load.

Verified on a live BrowserStack WebdriverIO v9 session against a web-typed Percy
runtime: through the service's own export, snapshot posts one snapshot carrying a
594-byte serialized DOM, where the Selenium SDK on the identical browser posts
none and logs "Wrong parameters applied for executeScript".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@harshit-browserstack harshit-browserstack changed the title fix(percy): restore Percy capture on WebdriverIO fix(percy): restore Percy capture on WebdriverIO (screenshot + snapshot) Sep 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔴 SDK PR Review gate is red. Pending:

  • The SDK PR Review Agent has not been run on the current head commit yet — run the SDK PR Review Agent (its verdict is advisory; this gate only requires that it ran on the latest commit).

It turns green once the SDK PR Review Agent has run on the current head commit (any verdict — the gate only requires that the review ran). A native reviewer approval is separately required by branch protection before merge.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

🔴 SDK PR Review gate is red. Pending:

  • The SDK PR Review Agent has not been run on the current head commit yet — run the SDK PR Review Agent (its verdict is advisory; this gate only requires that it ran on the latest commit).

It turns green once the SDK PR Review Agent has run on the current head commit (any verdict — the gate only requires that the review ran). A native reviewer approval is separately required by branch protection before merge.

@harshit-browserstack

harshit-browserstack commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

⚠️ Needs human review

File Status Reason
.changeset/pr-210.md ✅ All Clear Reviewed, no issues found
packages/browserstack-service/README.md 🔴 Author to Fix 1 1 finding — ungrounded, verify independently (carried, unchanged since prior review)
packages/browserstack-service/package.json ✅ All Clear Reviewed, no issues found (carried, unchanged since prior review)
packages/browserstack-service/src/Percy/PercySDK.ts ✅ All Clear Re-reviewed in full this run — the prior finding on snapshotHandler's stub is resolved
package-lock.json ⚠️ Needs Human Review Excluded from automated review (lockfile) — see review.md

Process integrity

The review gate could NOT certify this run — the verdict is held at ⚠️ pending regardless of findings:

  • G3 — u001: no full read receipt for pack:docs
  • G3 — u001: no full read receipt for slice
  • G3 — u002: no full read receipt for card:cards/_shared.md
  • G3 — u002: no full read receipt for card:cards/wdio-service.md
  • G3 — u002: no full read receipt for pack:config
  • G3 — u002: no full read receipt for pack:default
  • G3 — u002: no full read receipt for pack:external
  • G3 — u002: no full read receipt for pack:integration
  • G3 — u002: no full read receipt for pack:observability
  • G3 — u002: no full read receipt for slice

Change map (generated deterministically from the diff)

graph LR
  subgraph nwdio_service["wdio-service"]
    npackages_browserstack_service_src_Percy_PercySDK_ts["PercySDK.ts<br/>~59 lines"]
    n_changeset_pr_210_md["pr-210.md<br/>~21 lines"]
    npackages_browserstack_service_README_md["README.md<br/>~21 lines"]
    npackages_browserstack_service_package_json["package.json<br/>~11 lines"]
  end
Loading

↻ This verdict comment is the review anchor — it's updated in place on each run (the gate posts its status separately).

— SDK PR Review Agent

@github-actions

Copy link
Copy Markdown
Contributor

🟢 SDK PR Review gate is green — the SDK PR Review Agent has run on the current head commit (verdict: pending).

This gate confirms a review ran on the latest commit. The verdict itself is advisory — read the findings and use your judgement; it does not block merge. A native GitHub reviewer approval is still separately required by branch protection before this PR can merge.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

🟢 SDK PR Review gate is green — the SDK PR Review Agent has run on the current head commit (verdict: pending).

This gate confirms a review ran on the latest commit. The verdict itself is advisory — read the findings and use your judgement; it does not block merge. A native GitHub reviewer approval is still separately required by branch protection before this PR can merge.

@github-actions

Copy link
Copy Markdown
Contributor

🟢 SDK PR Review gate is green — the SDK PR Review Agent has run on the current head commit (verdict: pending).

This gate confirms a review ran on the latest commit. The verdict itself is advisory — read the findings and use your judgement; it does not block merge. A native GitHub reviewer approval is still separately required by branch protection before this PR can merge.

@harshit-browserstack
harshit-browserstack requested review from AakashHotchandani and removed request for yashdsaraf September 22, 2026 10:50
@harshit-browserstack

Copy link
Copy Markdown
Collaborator Author

RUN_TESTS

@harshit-browserstack

Copy link
Copy Markdown
Collaborator Author

RUN_SCA

The disabled-path stub stayed synchronous while its enabled branch became
async, so TypeScript inferred the export from the initial assignment and
published `snapshot` as `=> void` while `screenshot` and `screenshotApp`
were `Promise<unknown>`. Consumers awaiting it awaited a void.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🔴 SDK PR Review gate is red. Pending:

  • The SDK PR Review Agent has not been run on the current head commit yet — run the SDK PR Review Agent (its verdict is advisory; this gate only requires that it ran on the latest commit).

It turns green once the SDK PR Review Agent has run on the current head commit (any verdict — the gate only requires that the review ran). A native reviewer approval is separately required by branch protection before merge.

@harshit-browserstack

Copy link
Copy Markdown
Collaborator Author

RUN_TESTS

@github-actions

Copy link
Copy Markdown
Contributor

🟢 SDK PR Review gate is green — the SDK PR Review Agent has run on the current head commit (verdict: pending).

This gate confirms a review ran on the latest commit. The verdict itself is advisory — read the findings and use your judgement; it does not block merge. A native GitHub reviewer approval is still separately required by branch protection before this PR can merge.

@github-actions

Copy link
Copy Markdown
Contributor

🟢 SDK PR Review gate is green — the SDK PR Review Agent has run on the current head commit (verdict: pending).

This gate confirms a review ran on the latest commit. The verdict itself is advisory — read the findings and use your judgement; it does not block merge. A native GitHub reviewer approval is still separately required by branch protection before this PR can merge.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

🟢 SDK PR Review gate is green — the SDK PR Review Agent has run on the current head commit (verdict: pending).

This gate confirms a review ran on the latest commit. The verdict itself is advisory — read the findings and use your judgement; it does not block merge. A native GitHub reviewer approval is still separately required by branch protection before this PR can merge.

@rounak610
rounak610 changed the base branch from main to release_9.37.0 September 24, 2026 12:03
@rounak610
rounak610 merged commit 55017db into release_9.37.0 Sep 24, 2026
32 of 38 checks passed

@AakashHotchandani AakashHotchandani left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SDK PR Review — findings at 0939a858

Verdict: 🔴 Fix 2 blocking issues
0 critical · 2 warning(s) · 3 suggestion(s) · 0 needing human review

5 finding(s) inline. The signed verdict comment carries the merge-gate state.

— SDK PR Review Agent

"@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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Warning — [CONFIG] Defect A fix lives only in the lockfile; the declared range still admits broken 2.2.6/2.2.7

Problem

The release note says "Fixed Percy capture on WebdriverIO", but the fix for Defect A is only the package-lock.json move 2.2.6 → 2.2.8. Customers resolve the declared range, and packages/browserstack-service/package.json:60 (the line two above this anchor) still reads "@percy/selenium-webdriver": "^2.2.2", which admits the unloadable 2.2.6 and 2.2.7.

The PR body confirms the consequence: "a lockfile pinned below 2.2.8 does not self-heal on the next npm install — npm keeps a locked resolution while it satisfies the declared range". Those customers upgrade to this version and keep MODULE_NOT_FOUND → stubbed screenshot → no Percy coverage. Acknowledging the gap in the body does not close it.

Suggested Fix

Raise the floor so the broken window is excluded for every install:

"@percy/selenium-webdriver": "^2.2.8",

2.2.8 is the version the lockfile already resolves and whose index.js:14-21 guards require('selenium-webdriver'). Apply the same floor on the v8 line.

Impact / likelihood: medium — A customer whose lockfile already resolved 2.2.6 or 2.2.7 upgrades to this release and still gets 'Unsupported driver for percy' and zero Percy screenshots - the exact Defect A symptom the release note says is fixed. · medium — Any existing project that installed or refreshed its lockfile while 2.2.6/2.2.7 were latest (from 2026-04-01) and upgrades normally keeps that resolution; fresh installs are fine.

Confidence: 🟢 — package.json:60 keeps "@percy/selenium-webdriver": "^2.2.2", which admits 2.2.6/2.2.7 by semver; the only change to the resolved version is in package-lock.json, which a published library does not ship; the PR body states the locked resolution does not self-heal.

— SDK PR Review Agent

testCase: name || ''
}
return percySnapshot(browser, snapshotName, options)
return await runPercy(`snapshot "${snapshotName}"`, () => webSnapshot(browser, snapshotName, options))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Warning — [CORRECTNESS] snapshot's @percy/webdriverio fix never takes effect on a run the service manages

Problem

The changeset says "Fixed Percy web snapshots, which previously captured nothing on WebdriverIO", but the rebound snapshot only runs when process.env.PERCY_SNAPSHOT === 'true' (PercySDK.ts:66), and no product configuration reaches @percy/webdriverio with a web project:

  • percy on: launcher.ts:240-243 sets BEST_PLATFORM_CID, service.ts:139-140 sets PERCY_SNAPSHOT, but Percy.ts:132 always requests type=automate (or app) and Percy.ts:88 overrides PERCY_TOKEN with that project's token. @percy/webdriverio@3.3.2 index.js:40-41 throws You are using Percy on Automate session with WebdriverIO... for type === 'automate', so every call ends in runPercy's Percy snapshot ... failed log.
  • percy unset (the README's own web-project recipe): BEST_PLATFORM_CID is never set, so WDIO_WORKER_ID === BEST_PLATFORM_CID is false, PERCY_SNAPSHOT stays unset, and snapshot returns undefined without even a debug log.

The only way to reach the new binding is a hand-set PERCY_SNAPSHOT=true plus an externally started web-type Percy server, which is what the author's local-stub verification needed. The PR body itself notes the existing cases 'all run percy: true, which provisions an automate project where snapshot is misuse by design'. The release note therefore claims a fix customers cannot obtain through this service.

Suggested Fix

Either make the export reachable for web projects, or narrow the claim:

  • Open the gate for the web path: in snapshotHandler, run when PERCY_SNAPSHOT === 'true' or when the service did not start Percy (process.env.BROWSERSTACK_PERCY !== 'true'), and let @percy/webdriverio's own isPercyEnabled() decide; log at debug when skipping so a no-op is not silent.
  • Or drop the changeset line "Fixed Percy web snapshots" and document in the README that PercySDK.snapshot is not supported with this service, pointing to @percy/webdriverio directly (as the README already does).

This fix is unverified against launcher.ts BROWSERSTACK_PERCY setters on the CLI flow and against @percy/webdriverio isPercyEnabled behaviour under an automate server; confirm it before applying.

Impact / likelihood: medium — The changeset promises 'Fixed Percy web snapshots'; a customer calling PercySDK.snapshot still gets zero web snapshots - silently when percy is unset, or an error log when percy is on - which is the symptom this PR claims to fix. · high — Every service-managed run that calls PercySDK.snapshot: no configuration of the service both opens the PERCY_SNAPSHOT gate and yields a web-type Percy project.

Confidence: 🟢 — The gate chain is visible at HEAD: PERCY_SNAPSHOT is set only at service.ts:139-140 from BEST_PLATFORM_CID, which launcher.ts:240-243 sets only when the percy option is on; with percy on, Percy.ts:132 provisions an 'automate'/'app' project and @percy/webdriverio@3.3.2 index.js:40-41 throws for 'automate'.

— SDK PR Review Agent

*/
const percyWebdriverioSnapshot = tryRequire('@percy/webdriverio', null)

const webSnapshot = percyWebdriverioSnapshot || percySnapshot

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion — [CORRECTNESS] snapshot falls back to the Selenium SDK that this PR documents as capturing nothing

Problem

If @percy/webdriverio fails to load, tryRequire logs at debug and returns null, and webSnapshot silently becomes percySnapshot from @percy/selenium-webdriver. The comment directly above (PercySDK.ts:25-28) says that binding "captures nothing and swallows the failure" on a WebdriverIO browser.

The fallback is therefore never a working alternative; it only hides the load failure behind the original Defect B symptom. The PR relies on the <3.3.3 upper bound to keep it unreachable, which the body calls load-bearing.

Suggested Fix

Drop the fallback so a missing @percy/webdriverio takes the existing stub path, which logs at error level:

const webSnapshot = percyWebdriverioSnapshot

if (webSnapshot) at PercySDK.ts:64 then leaves the Unsupported driver for percy handler in place. Consider making that message name the package that failed to load.

Impact / likelihood: medium — When the fallback fires, the customer is back to zero snapshots with no error at error level, which re-creates the bug this PR fixes. · low — Needs @percy/webdriverio to fail to load inside the pinned >=3.3.0 <3.3.3 range (a broken install), or the pin to be relaxed onto an unloadable release, as the body says 3.3.3 is.

Confidence: 🟢 — The PR's own comment at PercySDK.ts:25-28 states that percySnapshot from @percy/selenium-webdriver captures nothing on a WebdriverIO browser and swallows the failure, and the PR body states the fallback 'silently restor[es] the exact defect this PR fixes'.

— SDK PR Review Agent

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) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion — [TEST COVERAGE] No test reaches any changed PercySDK branch

Problem

Every behavioural change in PercySDK.ts is verified only by manual runs (Jenkins, a local Percy stub). tests/Percy-Handler.test.ts:168-169 mocks PercySDK.screenshot and PercySDK.screenshotApp wholesale, and no other test imports the module (grep over 149/149 files at HEAD). Untested:

  • runPercy (PercySDK.ts:48-57): swallows and logs by default, rethrows under PERCY_RAISE_ERROR=true. Cheapest check: unit test with a rejecting call.
  • snapshotHandler (PercySDK.ts:64-79): binds to @percy/webdriverio, is gated on PERCY_SNAPSHOT, and passes testCase. Cheapest check: vi.mock('@percy/webdriverio') and assert it is called, not the Selenium SDK.
  • webSnapshot fallback (PercySDK.ts:37): Selenium binding when @percy/webdriverio fails to load. Cheapest check: mock the require to throw.
  • tryRequire (PercySDK.ts:16-18): debug log on load failure.
  • screenshotHandler / screenshotAppHandler (PercySDK.ts:117-133): now async and wrapped. Cheapest check: a misuse-guard rejection resolves undefined and logs.

Suggested Fix

Add a tests/PercySDK.test.ts that mocks @percy/webdriverio, @percy/selenium-webdriver and @percy/appium-app with vi.mock, re-imports the module per case with vi.resetModules(), and covers the bullets above, including PERCY_RAISE_ERROR=true.

This fix is unverified against the repo's vitest module-reset conventions; confirm it before applying.

Impact / likelihood: low — Missing test evidence only; customer behaviour is covered by the other findings. · high — Applies to every changed function in this PR.

Confidence: 🟢 — No test at HEAD imports PercySDK except tests/Percy-Handler.test.ts, which replaces screenshot and screenshotApp with mocks at L168-169, so no test reaches any changed branch.

— SDK PR Review Agent

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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion — [GRACEFUL DEGRADATION] runPercy makes percyAutoCapture's failure branch unreachable for screenshot errors

Problem

screenshot and screenshotApp are now wrapped in runPercy, which swallows the rejection. Their main internal caller, PercyHandler.percyAutoCapture (Percy-Handler.ts:51-75), relied on that rejection: its catch at L67-71 calls this._percyCaptureMap?.decrement(...) and PerformanceTester.end(AUTO_CAPTURE, false, err, ...).

After this change that branch runs only under PERCY_RAISE_ERROR=true. A failed capture is measured as a success and the per-session counter in PercyCaptureMap is not rolled back. The PR body describes the two screenshot entry points as "unchanged in behaviour", which holds for customers but not for this caller.

Suggested Fix

Keep the public exports guarded but let the internal caller see failures: have percyAutoCapture call an unwrapped internal helper (export screenshotHelper-based variants for internal use), or have runPercy return a { ok: false, error } sentinel that percyAutoCapture checks before its success path.

This fix is unverified against tests/Percy-Handler.test.ts spies on PercySDK.screenshot/screenshotApp; confirm it before applying.

Impact / likelihood: low — The customer's tests and Percy results are unaffected and the error is still logged at PercySDK.ts:55; what changes is SDK-internal: a failed auto-capture is recorded as a successful AUTO_CAPTURE/SCREENSHOT performance event and the capture-map counter is not rolled back, so later names in that session skip an index. · low — Only when percyScreenshot or percyAppScreenshot rejects during auto-capture, which needs a Percy-side failure first.

Confidence: 🟢 — Percy-Handler.ts:63 awaits PercySDK.screenshot/screenshotApp inside a try whose catch (L67-71) decrements the capture map and records a failed AUTO_CAPTURE; runPercy now resolves undefined on every error unless PERCY_RAISE_ERROR is set, so that catch is no longer reached for capture failures.

— SDK PR Review Agent

@AakashHotchandani

AakashHotchandani commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

🔴 Blocking findings — fix required

Findings: 0 critical · 2 warning(s) · 4 suggestion(s) · 0 needing human review — 1 posted in the findings review; 5 already on this PR from an earlier round, not repeated.

File Status Reason
.changeset/pr-210.md ✅ All Clear release-note claims judged against the code; shortfalls anchored in package.json and PercySDK.ts
packages/browserstack-service/README.md ✅ All Clear Percy defaults and capture modes match launcher.ts:433 and constants.ts:155
packages/browserstack-service/package.json 🔴 Author to Fix 1 declared @percy/selenium-webdriver range still admits the broken 2.2.6/2.2.7 (DEF-11); 1 suggestion
packages/browserstack-service/src/Percy/PercySDK.ts 🔴 Author to Fix 1 rebound snapshot is unreachable with a web project on any service-managed run (DEF-11); 3 suggestions

Change map (generated deterministically from the diff)

graph LR
  subgraph nwdio_service["wdio-service"]
    npackages_browserstack_service_src_Percy_PercySDK_ts["PercySDK.ts<br/>~59 lines"]
    npackages_browserstack_service_README_md["README.md<br/>~34 lines"]
    npackages_browserstack_service_package_json["package.json<br/>~11 lines"]
    n_changeset_pr_210_md["pr-210.md<br/>~8 lines"]
  end
Loading

↻ This verdict comment is the review anchor — it's updated in place on each run (the gate posts its status separately).

— SDK PR Review Agent

@github-actions

Copy link
Copy Markdown
Contributor

🟢 SDK PR Review gate is green — the SDK PR Review Agent has run on the current head commit (verdict: failure).

This gate confirms a review ran on the latest commit. The verdict itself is advisory — read the findings and use your judgement; it does not block merge. A native GitHub reviewer approval is still separately required by branch protection before this PR can merge.

@AakashHotchandani AakashHotchandani left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SDK PR Review — findings at 0939a858

Verdict: 🔴 Fix 2 blocking issues
0 critical · 2 warning(s) · 4 suggestion(s) · 0 needing human review

1 finding(s) inline; 5 already posted on this PR, not repeated. The signed verdict comment carries the merge-gate state.

Prior findings

UNRESOLVED 5

— SDK PR Review Agent

"@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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion — [CONFIG] New @percy/webdriverio dependency declares a peer range that excludes webdriverio 9.1+ · missed in prior round

🟡 Needs human review — this finding is not grounded in code or review knowledge; a reviewer's confirmation (or a reason it is safe) is what moves it.

Problem

This PR makes @percy/webdriverio an unconditional runtime dependency of the service for every customer, Percy users or not. Its published package.json (percy-webdriverio@v3.3.2, L31-33) declares "peerDependencies": { "webdriverio": "~6 || ~7 || ~8 || ~ 9" }; ~ 9 is >=9.0.0 <9.1.0. The service itself requires webdriverio ^9.0.0, and customers run current 9.x (Percy's own devDependency is ^9.2.1), so the peer is unsatisfied on essentially every install. The lock entry added here shows the same range.

Suggested Fix

Confirm what npm install @wdio/browserstack-service does in a project on webdriverio 9.2x (warning only vs. nested copy). If it only warns, note it in the PR/README; if it nests a second webdriverio, consider making @percy/webdriverio an optional peer of the service (the README already tells Percy web users to install it themselves) and ask Percy to widen the peer range to ^9.

This fix is unverified against npm/pnpm/yarn peer resolution on a customer project; confirm it before applying.

Impact / likelihood: low — Expected customer-visible effect is an install-time peer-dependency warning naming a package they did not add; capture itself is not affected. If a package manager instead nests a webdriverio 9.0.x copy to satisfy the peer, the impact would be higher; that is what a human should confirm. · high — Every install of the service on webdriverio 9.1 or later, i.e. essentially every current v9 customer, since the dependency is unconditional.

Confidence: 🟡 — The peer range is read from upstream; the effect on a customer's install (npm ERESOLVE override warning vs. a nested webdriverio 9.0.x copy) was not run here.

— SDK PR Review Agent

@github-actions

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants