Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/* eslint no-unused-vars: [2, {"vars": "local"}] */
"use strict";
// Import the inspector's head.js first (which itself imports shared-head.js).
Services.scriptloader.loadSubScript(
this
);
var {
getInplaceEditorForSpan: inplaceEditor,
} = require("resource://devtools/client/shared/inplace-editor.js");
var clipboard = require("resource://devtools/shared/platform/clipboard.js");
// If a test times out we want to see the complete log and not just the last few
// lines.
SimpleTest.requestCompleteLog();
// Toggle this pref on to see all DevTools event communication. This is hugely
// useful for fixing race conditions.
// Services.prefs.setBoolPref("devtools.dump.emit", true);
// Clear preferences that may be set during the course of tests.
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.dump.emit");
Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen");
Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
Services.prefs.clearUserPref("devtools.markup.pagesize");
Services.prefs.clearUserPref("devtools.inspector.showAllAnonymousContent");
});
/**
* Some tests may need to import one or more of the test helper scripts.
* A test helper script is simply a js file that contains common test code that
* is either not common-enough to be in head.js, or that is located in a
* separate directory.
* The script will be loaded synchronously and in the test's scope.
* @param {String} filePath The file path, relative to the current directory.
* Examples:
* - "helper_attributes_test_runner.js"
*/
function loadHelperScript(filePath) {
const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
}
/**
* 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
* @param {Boolean} Set to true in the event that the node shouldn't be found.
* @return {MarkupContainer}
*/
var getContainerForSelector = async function (
selector,
inspector,
expectFailure = false
) {
info("Getting the markup-container for node " + selector);
const nodeFront = await getNodeFront(selector, inspector);
const container = getContainerForNodeFront(nodeFront, inspector);
if (expectFailure) {
ok(!container, "Shouldn't find markup-container for selector: " + selector);
} else {
ok(container, "Found markup-container for selector: " + selector);
}
return container;
};
/**
* Retrieve the nodeValue for the firstChild of a provided selector on the content page.
*
* @param {String} selector
* @return {String} the nodeValue of the first
*/
function getFirstChildNodeValue(selector) {
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[selector],
_selector => {
return content.document.querySelector(_selector).firstChild.nodeValue;
}
);
}
/**
* 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");
return new Promise(resolve => {
markup._waitForChildren().then(() => {
executeSoon(resolve);
});
});
}
/**
* 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 = async function (selector, inspector) {
info("Clicking on the markup-container for node " + selector);
const nodeFront = await getNodeFront(selector, inspector);
const container = getContainerForNodeFront(nodeFront, inspector);
const updated = container.selected
? Promise.resolve()
: 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;
};
/**
* Focus a given editable element, enter edit mode, set value, and commit
* @param {DOMNode} field The element that gets editable after receiving focus
* and <ENTER> keypress
* @param {String} value The string value to be set into the edited field
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
*/
function setEditableFieldValue(field, value, inspector) {
field.focus();
EventUtils.sendKey("return", inspector.panelWin);
const input = inplaceEditor(field).input;
ok(input, "Found editable field for setting value: " + value);
input.value = value;
EventUtils.sendKey("return", inspector.panelWin);
}
/**
* Focus the new-attribute inplace-editor field of a node's markup container
* and enters the given text, then wait for it to be applied and the for the
* node to mutates (when new attribute(s) is(are) created)
* @param {String} selector The selector for the node to edit.
* @param {String} text The new attribute text to be entered (e.g. "id='test'")
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return a promise that resolves when the node has mutated
*/
var addNewAttributes = async function (selector, text, inspector) {
info(`Entering text "${text}" in new attribute field for node ${selector}`);
const container = await focusNode(selector, inspector);
ok(container, "The container for '" + selector + "' was found");
info("Listening for the markupmutation event");
const nodeMutated = inspector.once("markupmutation");
setEditableFieldValue(container.editor.newAttr, text, inspector);
await nodeMutated;
};
/**
* Checks that a node has the given attributes.
*
* @param {String} selector The selector for the node to check.
* @param {Object} expected An object containing the attributes to check.
* e.g. {id: "id1", class: "someclass"}
*
* Note that node.getAttribute() returns attribute values provided by the HTML
* parser. The parser only provides unescaped entities so & will return &.
*/
var assertAttributes = async function (selector, expected) {
const actualAttributes = await getContentPageElementAttributes(selector);
is(
actualAttributes.length,
Object.keys(expected).length,
"The node " + selector + " has the expected number of attributes."
);
for (const attr in expected) {
const foundAttr = actualAttributes.find(({ name }) => name === attr);
const foundValue = foundAttr ? foundAttr.value : undefined;
ok(foundAttr, "The node " + selector + " has the attribute " + attr);
is(
foundValue,
expected[attr],
"The node " + selector + " has the correct " + attr + " attribute value"
);
}
};
/**
* 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) {
const canUndo = inspector.markup.undo.canUndo();
ok(canUndo, "The last change in the markup-view can be undone");
if (!canUndo) {
return Promise.reject();
}
const 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) {
const canRedo = inspector.markup.undo.canRedo();
ok(canRedo, "The last change in the markup-view can be redone");
if (!canRedo) {
return Promise.reject();
}
const mutated = inspector.once("markupmutation");
inspector.markup.undo.redo();
return mutated;
}
/**
* Get the selector-search input box from the inspector panel
* @return {DOMNode}
*/
function getSelectorSearchBox(inspector) {
return inspector.panelWin.document.getElementById("inspector-searchbox");
}
/**
* Using the inspector panel's selector search box, search for a given selector.
* The selector input string will be entered in the input field and the <ENTER>
* keypress will be simulated.
* This function won't wait for any events and is not async. It's up to callers
* to subscribe to events and react accordingly.
*/
function searchUsingSelectorSearch(selector, inspector) {
info('Entering "' + selector + '" into the selector-search input field');
const field = getSelectorSearchBox(inspector);
field.focus();
field.value = selector;
EventUtils.sendKey("return", inspector.panelWin);
}
/**
* Check to see if the inspector menu items for editing are disabled.
* Things like Edit As HTML, Delete Node, etc.
* @param {NodeFront} nodeFront
* @param {InspectorPanel} inspector
* @param {Boolean} assert Should this function run assertions inline.
* @return A promise that resolves with a boolean indicating whether
* the menu items are disabled once the menu has been checked.
*/
var isEditingMenuDisabled = async function (
nodeFront,
inspector,
assert = true
) {
// To ensure clipboard contains something to paste.
clipboard.copyString("<p>test</p>");
await selectNode(nodeFront, inspector);
const allMenuItems = openContextMenuAndGetAllItems(inspector);
const deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete");
const editHTMLMenuItem = allMenuItems.find(
i => i.id === "node-menu-edithtml"
);
const pasteHTMLMenuItem = allMenuItems.find(
i => i.id === "node-menu-pasteouterhtml"
);
if (assert) {
ok(deleteMenuItem.disabled, "Delete menu item is disabled");
ok(editHTMLMenuItem.disabled, "Edit HTML menu item is disabled");
ok(pasteHTMLMenuItem.disabled, "Paste HTML menu item is disabled");
}
return (
deleteMenuItem.disabled &&
editHTMLMenuItem.disabled &&
pasteHTMLMenuItem.disabled
);
};
/**
* Check to see if the inspector menu items for editing are enabled.
* Things like Edit As HTML, Delete Node, etc.
* @param {NodeFront} nodeFront
* @param {InspectorPanel} inspector
* @param {Boolean} assert Should this function run assertions inline.
* @return A promise that resolves with a boolean indicating whether
* the menu items are enabled once the menu has been checked.
*/
var isEditingMenuEnabled = async function (
nodeFront,
inspector,
assert = true
) {
// To ensure clipboard contains something to paste.
clipboard.copyString("<p>test</p>");
await selectNode(nodeFront, inspector);
const allMenuItems = openContextMenuAndGetAllItems(inspector);
const deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete");
const editHTMLMenuItem = allMenuItems.find(
i => i.id === "node-menu-edithtml"
);
const pasteHTMLMenuItem = allMenuItems.find(
i => i.id === "node-menu-pasteouterhtml"
);
if (assert) {
ok(!deleteMenuItem.disabled, "Delete menu item is enabled");
ok(!editHTMLMenuItem.disabled, "Edit HTML menu item is enabled");
ok(!pasteHTMLMenuItem.disabled, "Paste HTML menu item is enabled");
}
return (
!deleteMenuItem.disabled &&
!editHTMLMenuItem.disabled &&
!pasteHTMLMenuItem.disabled
);
};
/**
* Wait for all current promises to be resolved. See this as executeSoon that
* can be used with yield.
*/
function promiseNextTick() {
return new Promise(resolve => {
executeSoon(resolve);
});
}
/**
* `await` with timeout.
*
* Usage:
* const badgeEventAdded = inspector.markup.once("badge-added-event");
* ...
* const result = await awaitWithTimeout(badgeEventAdded, 3000);
* is(result, "timeout", "Ensure that no event badges were added");
*
* @param {Promise} promise
* Promise to resolve
* @param {Number} ms
* Milliseconds to wait.
* @return "timeout" on timeout, otherwise the result of the fulfilled promise.
*/
async function awaitWithTimeout(promise, ms) {
const timeout = new Promise(resolve => {
// eslint-disable-next-line
const wait = setTimeout(() => {
clearTimeout(wait);
resolve("timeout");
}, ms);
});
return Promise.race([promise, timeout]);
}
/**
* Collapses the current text selection in an input field and tabs to the next
* field.
*/
function collapseSelectionAndTab(inspector) {
// collapse selection and move caret to end
EventUtils.sendKey("tab", inspector.panelWin);
// next element
EventUtils.sendKey("tab", inspector.panelWin);
}
/**
* Collapses the current text selection in an input field and tabs to the
* previous field.
*/
function collapseSelectionAndShiftTab(inspector) {
// collapse selection and move caret to end
EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, inspector.panelWin);
// previous element
EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, inspector.panelWin);
}
/**
* Check that the current focused element is an attribute element in the markup
* view.
* @param {String} attrName The attribute name expected to be found
* @param {Boolean} editMode Whether or not the attribute should be in edit mode
*/
function checkFocusedAttribute(attrName, editMode) {
const focusedAttr = Services.focus.focusedElement;
ok(focusedAttr, "Has a focused element");
const dataAttr = focusedAttr.parentNode.dataset.attr;
is(dataAttr, attrName, attrName + " attribute editor is currently focused.");
if (editMode) {
// Using a multiline editor for attributes, the focused element should be a textarea.
is(focusedAttr.tagName, "textarea", attrName + "is in edit mode");
} else {
is(focusedAttr.tagName, "span", attrName + "is not in edit mode");
}
}
/**
* Get attributes for node as how they are represented in editor.
*
* @param {String} selector
* @param {InspectorPanel} inspector
* @return {Promise}
* A promise that resolves with an array of attribute names
* (e.g. ["id", "class", "href"])
*/
var getAttributesFromEditor = async function (selector, inspector) {
const nodeList = (
await getContainerForSelector(selector, inspector)
).tagLine.querySelectorAll("[data-attr]");
return [...nodeList].map(node => node.getAttribute("data-attr"));
};
/**
* Simulate dragging a MarkupContainer by calling its mousedown and mousemove
* handlers.
* @param {InspectorPanel} inspector The current inspector-panel instance.
* @param {String|MarkupContainer} selector The selector to identify the node or
* the MarkupContainer for this node.
* @param {Number} xOffset Optional x offset to drag by.
* @param {Number} yOffset Optional y offset to drag by.
*/
async function simulateNodeDrag(
inspector,
selector,
xOffset = 10,
yOffset = 10
) {
const container =
typeof selector === "string"
? await getContainerForSelector(selector, inspector)
: selector;
container.elt.scrollIntoView(true);
const rect = container.tagLine.getBoundingClientRect();
const scrollX = inspector.markup.doc.documentElement.scrollLeft;
const scrollY = inspector.markup.doc.documentElement.scrollTop;
info("Simulate mouseDown on element " + selector);
container._onMouseDown({
target: container.tagLine,
button: 0,
pageX: scrollX + rect.x,
pageY: scrollY + rect.y,
stopPropagation: () => {},
preventDefault: () => {},
});
// _onMouseDown selects the node, so make sure to wait for the
// inspector-updated event if the current selection was different.
if (inspector.selection.nodeFront !== container.node) {
await inspector.once("inspector-updated");
}
info("Simulate mouseMove on element " + selector);
container.onMouseMove({
pageX: scrollX + rect.x + xOffset,
pageY: scrollY + rect.y + yOffset,
});
}
/**
* Simulate dropping a MarkupContainer by calling its mouseup handler. This is
* meant to be called after simulateNodeDrag has been called.
* @param {InspectorPanel} inspector The current inspector-panel instance.
* @param {String|MarkupContainer} selector The selector to identify the node or
* the MarkupContainer for this node.
*/
async function simulateNodeDrop(inspector, selector) {
info("Simulate mouseUp on element " + selector);
const container =
typeof selector === "string"
? await getContainerForSelector(selector, inspector)
: selector;
container.onMouseUp();
inspector.markup._onMouseUp();
}
/**
* Simulate drag'n'dropping a MarkupContainer by calling its mousedown,
* mousemove and mouseup handlers.
* @param {InspectorPanel} inspector The current inspector-panel instance.
* @param {String|MarkupContainer} selector The selector to identify the node or
* the MarkupContainer for this node.
* @param {Number} xOffset Optional x offset to drag by.
* @param {Number} yOffset Optional y offset to drag by.
*/
async function simulateNodeDragAndDrop(inspector, selector, xOffset, yOffset) {
await simulateNodeDrag(inspector, selector, xOffset, yOffset);
await simulateNodeDrop(inspector, selector);
}
/**
* Waits until the element has not scrolled for 30 consecutive frames.
*/
async function waitForScrollStop(doc) {
const el = doc.documentElement;
const win = doc.defaultView;
let lastScrollTop = el.scrollTop;
let stopFrameCount = 0;
while (stopFrameCount < 30) {
// Wait for a frame.
await new Promise(resolve => win.requestAnimationFrame(resolve));
// Check if the element has scrolled.
if (lastScrollTop == el.scrollTop) {
// No scrolling since the last frame.
stopFrameCount++;
} else {
// The element has scrolled. Reset the frame counter.
stopFrameCount = 0;
lastScrollTop = el.scrollTop;
}
}
return lastScrollTop;
}
/**
* Select a node in the inspector and try to delete it using the provided key. After that,
* check that the expected element is focused.
*
* @param {InspectorPanel} inspector
* The current inspector-panel instance.
* @param {String} key
* The key to simulate to delete the node
* @param {Object}
* - {String} selector: selector of the element to delete.
* - {String} focusedSelector: selector of the element that should be selected
* after deleting the node.
* - {String} pseudo: optional, "before" or "after" if the element focused after
* deleting the node is supposed to be a before/after pseudo-element.
*/
async function checkDeleteAndSelection(
inspector,
key,
{ selector, focusedSelector, pseudo }
) {
info(
"Test deleting node " +
selector +
" with " +
key +
", " +
"expecting " +
focusedSelector +
" to be focused"
);
info("Select node " + selector + " and make sure it is focused");
await selectNode(selector, inspector);
await clickContainer(selector, inspector);
info("Delete the node with: " + key);
const mutated = inspector.once("markupmutation");
EventUtils.sendKey(key, inspector.panelWin);
await Promise.all([mutated, inspector.once("inspector-updated")]);
let nodeFront = await getNodeFront(focusedSelector, inspector);
if (pseudo) {
// Update the selector for logging in case of failure.
focusedSelector = focusedSelector + "::" + pseudo;
// Retrieve the :before or :after pseudo element of the nodeFront.
const { nodes } = await inspector.walker.children(nodeFront);
nodeFront = pseudo === "before" ? nodes[0] : nodes[nodes.length - 1];
}
is(
inspector.selection.nodeFront,
nodeFront,
focusedSelector + " is selected after deletion"
);
info("Check that the node was really removed");
let node = await getNodeFront(selector, inspector);
ok(!node, "The node can't be found in the page anymore");
info("Undo the deletion to restore the original markup");
await undoChange(inspector);
node = await getNodeFront(selector, inspector);
ok(node, "The node is back");
}
/**
* Click on the reveal link the provided slotted container.
* Will resolve when selection emits "new-node-front".
*/
async function clickOnRevealLink(inspector, container) {
const onSelection = inspector.selection.once("new-node-front");
const revealLink = container.elt.querySelector(".reveal-link");
const tagline = revealLink.closest(".tag-line");
const win = inspector.markup.doc.defaultView;
// First send a mouseover on the tagline to force the link to be displayed.
EventUtils.synthesizeMouseAtCenter(tagline, { type: "mouseover" }, win);
EventUtils.synthesizeMouseAtCenter(revealLink, {}, win);
await onSelection;
}
/**
* Hit `key` on the reveal link in the provided slotted container.
* Will resolve when selection emits "new-node-front".
*/
async function keydownOnRevealLink(key, inspector, container) {
const revealLink = container.elt.querySelector(".reveal-link");
const win = inspector.markup.doc.defaultView;
const root = inspector.markup.getContainer(inspector.markup._rootNode);
root.elt.focus();
// we need to go through a ENTER + TAB key sequence to focus on
// the .reveal-link element with the keyboard
const revealFocused = once(revealLink, "focus");
EventUtils.synthesizeKey("KEY_Enter", {}, win);
EventUtils.synthesizeKey("KEY_Tab", {}, win);
info("Waiting for .reveal-link to be focused");
await revealFocused;
// hit `key` on the .reveal-link
const onSelection = inspector.selection.once("new-node-front");
EventUtils.synthesizeKey(key, {}, win);
await onSelection;
}