Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Errors
- This test failed 16 times in the preceding 30 days. quicksearch this test
- Manifest: devtools/client/framework/test/browser.toml
/* Any copyright is dedicated to the Public Domain.
"use strict";
// Tests that changing preferences in the options panel updates the prefs
// and toggles appropriate things in the toolbox.
var doc = null,
toolbox = null,
panelWin = null,
modifiedPrefs = [];
const L10N = new LocalizationHelper(
"devtools/client/locales/toolbox.properties"
);
const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
add_task(async function () {
const URL =
"data:text/html;charset=utf8,test for dynamically registering " +
"and unregistering tools";
registerNewTool();
const tab = await addTab(URL);
toolbox = await gDevTools.showToolboxForTab(tab);
doc = toolbox.doc;
await registerNewPerToolboxTool();
await testSelectTool();
await testOptionsShortcut();
await testOptions();
await testToggleTools();
// Test that registered WebExtensions becomes entries in the
// options panel and toggling their checkbox toggle the related
// preference.
await registerNewWebExtensions();
await testToggleWebExtensions();
await cleanup();
});
function registerNewTool() {
const toolDefinition = {
id: "testTool",
isToolSupported: () => true,
visibilityswitch: "devtools.test-tool.enabled",
url: "about:blank",
label: "someLabel",
};
ok(gDevTools, "gDevTools exists");
ok(
!gDevTools.getToolDefinitionMap().has("testTool"),
"The tool is not registered"
);
gDevTools.registerTool(toolDefinition);
ok(
gDevTools.getToolDefinitionMap().has("testTool"),
"The tool is registered"
);
}
// Register a fake WebExtension to check that it is
// listed in the toolbox options.
function registerNewWebExtensions() {
// Register some fake extensions and init the related preferences
// (similarly to ext-devtools.js).
for (let i = 0; i < 2; i++) {
const extPref = `devtools.webextensions.fakeExtId${i}.enabled`;
Services.prefs.setBoolPref(extPref, true);
toolbox.registerWebExtension(`fakeUUID${i}`, {
name: `Fake WebExtension ${i}`,
pref: extPref,
});
}
}
function registerNewPerToolboxTool() {
const toolDefinition = {
id: "test-pertoolbox-tool",
isToolSupported: () => true,
visibilityswitch: "devtools.test-pertoolbox-tool.enabled",
url: "about:blank",
label: "perToolboxSomeLabel",
};
ok(gDevTools, "gDevTools exists");
ok(
!gDevTools.getToolDefinitionMap().has("test-pertoolbox-tool"),
"The per-toolbox tool is not registered globally"
);
ok(toolbox, "toolbox exists");
ok(
!toolbox.hasAdditionalTool("test-pertoolbox-tool"),
"The per-toolbox tool is not yet registered to the toolbox"
);
toolbox.addAdditionalTool(toolDefinition);
ok(
!gDevTools.getToolDefinitionMap().has("test-pertoolbox-tool"),
"The per-toolbox tool is not registered globally"
);
ok(
toolbox.hasAdditionalTool("test-pertoolbox-tool"),
"The per-toolbox tool has been registered to the toolbox"
);
}
async function testSelectTool() {
info("Checking to make sure that the options panel can be selected.");
const onceSelected = toolbox.once("options-selected");
toolbox.selectTool("options");
await onceSelected;
ok(true, "Toolbox selected via selectTool method");
}
async function testOptionsShortcut() {
info("Selecting another tool, then reselecting options panel with keyboard.");
await toolbox.selectTool("webconsole");
is(toolbox.currentToolId, "webconsole", "webconsole is selected");
synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key");
synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
is(toolbox.currentToolId, "webconsole", "webconsole is reselected");
synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key");
}
async function testOptions() {
const tool = toolbox.getPanel("options");
panelWin = tool.panelWin;
const prefNodes = tool.panelDoc.querySelectorAll(
"input[type=checkbox][data-pref]"
);
// Store modified pref names so that they can be cleared on error.
for (const node of tool.panelDoc.querySelectorAll("[data-pref]")) {
const pref = node.getAttribute("data-pref");
modifiedPrefs.push(pref);
}
for (const node of prefNodes) {
const prefValue = GetPref(node.getAttribute("data-pref"));
// Test clicking the checkbox for each options pref
await testMouseClick(node, prefValue);
// Do again with opposite values to reset prefs
await testMouseClick(node, !prefValue);
}
const prefSelects = tool.panelDoc.querySelectorAll("select[data-pref]");
for (const node of prefSelects) {
await testSelect(node);
}
}
async function testSelect(select) {
const pref = select.getAttribute("data-pref");
const options = Array.from(select.options);
info("Checking select for: " + pref);
is(
`${select.options[select.selectedIndex].value}`,
`${GetPref(pref)}`,
"select starts out selected"
);
for (const option of options) {
if (options.indexOf(option) === select.selectedIndex) {
continue;
}
const observer = new PrefObserver("devtools.");
let changeSeen = false;
const changeSeenPromise = new Promise(resolve => {
observer.once(pref, () => {
changeSeen = true;
is(
`${GetPref(pref)}`,
`${option.value}`,
"Preference been switched for " + pref
);
resolve();
});
});
select.selectedIndex = options.indexOf(option);
const changeEvent = new Event("change");
select.dispatchEvent(changeEvent);
await changeSeenPromise;
ok(changeSeen, "Correct pref was changed");
observer.destroy();
}
}
async function testMouseClick(node, prefValue) {
const observer = new PrefObserver("devtools.");
const pref = node.getAttribute("data-pref");
let changeSeen = false;
const changeSeenPromise = new Promise(resolve => {
observer.once(pref, () => {
changeSeen = true;
is(GetPref(pref), !prefValue, "New value is correct for " + pref);
resolve();
});
});
node.scrollIntoView();
// We use executeSoon here to ensure that the element is in view and
// clickable.
executeSoon(function () {
info("Click event synthesized for pref " + pref);
EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
});
await changeSeenPromise;
ok(changeSeen, "Correct pref was changed");
observer.destroy();
}
async function testToggleWebExtensions() {
const disabledExtensions = new Set();
const toggleableWebExtensions = toolbox.listWebExtensions();
function toggleWebExtension(node) {
node.scrollIntoView();
EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
}
function assertExpectedDisabledExtensions() {
for (const ext of toggleableWebExtensions) {
if (disabledExtensions.has(ext)) {
ok(
!toolbox.isWebExtensionEnabled(ext.uuid),
`The WebExtension "${ext.name}" should be disabled`
);
} else {
ok(
toolbox.isWebExtensionEnabled(ext.uuid),
`The WebExtension "${ext.name}" should be enabled`
);
}
}
}
function assertAllExtensionsDisabled() {
const enabledUUIDs = toggleableWebExtensions
.filter(ext => toolbox.isWebExtensionEnabled(ext.uuid))
.map(ext => ext.uuid);
Assert.deepEqual(
enabledUUIDs,
[],
"All the registered WebExtensions should be disabled"
);
}
function assertAllExtensionsEnabled() {
const disabledUUIDs = toolbox
.listWebExtensions()
.filter(ext => !toolbox.isWebExtensionEnabled(ext.uuid))
.map(ext => ext.uuid);
Assert.deepEqual(
disabledUUIDs,
[],
"All the registered WebExtensions should be enabled"
);
}
function getWebExtensionNodes() {
const toolNodes = panelWin.document.querySelectorAll(
"#default-tools-box input[type=checkbox]:not([data-unsupported])," +
"#additional-tools-box input[type=checkbox]:not([data-unsupported])"
);
return [...toolNodes].filter(node => {
return toggleableWebExtensions.some(
({ uuid }) => node.getAttribute("id") === `webext-${uuid}`
);
});
}
let webExtensionNodes = getWebExtensionNodes();
is(
webExtensionNodes.length,
toggleableWebExtensions.length,
"There should be a toggle checkbox for every WebExtension registered"
);
for (const ext of toggleableWebExtensions) {
ok(
toolbox.isWebExtensionEnabled(ext.uuid),
`The WebExtension "${ext.name}" is initially enabled`
);
}
// Store modified pref names so that they can be cleared on error.
for (const ext of toggleableWebExtensions) {
modifiedPrefs.push(ext.pref);
}
// Turn each registered WebExtension to disabled.
for (const node of webExtensionNodes) {
toggleWebExtension(node);
const toggledExt = toggleableWebExtensions.find(ext => {
return node.id == `webext-${ext.uuid}`;
});
ok(toggledExt, "Found a WebExtension for the checkbox element");
disabledExtensions.add(toggledExt);
assertExpectedDisabledExtensions();
}
assertAllExtensionsDisabled();
// Turn each registered WebExtension to enabled.
for (const node of webExtensionNodes) {
toggleWebExtension(node);
const toggledExt = toggleableWebExtensions.find(ext => {
return node.id == `webext-${ext.uuid}`;
});
ok(toggledExt, "Found a WebExtension for the checkbox element");
disabledExtensions.delete(toggledExt);
assertExpectedDisabledExtensions();
}
assertAllExtensionsEnabled();
// Unregister the WebExtensions one by one, and check that only the expected
// ones have been unregistered, and the remaining onea are still listed.
for (const ext of toggleableWebExtensions) {
ok(
!!toolbox.listWebExtensions().length,
"There should still be extensions registered"
);
toolbox.unregisterWebExtension(ext.uuid);
const registeredUUIDs = toolbox.listWebExtensions().map(item => item.uuid);
ok(
!registeredUUIDs.includes(ext.uuid),
`the WebExtension "${ext.name}" should have been unregistered`
);
webExtensionNodes = getWebExtensionNodes();
const checkboxEl = webExtensionNodes.find(
el => el.id === `webext-${ext.uuid}`
);
is(
checkboxEl,
undefined,
"The unregistered WebExtension checkbox should have been removed"
);
is(
registeredUUIDs.length,
webExtensionNodes.length,
"There should be the expected number of WebExtensions checkboxes"
);
}
is(
toolbox.listWebExtensions().length,
0,
"All WebExtensions have been unregistered"
);
webExtensionNodes = getWebExtensionNodes();
is(
webExtensionNodes.length,
0,
"There should not be any checkbox for the unregistered WebExtensions"
);
}
function getToolNode(id) {
return panelWin.document.getElementById(id);
}
async function testToggleTools() {
const toolNodes = panelWin.document.querySelectorAll(
"#default-tools-box input[type=checkbox]:not([data-unsupported])," +
"#additional-tools-box input[type=checkbox]:not([data-unsupported])"
);
const toolNodeIds = [...toolNodes].map(node => node.id);
const enabledToolIds = [...toolNodes]
.filter(node => node.checked)
.map(node => node.id);
const toggleableTools = gDevTools
.getDefaultTools()
.filter(tool => {
return tool.visibilityswitch;
})
.concat(gDevTools.getAdditionalTools())
.concat(toolbox.getAdditionalTools());
for (const node of toolNodes) {
const id = node.getAttribute("id");
ok(
toggleableTools.some(tool => tool.id === id),
"There should be a toggle checkbox for: " + id
);
}
// Store modified pref names so that they can be cleared on error.
for (const tool of toggleableTools) {
const pref = tool.visibilityswitch;
modifiedPrefs.push(pref);
}
// Toggle each tool
for (const id of toolNodeIds) {
await toggleTool(getToolNode(id));
}
// Toggle again to reset tool enablement state
for (const id of toolNodeIds) {
await toggleTool(getToolNode(id));
}
// Test that a tool can still be added when no tabs are present:
// Disable all tools
for (const id of enabledToolIds) {
await toggleTool(getToolNode(id));
}
// Re-enable the tools which are enabled by default
for (const id of enabledToolIds) {
await toggleTool(getToolNode(id));
}
// Toggle first, middle, and last tools to ensure that toolbox tabs are
// inserted in order
const firstToolId = toolNodeIds[0];
const middleToolId = toolNodeIds[(toolNodeIds.length / 2) | 0];
const lastToolId = toolNodeIds[toolNodeIds.length - 1];
await toggleTool(getToolNode(firstToolId));
await toggleTool(getToolNode(firstToolId));
await toggleTool(getToolNode(middleToolId));
await toggleTool(getToolNode(middleToolId));
await toggleTool(getToolNode(lastToolId));
await toggleTool(getToolNode(lastToolId));
}
/**
* Toggle tool node checkbox. Note: because toggling the checkbox will result in
* re-rendering of the tool list, we must re-query the checkboxes every time.
*/
async function toggleTool(node) {
const toolId = node.getAttribute("id");
const registeredPromise = new Promise(resolve => {
if (node.checked) {
gDevTools.once(
"tool-unregistered",
checkUnregistered.bind(null, toolId, resolve)
);
} else {
gDevTools.once(
"tool-registered",
checkRegistered.bind(null, toolId, resolve)
);
}
});
node.scrollIntoView();
EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
await registeredPromise;
}
function checkUnregistered(toolId, resolve, data) {
if (data == toolId) {
ok(true, "Correct tool removed");
// checking tab on the toolbox
ok(
!doc.getElementById("toolbox-tab-" + toolId),
"Tab removed for " + toolId
);
} else {
ok(false, "Something went wrong, " + toolId + " was not unregistered");
}
resolve();
}
async function checkRegistered(toolId, resolve, data) {
if (data == toolId) {
ok(true, "Correct tool added back");
// checking tab on the toolbox
const button = await lookupButtonForToolId(toolId);
ok(button, "Tab added back for " + toolId);
} else {
ok(false, "Something went wrong, " + toolId + " was not registered");
}
resolve();
}
function GetPref(name) {
const type = Services.prefs.getPrefType(name);
switch (type) {
case Services.prefs.PREF_STRING:
return Services.prefs.getCharPref(name);
case Services.prefs.PREF_INT:
return Services.prefs.getIntPref(name);
case Services.prefs.PREF_BOOL:
return Services.prefs.getBoolPref(name);
default:
throw new Error("Unknown type");
}
}
/**
* Find the button from specified toolId.
* Generally, button which access to the tool panel is in toolbox or
* tools menu(in the Chevron menu).
*/
async function lookupButtonForToolId(toolId) {
let button = doc.getElementById("toolbox-tab-" + toolId);
if (!button) {
// search from the tools menu.
await openChevronMenu(toolbox);
button = doc.querySelector("#tools-chevron-menupopup-" + toolId);
await closeChevronMenu(toolbox);
}
return button;
}
async function cleanup() {
gDevTools.unregisterTool("testTool");
await toolbox.destroy();
gBrowser.removeCurrentTab();
for (const pref of modifiedPrefs) {
Services.prefs.clearUserPref(pref);
}
toolbox = doc = panelWin = modifiedPrefs = null;
}