Dactyloidae/devtools/client/inspector/test/head.js

732 lines
24 KiB
JavaScript
Raw Normal View History

/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* import-globals-from ../../framework/test/shared-head.js */
/* import-globals-from ../../commandline/test/helpers.js */
/* import-globals-from ../../shared/test/test-actor-registry.js */
/* import-globals-from ../../inspector/test/shared-head.js */
"use strict";
// Load the shared-head file first.
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
this);
// Services.prefs.setBoolPref("devtools.debugger.log", true);
// SimpleTest.registerCleanupFunction(() => {
// Services.prefs.clearUserPref("devtools.debugger.log");
// });
// Import the GCLI test helper
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
this);
// Import helpers registering the test-actor in remote targets
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
this);
// Import helpers for the inspector that are also shared with others
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
this);
const {LocalizationHelper} = require("devtools/shared/l10n");
const INSPECTOR_L10N =
new LocalizationHelper("devtools/client/locales/inspector.properties");
flags.testing = true;
registerCleanupFunction(() => {
flags.testing = false;
});
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
});
registerCleanupFunction(function* () {
// Move the mouse outside inspector. If the test happened fake a mouse event
// somewhere over inspector the pointer is considered to be there when the
// next test begins. This might cause unexpected events to be emitted when
// another test moves the mouse.
EventUtils.synthesizeMouseAtPoint(1, 1, {type: "mousemove"}, window);
});
var navigateTo = Task.async(function* (inspector, url) {
let markuploaded = inspector.once("markuploaded");
let onNewRoot = inspector.once("new-root");
let onUpdated = inspector.once("inspector-updated");
info("Navigating to: " + url);
let activeTab = inspector.toolbox.target.activeTab;
yield activeTab.navigateTo(url);
info("Waiting for markup view to load after navigation.");
yield markuploaded;
info("Waiting for new root.");
yield onNewRoot;
info("Waiting for inspector to update after new-root event.");
yield onUpdated;
});
/**
* Start the element picker and focus the content window.
* @param {Toolbox} toolbox
* @param {Boolean} skipFocus - Allow tests to bypass the focus event.
*/
var startPicker = Task.async(function* (toolbox, skipFocus) {
info("Start the element picker");
toolbox.win.focus();
yield toolbox.highlighterUtils.startPicker();
if (!skipFocus) {
// By default make sure the content window is focused since the picker may not focus
// the content window by default.
yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
content.focus();
});
}
});
/**
* Highlight a node and set the inspector's current selection to the node or
* the first match of the given css selector.
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @return a promise that resolves when the inspector is updated with the new
* node
*/
function selectAndHighlightNode(selector, inspector) {
info("Highlighting and selecting the node " + selector);
return selectNode(selector, inspector, "test-highlight");
}
/**
* Select node for a given selector, make it focusable and set focus in its
* container element.
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The current inspector-panel instance.
* @return {MarkupContainer}
*/
function* focusNode(selector, inspector) {
getContainerForNodeFront(inspector.walker.rootNode, inspector).elt.focus();
let nodeFront = yield getNodeFront(selector, inspector);
let container = getContainerForNodeFront(nodeFront, inspector);
yield selectNode(nodeFront, inspector);
EventUtils.sendKey("return", inspector.panelWin);
return container;
}
/**
* Set the inspector's current selection to null so that no node is selected
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @return a promise that resolves when the inspector is updated
*/
function clearCurrentNodeSelection(inspector) {
info("Clearing the current selection");
let updated = inspector.once("inspector-updated");
inspector.selection.setNodeFront(null);
return updated;
}
/**
* Open the inspector in a tab with given URL.
* @param {string} url The URL to open.
* @param {String} hostType Optional hostType, as defined in Toolbox.HostType
* @return A promise that is resolved once the tab and inspector have loaded
* with an object: { tab, toolbox, inspector }.
*/
var openInspectorForURL = Task.async(function* (url, hostType) {
let tab = yield addTab(url);
let { inspector, toolbox, testActor } = yield openInspector(hostType);
return { tab, inspector, toolbox, testActor };
});
function getActiveInspector() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
return gDevTools.getToolbox(target).getPanel("inspector");
}
/**
* Right click on a node in the test page and click on the inspect menu item.
* @param {TestActor}
* @param {String} selector The selector for the node to click on in the page.
* @return {Promise} Resolves to the inspector when it has opened and is updated
*/
var clickOnInspectMenuItem = Task.async(function* (testActor, selector) {
info("Showing the contextual menu on node " + selector);
let contentAreaContextMenu = document.querySelector(
"#contentAreaContextMenu");
let contextOpened = once(contentAreaContextMenu, "popupshown");
yield testActor.synthesizeMouse({
selector: selector,
center: true,
options: {type: "contextmenu", button: 2}
});
yield contextOpened;
info("Triggering the inspect action");
yield gContextMenu.inspectNode();
info("Hiding the menu");
let contextClosed = once(contentAreaContextMenu, "popuphidden");
contentAreaContextMenu.hidePopup();
yield contextClosed;
return getActiveInspector();
});
/**
* Get the NodeFront for a node that matches a given css selector inside a
* given iframe.
* @param {String|NodeFront} selector
* @param {String|NodeFront} frameSelector A selector that matches the iframe
* the node is in
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {Promise} Resolves when the inspector is updated with the new node
*/
var getNodeFrontInFrame = Task.async(function* (selector, frameSelector,
inspector) {
let iframe = yield getNodeFront(frameSelector, inspector);
let {nodes} = yield inspector.walker.children(iframe);
return inspector.walker.querySelector(nodes[0], selector);
});
var focusSearchBoxUsingShortcut = Task.async(function* (panelWin, callback) {
info("Focusing search box");
let searchBox = panelWin.document.getElementById("inspector-searchbox");
let focused = once(searchBox, "focus");
panelWin.focus();
synthesizeKeyShortcut(INSPECTOR_L10N.getStr("inspector.searchHTML.key"));
yield focused;
if (callback) {
callback();
}
});
/**
* Get the MarkupContainer object instance that corresponds to the given
* NodeFront
* @param {NodeFront} nodeFront
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {MarkupContainer}
*/
function getContainerForNodeFront(nodeFront, {markup}) {
return markup.getContainer(nodeFront);
}
/**
* Get the MarkupContainer object instance that corresponds to the given
* selector
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {MarkupContainer}
*/
var getContainerForSelector = Task.async(function* (selector, inspector) {
info("Getting the markup-container for node " + selector);
let nodeFront = yield getNodeFront(selector, inspector);
let container = getContainerForNodeFront(nodeFront, inspector);
info("Found markup-container " + container);
return container;
});
/**
* Simulate a mouse-over on the markup-container (a line in the markup-view)
* that corresponds to the selector passed.
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {Promise} Resolves when the container is hovered and the higlighter
* is shown on the corresponding node
*/
var hoverContainer = Task.async(function* (selector, inspector) {
info("Hovering over the markup-container for node " + selector);
let nodeFront = yield getNodeFront(selector, inspector);
let container = getContainerForNodeFront(nodeFront, inspector);
let highlit = inspector.toolbox.once("node-highlight");
EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousemove"},
inspector.markup.doc.defaultView);
return highlit;
});
/**
* Simulate a click on the markup-container (a line in the markup-view)
* that corresponds to the selector passed.
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {Promise} Resolves when the node has been selected.
*/
var clickContainer = Task.async(function* (selector, inspector) {
info("Clicking on the markup-container for node " + selector);
let nodeFront = yield getNodeFront(selector, inspector);
let container = getContainerForNodeFront(nodeFront, inspector);
let updated = inspector.once("inspector-updated");
EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"},
inspector.markup.doc.defaultView);
EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mouseup"},
inspector.markup.doc.defaultView);
return updated;
});
/**
* Simulate the mouse leaving the markup-view area
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return a promise when done
*/
function mouseLeaveMarkupView(inspector) {
info("Leaving the markup-view area");
let def = defer();
// Find another element to mouseover over in order to leave the markup-view
let btn = inspector.toolbox.doc.querySelector("#toolbox-controls");
EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"},
inspector.toolbox.win);
executeSoon(def.resolve);
return def.promise;
}
/**
* Dispatch the copy event on the given element
*/
function fireCopyEvent(element) {
let evt = element.ownerDocument.createEvent("Event");
evt.initEvent("copy", true, true);
element.dispatchEvent(evt);
}
/**
* Undo the last markup-view action and wait for the corresponding mutation to
* occur
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return a promise that resolves when the markup-mutation has been treated or
* rejects if no undo action is possible
*/
function undoChange(inspector) {
let canUndo = inspector.markup.undo.canUndo();
ok(canUndo, "The last change in the markup-view can be undone");
if (!canUndo) {
return promise.reject();
}
let mutated = inspector.once("markupmutation");
inspector.markup.undo.undo();
return mutated;
}
/**
* Redo the last markup-view action and wait for the corresponding mutation to
* occur
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return a promise that resolves when the markup-mutation has been treated or
* rejects if no redo action is possible
*/
function redoChange(inspector) {
let canRedo = inspector.markup.undo.canRedo();
ok(canRedo, "The last change in the markup-view can be redone");
if (!canRedo) {
return promise.reject();
}
let mutated = inspector.once("markupmutation");
inspector.markup.undo.redo();
return mutated;
}
/**
* A helper that fetches a front for a node that matches the given selector or
* doctype node if the selector is falsy.
*/
function* getNodeFrontForSelector(selector, inspector) {
if (selector) {
info("Retrieving front for selector " + selector);
return getNodeFront(selector, inspector);
}
info("Retrieving front for doctype node");
let {nodes} = yield inspector.walker.children(inspector.walker.rootNode);
return nodes[0];
}
/**
* A simple polling helper that executes a given function until it returns true.
* @param {Function} check A generator function that is expected to return true at some
* stage.
* @param {String} desc A text description to be displayed when the polling starts.
* @param {Number} attemptes Optional number of times we poll. Defaults to 10.
* @param {Number} timeBetweenAttempts Optional time to wait between each attempt.
* Defaults to 200ms.
*/
function* poll(check, desc, attempts = 10, timeBetweenAttempts = 200) {
info(desc);
for (let i = 0; i < attempts; i++) {
if (yield check()) {
return;
}
yield new Promise(resolve => setTimeout(resolve, timeBetweenAttempts));
}
throw new Error(`Timeout while: ${desc}`);
}
/**
* Encapsulate some common operations for highlighter's tests, to have
* the tests cleaner, without exposing directly `inspector`, `highlighter`, and
* `testActor` if not needed.
*
* @param {String}
* The highlighter's type
* @return
* A generator function that takes an object with `inspector` and `testActor`
* properties. (see `openInspector`)
*/
const getHighlighterHelperFor = (type) => Task.async(
function* ({inspector, testActor}) {
let front = inspector.inspector;
let highlighter = yield front.getHighlighterByType(type);
let prefix = "";
// Internals for mouse events
let prevX, prevY;
// Highlighted node
let highlightedNode = null;
return {
set prefix(value) {
prefix = value;
},
get highlightedNode() {
if (!highlightedNode) {
return null;
}
return {
getComputedStyle: function* (options = {}) {
return yield inspector.pageStyle.getComputed(
highlightedNode, options);
}
};
},
show: function* (selector = ":root", options) {
highlightedNode = yield getNodeFront(selector, inspector);
return yield highlighter.show(highlightedNode, options);
},
hide: function* () {
yield highlighter.hide();
},
isElementHidden: function* (id) {
return (yield testActor.getHighlighterNodeAttribute(
prefix + id, "hidden", highlighter)) === "true";
},
getElementTextContent: function* (id) {
return yield testActor.getHighlighterNodeTextContent(
prefix + id, highlighter);
},
getElementAttribute: function* (id, name) {
return yield testActor.getHighlighterNodeAttribute(
prefix + id, name, highlighter);
},
waitForElementAttributeSet: function* (id, name) {
yield poll(function* () {
let value = yield testActor.getHighlighterNodeAttribute(
prefix + id, name, highlighter);
return !!value;
}, `Waiting for element ${id} to have attribute ${name} set`);
},
waitForElementAttributeRemoved: function* (id, name) {
yield poll(function* () {
let value = yield testActor.getHighlighterNodeAttribute(
prefix + id, name, highlighter);
return !value;
}, `Waiting for element ${id} to have attribute ${name} removed`);
},
synthesizeMouse: function* (options) {
options = Object.assign({selector: ":root"}, options);
yield testActor.synthesizeMouse(options);
},
// This object will synthesize any "mouse" prefixed event to the
// `testActor`, using the name of method called as suffix for the
// event's name.
// If no x, y coords are given, the previous ones are used.
//
// For example:
// mouse.down(10, 20); // synthesize "mousedown" at 10,20
// mouse.move(20, 30); // synthesize "mousemove" at 20,30
// mouse.up(); // synthesize "mouseup" at 20,30
mouse: new Proxy({}, {
get: (target, name) =>
function* (x = prevX, y = prevY) {
prevX = x;
prevY = y;
yield testActor.synthesizeMouse({
selector: ":root", x, y, options: {type: "mouse" + name}});
}
}),
reflow: function* () {
yield testActor.reflow();
},
finalize: function* () {
highlightedNode = null;
yield highlighter.finalize();
}
};
}
);
// The expand all operation of the markup-view calls itself recursively and
// there's not one event we can wait for to know when it's done so use this
// helper function to wait until all recursive children updates are done.
function* waitForMultipleChildrenUpdates(inspector) {
// As long as child updates are queued up while we wait for an update already
// wait again
if (inspector.markup._queuedChildUpdates &&
inspector.markup._queuedChildUpdates.size) {
yield waitForChildrenUpdated(inspector);
return yield waitForMultipleChildrenUpdates(inspector);
}
return null;
}
/**
* Using the markupview's _waitForChildren function, wait for all queued
* children updates to be handled.
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return a promise that resolves when all queued children updates have been
* handled
*/
function waitForChildrenUpdated({markup}) {
info("Waiting for queued children updates to be handled");
let def = defer();
markup._waitForChildren().then(() => {
executeSoon(def.resolve);
});
return def.promise;
}
/**
* Wait for the toolbox to emit the styleeditor-selected event and when done
* wait for the stylesheet identified by href to be loaded in the stylesheet
* editor
*
* @param {Toolbox} toolbox
* @param {String} href
* Optional, if not provided, wait for the first editor to be ready
* @return a promise that resolves to the editor when the stylesheet editor is
* ready
*/
function waitForStyleEditor(toolbox, href) {
let def = defer();
info("Waiting for the toolbox to switch to the styleeditor");
toolbox.once("styleeditor-selected").then(() => {
let panel = toolbox.getCurrentPanel();
ok(panel && panel.UI, "Styleeditor panel switched to front");
// A helper that resolves the promise once it receives an editor that
// matches the expected href. Returns false if the editor was not correct.
let gotEditor = (event, editor) => {
let currentHref = editor.styleSheet.href;
if (!href || (href && currentHref.endsWith(href))) {
info("Stylesheet editor selected");
panel.UI.off("editor-selected", gotEditor);
editor.getSourceEditor().then(sourceEditor => {
info("Stylesheet editor fully loaded");
def.resolve(sourceEditor);
});
return true;
}
info("The editor was incorrect. Waiting for editor-selected event.");
return false;
};
// The expected editor may already be selected. Check the if the currently
// selected editor is the expected one and if not wait for an
// editor-selected event.
if (!gotEditor("styleeditor-selected", panel.UI.selectedEditor)) {
// The expected editor is not selected (yet). Wait for it.
panel.UI.on("editor-selected", gotEditor);
}
});
return def.promise;
}
/**
* Checks if document's active element is within the given element.
* @param {HTMLDocument} doc document with active element in question
* @param {DOMNode} container element tested on focus containment
* @return {Boolean}
*/
function containsFocus(doc, container) {
let elm = doc.activeElement;
while (elm) {
if (elm === container) {
return true;
}
elm = elm.parentNode;
}
return false;
}
/**
* Listen for a new tab to open and return a promise that resolves when one
* does and completes the load event.
*
* @return a promise that resolves to the tab object
*/
var waitForTab = Task.async(function* () {
info("Waiting for a tab to open");
yield once(gBrowser.tabContainer, "TabOpen");
let tab = gBrowser.selectedTab;
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
info("The tab load completed");
return tab;
});
/**
* Simulate the key input for the given input in the window.
*
* @param {String} input
* The string value to input
* @param {Window} win
* The window containing the panel
*/
function synthesizeKeys(input, win) {
for (let key of input.split("")) {
EventUtils.synthesizeKey(key, {}, win);
}
}
/**
* Given a tooltip object instance (see Tooltip.js), checks if it is set to
* toggle and hover and if so, checks if the given target is a valid hover
* target. This won't actually show the tooltip (the less we interact with XUL
* panels during test runs, the better).
*
* @return a promise that resolves when the answer is known
*/
function isHoverTooltipTarget(tooltip, target) {
if (!tooltip._toggle._baseNode || !tooltip.panel) {
return promise.reject(new Error(
"The tooltip passed isn't set to toggle on hover or is not a tooltip"));
}
return tooltip._toggle.isValidHoverTarget(target);
}
/**
* Same as isHoverTooltipTarget except that it will fail the test if there is no
* tooltip defined on hover of the given element
*
* @return a promise
*/
function assertHoverTooltipOn(tooltip, element) {
return isHoverTooltipTarget(tooltip, element).then(() => {
ok(true, "A tooltip is defined on hover of the given element");
}, () => {
ok(false, "No tooltip is defined on hover of the given element");
});
}
/**
* Open the inspector menu and return all of it's items in a flat array
* @param {InspectorPanel} inspector
* @param {Object} options to pass into openMenu
* @return An array of MenuItems
*/
function openContextMenuAndGetAllItems(inspector, options) {
let menu = inspector._openMenu(options);
// Flatten all menu items into a single array to make searching through it easier
let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
if (item.submenu) {
return addItem(item.submenu.items);
}
return item;
}));
return allItems;
}
/**
* Get the rule editor from the rule-view given its index
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {Number} childrenIndex
* The children index of the element to get
* @param {Number} nodeIndex
* The child node index of the element to get
* @return {DOMNode} The rule editor if any at this index
*/
function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) {
return nodeIndex !== undefined ?
view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor :
view.element.children[childrenIndex]._ruleEditor;
}
/**
* Get the text displayed for a given DOM Element's textContent within the
* markup view.
*
* @param {String} selector
* @param {InspectorPanel} inspector
* @return {String} The text displayed in the markup view
*/
function* getDisplayedNodeTextContent(selector, inspector) {
// We have to ensure that the textContent is displayed, for that the DOM
// Element has to be selected in the markup view and to be expanded.
yield selectNode(selector, inspector);
let container = yield getContainerForSelector(selector, inspector);
yield inspector.markup.expandNode(container.node);
yield waitForMultipleChildrenUpdates(inspector);
if (container) {
let textContainer = container.elt.querySelector("pre");
return textContainer.textContent;
}
return null;
}