browser_toolbox_options.js (16677B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Tests that changing preferences in the options panel updates the prefs 7 // and toggles appropriate things in the toolbox. 8 9 var doc = null, 10 toolbox = null, 11 panelWin = null, 12 modifiedPrefs = []; 13 const L10N = new LocalizationHelper( 14 "devtools/client/locales/toolbox.properties" 15 ); 16 const { PrefObserver } = require("resource://devtools/client/shared/prefs.js"); 17 const { 18 BOOLEAN_CONFIGURATION_PREFS, 19 } = require("resource://devtools/client/framework/toolbox.js"); 20 21 add_task(async function () { 22 const URL = 23 "data:text/html;charset=utf8,test for dynamically registering " + 24 "and unregistering tools"; 25 registerNewTool(); 26 const tab = await addTab(URL); 27 toolbox = await gDevTools.showToolboxForTab(tab); 28 29 doc = toolbox.doc; 30 await registerNewPerToolboxTool(); 31 await testSelectTool(); 32 await testOptionsShortcut(); 33 await testOptions(); 34 await testToggleTools(); 35 36 // Test that registered WebExtensions becomes entries in the 37 // options panel and toggling their checkbox toggle the related 38 // preference. 39 await registerNewWebExtensions(); 40 await testToggleWebExtensions(); 41 42 await cleanup(); 43 }); 44 45 function registerNewTool() { 46 const toolDefinition = { 47 id: "testTool", 48 isToolSupported: () => true, 49 visibilityswitch: "devtools.test-tool.enabled", 50 url: "about:blank", 51 label: "someLabel", 52 }; 53 54 ok(gDevTools, "gDevTools exists"); 55 ok( 56 !gDevTools.getToolDefinitionMap().has("testTool"), 57 "The tool is not registered" 58 ); 59 60 gDevTools.registerTool(toolDefinition); 61 ok( 62 gDevTools.getToolDefinitionMap().has("testTool"), 63 "The tool is registered" 64 ); 65 } 66 67 // Register a fake WebExtension to check that it is 68 // listed in the toolbox options. 69 function registerNewWebExtensions() { 70 // Register some fake extensions and init the related preferences 71 // (similarly to ext-devtools.js). 72 for (let i = 0; i < 2; i++) { 73 const extPref = `devtools.webextensions.fakeExtId${i}.enabled`; 74 Services.prefs.setBoolPref(extPref, true); 75 76 toolbox.registerWebExtension(`fakeUUID${i}`, { 77 name: `Fake WebExtension ${i}`, 78 pref: extPref, 79 }); 80 } 81 } 82 83 function registerNewPerToolboxTool() { 84 const toolDefinition = { 85 id: "test-pertoolbox-tool", 86 isToolSupported: () => true, 87 visibilityswitch: "devtools.test-pertoolbox-tool.enabled", 88 url: "about:blank", 89 label: "perToolboxSomeLabel", 90 }; 91 92 ok(gDevTools, "gDevTools exists"); 93 ok( 94 !gDevTools.getToolDefinitionMap().has("test-pertoolbox-tool"), 95 "The per-toolbox tool is not registered globally" 96 ); 97 98 ok(toolbox, "toolbox exists"); 99 ok( 100 !toolbox.hasAdditionalTool("test-pertoolbox-tool"), 101 "The per-toolbox tool is not yet registered to the toolbox" 102 ); 103 104 toolbox.addAdditionalTool(toolDefinition); 105 106 ok( 107 !gDevTools.getToolDefinitionMap().has("test-pertoolbox-tool"), 108 "The per-toolbox tool is not registered globally" 109 ); 110 ok( 111 toolbox.hasAdditionalTool("test-pertoolbox-tool"), 112 "The per-toolbox tool has been registered to the toolbox" 113 ); 114 } 115 116 async function testSelectTool() { 117 info("Checking to make sure that the options panel can be selected."); 118 119 const onceSelected = toolbox.once("options-selected"); 120 toolbox.selectTool("options"); 121 await onceSelected; 122 ok(true, "Toolbox selected via selectTool method"); 123 } 124 125 async function testOptionsShortcut() { 126 info("Selecting another tool, then reselecting options panel with keyboard."); 127 128 await toolbox.selectTool("webconsole"); 129 is(toolbox.currentToolId, "webconsole", "webconsole is selected"); 130 synthesizeKeyShortcut(L10N.getStr("toolbox.help.key")); 131 is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key"); 132 synthesizeKeyShortcut(L10N.getStr("toolbox.help.key")); 133 is(toolbox.currentToolId, "webconsole", "webconsole is reselected"); 134 synthesizeKeyShortcut(L10N.getStr("toolbox.help.key")); 135 is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key"); 136 } 137 138 async function testOptions() { 139 const tool = toolbox.getPanel("options"); 140 panelWin = tool.panelWin; 141 const prefNodes = tool.panelDoc.querySelectorAll( 142 "input[type=checkbox][data-pref]" 143 ); 144 ok( 145 [...prefNodes].some(prefNode => prefNode.hasAttribute("data-force-reload")), 146 "There's at least one checkbox with the data-force-reload attribute" 147 ); 148 149 // Store modified pref names so that they can be cleared on error. 150 for (const node of tool.panelDoc.querySelectorAll("[data-pref]")) { 151 const pref = node.getAttribute("data-pref"); 152 modifiedPrefs.push(pref); 153 } 154 155 for (const node of prefNodes) { 156 const prefValue = GetPref(node.getAttribute("data-pref")); 157 158 // Test clicking the checkbox for each options pref 159 await testMouseClick(node, prefValue); 160 161 // Do again with opposite values to reset prefs 162 await testMouseClick(node, !prefValue); 163 } 164 165 const prefSelects = tool.panelDoc.querySelectorAll("select[data-pref]"); 166 for (const node of prefSelects) { 167 await testSelect(node); 168 } 169 } 170 171 async function testSelect(select) { 172 const pref = select.getAttribute("data-pref"); 173 const options = Array.from(select.options); 174 info("Checking select for: " + pref); 175 176 is( 177 `${select.options[select.selectedIndex].value}`, 178 `${GetPref(pref)}`, 179 "select starts out selected" 180 ); 181 182 for (const option of options) { 183 if (options.indexOf(option) === select.selectedIndex) { 184 continue; 185 } 186 187 const observer = new PrefObserver("devtools."); 188 189 let changeSeen = false; 190 const changeSeenPromise = new Promise(resolve => { 191 observer.once(pref, () => { 192 changeSeen = true; 193 is( 194 `${GetPref(pref)}`, 195 `${option.value}`, 196 "Preference been switched for " + pref 197 ); 198 resolve(); 199 }); 200 }); 201 202 select.selectedIndex = options.indexOf(option); 203 const changeEvent = new Event("change"); 204 select.dispatchEvent(changeEvent); 205 206 await changeSeenPromise; 207 208 ok(changeSeen, "Correct pref was changed"); 209 observer.destroy(); 210 } 211 } 212 213 async function testMouseClick(node, prefValue) { 214 const observer = new PrefObserver("devtools."); 215 216 const pref = node.getAttribute("data-pref"); 217 let changeSeen = false; 218 const changeSeenPromise = new Promise(resolve => { 219 observer.once(pref, () => { 220 changeSeen = true; 221 is(GetPref(pref), !prefValue, "New value is correct for " + pref); 222 resolve(); 223 }); 224 }); 225 226 // if changing the setting reloads the page, waits for the toolbox to be reloaded 227 const waitForDevToolsReload = node.hasAttribute("data-force-reload") 228 ? await watchForDevToolsReload(gBrowser.selectedBrowser) 229 : null; 230 231 const onNewConfigurationApplied = Object.keys( 232 BOOLEAN_CONFIGURATION_PREFS 233 ).includes(pref) 234 ? toolbox.once("new-configuration-applied") 235 : null; 236 237 node.scrollIntoView(); 238 239 // We use executeSoon here to ensure that the element is in view and 240 // clickable. 241 executeSoon(function () { 242 info("Click event synthesized for pref " + pref); 243 EventUtils.synthesizeMouseAtCenter(node, {}, panelWin); 244 }); 245 246 await changeSeenPromise; 247 248 ok(changeSeen, "Correct pref was changed"); 249 250 if (onNewConfigurationApplied) { 251 await onNewConfigurationApplied; 252 ok(true, `Configuration was changed when updating pref "${pref}"`); 253 } 254 255 if (waitForDevToolsReload) { 256 await waitForDevToolsReload(); 257 ok(true, `The page was reloaded when toggling ${node.outerHTML}`); 258 } 259 260 observer.destroy(); 261 } 262 263 async function testToggleWebExtensions() { 264 const disabledExtensions = new Set(); 265 const toggleableWebExtensions = toolbox.listWebExtensions(); 266 267 function toggleWebExtension(node) { 268 node.scrollIntoView(); 269 EventUtils.synthesizeMouseAtCenter(node, {}, panelWin); 270 } 271 272 function assertExpectedDisabledExtensions() { 273 for (const ext of toggleableWebExtensions) { 274 if (disabledExtensions.has(ext)) { 275 ok( 276 !toolbox.isWebExtensionEnabled(ext.uuid), 277 `The WebExtension "${ext.name}" should be disabled` 278 ); 279 } else { 280 ok( 281 toolbox.isWebExtensionEnabled(ext.uuid), 282 `The WebExtension "${ext.name}" should be enabled` 283 ); 284 } 285 } 286 } 287 288 function assertAllExtensionsDisabled() { 289 const enabledUUIDs = toggleableWebExtensions 290 .filter(ext => toolbox.isWebExtensionEnabled(ext.uuid)) 291 .map(ext => ext.uuid); 292 293 Assert.deepEqual( 294 enabledUUIDs, 295 [], 296 "All the registered WebExtensions should be disabled" 297 ); 298 } 299 300 function assertAllExtensionsEnabled() { 301 const disabledUUIDs = toolbox 302 .listWebExtensions() 303 .filter(ext => !toolbox.isWebExtensionEnabled(ext.uuid)) 304 .map(ext => ext.uuid); 305 306 Assert.deepEqual( 307 disabledUUIDs, 308 [], 309 "All the registered WebExtensions should be enabled" 310 ); 311 } 312 313 function getWebExtensionNodes() { 314 const toolNodes = panelWin.document.querySelectorAll( 315 "#default-tools-box input[type=checkbox]:not([data-unsupported])," + 316 "#additional-tools-box input[type=checkbox]:not([data-unsupported])" 317 ); 318 319 return [...toolNodes].filter(node => { 320 return toggleableWebExtensions.some( 321 ({ uuid }) => node.getAttribute("id") === `webext-${uuid}` 322 ); 323 }); 324 } 325 326 let webExtensionNodes = getWebExtensionNodes(); 327 328 is( 329 webExtensionNodes.length, 330 toggleableWebExtensions.length, 331 "There should be a toggle checkbox for every WebExtension registered" 332 ); 333 334 for (const ext of toggleableWebExtensions) { 335 ok( 336 toolbox.isWebExtensionEnabled(ext.uuid), 337 `The WebExtension "${ext.name}" is initially enabled` 338 ); 339 } 340 341 // Store modified pref names so that they can be cleared on error. 342 for (const ext of toggleableWebExtensions) { 343 modifiedPrefs.push(ext.pref); 344 } 345 346 // Turn each registered WebExtension to disabled. 347 for (const node of webExtensionNodes) { 348 toggleWebExtension(node); 349 350 const toggledExt = toggleableWebExtensions.find(ext => { 351 return node.id == `webext-${ext.uuid}`; 352 }); 353 ok(toggledExt, "Found a WebExtension for the checkbox element"); 354 disabledExtensions.add(toggledExt); 355 356 assertExpectedDisabledExtensions(); 357 } 358 359 assertAllExtensionsDisabled(); 360 361 // Turn each registered WebExtension to enabled. 362 for (const node of webExtensionNodes) { 363 toggleWebExtension(node); 364 365 const toggledExt = toggleableWebExtensions.find(ext => { 366 return node.id == `webext-${ext.uuid}`; 367 }); 368 ok(toggledExt, "Found a WebExtension for the checkbox element"); 369 disabledExtensions.delete(toggledExt); 370 371 assertExpectedDisabledExtensions(); 372 } 373 374 assertAllExtensionsEnabled(); 375 376 // Unregister the WebExtensions one by one, and check that only the expected 377 // ones have been unregistered, and the remaining onea are still listed. 378 for (const ext of toggleableWebExtensions) { 379 ok( 380 !!toolbox.listWebExtensions().length, 381 "There should still be extensions registered" 382 ); 383 toolbox.unregisterWebExtension(ext.uuid); 384 385 const registeredUUIDs = toolbox.listWebExtensions().map(item => item.uuid); 386 ok( 387 !registeredUUIDs.includes(ext.uuid), 388 `the WebExtension "${ext.name}" should have been unregistered` 389 ); 390 391 webExtensionNodes = getWebExtensionNodes(); 392 393 const checkboxEl = webExtensionNodes.find( 394 el => el.id === `webext-${ext.uuid}` 395 ); 396 is( 397 checkboxEl, 398 undefined, 399 "The unregistered WebExtension checkbox should have been removed" 400 ); 401 402 is( 403 registeredUUIDs.length, 404 webExtensionNodes.length, 405 "There should be the expected number of WebExtensions checkboxes" 406 ); 407 } 408 409 is( 410 toolbox.listWebExtensions().length, 411 0, 412 "All WebExtensions have been unregistered" 413 ); 414 415 webExtensionNodes = getWebExtensionNodes(); 416 417 is( 418 webExtensionNodes.length, 419 0, 420 "There should not be any checkbox for the unregistered WebExtensions" 421 ); 422 } 423 424 function getToolNode(id) { 425 return panelWin.document.getElementById(id); 426 } 427 428 async function testToggleTools() { 429 const toolNodes = panelWin.document.querySelectorAll( 430 "#default-tools-box input[type=checkbox]:not([data-unsupported])," + 431 "#additional-tools-box input[type=checkbox]:not([data-unsupported])" 432 ); 433 const toolNodeIds = [...toolNodes].map(node => node.id); 434 const enabledToolIds = [...toolNodes] 435 .filter(node => node.checked) 436 .map(node => node.id); 437 438 const toggleableTools = gDevTools 439 .getDefaultTools() 440 .filter(tool => { 441 return tool.visibilityswitch; 442 }) 443 .concat(gDevTools.getAdditionalTools()) 444 .concat(toolbox.getAdditionalTools()); 445 446 for (const node of toolNodes) { 447 const id = node.getAttribute("id"); 448 ok( 449 toggleableTools.some(tool => tool.id === id), 450 "There should be a toggle checkbox for: " + id 451 ); 452 } 453 454 // Store modified pref names so that they can be cleared on error. 455 for (const tool of toggleableTools) { 456 const pref = tool.visibilityswitch; 457 modifiedPrefs.push(pref); 458 } 459 460 // Toggle each tool 461 for (const id of toolNodeIds) { 462 await toggleTool(getToolNode(id)); 463 } 464 465 // Toggle again to reset tool enablement state 466 for (const id of toolNodeIds) { 467 await toggleTool(getToolNode(id)); 468 } 469 470 // Test that a tool can still be added when no tabs are present: 471 // Disable all tools 472 for (const id of enabledToolIds) { 473 await toggleTool(getToolNode(id)); 474 } 475 // Re-enable the tools which are enabled by default 476 for (const id of enabledToolIds) { 477 await toggleTool(getToolNode(id)); 478 } 479 480 // Toggle first, middle, and last tools to ensure that toolbox tabs are 481 // inserted in order 482 const firstToolId = toolNodeIds[0]; 483 const middleToolId = toolNodeIds[(toolNodeIds.length / 2) | 0]; 484 const lastToolId = toolNodeIds[toolNodeIds.length - 1]; 485 486 await toggleTool(getToolNode(firstToolId)); 487 await toggleTool(getToolNode(firstToolId)); 488 await toggleTool(getToolNode(middleToolId)); 489 await toggleTool(getToolNode(middleToolId)); 490 await toggleTool(getToolNode(lastToolId)); 491 await toggleTool(getToolNode(lastToolId)); 492 } 493 494 /** 495 * Toggle tool node checkbox. Note: because toggling the checkbox will result in 496 * re-rendering of the tool list, we must re-query the checkboxes every time. 497 */ 498 async function toggleTool(node) { 499 const toolId = node.getAttribute("id"); 500 501 const registeredPromise = new Promise(resolve => { 502 if (node.checked) { 503 gDevTools.once( 504 "tool-unregistered", 505 checkUnregistered.bind(null, toolId, resolve) 506 ); 507 } else { 508 gDevTools.once( 509 "tool-registered", 510 checkRegistered.bind(null, toolId, resolve) 511 ); 512 } 513 }); 514 node.scrollIntoView(); 515 EventUtils.synthesizeMouseAtCenter(node, {}, panelWin); 516 517 await registeredPromise; 518 } 519 520 function checkUnregistered(toolId, resolve, data) { 521 if (data == toolId) { 522 ok(true, "Correct tool removed"); 523 // checking tab on the toolbox 524 ok( 525 !doc.getElementById("toolbox-tab-" + toolId), 526 "Tab removed for " + toolId 527 ); 528 } else { 529 ok(false, "Something went wrong, " + toolId + " was not unregistered"); 530 } 531 resolve(); 532 } 533 534 async function checkRegistered(toolId, resolve, data) { 535 if (data == toolId) { 536 ok(true, "Correct tool added back"); 537 // checking tab on the toolbox 538 const button = await lookupButtonForToolId(toolId); 539 ok(button, "Tab added back for " + toolId); 540 } else { 541 ok(false, "Something went wrong, " + toolId + " was not registered"); 542 } 543 resolve(); 544 } 545 546 function GetPref(name) { 547 const type = Services.prefs.getPrefType(name); 548 switch (type) { 549 case Services.prefs.PREF_STRING: 550 return Services.prefs.getCharPref(name); 551 case Services.prefs.PREF_INT: 552 return Services.prefs.getIntPref(name); 553 case Services.prefs.PREF_BOOL: 554 return Services.prefs.getBoolPref(name); 555 default: 556 throw new Error("Unknown type"); 557 } 558 } 559 560 /** 561 * Find the button from specified toolId. 562 * Generally, button which access to the tool panel is in toolbox or 563 * tools menu(in the Chevron menu). 564 */ 565 async function lookupButtonForToolId(toolId) { 566 let button = doc.getElementById("toolbox-tab-" + toolId); 567 if (!button) { 568 // search from the tools menu. 569 await openChevronMenu(toolbox); 570 button = doc.querySelector("#tools-chevron-menupopup-" + toolId); 571 572 await closeChevronMenu(toolbox); 573 } 574 return button; 575 } 576 577 async function cleanup() { 578 gDevTools.unregisterTool("testTool"); 579 await toolbox.destroy(); 580 gBrowser.removeCurrentTab(); 581 for (const pref of modifiedPrefs) { 582 Services.prefs.clearUserPref(pref); 583 } 584 toolbox = doc = panelWin = modifiedPrefs = null; 585 }