browser_interventions.js (10990B)
1 "use strict"; 2 3 const { WebExtensionPolicy } = SpecialPowers.Cu.getGlobalForObject( 4 SpecialPowers.Services 5 ); 6 7 const ValidIssueList = [ 8 "blocked-content", 9 "broken-audio", 10 "broken-captcha", 11 "broken-comments", 12 "broken-cookie-banner", 13 "broken-editor", 14 "broken-font", 15 "broken-images", 16 "broken-interactive-elements", 17 "broken-layout", 18 "broken-login", 19 "broken-map", 20 "broken-meetings", 21 "broken-printing", 22 "broken-redirect", 23 "broken-scrolling", 24 "broken-videos", 25 "broken-zooming", 26 "desktop-layout-not-mobile", 27 "extra-scrollbars", 28 "firefox-blocked-completely", 29 "frozen-tab", 30 "incorrect-viewport-dimensions", 31 "page-fails-to-load", 32 "redirect-loop", 33 "slow-performance", 34 "unsupported-warning", 35 "user-interface-frustration", 36 ]; 37 38 function addon_url(path) { 39 const uuid = WebExtensionPolicy.getByID( 40 "webcompat@mozilla.org" 41 ).mozExtensionHostname; 42 return `moz-extension://${uuid}/${path}`; 43 } 44 45 async function check_path_exists(path) { 46 try { 47 await (await fetch(addon_url(path))).text(); 48 } catch (e) { 49 return false; 50 } 51 return true; 52 } 53 54 function check_valid_array(a, key, id) { 55 if (a === undefined) { 56 return false; 57 } 58 const valid = Array.isArray(a); 59 ok(valid, `if defined, ${key} is an array for id ${id}`); 60 return valid; 61 } 62 63 // eslint-disable-next-line complexity 64 add_task(async function test_json_data() { 65 const addon = await AddonManager.getAddonByID("webcompat@mozilla.org"); 66 const addonURI = addon.getResourceURI(); 67 const checkableGlobalPrefs = 68 await WebCompatExtension.getCheckableGlobalPrefs(); 69 70 const exports = {}; 71 Services.scriptloader.loadSubScript( 72 addonURI.resolve("lib/intervention_helpers.js"), 73 exports 74 ); 75 Services.scriptloader.loadSubScript( 76 addonURI.resolve("lib/custom_functions.js"), 77 exports 78 ); 79 const helpers = exports.InterventionHelpers; 80 const custom_fns = exports.CUSTOM_FUNCTIONS; 81 82 for (const [name, fn] of Object.entries(helpers.skip_if_functions)) { 83 Assert.strictEqual(typeof fn, "function", `Skip-if ${name} is a function`); 84 } 85 86 for (const [name, { disable, enable }] of Object.entries(custom_fns)) { 87 Assert.strictEqual( 88 typeof enable, 89 "function", 90 `Custom function ${name} has enable function` 91 ); 92 Assert.strictEqual( 93 typeof disable, 94 "function", 95 `Custom function ${name} has disable function` 96 ); 97 } 98 99 const json = await (await fetch(addon_url("data/interventions.json"))).json(); 100 const ids = new Set(); 101 for (const [id, config] of Object.entries(json)) { 102 const { bugs, hidden, interventions, label } = config; 103 ok(!!id, `id key exists for intervention ${JSON.stringify(config)}`); 104 if (id) { 105 ok(!ids.has(id), `id ${id} is defined more than once`); 106 ids.add(id); 107 } 108 109 if (hidden) { 110 ok( 111 hidden === false || hidden === true, 112 `hidden key is true or false for id ${id}` 113 ); 114 } 115 116 ok( 117 typeof label === "string" && !!label, 118 `label key exists and is set for id ${id}` 119 ); 120 121 ok( 122 typeof bugs === "object" && Object.keys(bugs).length, 123 `bugs key exists and has entries for id ${id}` 124 ); 125 for (const [bug, { issue, blocks, matches }] of Object.entries(bugs)) { 126 ok( 127 typeof bug === "string" && bug == String(parseInt(bug)), 128 `bug number is set properly for all bugs in id ${id}` 129 ); 130 131 ok( 132 ValidIssueList.includes(issue), 133 `issue key exists and is set for all bugs in id ${id}` 134 ); 135 136 ok( 137 !interventions.find(i => i.content_scripts || i.ua_string) || 138 (!!matches && Array.isArray(matches) && matches.length), 139 `matches key exists and is an array with items for id ${id}` 140 ); 141 try { 142 new MatchPatternSet(matches); 143 } catch (e) { 144 ok(false, `invalid matches entries for id ${id}: ${e}`); 145 } 146 147 if (blocks) { 148 ok( 149 Array.isArray(blocks) && matches.length, 150 `matches key exists and is an array with items for id ${id}` 151 ); 152 try { 153 new MatchPatternSet(blocks); 154 } catch (e) { 155 ok(false, `invalid blocks entries for id ${id}: ${e}`); 156 } 157 } 158 } 159 160 const non_custom_names = [ 161 "content_scripts", 162 "max_version", 163 "min_version", 164 "not_platforms", 165 "platforms", 166 "not_channels", 167 "only_channels", 168 "pref_check", 169 "skip_if", 170 "ua_string", 171 ]; 172 let custom_found = false; 173 for (let intervention of interventions) { 174 for (const name in intervention) { 175 const is_custom = name in custom_fns; 176 const is_non_custom = non_custom_names.includes(name); 177 ok( 178 is_custom || is_non_custom, 179 `key '${name}' is actually expected for id ${id}` 180 ); 181 if (is_custom) { 182 custom_found = true; 183 const { details, optionalDetails } = custom_fns[name]; 184 for (const customArgs of intervention[name]) { 185 for (const detailName in customArgs) { 186 ok( 187 details.includes(detailName) || 188 optionalDetails.includes(detailName), 189 `detail '${detailName}' is actually expected for custom function ${name} in id ${id}` 190 ); 191 } 192 for (const detailName of details) { 193 ok( 194 detailName in customArgs, 195 `expected detail '${detailName}' is being passed to custom function ${name} in id ${id}` 196 ); 197 } 198 } 199 } 200 } 201 for (const version_type of ["min_version", "max_version"]) { 202 if (version_type in intervention) { 203 const val = intervention[version_type]; 204 ok( 205 typeof val == "number" && val > 0, 206 `Invalid ${version_type} value ${JSON.stringify(val)}, should be a positive number` 207 ); 208 } 209 } 210 let { 211 content_scripts, 212 not_platforms, 213 not_channels, 214 only_channels, 215 platforms, 216 pref_check, 217 skip_if, 218 ua_string, 219 } = intervention; 220 ok( 221 !!platforms || !!not_platforms, 222 `platforms or not_platforms key exists for id ${id} intervention ${JSON.stringify(intervention)}` 223 ); 224 if (check_valid_array(not_platforms, "not_platforms", id)) { 225 let skipped = 0; 226 let possible = helpers.valid_platforms.length - 2; // without "all" and "desktop" 227 for (const platform of not_platforms) { 228 ok( 229 helpers.valid_platforms.includes(platform), 230 `Not-platform ${platform} is valid in id ${id}` 231 ); 232 if (platform == "desktop") { 233 skipped += possible - 1; 234 } else if (platform == "all") { 235 skipped = possible; 236 } else { 237 ++skipped; 238 } 239 } 240 Assert.less( 241 skipped, 242 possible, 243 `Not skipping all platforms for id ${id} intervention ${JSON.stringify(intervention)}` 244 ); 245 } 246 if (check_valid_array(platforms, "platforms", id)) { 247 for (const platform of platforms) { 248 ok( 249 helpers.valid_platforms.includes(platform), 250 `Platform ${platform} is valid in id ${id}` 251 ); 252 } 253 } 254 if (check_valid_array(not_channels, "not_channels", id)) { 255 let skipped = 0; 256 let possible = helpers.valid_channels.length; 257 for (const channel of not_channels) { 258 ok( 259 helpers.valid_channels.includes(channel), 260 `Not-channel ${channel} is valid in id ${id}` 261 ); 262 ++skipped; 263 } 264 Assert.less( 265 skipped, 266 possible, 267 `Not skipping all channels for id ${id} intervention ${JSON.stringify(intervention)}` 268 ); 269 } 270 if (check_valid_array(only_channels, "only_channels", id)) { 271 for (const channel of only_channels) { 272 ok( 273 helpers.valid_channels.includes(channel), 274 `Channel ${channel} is valid in id ${id}` 275 ); 276 } 277 } 278 ok( 279 content_scripts || ua_string || custom_found, 280 `Interventions are defined for id ${id}` 281 ); 282 ok( 283 pref_check === undefined || typeof pref_check === "object", 284 `pref_check is not given or is an object ${id}` 285 ); 286 if (pref_check) { 287 for (const [pref, value] of Object.entries(pref_check)) { 288 ok( 289 checkableGlobalPrefs.includes(pref), 290 `'${pref}' is allow-listed in AboutConfigPrefsAPI.ALLOWED_GLOBAL_PREFS` 291 ); 292 const type = typeof value; 293 const expectedType = Services.prefs.getPrefType(pref); 294 if (expectedType !== 0) { 295 // will be 0 if not defined/available on the given platform 296 ok( 297 (type === "boolean" && 298 expectedType === Ci.nsIPrefBranch.PREF_BOOL) || 299 (type === "number" && 300 expectedType === Ci.nsIPrefBranch.PREF_INT) || 301 (type === "string" && 302 expectedType === Ci.nsIPrefBranch.PREF_STRING), 303 `Given value (${JSON.stringify(value)}) for '${pref}' matches the pref's type` 304 ); 305 } 306 } 307 } 308 if (check_valid_array(skip_if, "skip_if", id)) { 309 for (const fn of skip_if) { 310 ok( 311 fn in helpers.skip_if_functions, 312 `'${fn}' is not in the skip_if_functions` 313 ); 314 } 315 } 316 if (content_scripts) { 317 if ("all_frames" in content_scripts) { 318 const all = content_scripts.all_frames; 319 ok( 320 all === false || all === true, 321 `all_frames key is true or false for content_scripts for id ${id}` 322 ); 323 } 324 for (const type of ["css", "js"]) { 325 if (!(type in content_scripts)) { 326 continue; 327 } 328 const paths = content_scripts[type]; 329 const check = Array.isArray(paths) && paths.length; 330 ok( 331 check, 332 `${type} content_scripts should be an array with at least one string for id ${id}` 333 ); 334 if (!check) { 335 continue; 336 } 337 for (let path of paths) { 338 if (!path.includes("/")) { 339 path = `injections/${type}/${path}`; 340 } 341 ok( 342 path.endsWith(`.${type}`), 343 `${path} should be a ${type.toUpperCase()} file` 344 ); 345 ok(await check_path_exists(path), `${path} exists for id ${id}`); 346 } 347 } 348 } 349 if (check_valid_array(ua_string, "ua_string", id)) { 350 for (let change of ua_string) { 351 if (typeof change !== "string") { 352 change = change.change; 353 } 354 ok( 355 change in helpers.ua_change_functions, 356 `'${change}' is not in the ua_change_functions` 357 ); 358 } 359 } 360 } 361 } 362 });