@@ -41,32 +41,86 @@ exports.isAccessibilitySupportedCypressVersion = (cypress_config_filename) => {
4141 return CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS . includes ( extension ) ;
4242}
4343
44- // Fallback: scan the raw cypress config source for the accessibility plugin
45- // import. Used only when the config could not be required (e.g. a TypeScript
46- // config before BrowserStack packages are installed), so that such users are
47- // not wrongly disabled. The substring matches require()/import of the plugin
48- // regardless of path style or imported symbol name.
49- const ACCESSIBILITY_PLUGIN_IMPORT_TOKEN = 'accessibility-automation/plugin' ;
44+ // Strip JS/TS comments so that commented-out plugin imports/calls are ignored
45+ // by the static scans below. Best-effort: handles block and line comments while
46+ // avoiding `://` in URLs.
47+ const stripComments = ( src ) => {
48+ return src
49+ . replace ( / \/ \* [ \s \S ] * ?\* \/ / g, ' ' ) // block comments
50+ . replace ( / ( ^ | [ ^ : ] ) \/ \/ [ ^ \n ] * / g, '$1' ) ; // line comments (skip URLs like http://)
51+ } ;
52+
53+ // Reads the cypress config source (comments stripped). Returns null if it cannot
54+ // be read.
55+ const readConfigSource = ( user_config ) => {
56+ const configPath = user_config . run_settings && user_config . run_settings . cypressConfigFilePath ;
57+ if ( ! configPath || ! fs . existsSync ( configPath ) ) return null ;
58+ return stripComments ( fs . readFileSync ( configPath , { encoding : 'utf-8' } ) ) ;
59+ } ;
60+
61+ // Finds the symbol the accessibility plugin is imported as, via require() or
62+ // import, regardless of path style. Returns the binding name or null.
63+ const getAccessibilityPluginBinding = ( content ) => {
64+ 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 * \) / ) ;
65+ const importMatch = 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 [ ' " ] / ) ;
66+ return ( requireMatch && requireMatch [ 1 ] ) || ( importMatch && importMatch [ 1 ] ) || null ;
67+ } ;
68+
69+ const isBindingCalled = ( content , binding ) => {
70+ const callRegex = new RegExp ( '\\b' + binding . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) + '\\s*\\(' ) ;
71+ return callRegex . test ( content ) ;
72+ } ;
5073
51- const scanConfigForAccessibilityPlugin = ( user_config ) => {
74+ // Static check: confirm the (already-imported) accessibility plugin is actually
75+ // invoked in the config source. Lenient — if the import binding cannot be located
76+ // via static parsing (unusual syntax) or the source cannot be read, we do NOT
77+ // veto the require-based detection (return true), to avoid wrongly disabling
78+ // valid configs.
79+ const isAccessibilityPluginInvokedInSource = ( user_config ) => {
5280 try {
53- const configPath = user_config . run_settings && user_config . run_settings . cypressConfigFilePath ;
54- if ( ! configPath || ! fs . existsSync ( configPath ) ) return false ;
55- const content = fs . readFileSync ( configPath , { encoding : 'utf-8' } ) ;
56- return content . includes ( ACCESSIBILITY_PLUGIN_IMPORT_TOKEN ) ;
81+ const content = readConfigSource ( user_config ) ;
82+ if ( content === null ) return true ;
83+ const binding = getAccessibilityPluginBinding ( content ) ;
84+ if ( ! binding ) return true ;
85+ return isBindingCalled ( content , binding ) ;
86+ } catch ( error ) {
87+ logger . debug ( `Unable to verify accessibility plugin invocation: ${ error . message || error } ` ) ;
88+ return true ;
89+ }
90+ } ;
91+
92+ // Pure static fallback: confirm the plugin is BOTH imported AND invoked. Used
93+ // only when the config could not be required (e.g. a TypeScript config before
94+ // BrowserStack packages are installed), so such users are still evaluated.
95+ const isAccessibilityPluginImportedAndCalledInSource = ( user_config ) => {
96+ try {
97+ const content = readConfigSource ( user_config ) ;
98+ if ( content === null ) return false ;
99+ const binding = getAccessibilityPluginBinding ( content ) ;
100+ if ( ! binding ) return false ;
101+ return isBindingCalled ( content , binding ) ;
57102 } catch ( error ) {
58103 logger . debug ( `Unable to scan cypress config for accessibility plugin: ${ error . message || error } ` ) ;
59104 return false ;
60105 }
61106} ;
62107
63108/**
64- * Determines whether the BrowserStack accessibility plugin is loaded in the
65- * user's cypress config. Reading the cypress config executes its top-level
66- * requires; the accessibility plugin sets BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED
67- * when loaded, which readCypressConfigFile propagates back to this process as a
68- * definitive 'true'/'false'. If the config could not be required (env var stays
69- * undefined), we fall back to a raw-text scan so users are not wrongly disabled.
109+ * Determines whether the BrowserStack accessibility plugin is genuinely wired
110+ * into the user's cypress config, i.e. both imported AND invoked.
111+ *
112+ * Detection combines two signals:
113+ * 1) Require-load: reading the cypress config executes its top-level requires;
114+ * the plugin sets BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED on load, which
115+ * readCypressConfigFile propagates back as a definitive 'true'/'false'. This
116+ * tells us whether the plugin is imported (and does not false-positive on a
117+ * commented-out require, since commented code never executes).
118+ * 2) Static source scan: confirms the imported plugin binding is actually called
119+ * in the config — so "imported but never called" is treated as not loaded.
120+ *
121+ * If the config could not be required (env var stays undefined, e.g. a TS config
122+ * before packages are installed), we fall back to a pure static scan that checks
123+ * for both import and invocation.
70124 */
71125exports . isAccessibilityPluginLoaded = ( user_config ) => {
72126 try {
@@ -76,15 +130,23 @@ exports.isAccessibilityPluginLoaded = (user_config) => {
76130 readCypressConfigFile ( user_config ) ;
77131
78132 const detection = process . env . BROWSERSTACK_ACCESSIBILITY_PLUGIN_LOADED ;
79- if ( detection === 'true' ) return true ;
133+ if ( detection === 'true' ) {
134+ // Imported via require — additionally require that it is actually invoked.
135+ const called = isAccessibilityPluginInvokedInSource ( user_config ) ;
136+ if ( ! called ) {
137+ logger . debug ( 'Accessibility plugin is imported but not invoked in the cypress config; treating as not loaded.' ) ;
138+ }
139+ return called ;
140+ }
80141 if ( detection === 'false' ) return false ;
81142
82- // Inconclusive (config could not be required) — fall back to a text scan.
143+ // Inconclusive (config could not be required) — fall back to a static scan
144+ // that checks for both import and invocation.
83145 logger . debug ( 'Accessibility plugin detection inconclusive from config require; falling back to source scan.' ) ;
84- return scanConfigForAccessibilityPlugin ( user_config ) ;
146+ return isAccessibilityPluginImportedAndCalledInSource ( user_config ) ;
85147 } catch ( error ) {
86148 logger . debug ( `Unable to determine if accessibility plugin is loaded: ${ error . message || error } ` ) ;
87- return scanConfigForAccessibilityPlugin ( user_config ) ;
149+ return isAccessibilityPluginImportedAndCalledInSource ( user_config ) ;
88150 }
89151}
90152
0 commit comments