From 0c1bfd83150d02b70bb3b8763cea5ba9bfc0bfdb Mon Sep 17 00:00:00 2001 From: Priyanka Vats Date: Fri, 23 Oct 2026 15:49:04 +0530 Subject: [PATCH 1/2] APS-22894 Cypress 16: read plugin values via Cypress.expose() with a Cypress.env() fallback in the TestHub and Accessibility support files Co-Authored-By: Claude Fable 5.1 --- bin/accessibility-automation/cypress/index.js | 50 +++++++++++++------ bin/accessibility-automation/plugin/index.js | 33 ++++++++---- bin/testObservability/cypress/index.js | 32 +++++++++--- bin/testObservability/plugin/index.js | 13 ++++- .../accessibility-automation/plugin/index.js | 50 +++++++++++++++++++ .../plugin/exposeToBrowser.js | 36 +++++++++++++ 6 files changed, 181 insertions(+), 33 deletions(-) create mode 100644 test/unit/bin/accessibility-automation/plugin/index.js create mode 100644 test/unit/bin/testObservability/plugin/exposeToBrowser.js diff --git a/bin/accessibility-automation/cypress/index.js b/bin/accessibility-automation/cypress/index.js index 442553b5..e18efdf6 100644 --- a/bin/accessibility-automation/cypress/index.js +++ b/bin/accessibility-automation/cypress/index.js @@ -1,7 +1,27 @@ /* Event listeners + custom commands for Cypress */ +/* + * Cypress 16 removed Cypress.env() from the browser; values set by the Node plugin now reach + * the browser through config.expose / Cypress.expose() (available from Cypress 15.10). The + * plugin writes every value to both config.env and config.expose, so read expose first and + * fall back to Cypress.env() for Cypress <= 15.x, where expose does not exist yet. [APS-22894] + */ +const bstackBrowserValue = (key) => { + try { + if (typeof Cypress.expose === 'function') { + const exposed = Cypress.expose(key); + if (exposed !== undefined && exposed !== null) return exposed; + } + } catch (e) {} + try { + return Cypress.env(key); + } catch (e) { + return undefined; + } +}; + const browserStackLog = (message) => { - if (!Cypress.env('BROWSERSTACK_LOGS')) return; + if (!bstackBrowserValue('BROWSERSTACK_LOGS')) return; cy.task('browserstack_log', message); } @@ -13,7 +33,7 @@ const browserStackLog = (message) => { let consecutiveA11yTimeouts = 0; let a11yCircuitOpen = false; let a11yCircuitLogged = false; -const getA11yCircuitLimit = () => parseInt(Cypress.env('ACCESSIBILITY_SCAN_CIRCUIT_LIMIT')) || 3; +const getA11yCircuitLimit = () => parseInt(bstackBrowserValue('ACCESSIBILITY_SCAN_CIRCUIT_LIMIT')) || 3; const noteA11yTimeout = () => { consecutiveA11yTimeouts += 1; if (!a11yCircuitOpen && consecutiveA11yTimeouts >= getA11yCircuitLimit()) { @@ -76,7 +96,7 @@ new Promise((resolve) => { } let settled = false; const finish = (val) => { if (!settled) { settled = true; clearTimeout(overallTimer); resolve(val); } }; - const overallTimeout = parseInt(Cypress.env('ACCESSIBILITY_SCAN_TIMEOUT')) || 25000; + const overallTimeout = parseInt(bstackBrowserValue('ACCESSIBILITY_SCAN_TIMEOUT')) || 25000; const overallTimer = setTimeout(() => { noteA11yTimeout(); finish("Accessibility scan timed out"); }, overallTimeout); try { @@ -252,7 +272,7 @@ new Promise((resolve) => { } let settled = false; const finish = (val) => { if (!settled) { settled = true; clearTimeout(overallTimer); resolve(val); } }; - const overallTimeout = parseInt(Cypress.env('ACCESSIBILITY_SCAN_TIMEOUT')) || 25000; + const overallTimeout = parseInt(bstackBrowserValue('ACCESSIBILITY_SCAN_TIMEOUT')) || 25000; const overallTimer = setTimeout(() => { noteA11yTimeout(); finish("Accessibility results save timed out"); }, overallTimeout); try { const isHttpOrHttps = /^(http|https):$/.test(win.location.protocol); @@ -314,24 +334,24 @@ new Promise((resolve) => { }) const shouldScanForAccessibility = (attributes) => { - if (Cypress.env("IS_ACCESSIBILITY_EXTENSION_LOADED") !== "true") return false; + if (bstackBrowserValue("IS_ACCESSIBILITY_EXTENSION_LOADED") !== "true") return false; - const extensionPath = Cypress.env("ACCESSIBILITY_EXTENSION_PATH"); + const extensionPath = bstackBrowserValue("ACCESSIBILITY_EXTENSION_PATH"); const isHeaded = Cypress.browser.isHeaded; if (!isHeaded || (extensionPath === undefined)) return false; let shouldScanTestForAccessibility = true; - if (Cypress.env("INCLUDE_TAGS_FOR_ACCESSIBILITY") || Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY")) { + if (bstackBrowserValue("INCLUDE_TAGS_FOR_ACCESSIBILITY") || bstackBrowserValue("EXCLUDE_TAGS_FOR_ACCESSIBILITY")) { try { let includeTagArray = []; let excludeTagArray = []; - if (Cypress.env("INCLUDE_TAGS_FOR_ACCESSIBILITY")) { - includeTagArray = Cypress.env("INCLUDE_TAGS_FOR_ACCESSIBILITY").split(";") + if (bstackBrowserValue("INCLUDE_TAGS_FOR_ACCESSIBILITY")) { + includeTagArray = bstackBrowserValue("INCLUDE_TAGS_FOR_ACCESSIBILITY").split(";") } - if (Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY")) { - excludeTagArray = Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY").split(";") + if (bstackBrowserValue("EXCLUDE_TAGS_FOR_ACCESSIBILITY")) { + excludeTagArray = bstackBrowserValue("EXCLUDE_TAGS_FOR_ACCESSIBILITY").split(";") } const fullTestName = attributes.title; @@ -383,8 +403,8 @@ afterEach(() => { cy.wrap(performScan(win), {timeout: 30000}).then(() => { try { let os_data; - if (Cypress.env("OS")) { - os_data = Cypress.env("OS"); + if (bstackBrowserValue("OS")) { + os_data = bstackBrowserValue("OS"); } else { os_data = Cypress.platform === 'linux' ? 'mac' : "win" } @@ -404,8 +424,8 @@ afterEach(() => { const payloadToSend = { "thTestRunUuid": testRunUuid, - "thBuildUuid": Cypress.env("BROWSERSTACK_TESTHUB_UUID"), - "thJwtToken": Cypress.env("BROWSERSTACK_TESTHUB_JWT") + "thBuildUuid": bstackBrowserValue("BROWSERSTACK_TESTHUB_UUID"), + "thJwtToken": bstackBrowserValue("BROWSERSTACK_TESTHUB_JWT") }; browserStackLog(`Payload to send: ${JSON.stringify(payloadToSend)}`); diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index c51b2606..b744bc3d 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -3,6 +3,17 @@ const { decodeJWTToken } = require("../../helpers/utils"); const utils = require('../../helpers/utils'); const http = require('http'); +// Cypress 16 removed Cypress.env() from the browser. Values the support file needs are +// written to config.expose (read by Cypress.expose(), Cypress >= 15.10) as well as config.env +// (read by Cypress.env(), Cypress <= 15.x). Unknown config.expose is accepted by Cypress 15.8+ +// without error. [APS-22894] +const exposeToBrowser = (config, key, value) => { + config.env = config.env || {}; + config.env[key] = value; + config.expose = config.expose || {}; + config.expose[key] = value; +}; + process.env.BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED = 'true'; const browserstackAccessibility = (on, config) => { @@ -11,7 +22,7 @@ const browserstackAccessibility = (on, config) => { process.env.BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED = 'true'; let browser_validation = true; if (process.env.BROWSERSTACK_ACCESSIBILITY_DEBUG === 'true') { - config.env.BROWSERSTACK_LOGS = 'true'; + exposeToBrowser(config, 'BROWSERSTACK_LOGS', 'true'); process.env.BROWSERSTACK_LOGS = 'true'; } on('task', { @@ -99,18 +110,18 @@ const browserstackAccessibility = (on, config) => { } catch(err) {} }) - config.env.ACCESSIBILITY_EXTENSION_PATH = process.env.ACCESSIBILITY_EXTENSION_PATH - config.env.OS_VERSION = process.env.OS_VERSION - config.env.OS = process.env.OS - config.env.BROWSERSTACK_TESTHUB_UUID = process.env.BROWSERSTACK_TESTHUB_UUID - config.env.BROWSERSTACK_TESTHUB_JWT = process.env.BROWSERSTACK_TESTHUB_JWT - config.env.BROWSERSTACK_TESTHUB_API_PORT = process.env.BROWSERSTACK_TESTHUB_API_PORT - config.env.REPORTER_API_PORT_NO = process.env.REPORTER_API_PORT_NO + exposeToBrowser(config, 'ACCESSIBILITY_EXTENSION_PATH', process.env.ACCESSIBILITY_EXTENSION_PATH) + exposeToBrowser(config, 'OS_VERSION', process.env.OS_VERSION) + exposeToBrowser(config, 'OS', process.env.OS) + exposeToBrowser(config, 'BROWSERSTACK_TESTHUB_UUID', process.env.BROWSERSTACK_TESTHUB_UUID) + exposeToBrowser(config, 'BROWSERSTACK_TESTHUB_JWT', process.env.BROWSERSTACK_TESTHUB_JWT) + exposeToBrowser(config, 'BROWSERSTACK_TESTHUB_API_PORT', process.env.BROWSERSTACK_TESTHUB_API_PORT) + exposeToBrowser(config, 'REPORTER_API_PORT_NO', process.env.REPORTER_API_PORT_NO) - config.env.IS_ACCESSIBILITY_EXTENSION_LOADED = browser_validation.toString() + exposeToBrowser(config, 'IS_ACCESSIBILITY_EXTENSION_LOADED', browser_validation.toString()) - config.env.INCLUDE_TAGS_FOR_ACCESSIBILITY = process.env.ACCESSIBILITY_INCLUDETAGSINTESTINGSCOPE - config.env.EXCLUDE_TAGS_FOR_ACCESSIBILITY = process.env.ACCESSIBILITY_EXCLUDETAGSINTESTINGSCOPE + exposeToBrowser(config, 'INCLUDE_TAGS_FOR_ACCESSIBILITY', process.env.ACCESSIBILITY_INCLUDETAGSINTESTINGSCOPE) + exposeToBrowser(config, 'EXCLUDE_TAGS_FOR_ACCESSIBILITY', process.env.ACCESSIBILITY_EXCLUDETAGSINTESTINGSCOPE) return config; } diff --git a/bin/testObservability/cypress/index.js b/bin/testObservability/cypress/index.js index cef4aeed..8707e214 100644 --- a/bin/testObservability/cypress/index.js +++ b/bin/testObservability/cypress/index.js @@ -1,5 +1,25 @@ /* Event listeners + custom commands for Cypress */ +/* + * Cypress 16 removed Cypress.env() from the browser; values set by the Node plugin now reach + * the browser through config.expose / Cypress.expose() (available from Cypress 15.10). The + * plugin writes every value to both config.env and config.expose, so read expose first and + * fall back to Cypress.env() for Cypress <= 15.x, where expose does not exist yet. [APS-22894] + */ +const bstackBrowserValue = (key) => { + try { + if (typeof Cypress.expose === 'function') { + const exposed = Cypress.expose(key); + if (exposed !== undefined && exposed !== null) return exposed; + } + } catch (e) {} + try { + return Cypress.env(key); + } catch (e) { + return undefined; + } +}; + /* Used to detect Gherkin steps */ const STEP_KEYWORDS = ['given', 'when', 'then', 'and', 'but', '*']; @@ -42,12 +62,12 @@ const sanitizeForTask = (data) => { const browserStackLog = (message) => { - if (!Cypress.env('BROWSERSTACK_LOGS')) return; + if (!bstackBrowserValue('BROWSERSTACK_LOGS')) return; cy.task('browserstack_log', message); } const shouldSkipCommand = (command) => { - if (!Cypress.env('BROWSERSTACK_O11Y_LOGS')) { + if (!bstackBrowserValue('BROWSERSTACK_O11Y_LOGS')) { return true; } /* the batch task is filtered too, else each dispatch refills the queue */ @@ -55,7 +75,7 @@ const shouldSkipCommand = (command) => { } Cypress.on('log:changed', (attrs) => { - if (!Cypress.env('BROWSERSTACK_O11Y_LOGS')) return; + if (!bstackBrowserValue('BROWSERSTACK_O11Y_LOGS')) return; if (!attrs) return; if (!attrs.createdAtTimestamp || !attrs.updatedAtTimestamp) return; if (attrs.state !== 'passed' && attrs.state !== 'failed') return; @@ -237,7 +257,7 @@ Cypress.on('command:end', (command) => { }); Cypress.on('command:enqueued', (attrs) => { - if (!Cypress.env('BROWSERSTACK_O11Y_LOGS')) return; + if (!bstackBrowserValue('BROWSERSTACK_O11Y_LOGS')) return; if (!attrs || attrs.name !== 'log') return; const args = attrs.args || []; if (args.includes('test_observability_log') || args.includes('test_observability_command')) return; @@ -420,7 +440,7 @@ const flushEventsQueue = () => { beforeEach(() => { /* browserstack internal helper hook */ - if (!Cypress.env('BROWSERSTACK_O11Y_LOGS')) { + if (!bstackBrowserValue('BROWSERSTACK_O11Y_LOGS')) { return; } @@ -430,7 +450,7 @@ beforeEach(() => { afterEach(function() { /* browserstack internal helper hook */ - if (!Cypress.env('BROWSERSTACK_O11Y_LOGS')) { + if (!bstackBrowserValue('BROWSERSTACK_O11Y_LOGS')) { return; } diff --git a/bin/testObservability/plugin/index.js b/bin/testObservability/plugin/index.js index 286a4492..f5dee4ab 100644 --- a/bin/testObservability/plugin/index.js +++ b/bin/testObservability/plugin/index.js @@ -2,10 +2,21 @@ const ipc = require('node-ipc'); const { connectIPCClient } = require('./ipcClient'); const { IPC_EVENTS } = require('../helper/constants'); +// Cypress 16 removed Cypress.env() from the browser. Values the support file needs are +// written to config.expose (read by Cypress.expose(), Cypress >= 15.10) as well as config.env +// (read by Cypress.env(), Cypress <= 15.x). Unknown config.expose is accepted by Cypress 15.8+ +// without error. [APS-22894] +const exposeToBrowser = (config, key, value) => { + config.env = config.env || {}; + config.env[key] = value; + config.expose = config.expose || {}; + config.expose[key] = value; +}; + const browserstackTestObservabilityPlugin = (on, config, callbacks) => { try { - config.env.BROWSERSTACK_O11Y_LOGS = 'true'; + exposeToBrowser(config, 'BROWSERSTACK_O11Y_LOGS', 'true'); process.env.BROWSERSTACK_O11Y_LOGS = 'true'; } catch (err) {} diff --git a/test/unit/bin/accessibility-automation/plugin/index.js b/test/unit/bin/accessibility-automation/plugin/index.js new file mode 100644 index 00000000..7f648956 --- /dev/null +++ b/test/unit/bin/accessibility-automation/plugin/index.js @@ -0,0 +1,50 @@ +'use strict'; +const { expect } = require('chai'); +const sinon = require('sinon'); + +// APS-22894: Cypress 16 removed Cypress.env() from the browser, so every value the +// accessibility support file reads must also be written to config.expose. +describe('accessibility plugin exposes browser values', () => { + const KEYS = [ + 'ACCESSIBILITY_EXTENSION_PATH', 'OS_VERSION', 'OS', 'BROWSERSTACK_TESTHUB_UUID', + 'BROWSERSTACK_TESTHUB_JWT', 'BROWSERSTACK_TESTHUB_API_PORT', 'REPORTER_API_PORT_NO', + 'IS_ACCESSIBILITY_EXTENSION_LOADED', 'INCLUDE_TAGS_FOR_ACCESSIBILITY', 'EXCLUDE_TAGS_FOR_ACCESSIBILITY', + ]; + const saved = {}; + const ENV = { + ACCESSIBILITY_EXTENSION_PATH: '/tmp/a11y-ext', OS_VERSION: '11', OS: 'Windows', + BROWSERSTACK_TESTHUB_UUID: 'uuid-1', BROWSERSTACK_TESTHUB_JWT: 'jwt-1', + BROWSERSTACK_TESTHUB_API_PORT: '4567', REPORTER_API_PORT_NO: '4568', + ACCESSIBILITY_INCLUDETAGSINTESTINGSCOPE: 'a;b', ACCESSIBILITY_EXCLUDETAGSINTESTINGSCOPE: 'c', + BROWSERSTACK_ACCESSIBILITY_DEBUG: 'true', + }; + + beforeEach(() => { + Object.keys(ENV).forEach((k) => { saved[k] = process.env[k]; process.env[k] = ENV[k]; }); + }); + afterEach(() => { + Object.keys(ENV).forEach((k) => { if (saved[k] === undefined) delete process.env[k]; else process.env[k] = saved[k]; }); + }); + + const run = (config) => { + const plugin = require('../../../../../bin/accessibility-automation/plugin/index.js'); + return plugin(sinon.stub(), config); + }; + + it('mirrors every browser-read value into config.expose', () => { + const config = run({ env: {} }); + KEYS.forEach((key) => { + expect(config.env, key).to.have.property(key); + expect(config.expose[key], key).to.eql(config.env[key]); + }); + expect(config.expose.ACCESSIBILITY_EXTENSION_PATH).to.eql('/tmp/a11y-ext'); + expect(config.expose.INCLUDE_TAGS_FOR_ACCESSIBILITY).to.eql('a;b'); + expect(config.expose.IS_ACCESSIBILITY_EXTENSION_LOADED).to.eql('true'); + }); + + it('exposes BROWSERSTACK_LOGS when accessibility debug is on', () => { + const config = run({ env: {} }); + expect(config.env.BROWSERSTACK_LOGS).to.eql('true'); + expect(config.expose.BROWSERSTACK_LOGS).to.eql('true'); + }); +}); diff --git a/test/unit/bin/testObservability/plugin/exposeToBrowser.js b/test/unit/bin/testObservability/plugin/exposeToBrowser.js new file mode 100644 index 00000000..74d81258 --- /dev/null +++ b/test/unit/bin/testObservability/plugin/exposeToBrowser.js @@ -0,0 +1,36 @@ +'use strict'; +const { expect } = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +// APS-22894: Cypress 16 removed Cypress.env() from the browser, so every value the +// support file reads must also be written to config.expose (Cypress.expose()). +describe('testObservability plugin exposes browser values', () => { + const load = () => proxyquire('../../../../../bin/testObservability/plugin/index.js', { + 'node-ipc': { of: {} }, + './ipcClient': { connectIPCClient: sinon.stub() }, + }); + + it('writes BROWSERSTACK_O11Y_LOGS to both config.env and config.expose', () => { + const plugin = load(); + const config = { env: {} }; + plugin(sinon.stub(), config, {}); + expect(config.env.BROWSERSTACK_O11Y_LOGS).to.eql('true'); + expect(config.expose.BROWSERSTACK_O11Y_LOGS).to.eql('true'); + }); + + it('creates config.expose when the project config has none', () => { + const plugin = load(); + const config = { env: {} }; + plugin(sinon.stub(), config, {}); + expect(config.expose).to.be.an('object'); + }); + + it('keeps values a customer already exposed', () => { + const plugin = load(); + const config = { env: {}, expose: { CUSTOMER_FLAG: '1' } }; + plugin(sinon.stub(), config, {}); + expect(config.expose.CUSTOMER_FLAG).to.eql('1'); + expect(config.expose.BROWSERSTACK_O11Y_LOGS).to.eql('true'); + }); +}); From 626846245ba68c4bdaaf250c440bc9383f5c99c1 Mon Sep 17 00:00:00 2001 From: Anish Sinha Date: Fri, 30 Oct 2026 10:31:14 +0530 Subject: [PATCH 2/2] APS-22894 Cypress 16: forward customer ACCESSIBILITY_SCAN_TIMEOUT / CIRCUIT_LIMIT to config.expose Customers set these in their Cypress env config. Cypress 16 no longer passes config.env to the browser, so the support file fell back to the defaults (25000 ms / 3). Copy them into config.expose when set, without overriding a value the customer already exposed. Co-Authored-By: Claude Opus 5.5 --- bin/accessibility-automation/plugin/index.js | 8 ++++++++ .../accessibility-automation/plugin/index.js | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index b744bc3d..3d9b5011 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -123,6 +123,14 @@ const browserstackAccessibility = (on, config) => { exposeToBrowser(config, 'INCLUDE_TAGS_FOR_ACCESSIBILITY', process.env.ACCESSIBILITY_INCLUDETAGSINTESTINGSCOPE) exposeToBrowser(config, 'EXCLUDE_TAGS_FOR_ACCESSIBILITY', process.env.ACCESSIBILITY_EXCLUDETAGSINTESTINGSCOPE) + // Customer-tunable keys arrive via config.env, which Cypress 16 no longer passes to the browser. + const customerScanKeys = ['ACCESSIBILITY_SCAN_TIMEOUT', 'ACCESSIBILITY_SCAN_CIRCUIT_LIMIT']; + customerScanKeys.forEach((key) => { + if (config.env[key] !== undefined && config.expose[key] === undefined) { + config.expose[key] = config.env[key]; + } + }); + return config; } diff --git a/test/unit/bin/accessibility-automation/plugin/index.js b/test/unit/bin/accessibility-automation/plugin/index.js index 7f648956..ff774b5c 100644 --- a/test/unit/bin/accessibility-automation/plugin/index.js +++ b/test/unit/bin/accessibility-automation/plugin/index.js @@ -47,4 +47,21 @@ describe('accessibility plugin exposes browser values', () => { expect(config.env.BROWSERSTACK_LOGS).to.eql('true'); expect(config.expose.BROWSERSTACK_LOGS).to.eql('true'); }); + + it('forwards customer-set scan timeout and circuit limit from config.env to config.expose', () => { + const config = run({ env: { ACCESSIBILITY_SCAN_TIMEOUT: 1234, ACCESSIBILITY_SCAN_CIRCUIT_LIMIT: 5 } }); + expect(config.expose.ACCESSIBILITY_SCAN_TIMEOUT).to.eql(1234); + expect(config.expose.ACCESSIBILITY_SCAN_CIRCUIT_LIMIT).to.eql(5); + }); + + it('does not override scan values the customer already set in config.expose', () => { + const config = run({ env: { ACCESSIBILITY_SCAN_TIMEOUT: 1234 }, expose: { ACCESSIBILITY_SCAN_TIMEOUT: 9999 } }); + expect(config.expose.ACCESSIBILITY_SCAN_TIMEOUT).to.eql(9999); + }); + + it('leaves scan keys out of config.expose when the customer did not set them', () => { + const config = run({ env: {} }); + expect(config.expose).to.not.have.property('ACCESSIBILITY_SCAN_TIMEOUT'); + expect(config.expose).to.not.have.property('ACCESSIBILITY_SCAN_CIRCUIT_LIMIT'); + }); });