Skip to content

Disable accessibility when browserstackAccessibility plugin not loaded#1132

Open
pranay-v29 wants to merge 3 commits into
masterfrom
disable-accessibility-when-plugin-not-loaded
Open

Disable accessibility when browserstackAccessibility plugin not loaded#1132
pranay-v29 wants to merge 3 commits into
masterfrom
disable-accessibility-when-plugin-not-loaded

Conversation

@pranay-v29

@pranay-v29 pranay-v29 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

When a build requests accessibility (browserstack.json/browser caps) but the browserstackAccessibility plugin is not wired into the cypress config, the CLI now detects this before the build start event, explicitly disables accessibility so the build is not counted as an a11y build, warns the user with the setup doc link, and instruments the end-of-session EDS event (accessibility_plugin_not_loaded) so such builds can be excluded from stability queries.

Detection is code-based: the plugin sets BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED when its module is loaded/invoked by the cypress config; requireModule writes a flag file the parent process reads back. Falls back to a raw-source scan only when the config could not be required (e.g. TS before bstack packages are installed).

Detection is also done at import and invocation of plugin.

When a build requests accessibility (browserstack.json/browser caps) but the
browserstackAccessibility plugin is not wired into the cypress config, the CLI
now detects this before the build start event, explicitly disables accessibility
so the build is not counted as an a11y build, warns the user with the setup doc
link, and instruments the end-of-session EDS event (accessibility_plugin_not_loaded)
so such builds can be excluded from stability queries.

Detection is code-based: the plugin sets BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED
when its module is loaded/invoked by the cypress config; requireModule writes a
flag file the parent process reads back. Falls back to a raw-source scan only when
the config could not be required (e.g. TS before bstack packages are installed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pranay-v29 pranay-v29 requested a review from a team as a code owner June 22, 2026 05:58
};

const isBindingCalled = (content, binding) => {
const callRegex = new RegExp('\\b' + binding.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*\\(');

@kamal-kaur04 kamal-kaur04 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.

Automated SDK-style review — accessibility-plugin-not-loaded detection.

Design is sound and degrades safely; nothing build-breaking. Inline notes below: 2 warnings worth resolving before merge (false-negative on unusual import syntax; redundant TS config compile on every a11y run) and 3 cleanup suggestions. Posted as comments — final approve/merge call is the human reviewer's.

Comment thread bin/commands/runs.js
// If accessibility is requested but the BrowserStack accessibility plugin is
// not loaded in the cypress config, explicitly disable accessibility before
// the build start event so the build is not treated as an accessibility build.
if (isAccessibilitySession && isBrowserstackInfra && !isAccessibilityPluginLoaded(bsConfig)) {

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 — redundant config read/compile on every a11y build.

isAccessibilityPluginLoaded(bsConfig) calls readCypressConfigFile, which is already invoked at capabilityHelper.js:251 and helper.js:378 during a normal run. This adds a third call. For a TS config each call runs convertTsConfigcp.execSync(tsc …) (a fresh sync TypeScript compile) plus a child-process require that re-executes the user config's top-level side-effects.

Consider reading the config once and threading the detection signal through, or memoizing on BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED once an earlier read in the same run has populated it, only forcing a fresh read when it's still undefined.


// Finds the symbol the accessibility plugin is imported as, via require() or
// import, regardless of path style. Returns the binding name or null.
const getAccessibilityPluginBinding = (content) => {

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 — binding regex misses valid import forms → silent a11y disable.

These regexes only match const/let/var X = require('…/plugin') and import X from '…/plugin'. They do not match import * as X from …, import { X } from …, or dynamic await import(…).

On the require-succeeded path this is benign (the invocation scan is lenient). But on the inconclusive path (TS config before bstack packages are installed), isAccessibilityPluginImportedAndCalledInSource is strict and returns false when the binding isn't found — so a correctly-wired config using namespace/dynamic import gets accessibility silently disabled (a billed feature).

Suggest widening the regex to cover import * as X / import { X }, or biasing the strict fallback toward keeping a11y on when the plugin path is present in source at all.

// Strip JS/TS comments so that commented-out plugin imports/calls are ignored
// by the static scans below. Best-effort: handles block and line comments while
// avoiding `://` in URLs.
const stripComments = (src) => {

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 — stripComments can corrupt string literals.

The :// guard protects URLs, but a // inside a non-URL string (e.g. const re = "a//b") is still stripped, and the block-comment pass strips /* */ inside strings too. ^ isn't multiline-anchored (a preceding \n satisfies [^:], so line-start comments are still handled). Impact is low since the require-load marker is the primary signal — worth a comment noting the scan is intentionally lossy.

// sending the build start event, and uses this marker to determine whether the
// accessibility plugin is actually wired in. Unlike a static text scan of the
// config file, this does NOT false-positive on commented-out requires.
process.env.BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED = 'true';

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 — load-bearing process.env mutation as an import side-effect.

Setting BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED='true' at module top level is the intended detection mechanism, but a global mutation on require is easy to miss. Since the whole feature depends on it, consider making the invariant unmissable so anyone refactoring this module preserves it.

try {
const accessibilityPluginLoaded = process.env.BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED === 'true';
fs.writeFileSync(
config.accessibilityPluginFlagFileName,

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 — temp flag file can be left behind.

tmpA11yPluginLoaded.json is unlinked only inside loadJsFile after a successful read. If loadJsFile returns early (e.g. the configJsonFileName read throws before the propagation block), the file lingers, and a stale file from a crashed prior run could be read on a path that bypasses a fresh write. The child overwrites it on the normal path, so risk is low — but a guaranteed cleanup (or writing under an os-temp dir) would be safer.

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.

3 participants