michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const Cu = Components.utils; michael@0: let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); michael@0: let TargetFactory = devtools.TargetFactory; michael@0: let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); michael@0: let promise = devtools.require("devtools/toolkit/deprecated-sync-thenables"); michael@0: let {getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor"); michael@0: michael@0: // All test are asynchronous michael@0: waitForExplicitFinish(); michael@0: michael@0: //Services.prefs.setBoolPref("devtools.dump.emit", true); michael@0: michael@0: // Set the testing flag on gDevTools and reset it when the test ends michael@0: gDevTools.testing = true; michael@0: registerCleanupFunction(() => gDevTools.testing = false); michael@0: michael@0: // Clear preferences that may be set during the course of tests. michael@0: registerCleanupFunction(() => { michael@0: Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen"); michael@0: Services.prefs.clearUserPref("devtools.inspector.sidebarOpen"); michael@0: Services.prefs.clearUserPref("devtools.inspector.activeSidebar"); michael@0: Services.prefs.clearUserPref("devtools.dump.emit"); michael@0: Services.prefs.clearUserPref("devtools.markup.pagesize"); michael@0: }); michael@0: michael@0: // Auto close the toolbox and close the test tabs when the test ends michael@0: registerCleanupFunction(() => { michael@0: try { michael@0: let target = TargetFactory.forTab(gBrowser.selectedTab); michael@0: gDevTools.closeToolbox(target); michael@0: } catch (ex) { michael@0: dump(ex); michael@0: } michael@0: while (gBrowser.tabs.length > 1) { michael@0: gBrowser.removeCurrentTab(); michael@0: } michael@0: }); michael@0: michael@0: const TEST_URL_ROOT = "http://mochi.test:8888/browser/browser/devtools/markupview/test/"; michael@0: michael@0: /** michael@0: * Define an async test based on a generator function michael@0: */ michael@0: function asyncTest(generator) { michael@0: return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish); michael@0: } michael@0: michael@0: /** michael@0: * Add a new test tab in the browser and load the given url. michael@0: * @param {String} url The url to be loaded in the new tab michael@0: * @return a promise that resolves to the tab object when the url is loaded michael@0: */ michael@0: function addTab(url) { michael@0: info("Adding a new tab with URL: '" + url + "'"); michael@0: let def = promise.defer(); michael@0: michael@0: let tab = gBrowser.selectedTab = gBrowser.addTab(); michael@0: gBrowser.selectedBrowser.addEventListener("load", function onload() { michael@0: gBrowser.selectedBrowser.removeEventListener("load", onload, true); michael@0: info("URL '" + url + "' loading complete"); michael@0: waitForFocus(() => { michael@0: def.resolve(tab); michael@0: }, content); michael@0: }, true); michael@0: content.location = url; michael@0: michael@0: return def.promise; michael@0: } michael@0: michael@0: /** michael@0: * Some tests may need to import one or more of the test helper scripts. michael@0: * A test helper script is simply a js file that contains common test code that michael@0: * is either not common-enough to be in head.js, or that is located in a separate michael@0: * directory. michael@0: * The script will be loaded synchronously and in the test's scope. michael@0: * @param {String} filePath The file path, relative to the current directory. michael@0: * Examples: michael@0: * - "helper_attributes_test_runner.js" michael@0: * - "../../../commandline/test/helpers.js" michael@0: */ michael@0: function loadHelperScript(filePath) { michael@0: let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); michael@0: Services.scriptloader.loadSubScript(testDir + "/" + filePath, this); michael@0: } michael@0: michael@0: /** michael@0: * Reload the current page michael@0: * @return a promise that resolves when the inspector has emitted the event michael@0: * new-root michael@0: */ michael@0: function reloadPage(inspector) { michael@0: info("Reloading the page"); michael@0: let newRoot = inspector.once("new-root"); michael@0: content.location.reload(); michael@0: return newRoot; michael@0: } michael@0: michael@0: /** michael@0: * Open the toolbox, with the inspector tool visible. michael@0: * @return a promise that resolves when the inspector is ready michael@0: */ michael@0: function openInspector() { michael@0: info("Opening the inspector panel"); michael@0: let def = promise.defer(); michael@0: michael@0: let target = TargetFactory.forTab(gBrowser.selectedTab); michael@0: gDevTools.showToolbox(target, "inspector").then(function(toolbox) { michael@0: info("The toolbox is open"); michael@0: let inspector = toolbox.getCurrentPanel(); michael@0: inspector.once("inspector-updated", () => { michael@0: info("The inspector panel is active and ready"); michael@0: def.resolve({toolbox: toolbox, inspector: inspector}); michael@0: }); michael@0: }).then(null, console.error); michael@0: michael@0: return def.promise; michael@0: } michael@0: michael@0: /** michael@0: * Simple DOM node accesor function that takes either a node or a string css michael@0: * selector as argument and returns the corresponding node michael@0: * @param {String|DOMNode} nodeOrSelector michael@0: * @return {DOMNode} michael@0: */ michael@0: function getNode(nodeOrSelector) { michael@0: info("Getting the node for '" + nodeOrSelector + "'"); michael@0: return typeof nodeOrSelector === "string" ? michael@0: content.document.querySelector(nodeOrSelector) : michael@0: nodeOrSelector; michael@0: } michael@0: michael@0: /** michael@0: * Set the inspector's current selection to a node or to the first match of the michael@0: * given css selector michael@0: * @param {String|DOMNode} nodeOrSelector michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox michael@0: * @param {String} reason Defaults to "test" which instructs the inspector not to highlight the node upon selection michael@0: * @return a promise that resolves when the inspector is updated with the new michael@0: * node michael@0: */ michael@0: function selectNode(nodeOrSelector, inspector, reason="test") { michael@0: info("Selecting the node for '" + nodeOrSelector + "'"); michael@0: let node = getNode(nodeOrSelector); michael@0: let updated = inspector.once("inspector-updated"); michael@0: inspector.selection.setNode(node, reason); michael@0: return updated; michael@0: } michael@0: michael@0: /** michael@0: * Get the MarkupContainer object instance that corresponds to the given michael@0: * HTML node michael@0: * @param {DOMNode|String} nodeOrSelector The DOM node for which the michael@0: * container is required michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently michael@0: * loaded in the toolbox michael@0: * @return {MarkupContainer} michael@0: */ michael@0: function getContainerForRawNode(nodeOrSelector, {markup}) { michael@0: let front = markup.walker.frontForRawNode(getNode(nodeOrSelector)); michael@0: let container = markup.getContainer(front); michael@0: info("Markup-container object for " + nodeOrSelector + " " + container); michael@0: return container; michael@0: } michael@0: michael@0: /** michael@0: * Using the markupview's _waitForChildren function, wait for all queued michael@0: * children updates to be handled. michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently michael@0: * loaded in the toolbox michael@0: * @return a promise that resolves when all queued children updates have been michael@0: * handled michael@0: */ michael@0: function waitForChildrenUpdated({markup}) { michael@0: info("Waiting for queued children updates to be handled"); michael@0: let def = promise.defer(); michael@0: markup._waitForChildren().then(() => { michael@0: executeSoon(def.resolve); michael@0: }); michael@0: return def.promise; michael@0: } michael@0: michael@0: /** michael@0: * Simulate a mouse-over on the markup-container (a line in the markup-view) michael@0: * that corresponds to the node or selector passed. michael@0: * @param {String|DOMNode} nodeOrSelector michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox michael@0: * @return a promise that resolves when the container is hovered and the higlighter michael@0: * is shown on the corresponding node michael@0: */ michael@0: function hoverContainer(nodeOrSelector, inspector) { michael@0: info("Hovering over the markup-container for node " + nodeOrSelector); michael@0: let highlit = inspector.toolbox.once("node-highlight"); michael@0: let container = getContainerForRawNode(getNode(nodeOrSelector), inspector); michael@0: EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousemove"}, michael@0: inspector.markup.doc.defaultView); michael@0: return highlit; michael@0: } michael@0: michael@0: /** michael@0: * Simulate a click on the markup-container (a line in the markup-view) michael@0: * that corresponds to the node or selector passed. michael@0: * @param {String|DOMNode} nodeOrSelector michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox michael@0: * @return a promise that resolves when the node has been selected. michael@0: */ michael@0: function clickContainer(nodeOrSelector, inspector) { michael@0: info("Clicking on the markup-container for node " + nodeOrSelector); michael@0: let updated = inspector.once("inspector-updated"); michael@0: let container = getContainerForRawNode(getNode(nodeOrSelector), inspector); michael@0: EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"}, michael@0: inspector.markup.doc.defaultView); michael@0: EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mouseup"}, michael@0: inspector.markup.doc.defaultView); michael@0: return updated; michael@0: } michael@0: michael@0: /** michael@0: * Checks if the highlighter is visible currently michael@0: * @return {Boolean} michael@0: */ michael@0: function isHighlighterVisible() { michael@0: let highlighter = gBrowser.selectedBrowser.parentNode michael@0: .querySelector(".highlighter-container .box-model-root"); michael@0: return highlighter && !highlighter.hasAttribute("hidden"); michael@0: } michael@0: michael@0: /** michael@0: * Simulate the mouse leaving the markup-view area michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox michael@0: * @return a promise when done michael@0: */ michael@0: function mouseLeaveMarkupView(inspector) { michael@0: info("Leaving the markup-view area"); michael@0: let def = promise.defer(); michael@0: michael@0: // Find another element to mouseover over in order to leave the markup-view michael@0: let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button"); michael@0: michael@0: EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"}, michael@0: inspector.toolbox.doc.defaultView); michael@0: executeSoon(def.resolve); michael@0: michael@0: return def.promise; michael@0: } michael@0: michael@0: /** michael@0: * Focus a given editable element, enter edit mode, set value, and commit michael@0: * @param {DOMNode} field The element that gets editable after receiving focus michael@0: * and keypress michael@0: * @param {String} value The string value to be set into the edited field michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently michael@0: * loaded in the toolbox michael@0: */ michael@0: function setEditableFieldValue(field, value, inspector) { michael@0: field.focus(); michael@0: EventUtils.sendKey("return", inspector.panelWin); michael@0: let input = inplaceEditor(field).input; michael@0: ok(input, "Found editable field for setting value: " + value); michael@0: input.value = value; michael@0: EventUtils.sendKey("return", inspector.panelWin); michael@0: } michael@0: michael@0: /** michael@0: * Focus the new-attribute inplace-editor field of the nodeOrSelector's markup michael@0: * container, and enters the given text, then wait for it to be applied and the michael@0: * for the node to mutates (when new attribute(s) is(are) created) michael@0: * @param {DOMNode|String} nodeOrSelector The node or node selector to edit. michael@0: * @param {String} text The new attribute text to be entered (e.g. "id='test'") michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently michael@0: * loaded in the toolbox michael@0: * @return a promise that resolves when the node has mutated michael@0: */ michael@0: function addNewAttributes(nodeOrSelector, text, inspector) { michael@0: info("Entering text '" + text + "' in node '" + nodeOrSelector + "''s new attribute field"); michael@0: michael@0: let container = getContainerForRawNode(nodeOrSelector, inspector); michael@0: ok(container, "The container for '" + nodeOrSelector + "' was found"); michael@0: michael@0: info("Listening for the markupmutation event"); michael@0: let nodeMutated = inspector.once("markupmutation"); michael@0: setEditableFieldValue(container.editor.newAttr, text, inspector); michael@0: return nodeMutated; michael@0: } michael@0: michael@0: /** michael@0: * Checks that a node has the given attributes michael@0: * michael@0: * @param {DOMNode|String} nodeOrSelector The node or node selector to check. michael@0: * @param {Object} attrs An object containing the attributes to check. michael@0: * e.g. {id: "id1", class: "someclass"} michael@0: * michael@0: * Note that node.getAttribute() returns attribute values provided by the HTML michael@0: * parser. The parser only provides unescaped entities so & will return &. michael@0: */ michael@0: function assertAttributes(nodeOrSelector, attrs) { michael@0: let node = getNode(nodeOrSelector); michael@0: michael@0: is(node.attributes.length, Object.keys(attrs).length, michael@0: "Node has the correct number of attributes."); michael@0: for (let attr in attrs) { michael@0: is(node.getAttribute(attr), attrs[attr], michael@0: "Node has the correct " + attr + " attribute."); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Undo the last markup-view action and wait for the corresponding mutation to michael@0: * occur michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently michael@0: * loaded in the toolbox michael@0: * @return a promise that resolves when the markup-mutation has been treated or michael@0: * rejects if no undo action is possible michael@0: */ michael@0: function undoChange(inspector) { michael@0: let canUndo = inspector.markup.undo.canUndo(); michael@0: ok(canUndo, "The last change in the markup-view can be undone"); michael@0: if (!canUndo) { michael@0: return promise.reject(); michael@0: } michael@0: michael@0: let mutated = inspector.once("markupmutation"); michael@0: inspector.markup.undo.undo(); michael@0: return mutated; michael@0: } michael@0: michael@0: /** michael@0: * Redo the last markup-view action and wait for the corresponding mutation to michael@0: * occur michael@0: * @param {InspectorPanel} inspector The instance of InspectorPanel currently michael@0: * loaded in the toolbox michael@0: * @return a promise that resolves when the markup-mutation has been treated or michael@0: * rejects if no redo action is possible michael@0: */ michael@0: function redoChange(inspector) { michael@0: let canRedo = inspector.markup.undo.canRedo(); michael@0: ok(canRedo, "The last change in the markup-view can be redone"); michael@0: if (!canRedo) { michael@0: return promise.reject(); michael@0: } michael@0: michael@0: let mutated = inspector.once("markupmutation"); michael@0: inspector.markup.undo.redo(); michael@0: return mutated; michael@0: } michael@0: michael@0: /** michael@0: * Get the selector-search input box from the inspector panel michael@0: * @return {DOMNode} michael@0: */ michael@0: function getSelectorSearchBox(inspector) { michael@0: return inspector.panelWin.document.getElementById("inspector-searchbox"); michael@0: } michael@0: michael@0: /** michael@0: * Using the inspector panel's selector search box, search for a given selector. michael@0: * The selector input string will be entered in the input field and the michael@0: * keypress will be simulated. michael@0: * This function won't wait for any events and is not async. It's up to callers michael@0: * to subscribe to events and react accordingly. michael@0: */ michael@0: function searchUsingSelectorSearch(selector, inspector) { michael@0: info("Entering \"" + selector + "\" into the selector-search input field"); michael@0: let field = getSelectorSearchBox(inspector); michael@0: field.focus(); michael@0: field.value = selector; michael@0: EventUtils.sendKey("return", inspector.panelWin); michael@0: } michael@0: michael@0: /** michael@0: * This shouldn't be used in the tests, but is useful when writing new tests or michael@0: * debugging existing tests in order to introduce delays in the test steps michael@0: * @param {Number} ms The time to wait michael@0: * @return A promise that resolves when the time is passed michael@0: */ michael@0: function wait(ms) { michael@0: let def = promise.defer(); michael@0: content.setTimeout(def.resolve, ms); michael@0: return def.promise; michael@0: }