Skip to content

feat(app-a11y): scan the WDIO config-level before() window, and report it as a hook - #168

Open
kamal-kaur04 wants to merge 18 commits into
SDK-7422-app-a11y-scan-registrationfrom
SDK-7422-before-hook
Open

feat(app-a11y): scan the WDIO config-level before() window, and report it as a hook#168
kamal-kaur04 wants to merge 18 commits into
SDK-7422-app-a11y-scan-registrationfrom
SDK-7422-before-hook

Conversation

@kamal-kaur04

@kamal-kaur04 kamal-kaur04 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Stacked PR. Base is SDK-7422-app-a11y-scan-registration (#165), not main, so the diff here is the before-hook work alone — 507 insertions across 5 files. It retargets main automatically when #165 merges. One commit (adb8b23, the @Measure placement fix) also exists on the base as bd75537; same change, different SHA, so it shows in the commit list but not in the diff, and drops out on rebase.

What is this about?

Driver commands issued from WDIO's config-level before() were never scanned for App-A11y, and the rake counted them as expected coverage anyway. On the account that prompted SDK-7422 that window is app launch, login and account selection — the screens a customer most expects covered.

Those hooks are not test-framework hooks, so when they run neither onHookStart nor onBeforeTest has fired and the scan gate has no entry for the session at all. The gate now opens at driver creation, unconditionally when autoScanning is on — the same policy onHookStart already applies to Mocha hooks. Per-test include/exclude filters need a test to evaluate and there is none yet; the following onBeforeTest recomputes the gate for the test proper.

The scans needed a parent

Scans from that window went out with neither thTestRunUuid (no test exists yet) nor thHookRunUuid, so App-A11y's lookup of the scan's parent in BTCER found nothing — the scan reached the hub and then belonged to nothing:

PHASE >> config-level before() STARTED
   scan  method=execute  testRun=ABSENT  hookRun=ABSENT     ← belongs to nothing
PHASE >> mocha before all STARTED
   scan  method=back     testRun=8e466b38  hookRun=6caa3175

The window is now reported as a BEFORE_ALL hook run through the existing framework path (framework.trackEvent), so the framework mints the uuid, the binary emits HookRunStarted/HookRunFinished, and this module's own onHookStart observer stamps it onto the scans. No new plumbing, one source of truth for the uuid.

Two things fell out better than designed: resolveInstance already creates an instance for a BEFORE_* PRE when none exists (it has to — suite-level before all fires before any INIT_TEST), and the first test then reuses that instance, so the hook lands with a real test_run_id rather than orphaned.

It only fires when the user actually has such a hook

Opening this from the WDIO lifecycle would report a hook that never ran on every build in the fleet. And it cannot be detected from config: ConfigParser.addService folds the user's config hooks and every service's hooks into one config.before array (each hook.bind(service), so even name inspection fails) and the runner fires them together with Promise.all. A service is a sibling of the user's hook, not an observer of it.

So the hook run is opened on demand from the scan path — the exact condition that matters, since the only reason it exists is to parent a scan.

config hook events reported
with a config-level before()axzkajmr2odsx8dvyx2wfxw6mxtec3d9sbpmp08t 5 — 4 Mocha + 1 for the window
without it — lpbvftwaslksugogditfsvc1pspzd4fwgtneyxnh 4 — Mocha only, nothing extra

A failed hook is reported as failed

WDIO swallows a throwing config hook — executeHooksWithArgs resolves with the error rather than rejecting, deliberately ("so in case of a rejection it won't cause the hook to fail"). Run one that throws and the exit code stays 0, the spec reporter says 1 passing, Allure is green, and the TRA build reads passed. The failure exists in exactly one place: a log.error on stdout.

An early version of this branch made that worse — it reported the hook run as passed. Unreported means the dashboard omits a failure; passed means it asserts something false about it. The instrumentation wrapper is the only thing in the process that sees the rejection, so it records the message and the hook run reports the truth:

before: HookRunFinished BEFORE_ALL result=passed  uuid=1bcd33f4
after:  HookRunFinished BEFORE_ALL result=failed  uuid=d9c3ccb7

TRA build verdict moves with it — passed {passed: 2}failed {passed: 1, failed: 1} (qkfnla45gogeddlwlfbszpft6ff6estskur5yqoq).

beforeSuite counts too, and is easy to miss: the Mocha adapter registers it as a root before-all (this._runner.suite.beforeAll(this.wrapHook('beforeSuite'))), so it runs inside the same window. Keyed only on 'before', a throwing beforeSuite reported a passed window (eq3p9ktfb1uiiq5mtxqj670cu42o6vf82z6nzcdx). Mocha's own hooks stay excluded — each has its own hook run with its own result, so folding them in would report one failure twice.

The hook is named after the hook

The name comes from the live hook stack, not a fixed label, because the window does not always open in before():

first scannable command comes from reported name build
before() wdio "before" hook mu2hqmwrk4ygyh1yp8queidnxfdeeju7dk4ubbdx
beforeSuite (before() touches no driver) wdio "beforeSuite" hook eestbqwlk8mvjhzqkijfl0lc4gnzqxredgofblrw

Captured once, when the window opens: the binary takes a hook's name from the record pushed at PRE, and by POST a different hook is running. So the name says where the window opened, not where it failed — the failure message carries the failing hook's name, so both facts survive.

Verification

Bench: test-samples/app_automate-wdio_mocha-android/examples/repro-triple/ — phase boundaries annotated into the session log with browserstack_executor, so every scan is attributed by position rather than inferred from its command type. Verdicts read from the App Automate session-logs API.

window baseline 9.35.0 this branch
config-level before() 0 7execute ×2, executeScript ×2, click ×3
mocha before all 2 2
mocha before each #1 1 1
test 1, pre-reload 3 3
6 total 13 total

hookInstrumentation is what makes the window observable at all — [hook-window] before#0 (bound before) finished in 37970ms — and it is logging only, no events.

Unit: 4 new files/suites worth — instrumentation lifecycle (incl. sync-stays-sync and unwinding on throw), the on-demand hook run (opens once, never while a framework hook is the parent, closes at the first test, never closes what it did not open), dynamic naming, and failure reporting. Full suite at parity — 75 pre-existing failures before and after. npm run lint clean.

Global-hook scans carry no test run uuid (178c541)

The scan is a browserstack_executor: {"action":"appAllyScan", arguments: {...}} payload, so the session log carries the exact params sent. work/SDK-7422-app-a11y-fixes/uuid-verdict.py <build> <session> prints a per-scan census — phase, thTestRunUuid, thHookRunUuid, command.

flow build sessions window scans test-body scans
mocha, pre-fix rpgacbl0dyjcmmg34kwm3y5xui1bdirosz16ncll 5a6695f7c00b047ca90b7d367d580f5f3058bc8f test uuid present22ec974c, test 1's own present
mocha, post-fix r89xojas5cvljamjpwrj8x7zcf3pc3umt8qyoptk 72d8f60ebfd6485d670021fb68f43c4163ee6897, e60022566902b3f72651c4784448e2d39ff73302 test uuid absent, hook uuid kept present
cucumber, pre-fix fyiiwkwdl8deghsyanxtq7e0shsrlkdc59xrmg01 dc355a966c7539ccb73ae42e8af69b75c1495b0d absent present
cucumber, post-fix yldiivrvnkhf0xi7ttvzekksvxoafxulcrx66gac d316f0f4dd32466cbe16b444a959abccd6242328 absent present

Pre-fix, mocha's two before all scans ran before any test existed and still carried the uuid the test body later reported under. Post-fix they carry only their hook uuid, and all 16 post-reload scans still land, so the base branch's reload fix is unaffected.

Cucumber is identical either side because the defect was CLI-flow-only: mocha mints TEST_ANALYTICS_ID at instance creation, so the window had a uuid to leak, while the classic path sets it at Listener.testStarted and never had one. The guard is wired on both paths so the classic path cannot regress if that ever moves earlier.

Known limits, stated rather than implied

  • A command in the hook's first tick can still escape. WDIO runs same-named hooks concurrently, and registration completes ~6 ms after session creation (measured: session created 08:03:57.817, wrapping done 08:03:57.823). Not closable from inside a service — no driver object exists to wrap earlier. Any prior driver call absorbs it.
  • The exit code still stays 0 on a failed config hook. We report it to TRA; the customer's CI stays green. That is WDIO's design and no service can override it.
  • Reporters show none of this. hook:start/hook:end come from @wdio/mocha-framework; the runner emits nothing for config hooks, so Allure/spec/junit cannot see the window. A reviewer checking the Allure report will find no trace of this change.
  • before each Browserstack wdio service code #1 loses its test uuid — it falls inside the pre-test window, while chore: update package and lock files devDependencies #2/Updated README. #3 keep theirs. Those scans keep their own hook run uuid, and hook-run→test parentage is untouched here; only scan params changed.
  • Scans from a hook that later failed are still counted as expected coverage by the rake — interacts with SDK-7406.

Related Jira task/s

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

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)

  • Accessibility scans now run for driver commands issued from your WDIO config's before() hook, so screens visited during setup are covered.
  • Hooks in your WDIO config that touch the driver now appear on the build, including when they fail — previously a failing before() was reported nowhere.

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

  • accessibilityModule: open the scan gate at onBeforeExecute (driver creation) so the pre-test window — WDIO config-level before/beforeSession — is scanned instead of silently uncovered while still counting as expected coverage.
  • accessibilityModule: report that window as a BEFORE_ALL hook run via the existing framework.trackEvent path, opened ON DEMAND from the scan path so suites without such a hook report nothing extra. Named from the live hook stack; 'idle' → 'attempted' → 'open' → 'closed' so a failed PRE is neither retried per command nor closed by a POST pairing with nothing.
  • New hookInstrumentation.ts: wrap the handlers of every session-scoped config hook to log start/finish, record failures, and expose the executing hook. Sync handlers stay sync so a sync throw is not converted into a rejection. Record resets per instrumented config.
  • Failure reporting spans PRE_TEST_WINDOW_HOOKS (before, beforeSuite) — the latter runs in the same window because the Mocha adapter registers it as a root before-all.

Checklist

  • Ready to review
  • Has it been tested locally?

PR Validations

Run Tests: Comment RUN_TESTS to trigger sanity tests.

…n TRA can resolve

Rebuilt on top of the reloadSession fix and its review corrections, so this branch is now the
before() work alone — the earlier parked version predated those and still carried the reverted
alias config-capture.

Driver commands issued from WDIO's config-level before()/beforeSession() were never scanned.
Those hooks are not test-framework hooks, so neither onHookStart nor onBeforeTest had run and
the scan gate had no entry for the session at all — while the rake still counted those commands
as expected coverage. The gate now opens at driver creation, unconditionally when autoScanning
is on, matching what onHookStart already does for Mocha hooks.

Scans from that window also had no parent: no thTestRunUuid (no test exists yet) and no
thHookRunUuid, so App-A11y's lookup of the scan's parent in BTCER found nothing. The window is
now reported as a BEFORE_ALL hook run through the existing framework path, so the framework
mints the uuid, the binary emits HookRunStarted/Finished, and onHookStart stamps it onto the
scans.

Opened ON DEMAND, from the scan path only. WDIO gives a service no event for a user's
config-level before() — ConfigParser folds the user's config hooks and every service's hooks
into one array the runner fires with Promise.all — so there is nothing to detect, and opening it
from the lifecycle would report a hook that never ran on every build in the fleet.

hookInstrumentation logs the start and finish of every session-scoped config hook, which is how
that window becomes observable at all. Logging only, no events.

SDK-7422
The predicate added in cec234c landed between the decorator and the method, so the decoration
moved to it: onReload stopped emitting its SDK_HOOK measurement, and the predicate started
reporting one under hookType 'onReload' on every call — including from the before hook, so every
session reported an onReload measurement whether or not it ever reloaded.

Behaviour was unaffected (PerformanceTester.measure returns the raw value on the synchronous
path), which is why nothing surfaced it: valid TypeScript, lint has no opinion, and the unit
suites mock Measure into a pass-through so the binding has no observable behaviour to assert.
Hence a structural test over the decorated files instead — verified to fail with the mistake
reintroduced and pass without it.

Doc comment corrected too: the two accessibility paths are NOT strictly exclusive. The module is
registered on startBinResponse.accessibility?.success alone, independent of provider, so under a
non-BrowserStack provider it can hold real state while the classic handler's before() has also
run. What the predicate decides is which one owns the CLASSIC handler's state.

SDK-7422
Found by running the case rather than reasoning about it: with a config-level before() that
throws, WDIO leaves the exit code at 0 (executeHooksWithArgs resolves WITH the error instead of
rejecting — deliberately, so a throwing config hook cannot fail the run), every reporter shows
green, and before this commit the hook run this branch emits said result=passed.

That is worse than the pre-existing silence. Unreported means the dashboard omits the failure;
reporting `passed` means it asserts something false about it.

The instrumentation wrapper is the only thing in the process that sees the rejection, so it now
records the message per hook name, and closePreTestHookRun reads it and reports
{ passed: false, error: { message } }.

Measured on the same bench, one line apart in the same log:
  before: HookRunFinished BEFORE_ALL result=passed  uuid=1bcd33f4
  after:  HookRunFinished BEFORE_ALL result=failed  uuid=d9c3ccb7
and the TRA build verdict moves from passed {passed: 2} to failed {passed: 1, failed: 1}.

SDK-7422
Keying the failure lookup on the literal 'before' was too narrow. `beforeSuite` is a config hook
with a live driver that the Mocha adapter registers as a root before-all
(`this._runner.suite.beforeAll(this.wrapHook('beforeSuite'))`), so it runs after the config-level
before and still ahead of the first test — inside the same window this hook run covers. A
beforeSuite that threw was reported as a passed window.

PRE_TEST_WINDOW_HOOKS names the hooks that can run there, and the message is prefixed with the
hook that failed. Mocha's own hooks stay excluded: each has its own hook run with its own result,
so folding them in would report the same failure twice.

Instrumenting a config now also clears the record for those hooks. The map is process state for
the worker's lifetime, so without it a failure could be reported against a later config.

Verified on device with a clean before() and a throwing beforeSuite (build
eq3p9ktfb1uiiq5mtxqj670cu42o6vf82z6nzcdx):
  [hook-window] before#0      finished in 2695ms
  [hook-window] beforeSuite#0 rejected: BOOM: beforeSuite failed in 4132ms
  HookRunFinished BEFORE_ALL result=failed  uuid=187517a8

SDK-7422
The label was fixed at 'wdio config-level before hook', which names the wrong hook whenever the
window opens somewhere else — and it can: beforeSuite runs in the same window, so a suite whose
before() touches no driver gets its window opened from beforeSuite instead.

The instrumentation already wraps every session-scoped hook, so it now also keeps a stack of the
hooks currently executing and exposes the innermost one. The name is captured once, when the
window opens, because the binary takes a hook's name from the record pushed at PRE and by POST
time a different hook is running.

Verified on device, two shapes:
  before() does the first scannable command  -> 'wdio before() hook (pre-test window)'
  before() touches nothing, beforeSuite does -> 'wdio beforeSuite() hook (pre-test window)'
                                                (build eestbqwlk8mvjhzqkijfl0lc4gnzqxredgofblrw)

Note the name says where the window OPENED, not where it failed — those differ, and the failure
message carries the failing hook's name, so both facts survive.

SDK-7422
…dow suffix

`wdio "before" hook` / `wdio "beforeSuite" hook` instead of
`wdio before() hook (pre-test window)`.

Quoting matches how Mocha names its own hooks ("before all" hook: ...), so the dashboard reads
consistently across both kinds, and the suffix was internal vocabulary that meant nothing to
someone looking at a hook in a build.

Verified on device:
  wdio "before" hook       result=passed  3a0c7942  (build mu2hqmwrk4ygyh1yp8queidnxfdeeju7dk4ubbdx)
  wdio "beforeSuite" hook  result=failed  1184570a  (build xprccsrzaofseerwsevv5pgcec55h9jufvjzzf2s)
Scans in that window are unchanged — 7 in the config-level hook on the normal bench.

SDK-7422
@kamal-kaur04
kamal-kaur04 requested a review from a team as a code owner August 26, 2026 16:49
@kamal-kaur04
kamal-kaur04 requested review from AakashHotchandani and yashdsaraf and removed request for a team August 26, 2026 16:49
Only four of the eight session-scoped hooks had ever been seen firing in a device run, because
earlier benches did not define the rest. A bench that defines all eight confirms every one is
instrumented and logs its start and finish:

  instrumenting before: 1 · beforeSuite: 1 · beforeHook: 1 · beforeTest: 3
                afterTest: 2 · afterHook: 1 · afterSuite: 1 · after: 2
  start/finish lines: before 2 · beforeSuite 2 · beforeHook 8 · beforeTest 6
                      afterTest 4 · afterHook 8 · afterSuite 2 · after 4

(beforeHook/afterHook fire four times each — once per Mocha hook. The handler counts above 1 are
expect-webdriverio's snapshot and soft-assert services sharing the array, which the index in the
label distinguishes.)

The test asserts it over BROWSER_CONTEXT_HOOKS itself, so adding a name to that list without the
wrapping actually reaching it fails here rather than showing up as a silence in a device log.

SDK-7422
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Debug-log coverage across the patched set — verified, then pinned (74db214)

Only four of the eight session-scoped hooks had ever been seen firing on a device, because earlier benches didn't define the rest. So I built one that defines all eight (examples/repro-triple/repro-all-hooks.conf.ts) and checked what the instrumentation actually produced.

All eight ran, all eight were instrumented, all eight logged:

hook handlers wrapped at construction start/finish lines
before 1 2
beforeSuite 1 2
beforeHook 1 8
beforeTest 3 6
afterTest 2 4
afterHook 1 8
afterSuite 1 2
after 2 4

Two things in that table are worth reading rather than skimming:

  • beforeHook / afterHook log 8 lines each — they fire once per Mocha hook, and this spec has four (before all, before each, after each, after all). That's 4 invocations × start+finish.
  • The handler counts above 1 are not the user's. beforeTest: 3, afterTest: 2, after: 2 include expect-webdriverio's snapshot and soft-assert services, which share those arrays. The index in the label (beforeTest#0, #1, #2) is what separates them; "registered at construction" means the user's hooks plus any service constructed before ours, never our own.

Pinned with a test that iterates BROWSER_CONTEXT_HOOKS itself and asserts a started and a finished line for each, so adding a name to that list without the wrapping reaching it fails in CI rather than showing up as a silence in a device log.

Suite: 1,153 passing, 75 pre-existing failures, lint clean.

github-actions Bot and others added 3 commits August 26, 2026 16:55
…tion unable to break a hook

Comments cut to the file's density: hookInstrumentation 192 -> 147 lines with no code removed
beyond `getHookFailure`, which only tests used. Comment share of the accessibilityModule diff
drops from 47/106 lines to 20/79.

Exception safety, which matters because this wrapper runs inside the user's hook:
- every log and every bookkeeping step goes through `quietly()`, so a throwing logger or a
  hostile object cannot surface as the customer's hook failing
- the entry point's own catch-block log is contained too — that one could escape the function
- `isThenable` tolerates a throwing `.then` getter
- one unwrappable handler is handed back untouched instead of costing the rest their wrapping

The handler's own error is never routed through any of that: it propagates unchanged, verified
by asserting identity of the thrown object.

Four tests for it, two of which found real holes while being written — the unprotected catch-block
log, and a wrong premise about Object.freeze (freezing a handler does not prevent wrapping it,
since defineProperty runs on the new wrapper).

SDK-7422
The gate fix and the instrumentation were both mocha/CLI-shaped. Two gaps closed.

Non-CLI flow: the classic AccessibilityHandler had the same defect the CLI module did — nothing
registered the session before the first test, so commands from a config-level hook went unscanned
while still counting as expected coverage. `before()` now opens the gate when autoScanning is on;
beforeTest/beforeScenario/beforeHook each recompute it. This is the only path cucumber and jasmine
ever take (CLISupportedFrameworks is ['mocha']), and the path mocha takes under multiremote.

Cucumber lifecycle: beforeFeature/beforeScenario/beforeStep/afterStep/afterScenario/afterFeature
were not instrumented at all, so a cucumber suite's hook windows were invisible. Added, and
beforeFeature joins PRE_TEST_WINDOW_HOOKS since it precedes the first scenario.

Measured, cucumber on device (Direct flow by construction):
  prod 9.35.0  config-level before() window: 0 scans   (kfribnppwbyt98ob8xc9lftkjvbm3xeroffaqjy1)
  this branch  config-level before() window: 1 scan    (4ouu5t3ci52ouoo3pmxcczehbzxatuln4fqhxv08)
  all 7 cucumber hooks + before/after logged start/finish
  a throwing before(): exit 0, "1 passed", and the only record is ours —
    [hook-window] before#0 rejected: BOOM: cucumber config-level before failed in 3050ms
    (tupaf5ev3rr3qclpvq2sx9628hs7ot6lvzev2phd)

Remaining asymmetry, deliberately not papered over: the hook RUN is still reported only on the
CLI path. Direct flow reports hooks through insights-handler, whose hook state is shared with the
real hooks (setCurrentHook drives log parenting), so injecting a synthetic hook there needs its
own design and testing rather than a rider on this change.

SDK-7422
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Both flows, both frameworks — implemented and run (c27900b)

The fix was mocha/CLI-shaped in two ways, and asking for cucumber surfaced both.

1. The non-CLI flow had the same defect

AccessibilityHandler (the classic path) registers the session in beforeTest, beforeScenario and beforeHook — never before the first test. So a config-level hook's commands went unscanned there too, while still counting as expected coverage. before() now opens the gate when autoScanning is on, and each of those three still recomputes it.

This is the only path cucumber and jasmine ever take (CLISupportedFrameworks = ['mocha']), and the path mocha takes under multiremote.

2. Cucumber's hooks were not instrumented at all

beforeFeature, beforeScenario, beforeStep, afterStep, afterScenario, afterFeature were missing from BROWSER_CONTEXT_HOOKS — every one has a live driver, so a cucumber suite's hook windows were invisible. Added, and beforeFeature joins PRE_TEST_WINDOW_HOOKS since it precedes the first scenario.

What was actually run

Bench: examples/repro-cucumber/ — feature + steps + two configs, every cucumber lifecycle hook defined.

case build result
cucumber, prod 9.35.0 kfribnppwbyt98ob8xc9lftkjvbm3xeroffaqjy1 config-level before() window: 0 scans
cucumber, this branch 4ouu5t3ci52ouoo3pmxcczehbzxatuln4fqhxv08 window: 1 scan; all 7 cucumber hooks + before/after logged start/finish
cucumber, failing before() tupaf5ev3rr3qclpvq2sx9628hs7ot6lvzev2phd exit 0, 1 passed — and the only record of the failure is ours
mocha CLI, all 8 hooks defined (earlier) every hook instrumented, beforeHook/afterHook 4× each
mocha CLI, failing before() / beforeSuite qkfnla45… / eq3p9ktf… hook run result=failed, TRA verdict failed

The cucumber failing path, in full:

ERROR @wdio/utils:shim: Error: BOOM: cucumber config-level before failed
[hook-window] before#0 (bound before) rejected: BOOM: cucumber config-level before failed in 3050ms
Spec Files: 1 passed, 1 total          ← WDIO swallows it, exit code 0

The one asymmetry left, stated plainly

CLI (mocha) non-CLI (cucumber, jasmine, mocha-multiremote)
config-level hook window scanned ✅ 7 scans ✅ 1 scan (was 0)
window bounded in logs
failure recorded
hook run reported to TRA wdio "before" hook, passed/failed not implemented

Direct flow reports hooks through insights-handler, and its hook state is shared with the real hooks — setCurrentHook is what parents subsequent logs. Injecting a synthetic hook there could mis-parent a customer's logs, so it needs its own design and test pass rather than riding on this change. Scans in that window therefore stay unattributed on the non-CLI path, exactly as they are on main today; what changes is that they now happen.

Unit: 1,160 passing (75 pre-existing failures), lint clean. New coverage includes the classic gate opening in before(), staying closed when autoScanning is off, and being recomputed by a filtered test.

Per the repo convention that constants live in one place: BROWSER_CONTEXT_HOOKS,
PRE_TEST_WINDOW_HOOKS, the [hook-window] log tag and the reported hook title now come from
constants.ts, with hookInstrumentation and accessibilityModule importing them. That also removes
the last string literals duplicated between the two — the 'wdio' title prefix appeared in both a
template and its fallback.

Tests import BROWSER_CONTEXT_HOOKS from constants too, so the list has a single definition.

Re-verified on device after the move, both flows unchanged:
  mocha CLI        config-level before() window: 7 scans (7wtmrijn2s3fchavf5bs6p0uprfd0uxjrboenkeg)
  cucumber non-CLI config-level before() window: 1 scan  (7tzkeeepk1x80zwtczn8gqu67kv2vcpqbvqvmbec)

SDK-7422
Closes the CLI/non-CLI asymmetry for the framework that always takes the non-CLI path. Jasmine
and mocha-multiremote are deliberately left out — neither reports hooks on a path this can reuse,
so they would need their own design.

Direct flow reports cucumber hooks through insights-handler, so that is where the pre-test window
now goes: reportPreTestHookStarted/Finished build the same TestMeta shape the real cucumber hooks
use and go out via listener.hookStarted/hookFinished. Two small things made it fit —
getHookRunDataForCucumber now prefers a stored `name` over deriving one from hookType, so a
synthesised hook can carry its own label, and the run is stashed under a fixed key so its finish
can find its start.

The accessibility handler drives it, on demand from the scan path and closed at the first
test/scenario, exactly as the CLI module does. It takes the reporter by injection rather than
reaching for insights-handler itself, so it stays unaware of which framework it is serving and
the service decides who gets one.

Measured on device, cucumber:
  happy   [BEFORE_ALL] wdio "before" hook  status=passed  build zzppjaz9vfv1tsexw1nkdbuguqyqhfxsyzopgyrm
  failing [BEFORE_ALL] wdio "before" hook  status=failed  build czrlfmfadwt32xjgdtsmzuup46wmutqqftujmje7
  build verdict follows: passed {passed: 2} vs failed {passed: 1, failed: 1}
  the window's scan still lands (1x back), and the scenario itself still reports passed —
  WDIO swallows the hook error either way, which is why the hook row is the only place it shows

SDK-7422
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Cucumber now reports the hook run too — the asymmetry is closed (0fafe79)

Base branch merged in first (56c8160, a real merge — no rewrite), constants moved to constants.ts, and then the piece that was missing: cucumber gets the hook run.

Direct flow already reports cucumber hooks through insights-handler, so that is where the pre-test window goes rather than inventing a channel. Two small adjustments made it fit:

  • getHookRunDataForCucumber now prefers a stored name over deriving one from hookType, so a synthesised hook can carry its own label. TestMeta already had the field; the builder was overwriting it.
  • The run is stashed under a fixed key, so its finish can find its start — the same guard the real cucumber hooks rely on.

The accessibility handler drives it, on demand from the scan path and closed at the first scenario, exactly as the CLI module does. It receives the reporter by injection rather than reaching for insights-handler itself, so the handler stays unaware of which framework it serves and the service decides who gets one.

On device

case hook on the dashboard build verdict build
happy [BEFORE_ALL] wdio "before" hookpassed passed {passed: 2} zzppjaz9vfv1tsexw1nkdbuguqyqhfxsyzopgyrm
before() throws [BEFORE_ALL] wdio "before" hookfailed failed {passed: 1, failed: 1} czrlfmfadwt32xjgdtsmzuup46wmutqqftujmje7

The scenario itself reports passed in both — WDIO swallows the hook error regardless — so that hook row is the only place the failure is visible. Which is the whole point of reporting it. The window's scan still lands (1x back) either way.

Parity, updated

CLI (mocha) cucumber jasmine / mocha-multiremote
window scanned
window bounded in logs
failure recorded
hook run reported new ❌ by decision

Jasmine and mocha-multiremote are left out deliberately: neither reports hooks on a path this can reuse, so each needs its own design rather than a guess. The scanning fix covers them regardless, since it lives in the classic handler's before().

Unit: 1,164 passing (75 pre-existing failures), lint clean. Four new tests for the cucumber lifecycle — opens once, closes at the first scenario, never closes what it did not open, and reports nothing when no reporter is installed.

…ng jasmine untouched

Both the gate and the hook instrumentation were framework-agnostic, so a jasmine or multiremote
run would have picked them up. Now gated on PRE_TEST_SCAN_FRAMEWORKS (mocha, cucumber) and
not-multiremote, at the two places that act: the classic handler's before() and the service's
call to instrumentBrowserContextHooks.

Checked against the code rather than taking the premise on trust, and it contradicts it twice:
TEST_HOOK_FRAMEWORKS is ['mocha', 'jasmine'] with a comment that WDIO's jasmine adapter emits the
same service hooks as mocha (SDK-7190), and util.ts states plainly that every WDIO framework
(mocha/jasmine/cucumber) supports App Automate. There is no framework gate on App-A11y anywhere,
and no multiremote gate outside the launcher's CLI check. So "unsupported" is a product position,
not something the code enforces — worth knowing, because it means jasmine is only unscanned today
by accident of provisioning rather than by design.

Measured on device, same jasmine bench with a driver-touching config-level before():
  prod 9.35.0  0 scans, whole session   (jhiej0xnkfxdigpk29psmzykmldy96mvysjzxirs)
  this branch  0 scans, whole session   (ca39y63f7h2qmuck5tylhcaajlxj2fxhtlkv1vya)
  0 [hook-window] lines and 0 hook runs in the jasmine build segment
  test passes identically on both
So jasmine is byte-for-byte unchanged, which is the requirement.

SDK-7422
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Jasmine and multiremote scoped out — and the premise checked against the code (e3b0d1f)

Both the gate and the hook instrumentation were framework-agnostic, so a jasmine or multiremote run would have picked them up. Now gated on PRE_TEST_SCAN_FRAMEWORKS (mocha, cucumber) and not-multiremote, at the two places that act: the classic handler's before() and the service's call to instrumentBrowserContextHooks.

Verified on device, both directions

Same jasmine bench, with a config-level before() that touches the driver:

scans, whole session [hook-window] lines hook runs test
prod 9.35.0 — jhiej0xnkfxdigpk29psmzykmldy96mvysjzxirs 0 0 0 passes
this branch — ca39y63f7h2qmuck5tylhcaajlxj2fxhtlkv1vya 0 0 0 passes

Byte-for-byte unchanged, which is the requirement. Bench kept at examples/repro-jasmine/ so it stays a standing control.

But the code does not say what the premise says

Worth recording, because it changes how the exclusion should be read. I checked rather than trusting it, and it contradicts the premise in two places:

// accessibility-handler.ts
/**
 * WDIO's jasmine adapter emits the same service hooks as mocha (SDK-7190);
 * cucumber goes through beforeScenario/afterScenario instead.
 */
private static readonly TEST_HOOK_FRAMEWORKS = ['mocha', 'jasmine']
// util.ts
* handles the three edge cases, and clears the app on conflict [...] There is NO Tier-2 branch:
* every WebdriverIO test framework (mocha/jasmine/cucumber) supports App Automate.

There is no framework gate on App-A11y anywhere in the service, and no multiremote gate outside the launcher's CLI check. beforeTest admits jasmine and sets the scan gate; commandWrapper has no framework check at all.

So "App-A11y is unsupported on jasmine" is a product position, not something the code enforces. The reason jasmine shows 0 scans on prod today is not a guard — it is that nothing provisions the wrapped-command set on that path. That is a meaningfully different situation from being switched off, and if the unsupported status is meant to hold, an explicit guard is the thing that would make it true.

I have not added such a guard: it would change jasmine's behaviour, which is the opposite of the instruction here. Flagging it as its own decision.

Unit: 1,167 passing (75 pre-existing failures), lint clean. Three new tests — jasmine gets no gate, multiremote gets no gate, and jasmine opens no hook run even with a reporter installed.

A multiremote browser has no sessionId — MultiRemoteDriver defines none and the element path
deletes it, which is why wdio-runner reads ids per instance instead. The service passes
browser.sessionId straight to the handler, so it receives undefined, and the classic scan gate is
keyed on session id: commandWrapper's `this._sessionId && map[this._sessionId]` can never match.

So multiremote was never scanning by construction, not by policy. Recording it as a test because
the reason is three files away from the code that depends on it, and because the pre-test gate
would otherwise have written an 'undefined' key into the shared map.

SDK-7422
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Correction to my earlier comment: multiremote was never scanning, and not by policy

I wrote above that there is "no multiremote gate outside the launcher's CLI check". That is true of explicit guards and misleading as a conclusion — there is a structural one, and it is stronger than a policy flag.

A multiremote browser has no sessionId:

// webdriverio/src/multiremote.ts — MultiRemoteDriver
export class MultiRemoteDriver {
    instances: string[]
    isMultiremote = true as const
    __propertiesObject__: Record<string, PropertyDescriptor>
    // ...no sessionId
// same file, element path
// @ts-expect-error ToDo(Christian): remove eventually
delete client.sessionId

Which is exactly why wdio-runner reads ids per instance for multiremote rather than off the browser:

prev[multiRemoteBrowser.getInstance(browserName).sessionId] = ...
caps[browserName].sessionId = multiRemoteBrowser.getInstance(browserName).sessionId

The service passes this._browser.sessionId straight into AccessibilityHandler.before(), so under multiremote the handler receives undefined. And the entire classic scan gate is keyed on session id:

this._sessionId && AccessibilityHandler._a11yScanSessionMap[this._sessionId] && ...

undefined && is falsy, so commandWrapper can never scan, and beforeTest's if (this._sessionId) never writes. Multiremote a11y is dead by construction — one session id cannot address N sessions.

Two consequences worth stating:

  1. Mocha + multiremote never had App-A11y, so nothing is being taken away by scoping it out. The launcher's !isMultiremote on CLI bootstrap only decides which path it takes; the path it lands on cannot scan.
  2. Without my explicit guard, the pre-test gate would have written an 'undefined' key into the shared static map. Still no scans — _sessionId is falsy either way — but junk in a process-wide map shared across instances. The guard prevents that, which is a better reason for it than the one I first gave.

Pinned as a test (c79e2ba), because the reason lives three files away from the code that depends on it.

Contrast with jasmine, which is worth keeping distinct: jasmine has a session id and is in TEST_HOOK_FRAMEWORKS, so it is unscanned for a much softer reason (nothing provisions the wrapped-command set on that path). Multiremote cannot scan; jasmine merely does not.

…sic path

The cucumber hook run was reaching TRA while the scans in its window still went out with no
parent at all — thTestRunUuid ABSENT and thHookRunUuid ABSENT — so the join the hook run exists
for could not happen. Caught by reading a real payload rather than trusting the previous commit's
claim that the asymmetry was closed.

The mechanism was already there: commandWrapper passes this._currentHookRunUuid to
performA11yScan as thHookRunUuid, exactly as beforeHook/afterHook use it for framework hooks. The
pre-test window just never set it. Set on open, cleared on close so a test-body scan is never
stamped as a hook scan.

SDK-7422
…an open

The previous commit stamped the hook uuid onto the scan but nothing changed on device: the
window's scan still went out with thHookRunUuid ABSENT. The reason was ordering, not the stamp —
the reporter was installed after InsightsHandler was constructed, and WDIO runs the user's
config-level before() concurrently with ours, so its first command reached the scan path while
this method was still setting up. ensurePreTestHookRun found no reporter and did nothing.

Installed before AccessibilityHandler.before(), and the callbacks now read _insightsHandler at
CALL time instead of capturing it, so being wired before it exists is fine.

Measured, cucumber with two commands in the hook (fyiiwkwdl8deghsyanxtq7e0shsrlkdc59xrmg01):
  scan 1  hookRun=9384b3f5   <- the window, now parented
  scan 2  hookRun=9384b3f5
  scan 3..5  testRun=0032ebfd, hookRun ABSENT   <- scenario body, correctly not hook scans

Worth noting what the window's scans still lack: thTestRunUuid is ABSENT there, because on the
Direct path that env var is only set when a test actually starts. The hook uuid is the join, which
is what we agreed was sufficient.

SDK-7422
A scan fired from a WDIO config-level hook belongs to no test, so it must not carry one.
TEST_ANALYTICS_ID in that window holds a uuid mocha minted at instance creation — a test that has
not started — and sending it attributed the scan to a test it did not come from. The hook run
uuid is the parent there, which is the whole reason it exists.

_getParamsForAppAccessibility takes an isGlobalHook flag; both scan paths thread it from an
explicit "pre-test window active" flag, set when the gate opens at driver creation and cleared at
the first test. Cleared unconditionally on the classic path: the window is over whether or not a
hook run was opened, and leaving it set would strip the uuid from every later test-body scan.

Two existing tests asserted the LAST argument of performA11yScan, so appending a parameter made
them silently follow it — now asserted by position. Two more asserted an exact 3-arg call that is
now 4; corrected to expect the forwarded undefined rather than pretending the arity is unchanged.

SDK-7422
@kamal-kaur04

Copy link
Copy Markdown
Collaborator Author

Device-verified: global-hook scans no longer carry a test run uuid (178c541)

178c541 went up without a device run behind it, so here it is — pre-fix and post-fix, both flows.

Verification reads the session log, not the SDK log: the scan is a browserstack_executor: {"action":"appAllyScan", arguments: {...}} payload, so the log carries the exact params we sent. work/SDK-7422-app-a11y-fixes/uuid-verdict.py <build> <session> prints a per-scan census — phase (from the bench's PHASE_MARKER annotations), thTestRunUuid, thHookRunUuid, command.

Pre-fix — the defect, mocha/CLI flow

Build rpgacbl0dyjcmmg34kwm3y5xui1bdirosz16ncll · session 5a6695f7c00b047ca90b7d367d580f5f3058bc8f

 #  phase at scan                    test uuid  hook uuid  command
 1  mocha before all STARTED         22ec974c   4ffc67e8   back
 2  mocha before all STARTED         22ec974c   4ffc67e8   back
 3  mocha before each #1 STARTED     22ec974c   157abbac   back
 4  test 1 body STARTED (pre-reload) 22ec974c   -          back
 5  test 1 body STARTED (pre-reload) 22ec974c   -          back
 6  test 1 body STARTED (pre-reload) 22ec974c   -          back

Scans 1–2 run before any test exists and still carry 22ec974c — the very uuid the test body later reports under. That is the mis-attribution: a hook scan claiming to be test 1's.

Post-fix — same conf, same device

Build r89xojas5cvljamjpwrj8x7zcf3pc3umt8qyoptk · sessions 72d8f60ebfd6485d670021fb68f43c4163ee6897 (original) and e60022566902b3f72651c4784448e2d39ff73302 (reloaded)

session 72d8f60e... (pre-reload)
 #  phase at scan                    test uuid  hook uuid  command
 1  mocha before all STARTED         -          d8c4a1e2   back
 2  mocha before all STARTED         -          d8c4a1e2   back
 3  mocha before each #1 STARTED     -          0fc93e9f   back
 4  test 1 body STARTED (pre-reload) 8ce5cfc4   -          back
 5  test 1 body STARTED (pre-reload) 8ce5cfc4   -          back
 6  test 1 body STARTED (pre-reload) 8ce5cfc4   -          back

session e6002256... (post-reload)
 #  phase at scan                    test uuid  hook uuid  command
 1  test 1 body RESUMED              8ce5cfc4   -          click
 …
 8  mocha before each #2 STARTED     847ed76c   0cc91e13   back
 9  test 2 body STARTED              847ed76c   -          click
13  mocha before each #3 STARTED     739f7e4e   4691ebb0   back
14  test 3 body STARTED              739f7e4e   -          back

Window scans lose the test uuid and keep their hook uuid; every test-body scan keeps its own. All 16 post-reload scans still land, so the D2 fix on the base branch is unaffected.

The nuance worth naming

before each #1 also falls inside the window, so it loses its test uuid while #2 and #3 — which run after the window closes — keep theirs (scans 8 and 13 carry both uuids). That asymmetry is real. It costs nothing: those scans keep their own hook run uuid, and hook-run→test parentage is untouched by this commit, which only changed scan params. Narrowing the window to exclude framework beforeEach would need the window to close on the framework hook rather than on the first test, and that would reopen the gap this window exists to close.

Cucumber — no-op, and that is the expected result

build window scans test-body scans
pre-fix fyiiwkwdl8deghsyanxtq7e0shsrlkdc59xrmg01 no test uuid stamped
post-fix yldiivrvnkhf0xi7ttvzekksvxoafxulcrx66gac no test uuid stamped

Identical, because the defect was CLI-flow-only: mocha mints TEST_ANALYTICS_ID at instance creation, so the window had a uuid to leak, while the classic path sets it at Listener.testStarted and never had one. The guard is wired on both paths anyway so the classic path cannot regress if that ever moves earlier.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant