490 lines
14 KiB
JavaScript
490 lines
14 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
// shared-head.js handles imports, constants, and utility functions
|
|
Services.scriptloader.loadSubScript(
|
|
"chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
|
|
this
|
|
);
|
|
Services.scriptloader.loadSubScript(
|
|
"chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
|
|
this
|
|
);
|
|
|
|
const EventEmitter = require("resource://devtools/shared/event-emitter.js");
|
|
|
|
/**
|
|
* Retrieve all tool ids compatible with a target created for the provided tab.
|
|
*
|
|
* @param {XULTab} tab
|
|
* The tab for which we want to get the list of supported toolIds
|
|
* @return {Array<String>} array of tool ids
|
|
*/
|
|
async function getSupportedToolIds(tab) {
|
|
info("Getting the entire list of tools supported in this tab");
|
|
|
|
let shouldDestroyToolbox = false;
|
|
|
|
// Get the toolbox for this tab, or create one if needed.
|
|
let toolbox = gDevTools.getToolboxForTab(tab);
|
|
if (!toolbox) {
|
|
toolbox = await gDevTools.showToolboxForTab(tab);
|
|
shouldDestroyToolbox = true;
|
|
}
|
|
|
|
const toolIds = gDevTools
|
|
.getToolDefinitionArray()
|
|
.filter(def => def.isToolSupported(toolbox))
|
|
.map(def => def.id);
|
|
|
|
if (shouldDestroyToolbox) {
|
|
// Only close the toolbox if it was explicitly created here.
|
|
await toolbox.destroy();
|
|
}
|
|
|
|
return toolIds;
|
|
}
|
|
|
|
function toggleAllTools(state) {
|
|
for (const [, tool] of gDevTools._tools) {
|
|
if (!tool.visibilityswitch) {
|
|
continue;
|
|
}
|
|
if (state) {
|
|
Services.prefs.setBoolPref(tool.visibilityswitch, true);
|
|
} else {
|
|
Services.prefs.clearUserPref(tool.visibilityswitch);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getParentProcessActors(callback) {
|
|
const commands = await CommandsFactory.forMainProcess();
|
|
const mainProcessTargetFront = await commands.descriptorFront.getTarget();
|
|
|
|
callback(commands.client, mainProcessTargetFront);
|
|
}
|
|
|
|
function getSourceActor(aSources, aURL) {
|
|
const item = aSources.getItemForAttachment(a => a.source.url === aURL);
|
|
return item && item.value;
|
|
}
|
|
|
|
/**
|
|
* Synthesize a keypress from a <key> element, taking into account
|
|
* any modifiers.
|
|
* @param {Element} el the <key> element to synthesize
|
|
*/
|
|
function synthesizeKeyElement(el) {
|
|
const key = el.getAttribute("key") || el.getAttribute("keycode");
|
|
const mod = {};
|
|
el.getAttribute("modifiers")
|
|
.split(" ")
|
|
.forEach(m => (mod[m + "Key"] = true));
|
|
info(`Synthesizing: key=${key}, mod=${JSON.stringify(mod)}`);
|
|
EventUtils.synthesizeKey(key, mod, el.ownerDocument.defaultView);
|
|
}
|
|
|
|
/* Check the toolbox host type and prefs to make sure they match the
|
|
* expected values
|
|
* @param {Toolbox}
|
|
* @param {HostType} hostType
|
|
* One of {SIDE, BOTTOM, WINDOW} from Toolbox.HostType
|
|
* @param {HostType} Optional previousHostType
|
|
* The host that will be switched to when calling switchToPreviousHost
|
|
*/
|
|
function checkHostType(toolbox, hostType, previousHostType) {
|
|
is(toolbox.hostType, hostType, "host type is " + hostType);
|
|
|
|
const pref = Services.prefs.getCharPref("devtools.toolbox.host");
|
|
is(pref, hostType, "host pref is " + hostType);
|
|
|
|
if (previousHostType) {
|
|
is(
|
|
Services.prefs.getCharPref("devtools.toolbox.previousHost"),
|
|
previousHostType,
|
|
"The previous host is correct"
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new <script> referencing URL. Return a promise that
|
|
* resolves when this has happened
|
|
* @param {String} url
|
|
* the url
|
|
* @return {Promise} a promise that resolves when the element has been created
|
|
*/
|
|
function createScript(url) {
|
|
info(`Creating script: ${url}`);
|
|
// This is not ideal if called multiple times, as it loads the frame script
|
|
// separately each time. See bug 1443680.
|
|
return SpecialPowers.spawn(gBrowser.selectedBrowser, [url], urlChild => {
|
|
const script = content.document.createElement("script");
|
|
script.setAttribute("src", urlChild);
|
|
content.document.body.appendChild(script);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wait for the toolbox to notice that a given source is loaded
|
|
* @param {Toolbox} toolbox
|
|
* @param {String} url
|
|
* the url to wait for
|
|
* @return {Promise} a promise that is resolved when the source is loaded
|
|
*/
|
|
function waitForSourceLoad(toolbox, url) {
|
|
info(`Waiting for source ${url} to be available...`);
|
|
return new Promise(resolve => {
|
|
const { resourceCommand } = toolbox;
|
|
|
|
function onAvailable(sources) {
|
|
for (const source of sources) {
|
|
if (source.url === url) {
|
|
resourceCommand.unwatchResources([resourceCommand.TYPES.SOURCE], {
|
|
onAvailable,
|
|
});
|
|
resolve();
|
|
}
|
|
}
|
|
}
|
|
resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], {
|
|
onAvailable,
|
|
// Ignore the cached resources as we always listen *before*
|
|
// the action creating a source.
|
|
ignoreExistingResources: true,
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When a Toolbox is started it creates a DevToolPanel for each of the tools
|
|
* by calling toolDefinition.build(). The returned object should
|
|
* at least implement these functions. They will be used by the ToolBox.
|
|
*
|
|
* There may be no benefit in doing this as an abstract type, but if nothing
|
|
* else gives us a place to write documentation.
|
|
*/
|
|
function DevToolPanel(iframeWindow, toolbox) {
|
|
EventEmitter.decorate(this);
|
|
|
|
this._toolbox = toolbox;
|
|
this._window = iframeWindow;
|
|
}
|
|
|
|
DevToolPanel.prototype = {
|
|
open() {
|
|
return new Promise(resolve => {
|
|
executeSoon(() => {
|
|
resolve(this);
|
|
});
|
|
});
|
|
},
|
|
|
|
get document() {
|
|
return this._window.document;
|
|
},
|
|
|
|
get target() {
|
|
return this._toolbox.target;
|
|
},
|
|
|
|
get toolbox() {
|
|
return this._toolbox;
|
|
},
|
|
|
|
destroy() {
|
|
return Promise.resolve(null);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Create a simple devtools test panel that implements the minimum API needed to be
|
|
* registered and opened in the toolbox.
|
|
*/
|
|
function createTestPanel(iframeWindow, toolbox) {
|
|
return new DevToolPanel(iframeWindow, toolbox);
|
|
}
|
|
|
|
async function openChevronMenu(toolbox) {
|
|
const chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
|
|
EventUtils.synthesizeMouseAtCenter(chevronMenuButton, {}, toolbox.win);
|
|
|
|
const menuPopup = toolbox.doc.getElementById(
|
|
"tools-chevron-menu-button-panel"
|
|
);
|
|
ok(menuPopup, "tools-chevron-menupopup is available");
|
|
|
|
info("Waiting for the menu popup to be displayed");
|
|
await waitUntil(() => menuPopup.classList.contains("tooltip-visible"));
|
|
}
|
|
|
|
async function closeChevronMenu(toolbox) {
|
|
// In order to close the popup menu with escape key, set the focus to the chevron
|
|
// button at first.
|
|
const chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
|
|
chevronMenuButton.focus();
|
|
|
|
EventUtils.sendKey("ESCAPE", toolbox.doc.defaultView);
|
|
const menuPopup = toolbox.doc.getElementById(
|
|
"tools-chevron-menu-button-panel"
|
|
);
|
|
|
|
info("Closing the chevron popup menu");
|
|
await waitUntil(() => !menuPopup.classList.contains("tooltip-visible"));
|
|
}
|
|
|
|
function prepareToolTabReorderTest(toolbox, startingOrder) {
|
|
Services.prefs.setCharPref(
|
|
"devtools.toolbox.tabsOrder",
|
|
startingOrder.join(",")
|
|
);
|
|
ok(
|
|
!toolbox.doc.getElementById("tools-chevron-menu-button"),
|
|
"The size of the screen being too small"
|
|
);
|
|
|
|
for (const id of startingOrder) {
|
|
ok(getElementByToolId(toolbox, id), `Tab element should exist for ${id}`);
|
|
}
|
|
}
|
|
|
|
async function dndToolTab(toolbox, dragTarget, dropTarget, passedTargets = []) {
|
|
info(`Drag ${dragTarget} to ${dropTarget}`);
|
|
const dragTargetEl = getElementByToolIdOrExtensionIdOrSelector(
|
|
toolbox,
|
|
dragTarget
|
|
);
|
|
|
|
const onReady = dragTargetEl.classList.contains("selected")
|
|
? Promise.resolve()
|
|
: toolbox.once("select");
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
dragTargetEl,
|
|
{ type: "mousedown" },
|
|
dragTargetEl.ownerGlobal
|
|
);
|
|
await onReady;
|
|
|
|
for (const passedTarget of passedTargets) {
|
|
info(`Via ${passedTarget}`);
|
|
const passedTargetEl = getElementByToolIdOrExtensionIdOrSelector(
|
|
toolbox,
|
|
passedTarget
|
|
);
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
passedTargetEl,
|
|
{ type: "mousemove" },
|
|
passedTargetEl.ownerGlobal
|
|
);
|
|
}
|
|
|
|
if (dropTarget) {
|
|
const dropTargetEl = getElementByToolIdOrExtensionIdOrSelector(
|
|
toolbox,
|
|
dropTarget
|
|
);
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
dropTargetEl,
|
|
{ type: "mousemove" },
|
|
dropTargetEl.ownerGlobal
|
|
);
|
|
EventUtils.synthesizeMouseAtCenter(
|
|
dropTargetEl,
|
|
{ type: "mouseup" },
|
|
dropTargetEl.ownerGlobal
|
|
);
|
|
} else {
|
|
const containerEl = toolbox.doc.getElementById("toolbox-container");
|
|
EventUtils.synthesizeMouse(
|
|
containerEl,
|
|
0,
|
|
0,
|
|
{ type: "mouseout" },
|
|
containerEl.ownerGlobal
|
|
);
|
|
}
|
|
|
|
// Wait for updating the preference.
|
|
await new Promise(resolve => {
|
|
const onUpdated = () => {
|
|
Services.prefs.removeObserver("devtools.toolbox.tabsOrder", onUpdated);
|
|
resolve();
|
|
};
|
|
|
|
Services.prefs.addObserver("devtools.toolbox.tabsOrder", onUpdated);
|
|
});
|
|
}
|
|
|
|
function assertToolTabOrder(toolbox, expectedOrder) {
|
|
info("Check the order of the tabs on the toolbar");
|
|
|
|
const tabEls = toolbox.doc.querySelectorAll(".devtools-tab");
|
|
|
|
for (let i = 0; i < expectedOrder.length; i++) {
|
|
const isOrdered =
|
|
tabEls[i].dataset.id === expectedOrder[i] ||
|
|
tabEls[i].dataset.extensionId === expectedOrder[i];
|
|
ok(isOrdered, `The tab[${expectedOrder[i]}] should exist at [${i}]`);
|
|
}
|
|
}
|
|
|
|
function assertToolTabSelected(toolbox, dragTarget) {
|
|
info("Check whether the drag target was selected");
|
|
const dragTargetEl = getElementByToolIdOrExtensionIdOrSelector(
|
|
toolbox,
|
|
dragTarget
|
|
);
|
|
ok(
|
|
dragTargetEl.classList.contains("selected"),
|
|
"The dragged tool should be selected"
|
|
);
|
|
}
|
|
|
|
function assertToolTabPreferenceOrder(expectedOrder) {
|
|
info("Check the order in DevTools preference for tabs order");
|
|
is(
|
|
Services.prefs.getCharPref("devtools.toolbox.tabsOrder"),
|
|
expectedOrder.join(","),
|
|
"The preference should be correct"
|
|
);
|
|
}
|
|
|
|
function getElementByToolId(toolbox, id) {
|
|
for (const tabEl of toolbox.doc.querySelectorAll(".devtools-tab")) {
|
|
if (tabEl.dataset.id === id || tabEl.dataset.extensionId === id) {
|
|
return tabEl;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getElementByToolIdOrExtensionIdOrSelector(toolbox, idOrSelector) {
|
|
const tabEl = getElementByToolId(toolbox, idOrSelector);
|
|
return tabEl ? tabEl : toolbox.doc.querySelector(idOrSelector);
|
|
}
|
|
|
|
/**
|
|
* Returns a toolbox tab element, even if it's overflowed
|
|
**/
|
|
function getToolboxTab(doc, toolId) {
|
|
return (
|
|
doc.getElementById(`toolbox-tab-${toolId}`) ||
|
|
doc.getElementById(`tools-chevron-menupopup-${toolId}`)
|
|
);
|
|
}
|
|
|
|
function getWindow(toolbox) {
|
|
return toolbox.topWindow;
|
|
}
|
|
|
|
async function resizeWindow(toolbox, width, height) {
|
|
const hostWindow = toolbox.win.parent;
|
|
const originalWidth = hostWindow.outerWidth;
|
|
const originalHeight = hostWindow.outerHeight;
|
|
const toWidth = width || originalWidth;
|
|
const toHeight = height || originalHeight;
|
|
|
|
const onResize = once(hostWindow, "resize");
|
|
hostWindow.resizeTo(toWidth, toHeight);
|
|
await onResize;
|
|
}
|
|
|
|
function assertSelectedLocationInDebugger(debuggerPanel, line, column) {
|
|
const location = debuggerPanel._selectors.getSelectedLocation(
|
|
debuggerPanel._getState()
|
|
);
|
|
is(location.line, line);
|
|
is(location.column, column);
|
|
}
|
|
|
|
/**
|
|
* Open a new tab on about:devtools-toolbox with the provided params object used as
|
|
* queryString.
|
|
*/
|
|
async function openAboutToolbox(params) {
|
|
info("Open about:devtools-toolbox");
|
|
const querystring = new URLSearchParams();
|
|
Object.keys(params).forEach(x => querystring.append(x, params[x]));
|
|
|
|
const tab = await addTab(`about:devtools-toolbox?${querystring}`);
|
|
const browser = tab.linkedBrowser;
|
|
|
|
return {
|
|
tab,
|
|
document: browser.contentDocument,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load FTL.
|
|
*
|
|
* @param {Toolbox} toolbox
|
|
* Toolbox instance.
|
|
* @param {String} path
|
|
* Path to the FTL file.
|
|
*/
|
|
function loadFTL(toolbox, path) {
|
|
const win = toolbox.doc.ownerGlobal;
|
|
|
|
if (win.MozXULElement) {
|
|
win.MozXULElement.insertFTLIfNeeded(path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit a reload key shortcut from a given toolbox, and wait for the reload to
|
|
* be completed.
|
|
*
|
|
* @param {String} shortcut
|
|
* The key shortcut to send, as expected by the devtools shortcuts
|
|
* helpers (eg. "CmdOrCtrl+F5").
|
|
* @param {Toolbox} toolbox
|
|
* The toolbox through which the event should be emitted.
|
|
*/
|
|
async function sendToolboxReloadShortcut(shortcut, toolbox) {
|
|
const promises = [];
|
|
|
|
// If we have a jsdebugger panel, wait for it to complete its reload.
|
|
const jsdebugger = toolbox.getPanel("jsdebugger");
|
|
if (jsdebugger) {
|
|
promises.push(jsdebugger.once("reloaded"));
|
|
}
|
|
|
|
// If we have an inspector panel, wait for it to complete its reload.
|
|
const inspector = toolbox.getPanel("inspector");
|
|
if (inspector) {
|
|
promises.push(
|
|
inspector.once("reloaded"),
|
|
inspector.once("inspector-updated")
|
|
);
|
|
}
|
|
|
|
const loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
|
promises.push(loadPromise);
|
|
|
|
info("Focus the toolbox window and emit the reload shortcut: " + shortcut);
|
|
toolbox.win.focus();
|
|
synthesizeKeyShortcut(shortcut, toolbox.win);
|
|
|
|
info("Wait for page and toolbox reload promises");
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
function getErrorIcon(toolbox) {
|
|
return toolbox.doc.querySelector(".toolbox-error");
|
|
}
|
|
|
|
function getErrorIconCount(toolbox) {
|
|
const textContent = getErrorIcon(toolbox)?.textContent;
|
|
try {
|
|
const int = parseInt(textContent, 10);
|
|
// 99+ parses to 99, so we check if the parsedInt does not match the textContent.
|
|
return int.toString() === textContent ? int : textContent;
|
|
} catch (e) {
|
|
// In case the parseInt threw, return the actual textContent so the test can display
|
|
// an easy to debug failure.
|
|
return textContent;
|
|
}
|
|
}
|