From 0c1bfd83150d02b70bb3b8763cea5ba9bfc0bfdb Mon Sep 17 00:00:00 2001 From: Priyanka Vats Date: Fri, 23 Oct 2026 15:49:04 +0530 Subject: [PATCH] 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'); + }); +});