diff options
Diffstat (limited to 'devtools/client/inspector/test/head.js')
-rw-r--r-- | devtools/client/inspector/test/head.js | 732 |
1 files changed, 732 insertions, 0 deletions
diff --git a/devtools/client/inspector/test/head.js b/devtools/client/inspector/test/head.js new file mode 100644 index 000000000..f251568df --- /dev/null +++ b/devtools/client/inspector/test/head.js @@ -0,0 +1,732 @@ +/* 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; +} |