Skip to content

Commit a7bea68

Browse files
committed
fixed the case of plugin loaded but not invoked
1 parent 34aaa6f commit a7bea68

1 file changed

Lines changed: 83 additions & 21 deletions

File tree

bin/accessibility-automation/helper.js

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*require\(\s*['"][^'"]*accessibility-automation\/plugin['"]\s*\)/);
65+
const importMatch = content.match(/import\s+([A-Za-z0-9_$]+)\s+from\s+['"][^'"]*accessibility-automation\/plugin['"]/);
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
*/
71125
exports.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

Comments
 (0)