@@ -41,6 +41,142 @@ exports.isAccessibilitySupportedCypressVersion = (cypress_config_filename) => {
4141 return CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS . includes ( extension ) ;
4242}
4343
44+ // Token identifying the accessibility plugin module path in source.
45+ const ACCESSIBILITY_PLUGIN_PATH_TOKEN = 'accessibility-automation/plugin' ;
46+
47+ // Strip JS/TS comments so that commented-out plugin imports/calls are ignored
48+ // by the static scans below.
49+ //
50+ // NOTE: this is an intentionally best-effort / lossy scrubber, NOT a real parser.
51+ // It can also strip `//` or `/* */` sequences that appear inside string literals,
52+ // and the `[^:]` guard only avoids `://` (URLs). This is acceptable because these
53+ // static scans are a secondary signal: the authoritative "is the plugin imported"
54+ // check is the require-load marker (BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED), so a
55+ // mis-stripped string literal cannot cause a false import detection.
56+ const stripComments = ( src ) => {
57+ return src
58+ . replace ( / \/ \* [ \s \S ] * ?\* \/ / g, ' ' ) // block comments
59+ . replace ( / ( ^ | [ ^ : ] ) \/ \/ [ ^ \n ] * / g, '$1' ) ; // line comments (skip URLs like http://)
60+ } ;
61+
62+ // Reads the cypress config source (comments stripped). Returns null if it cannot
63+ // be read.
64+ const readConfigSource = ( user_config ) => {
65+ const configPath = user_config . run_settings && user_config . run_settings . cypressConfigFilePath ;
66+ if ( ! configPath || ! fs . existsSync ( configPath ) ) return null ;
67+ return stripComments ( fs . readFileSync ( configPath , { encoding : 'utf-8' } ) ) ;
68+ } ;
69+
70+ // Finds the symbol the accessibility plugin is imported as, via require() or
71+ // import, regardless of path style. Handles `require()`, default `import X from`,
72+ // and namespace `import * as X from`. Returns the binding name or null. Named
73+ // (`import { X }`) and dynamic (`await import(...)`) forms are not parsed here —
74+ // the strict fallback biases toward keeping accessibility on for those (see
75+ // isAccessibilityPluginImportedAndCalledInSource).
76+ const getAccessibilityPluginBinding = ( content ) => {
77+ const requireMatch = content . match ( / (?: c o n s t | l e t | v a r ) \s + ( [ A - Z a - z 0 - 9 _ $ ] + ) \s * = \s * r e q u i r e \( \s * [ ' " ] [ ^ ' " ] * a c c e s s i b i l i t y - a u t o m a t i o n \/ p l u g i n [ ' " ] \s * \) / ) ;
78+ const importNamespaceMatch = content . match ( / i m p o r t \s + \* \s + a s \s + ( [ A - Z a - z 0 - 9 _ $ ] + ) \s + f r o m \s + [ ' " ] [ ^ ' " ] * a c c e s s i b i l i t y - a u t o m a t i o n \/ p l u g i n [ ' " ] / ) ;
79+ const importDefaultMatch = content . match ( / i m p o r t \s + ( [ A - Z a - z 0 - 9 _ $ ] + ) \s + f r o m \s + [ ' " ] [ ^ ' " ] * a c c e s s i b i l i t y - a u t o m a t i o n \/ p l u g i n [ ' " ] / ) ;
80+ return ( requireMatch && requireMatch [ 1 ] ) ||
81+ ( importNamespaceMatch && importNamespaceMatch [ 1 ] ) ||
82+ ( importDefaultMatch && importDefaultMatch [ 1 ] ) ||
83+ null ;
84+ } ;
85+
86+ const isBindingCalled = ( content , binding ) => {
87+ const callRegex = new RegExp ( '\\b' + binding . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) + '\\s*\\(' ) ;
88+ return callRegex . test ( content ) ;
89+ } ;
90+
91+ // Static check: confirm the (already-imported) accessibility plugin is actually
92+ // invoked in the config source. Lenient — if the import binding cannot be located
93+ // via static parsing (unusual syntax) or the source cannot be read, we do NOT
94+ // veto the require-based detection (return true), to avoid wrongly disabling
95+ // valid configs.
96+ const isAccessibilityPluginInvokedInSource = ( user_config ) => {
97+ try {
98+ const content = readConfigSource ( user_config ) ;
99+ if ( content === null ) return true ;
100+ const binding = getAccessibilityPluginBinding ( content ) ;
101+ if ( ! binding ) return true ;
102+ return isBindingCalled ( content , binding ) ;
103+ } catch ( error ) {
104+ logger . debug ( `Unable to verify accessibility plugin invocation: ${ error . message || error } ` ) ;
105+ return true ;
106+ }
107+ } ;
108+
109+ // Pure static fallback: used only when the config could not be required (e.g. a
110+ // TypeScript config before BrowserStack packages are installed), so such users
111+ // are still evaluated. Biased toward KEEPING accessibility enabled: if the plugin
112+ // path is present but we cannot confidently parse the import binding (e.g. named
113+ // `import { X }` or dynamic `await import(...)`), we do not disable — silently
114+ // turning off a billed feature based on a lossy source scan is worse than leaving
115+ // it on. We only return false when there is positive evidence the plugin is not
116+ // wired in (path absent, or binding parsed and demonstrably never called).
117+ const isAccessibilityPluginImportedAndCalledInSource = ( user_config ) => {
118+ try {
119+ const content = readConfigSource ( user_config ) ;
120+ if ( content === null ) return false ;
121+ // Plugin path not referenced at all -> definitely not imported.
122+ if ( ! content . includes ( ACCESSIBILITY_PLUGIN_PATH_TOKEN ) ) return false ;
123+ const binding = getAccessibilityPluginBinding ( content ) ;
124+ // Path present but binding not parseable (namespace/named/dynamic import) ->
125+ // keep accessibility on rather than risk a false disable.
126+ if ( ! binding ) return true ;
127+ // Binding parsed: trust the precise call check (catches import-without-call).
128+ return isBindingCalled ( content , binding ) ;
129+ } catch ( error ) {
130+ logger . debug ( `Unable to scan cypress config for accessibility plugin: ${ error . message || error } ` ) ;
131+ return false ;
132+ }
133+ } ;
134+
135+ /**
136+ * Determines whether the BrowserStack accessibility plugin is genuinely wired
137+ * into the user's cypress config, i.e. both imported AND invoked.
138+ *
139+ * Detection combines two signals:
140+ * 1) Require-load: reading the cypress config executes its top-level requires;
141+ * the plugin sets BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED on load, which
142+ * readCypressConfigFile propagates back as a definitive 'true'/'false'. This
143+ * tells us whether the plugin is imported (and does not false-positive on a
144+ * commented-out require, since commented code never executes).
145+ * 2) Static source scan: confirms the imported plugin binding is actually called
146+ * in the config — so "imported but never called" is treated as not loaded.
147+ *
148+ * If the config could not be required (env var stays undefined, e.g. a TS config
149+ * before packages are installed), we fall back to a pure static scan that checks
150+ * for both import and invocation.
151+ */
152+ exports . isAccessibilityPluginLoaded = ( user_config ) => {
153+ try {
154+ // Reset before reading so a stale value from a previous run cannot leak in.
155+ delete process . env . BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED ;
156+ const { readCypressConfigFile } = require ( '../helpers/readCypressConfigUtil' ) ;
157+ readCypressConfigFile ( user_config ) ;
158+
159+ const detection = process . env . BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED ;
160+ if ( detection === 'true' ) {
161+ // Imported via require — additionally require that it is actually invoked.
162+ const called = isAccessibilityPluginInvokedInSource ( user_config ) ;
163+ if ( ! called ) {
164+ logger . debug ( 'Accessibility plugin is imported but not invoked in the cypress config; treating as not loaded.' ) ;
165+ }
166+ return called ;
167+ }
168+ if ( detection === 'false' ) return false ;
169+
170+ // Inconclusive (config could not be required) — fall back to a static scan
171+ // that checks for both import and invocation.
172+ logger . debug ( 'Accessibility plugin detection inconclusive from config require; falling back to source scan.' ) ;
173+ return isAccessibilityPluginImportedAndCalledInSource ( user_config ) ;
174+ } catch ( error ) {
175+ logger . debug ( `Unable to determine if accessibility plugin is loaded: ${ error . message || error } ` ) ;
176+ return isAccessibilityPluginImportedAndCalledInSource ( user_config ) ;
177+ }
178+ }
179+
44180exports . createAccessibilityTestRun = async ( user_config , framework ) => {
45181
46182 try {
0 commit comments