gecko-dev/browser/devtools/inspector/test/head.js

787 строки
26 KiB
JavaScript
Исходник Ответственный История

Этот файл содержит невидимые символы Юникода!

Этот файл содержит невидимые символы Юникода, которые могут быть отображены не так, как показано ниже. Если это намеренно, можете спокойно проигнорировать это предупреждение. Используйте кнопку Экранировать, чтобы показать скрытые символы.

/* 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/. */
"use strict";
const Cu = Components.utils;
const Ci = Components.interfaces;
const Cc = Components.classes;
// Services.prefs.setBoolPref("devtools.debugger.log", true);
// SimpleTest.registerCleanupFunction(() => {
// Services.prefs.clearUserPref("devtools.debugger.log");
// });
// Uncomment this pref to dump all devtools emitted events to the console.
// Services.prefs.setBoolPref("devtools.dump.emit", true);
const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/inspector/test/";
const ROOT_TEST_DIR = getRootDirectory(gTestPath);
const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
// All test are asynchronous
waitForExplicitFinish();
let {TargetFactory, require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
// Import the GCLI test helper
let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
gDevTools.testing = true;
registerCleanupFunction(() => {
gDevTools.testing = false;
});
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.dump.emit");
Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
});
registerCleanupFunction(function*() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
yield gDevTools.closeToolbox(target);
// 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);
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});
/**
* Add a new test tab in the browser and load the given url.
* @param {String} url The url to be loaded in the new tab
* @return a promise that resolves to the tab object when the url is loaded
*/
let addTab = Task.async(function* (url) {
info("Adding a new tab with URL: '" + url + "'");
window.focus();
let tab = gBrowser.selectedTab = gBrowser.addTab(url);
let browser = tab.linkedBrowser;
info("Loading the helper frame script " + FRAME_SCRIPT_URL);
browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
yield once(browser, "load", true);
info("URL '" + url + "' loading complete");
return tab;
});
/**
* Simple DOM node accesor function that takes either a node or a string css
* selector as argument and returns the corresponding node
* @param {String|DOMNode} nodeOrSelector
* @param {Object} options
* An object containing any of the following options:
* - document: HTMLDocument that should be queried for the selector.
* Default: content.document.
* - expectNoMatch: If true and a node matches the given selector, a
* failure is logged for an unexpected match.
* If false and nothing matches the given selector, a
* failure is logged for a missing match.
* Default: false.
* @return {DOMNode}
*/
function getNode(nodeOrSelector, options = {}) {
let document = options.document || content.document;
let noMatches = !!options.expectNoMatch;
if (typeof nodeOrSelector === "string") {
info("Looking for a node that matches selector " + nodeOrSelector);
let node = document.querySelector(nodeOrSelector);
if (noMatches) {
ok(!node, "Selector " + nodeOrSelector + " didn't match any nodes.");
}
else {
ok(node, "Selector " + nodeOrSelector + " matched a node.");
}
return node;
}
info("Looking for a node but selector was not a string.");
return nodeOrSelector;
}
/**
* 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");
}
/**
* Set the inspector's current selection to the first match of the given css
* selector
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @param {String} reason Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @return {Promise} Resolves when the inspector is updated with the new node
*/
let selectNode = Task.async(function*(selector, inspector, reason="test") {
info("Selecting the node for '" + selector + "'");
let nodeFront = yield getNodeFront(selector, inspector);
let updated = inspector.once("inspector-updated");
inspector.selection.setNodeFront(nodeFront, reason);
yield 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 }.
*/
let openInspectorForURL = Task.async(function*(url, hostType) {
let tab = yield addTab(url);
let { inspector, toolbox } = yield openInspector(null, hostType);
return { tab, inspector, toolbox };
});
/**
* Open the toolbox, with the inspector tool visible.
* @param {Function} cb Optional callback, if you don't want to use the returned
* promise
* @param {String} hostType Optional hostType, as defined in Toolbox.HostType
* @return a promise that resolves when the inspector is ready
*/
let openInspector = Task.async(function*(cb, hostType) {
info("Opening the inspector");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let inspector, toolbox;
// Checking if the toolbox and the inspector are already loaded
// The inspector-updated event should only be waited for if the inspector
// isn't loaded yet
toolbox = gDevTools.getToolbox(target);
if (toolbox) {
inspector = toolbox.getPanel("inspector");
if (inspector) {
info("Toolbox and inspector already open");
if (cb) {
return cb(inspector, toolbox);
} else {
return {
toolbox: toolbox,
inspector: inspector
};
}
}
}
info("Opening the toolbox");
toolbox = yield gDevTools.showToolbox(target, "inspector", hostType);
yield waitForToolboxFrameFocus(toolbox);
inspector = toolbox.getPanel("inspector");
info("Waiting for the inspector to update");
yield inspector.once("inspector-updated");
if (cb) {
return cb(inspector, toolbox);
} else {
return {
toolbox: toolbox,
inspector: inspector
};
}
});
/**
* Wait for the toolbox frame to receive focus after it loads
* @param {Toolbox} toolbox
* @return a promise that resolves when focus has been received
*/
function waitForToolboxFrameFocus(toolbox) {
info("Making sure that the toolbox's frame is focused");
let def = promise.defer();
let win = toolbox.frame.contentWindow;
waitForFocus(def.resolve, win);
return def.promise;
}
function getActiveInspector() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
return gDevTools.getToolbox(target).getPanel("inspector");
}
/**
* Get the NodeFront for a node that matches a given css selector, via the
* protocol.
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {Promise} Resolves to the NodeFront instance
*/
function getNodeFront(selector, {walker}) {
if (selector._form) {
return selector;
}
return walker.querySelector(walker.rootNode, selector);
}
/**
* 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
* @param {String} reason Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @return {Promise} Resolves when the inspector is updated with the new node
*/
let getNodeFrontInFrame = Task.async(function*(selector, frameSelector,
inspector, reason="test") {
let iframe = yield getNodeFront(frameSelector, inspector);
let {nodes} = yield inspector.walker.children(iframe);
return inspector.walker.querySelector(nodes[0], selector);
});
/**
* Get the current rect of the border region of the box-model highlighter
*/
let getSimpleBorderRect = Task.async(function*(toolbox) {
let {border} = yield getBoxModelStatus(toolbox);
let {p1, p2, p3, p4} = border.points;
return {
top: p1.y,
left: p1.x,
width: p2.x - p1.x,
height: p4.y - p1.y
};
});
function getHighlighterActorID(highlighter) {
let actorID = highlighter.actorID;
let connPrefix = actorID.substring(0, actorID.indexOf(highlighter.typeName));
return {actorID, connPrefix};
}
/**
* Get the current positions and visibility of the various box-model highlighter
* elements.
*/
let getBoxModelStatus = Task.async(function*(toolbox) {
let isVisible = yield isHighlighting(toolbox);
let ret = {
visible: isVisible
};
for (let region of ["margin", "border", "padding", "content"]) {
let points = yield getPointsForRegion(region, toolbox);
let visible = yield isRegionHidden(region, toolbox);
ret[region] = {points, visible};
}
ret.guides = {};
for (let guide of ["top", "right", "bottom", "left"]) {
ret.guides[guide] = yield getGuideStatus(guide, toolbox);
}
return ret;
});
/**
* Get data about one of the toolbox box-model highlighter's guides.
* @param {String} location One of top, right, bottom, left.
* @param {Toolbox} toolbox The toolbox instance, used to retrieve the highlighter.
* @return {Object} The returned object has the following form:
* - visible {Boolean} Whether that guide is visible.
* - x1/y1/x2/y2 {String} The <line>'s coordinates.
*/
let getGuideStatus = Task.async(function*(location, {highlighter}) {
let id = "box-model-guide-" + location;
let hidden = yield getHighlighterNodeAttribute(highlighter, id, "hidden");
let x1 = yield getHighlighterNodeAttribute(highlighter, id, "x1");
let y1 = yield getHighlighterNodeAttribute(highlighter, id, "y1");
let x2 = yield getHighlighterNodeAttribute(highlighter, id, "x2");
let y2 = yield getHighlighterNodeAttribute(highlighter, id, "y2");
return {
visible: !hidden,
x1: x1,
y1: y1,
x2: x2,
y2: y2
};
});
/**
* Get the coordinates of the rectangle that is defined by the 4 guides displayed
* in the toolbox box-model highlighter.
* @param {Toolbox} toolbox The toolbox instance, used to retrieve the highlighter.
* @return {Object} Null if at least one guide is hidden. Otherwise an object
* with p1, p2, p3, p4 properties being {x, y} objects.
*/
let getGuidesRectangle = Task.async(function*(toolbox) {
let tGuide = yield getGuideStatus("top", toolbox);
let rGuide = yield getGuideStatus("right", toolbox);
let bGuide = yield getGuideStatus("bottom", toolbox);
let lGuide = yield getGuideStatus("left", toolbox);
if (!tGuide.visible || !rGuide.visible || !bGuide.visible || !lGuide.visible) {
return null;
}
return {
p1: {x: lGuide.x1, y: tGuide.y1},
p2: {x: rGuide.x1, y: tGuide. y1},
p3: {x: rGuide.x1, y: bGuide.y1},
p4: {x: lGuide.x1, y: bGuide.y1}
};
});
/**
* Get the coordinate (points defined by the d attribute) from one of the path
* elements in the box model highlighter.
*/
let getPointsForRegion = Task.async(function*(region, toolbox) {
let d = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-" + region, "d");
let polygons = d.match(/M[^M]+/g);
if (!polygons) {
return null;
}
let points = polygons[0].trim().split(" ").map(i => {
return i.replace(/M|L/, "").split(",")
});
return {
p1: {
x: parseFloat(points[0][0]),
y: parseFloat(points[0][1])
},
p2: {
x: parseFloat(points[1][0]),
y: parseFloat(points[1][1])
},
p3: {
x: parseFloat(points[2][0]),
y: parseFloat(points[2][1])
},
p4: {
x: parseFloat(points[3][0]),
y: parseFloat(points[3][1])
}
};
});
/**
* Is a given region path element of the box-model highlighter currently
* hidden?
*/
let isRegionHidden = Task.async(function*(region, toolbox) {
let value = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-" + region, "hidden");
return value !== null;
});
/**
* Is the highlighter currently visible on the page?
*/
let isHighlighting = Task.async(function*(toolbox) {
let value = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-elements", "hidden");
return value === null;
});
let getHighlitNode = Task.async(function*(toolbox) {
let {visible, content} = yield getBoxModelStatus(toolbox);
let points = content.points;
if (visible) {
let x = (points.p1.x + points.p2.x + points.p3.x + points.p4.x) / 4;
let y = (points.p1.y + points.p2.y + points.p3.y + points.p4.y) / 4;
let {objects} = yield executeInContent("Test:ElementFromPoint", {x, y});
return objects.element;
}
});
/**
* Assert that the box-model highlighter's current position corresponds to the
* given node boxquads.
* @param {String} selector The selector for the node to get the boxQuads from
* @param {String} prefix An optional prefix for logging information to the
* console.
*/
let isNodeCorrectlyHighlighted = Task.async(function*(selector, toolbox, prefix="") {
let boxModel = yield getBoxModelStatus(toolbox);
let {data: regions} = yield executeInContent("Test:GetAllAdjustedQuads",
{selector});
for (let boxType of ["content", "padding", "border", "margin"]) {
let [quad] = regions[boxType];
for (let point in boxModel[boxType].points) {
is(boxModel[boxType].points[point].x, quad[point].x,
"Node " + selector + " " + boxType + " point " + point +
" x coordinate is correct");
is(boxModel[boxType].points[point].y, quad[point].y,
"Node " + selector + " " + boxType + " point " + point +
" y coordinate is correct");
}
}
});
function synthesizeKeyFromKeyTag(aKeyId, aDocument = null) {
let document = aDocument || document;
let key = document.getElementById(aKeyId);
isnot(key, null, "Successfully retrieved the <key> node");
let modifiersAttr = key.getAttribute("modifiers");
let name = null;
if (key.getAttribute("keycode"))
name = key.getAttribute("keycode");
else if (key.getAttribute("key"))
name = key.getAttribute("key");
isnot(name, null, "Successfully retrieved keycode/key");
let modifiers = {
shiftKey: modifiersAttr.match("shift"),
ctrlKey: modifiersAttr.match("ctrl"),
altKey: modifiersAttr.match("alt"),
metaKey: modifiersAttr.match("meta"),
accelKey: modifiersAttr.match("accel")
}
EventUtils.synthesizeKey(name, modifiers);
}
let focusSearchBoxUsingShortcut = Task.async(function* (panelWin, callback) {
info("Focusing search box");
let searchBox = panelWin.document.getElementById("inspector-searchbox");
let focused = once(searchBox, "focus");
panelWin.focus();
synthesizeKeyFromKeyTag("nodeSearchKey", panelWin.document);
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}
*/
let 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
*/
let 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.
*/
let 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;
});
/**
* Zoom the current page to a given level.
* @param {Number} level The new zoom level.
* @param {String} actorID Optional highlighter actor ID. If provided, the
* returned promise will only resolve when the highlighter has updated to the
* new zoom level.
* @return {Promise}
*/
let zoomPageTo = Task.async(function*(level, actorID, connPrefix) {
yield executeInContent("Test:ChangeZoomLevel",
{level, actorID, connPrefix});
});
/**
* Get the value of an attribute on one of the highlighter's node.
* @param {Front} highlighter The front of the highlighter.
* @param {String} nodeID The Id of the node in the highlighter.
* @param {String} name The name of the attribute.
* @return {String} value
*/
let getHighlighterNodeAttribute = Task.async(function*(highlighter, nodeID, name) {
let {actorID, connPrefix} = getHighlighterActorID(highlighter);
let {data: value} = yield executeInContent("Test:GetHighlighterAttribute",
{nodeID, name, actorID, connPrefix});
return value;
});
/**
* Get the "d" attribute value for one of the box-model highlighter's region
* <path> elements, and parse it to a list of points.
* @param {String} region The box model region name.
* @param {Front} highlighter The front of the highlighter.
* @return {Object} The object returned has the following form:
* - d {String} the d attribute value
* - points {Array} an array of all the polygons defined by the path. Each box
* is itself an Array of points, themselves being [x,y] coordinates arrays.
*/
let getHighlighterRegionPath = Task.async(function*(region, highlighter) {
let d = yield getHighlighterNodeAttribute(highlighter, "box-model-" + region, "d");
if (!d) {
return {d: null};
}
let polygons = d.match(/M[^M]+/g);
if (!polygons) {
return {d};
}
let points = [];
for (let polygon of polygons) {
points.push(polygon.trim().split(" ").map(i => {
return i.replace(/M|L/, "").split(",")
}));
}
return {d, points};
});
/**
* Get the textContent value of one of the highlighter's node.
* @param {Front} highlighter The front of the highlighter.
* @param {String} nodeID The Id of the node in the highlighter.
* @return {String} value
*/
let getHighlighterNodeTextContent = Task.async(function*(highlighter, nodeID) {
let {actorID, connPrefix} = getHighlighterActorID(highlighter);
let {data: value} = yield executeInContent("Test:GetHighlighterTextContent",
{nodeID, actorID, connPrefix});
return value;
});
/**
* Subscribe to a given highlighter event and return a promise that resolves
* when the event is received.
* @param {String} event The name of the highlighter event to listen to.
* @param {Front} highlighter The front of the highlighter.
* @return {Promise}
*/
function waitForHighlighterEvent(event, highlighter) {
let {actorID, connPrefix} = getHighlighterActorID(highlighter);
return executeInContent("Test:WaitForHighlighterEvent",
{event, actorID, connPrefix});
}
/**
* 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 = promise.defer();
// Find another element to mouseover over in order to leave the markup-view
let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button");
EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"},
inspector.toolbox.doc.defaultView);
executeSoon(def.resolve);
return def.promise;
}
/**
* Wait for eventName on target.
* @param {Object} target An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
* @return A promise that resolves when the event has been handled
*/
function once(target, eventName, useCapture=false) {
info("Waiting for event: '" + eventName + "' on " + target + ".");
let deferred = promise.defer();
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
["addListener", "removeListener"],
["on", "off"]
]) {
if ((add in target) && (remove in target)) {
target[add](eventName, function onEvent(...aArgs) {
info("Got event: '" + eventName + "' on " + target + ".");
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
}, useCapture);
break;
}
}
return deferred.promise;
}
/**
* Wait for a content -> chrome message on the message manager (the window
* messagemanager is used).
* @param {String} name The message name
* @return {Promise} A promise that resolves to the response data when the
* message has been received
*/
function waitForContentMessage(name) {
let mm = gBrowser.selectedBrowser.messageManager;
let def = promise.defer();
mm.addMessageListener(name, function onMessage(msg) {
mm.removeMessageListener(name, onMessage);
def.resolve(msg);
});
return def.promise;
}
function wait(ms) {
let def = promise.defer();
setTimeout(def.resolve, ms);
return def.promise;
}
/**
* Send an async message to the frame script (chrome -> content) and wait for a
* response message with the same name (content -> chrome).
* @param {String} name The message name. Should be one of the messages defined
* in doc_frame_script.js
* @param {Object} data Optional data to send along
* @param {Object} objects Optional CPOW objects to send along
* @param {Boolean} expectResponse If set to false, don't wait for a response
* with the same name from the content script. Defaults to true.
* @return {Promise} Resolves to the response data if a response is expected,
* immediately resolves otherwise
*/
function executeInContent(name, data={}, objects={}, expectResponse=true) {
let mm = gBrowser.selectedBrowser.messageManager;
mm.sendAsyncMessage(name, data, objects);
if (expectResponse) {
return waitForContentMessage(name);
} else {
return promise.resolve();
}
}
/**
* 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;
}