mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 22:55:23 +00:00
ed8fb019e9
Bail out from the markup view onKeyDown handler if any modifier is currently true. All shortcuts specified in this handler are intended to be used without modifier, so for now this approach is fine. Added a test checking the use case mentioned in Bug 1230325, with the S shortcut. In order to write the test, had to create an additional method on the test actor to be able to wait for events in the window of the content process. MozReview-Commit-ID: 67icou0HkfA --HG-- extra : rebase_source : e7235c487134505563cae3c5ac8bdabd42dc4363
985 lines
29 KiB
JavaScript
985 lines
29 KiB
JavaScript
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
// A helper actor for inspector and markupview tests.
|
|
|
|
var { Cc, Ci, Cu, Cr } = require("chrome");
|
|
const {getRect, getElementFromPoint, getAdjustedQuads} = require("devtools/shared/layout/utils");
|
|
const promise = require("promise");
|
|
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
|
|
var DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
|
|
var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
|
.getService(Ci.mozIJSSubScriptLoader);
|
|
|
|
// Set up a dummy environment so that EventUtils works. We need to be careful to
|
|
// pass a window object into each EventUtils method we call rather than having
|
|
// it rely on the |window| global.
|
|
let EventUtils = {};
|
|
EventUtils.window = {};
|
|
EventUtils.parent = {};
|
|
EventUtils._EU_Ci = Components.interfaces;
|
|
EventUtils._EU_Cc = Components.classes;
|
|
loader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
|
|
|
|
const protocol = require("devtools/server/protocol");
|
|
const {Arg, Option, method, RetVal, types} = protocol;
|
|
|
|
var dumpn = msg => {
|
|
dump(msg + "\n");
|
|
}
|
|
|
|
/**
|
|
* Get the instance of CanvasFrameAnonymousContentHelper used by a given
|
|
* highlighter actor.
|
|
* The instance provides methods to get/set attributes/text/style on nodes of
|
|
* the highlighter, inserted into the nsCanvasFrame.
|
|
* @see /devtools/server/actors/highlighters.js
|
|
* @param {String} actorID
|
|
*/
|
|
function getHighlighterCanvasFrameHelper(conn, actorID) {
|
|
let actor = conn.getActor(actorID);
|
|
if (actor && actor._highlighter) {
|
|
return actor._highlighter.markup;
|
|
}
|
|
}
|
|
|
|
var TestActor = exports.TestActor = protocol.ActorClass({
|
|
typeName: "testActor",
|
|
|
|
initialize: function(conn, tabActor, options) {
|
|
this.conn = conn;
|
|
this.tabActor = tabActor;
|
|
},
|
|
|
|
get content() {
|
|
return this.tabActor.window;
|
|
},
|
|
|
|
/**
|
|
* Helper to retrieve a DOM element.
|
|
* @param {string | array} selector Either a regular selector string
|
|
* or a selector array. If an array, each item, except the last one
|
|
* are considered matching an iframe, so that we can query element
|
|
* within deep iframes.
|
|
*/
|
|
_querySelector: function (selector) {
|
|
let document = this.content.document;
|
|
if (Array.isArray(selector)) {
|
|
let fullSelector = selector.join(" >> ");
|
|
while(selector.length > 1) {
|
|
let str = selector.shift();
|
|
let iframe = document.querySelector(str);
|
|
if (!iframe) {
|
|
throw new Error("Unable to find element with selector \"" + str + "\"" +
|
|
" (full selector:" + fullSelector + ")");
|
|
}
|
|
if (!iframe.contentWindow) {
|
|
throw new Error("Iframe selector doesn't target an iframe \"" + str + "\"" +
|
|
" (full selector:" + fullSelector + ")");
|
|
}
|
|
document = iframe.contentWindow.document;
|
|
}
|
|
selector = selector.shift();
|
|
}
|
|
let node = document.querySelector(selector);
|
|
if (!node) {
|
|
throw new Error("Unable to find element with selector \"" + selector + "\"");
|
|
}
|
|
return node;
|
|
},
|
|
/**
|
|
* Helper to get the number of elements matching a selector
|
|
* @param {string} CSS selector.
|
|
*/
|
|
getNumberOfElementMatches: protocol.method(function (selector,
|
|
root=this.content.document) {
|
|
return root.querySelectorAll(selector).length;
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string"),
|
|
},
|
|
response: {
|
|
value: RetVal("number")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Get a value for a given attribute name, on one of the elements of the box
|
|
* model highlighter, given its ID.
|
|
* @param {Object} msg The msg.data part expects the following properties
|
|
* - {String} nodeID The full ID of the element to get the attribute for
|
|
* - {String} name The name of the attribute to get
|
|
* - {String} actorID The highlighter actor ID
|
|
* @return {String} The value, if found, null otherwise
|
|
*/
|
|
getHighlighterAttribute: protocol.method(function (nodeID, name, actorID) {
|
|
let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
|
|
if (helper) {
|
|
return helper.getAttributeForElement(nodeID, name);
|
|
}
|
|
}, {
|
|
request: {
|
|
nodeID: Arg(0, "string"),
|
|
name: Arg(1, "string"),
|
|
actorID: Arg(2, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("string")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Get the textcontent of one of the elements of the box model highlighter,
|
|
* given its ID.
|
|
* @param {String} nodeID The full ID of the element to get the attribute for
|
|
* @param {String} actorID The highlighter actor ID
|
|
* @return {String} The textcontent value
|
|
*/
|
|
getHighlighterNodeTextContent: protocol.method(function (nodeID, actorID) {
|
|
let value;
|
|
let helper = getHighlighterCanvasFrameHelper(this.conn, actorID);
|
|
if (helper) {
|
|
value = helper.getTextContentForElement(nodeID);
|
|
}
|
|
return value;
|
|
}, {
|
|
request: {
|
|
nodeID: Arg(0, "string"),
|
|
actorID: Arg(1, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("string")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Get the number of box-model highlighters created by the SelectorHighlighter
|
|
* @param {String} actorID The highlighter actor ID
|
|
* @return {Number} The number of box-model highlighters created, or null if the
|
|
* SelectorHighlighter was not found.
|
|
*/
|
|
getSelectorHighlighterBoxNb: protocol.method(function (actorID) {
|
|
let highlighter = this.conn.getActor(actorID);
|
|
let {_highlighter: h} = highlighter;
|
|
if (!h || !h._highlighters) {
|
|
return null;
|
|
} else {
|
|
return h._highlighters.length;
|
|
}
|
|
}, {
|
|
request: {
|
|
highlighter: Arg(0, "string"),
|
|
},
|
|
response: {
|
|
value: RetVal("number")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Subscribe to the box-model highlighter's update event, modify an attribute of
|
|
* the currently highlighted node and send a message when the highlighter has
|
|
* updated.
|
|
* @param {String} the name of the attribute to be changed
|
|
* @param {String} the new value for the attribute
|
|
* @param {String} actorID The highlighter actor ID
|
|
*/
|
|
changeHighlightedNodeWaitForUpdate: protocol.method(function (name, value, actorID) {
|
|
let deferred = promise.defer();
|
|
|
|
let highlighter = this.conn.getActor(actorID);
|
|
let {_highlighter: h} = highlighter;
|
|
|
|
h.once("updated", () => {
|
|
deferred.resolve();
|
|
});
|
|
|
|
h.currentNode.setAttribute(name, value);
|
|
|
|
return deferred.promise;
|
|
}, {
|
|
request: {
|
|
name: Arg(0, "string"),
|
|
value: Arg(1, "string"),
|
|
actorID: Arg(2, "string")
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Subscribe to a given highlighter event and respond when the event is received.
|
|
* @param {String} event The name of the highlighter event to listen to
|
|
* @param {String} actorID The highlighter actor ID
|
|
*/
|
|
waitForHighlighterEvent: protocol.method(function (event, actorID) {
|
|
let highlighter = this.conn.getActor(actorID);
|
|
let {_highlighter: h} = highlighter;
|
|
|
|
return h.once(event);
|
|
}, {
|
|
request: {
|
|
event: Arg(0, "string"),
|
|
actorID: Arg(1, "string")
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Wait for a specific event on a node matching the provided selector.
|
|
* @param {String} eventName The name of the event to listen to
|
|
* @param {String} selector Optional: css selector of the node which should
|
|
* trigger the event. If ommitted, target will be the content window
|
|
*/
|
|
waitForEventOnNode: protocol.method(function (eventName, selector) {
|
|
return new Promise(resolve => {
|
|
let node = selector ? this._querySelector(selector) : this.content;
|
|
node.addEventListener(eventName, function onEvent() {
|
|
node.removeEventListener(eventName, onEvent);
|
|
resolve();
|
|
});
|
|
});
|
|
}, {
|
|
request: {
|
|
eventName: Arg(0, "string"),
|
|
selector: Arg(1, "nullable:string")
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Change the zoom level of the page.
|
|
* Optionally subscribe to the box-model highlighter's update event and waiting
|
|
* for it to refresh before responding.
|
|
* @param {Number} level The new zoom level
|
|
* @param {String} actorID Optional. The highlighter actor ID
|
|
*/
|
|
changeZoomLevel: protocol.method(function (level, actorID) {
|
|
dumpn("Zooming page to " + level);
|
|
let deferred = promise.defer();
|
|
|
|
if (actorID) {
|
|
let actor = this.conn.getActor(actorID);
|
|
let {_highlighter: h} = actor;
|
|
h.once("updated", () => {
|
|
deferred.resolve();
|
|
});
|
|
} else {
|
|
deferred.resolve();
|
|
}
|
|
|
|
let docShell = this.content.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShell);
|
|
docShell.contentViewer.fullZoom = level;
|
|
|
|
return deferred.promise;
|
|
}, {
|
|
request: {
|
|
level: Arg(0, "string"),
|
|
actorID: Arg(1, "string"),
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
assertElementAtPoint: protocol.method(function (x, y, selector) {
|
|
let elementAtPoint = getElementFromPoint(this.content.document, x, y);
|
|
if (!elementAtPoint) {
|
|
throw new Error("Unable to find element at (" + x + ", " + y + ")");
|
|
}
|
|
let node = this._querySelector(selector);
|
|
return node == elementAtPoint;
|
|
}, {
|
|
request: {
|
|
x: Arg(0, "number"),
|
|
y: Arg(1, "number"),
|
|
selector: Arg(2, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("boolean")
|
|
}
|
|
}),
|
|
|
|
|
|
/**
|
|
* Get all box-model regions' adjusted boxquads for the given element
|
|
* @param {String} selector The node selector to target a given element
|
|
* @return {Object} An object with each property being a box-model region, each
|
|
* of them being an object with the p1/p2/p3/p4 properties
|
|
*/
|
|
getAllAdjustedQuads: protocol.method(function(selector) {
|
|
let regions = {};
|
|
let node = this._querySelector(selector);
|
|
for (let boxType of ["content", "padding", "border", "margin"]) {
|
|
regions[boxType] = getAdjustedQuads(this.content, node, boxType);
|
|
}
|
|
|
|
return regions;
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("json")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Synthesize a mouse event on an element. This handler doesn't send a message
|
|
* back. Consumers should listen to specific events on the inspector/highlighter
|
|
* to know when the event got synthesized.
|
|
* @param {String} selector The node selector to get the node target for the event
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @param {Boolean} center If set to true, x/y will be ignored and
|
|
* synthesizeMouseAtCenter will be used instead
|
|
* @param {Object} options Other event options
|
|
*/
|
|
synthesizeMouse: protocol.method(function({ selector, x, y, center, options }) {
|
|
let node = this._querySelector(selector);
|
|
|
|
if (center) {
|
|
EventUtils.synthesizeMouseAtCenter(node, options, node.ownerDocument.defaultView);
|
|
} else {
|
|
EventUtils.synthesizeMouse(node, x, y, options, node.ownerDocument.defaultView);
|
|
}
|
|
}, {
|
|
request: {
|
|
object: Arg(0, "json")
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Synthesize a key event for an element. This handler doesn't send a message
|
|
* back. Consumers should listen to specific events on the inspector/highlighter
|
|
* to know when the event got synthesized.
|
|
*/
|
|
synthesizeKey: protocol.method(function ({key, options, content}) {
|
|
EventUtils.synthesizeKey(key, options, this.content);
|
|
}, {
|
|
request: {
|
|
args: Arg(0, "json")
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Check that an element currently has a pseudo-class lock.
|
|
* @param {String} selector The node selector to get the pseudo-class from
|
|
* @param {String} pseudo The pseudoclass to check for
|
|
* @return {Boolean}
|
|
*/
|
|
hasPseudoClassLock: protocol.method(function (selector, pseudo) {
|
|
let node = this._querySelector(selector);
|
|
return DOMUtils.hasPseudoClassLock(node, pseudo);
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string"),
|
|
pseudo: Arg(1, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("boolean")
|
|
}
|
|
}),
|
|
|
|
loadAndWaitForCustomEvent: protocol.method(function (url) {
|
|
let deferred = promise.defer();
|
|
let self = this;
|
|
// Wait for DOMWindowCreated first, as listening on the current outerwindow
|
|
// doesn't allow receiving test-page-processing-done.
|
|
this.tabActor.chromeEventHandler.addEventListener("DOMWindowCreated", function onWindowCreated() {
|
|
self.tabActor.chromeEventHandler.removeEventListener("DOMWindowCreated", onWindowCreated);
|
|
self.content.addEventListener("test-page-processing-done", function onEvent() {
|
|
self.content.removeEventListener("test-page-processing-done", onEvent);
|
|
deferred.resolve();
|
|
});
|
|
});
|
|
|
|
this.content.location = url;
|
|
return deferred.promise;
|
|
}, {
|
|
request: {
|
|
url: Arg(0, "string")
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
hasNode: protocol.method(function (selector) {
|
|
try {
|
|
// _querySelector throws if the node doesn't exists
|
|
this._querySelector(selector);
|
|
return true;
|
|
} catch(e) {
|
|
return false;
|
|
}
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("boolean")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Get the bounding rect for a given DOM node once.
|
|
* @param {String} selector selector identifier to select the DOM node
|
|
* @return {json} the bounding rect info
|
|
*/
|
|
getBoundingClientRect: protocol.method(function (selector) {
|
|
let node = this._querySelector(selector);
|
|
return node.getBoundingClientRect();
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string"),
|
|
},
|
|
response: {
|
|
value: RetVal("json")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Set a JS property on a DOM Node.
|
|
* @param {String} selector The node selector
|
|
* @param {String} property The property name
|
|
* @param {String} value The attribute value
|
|
*/
|
|
setProperty: protocol.method(function (selector, property, value) {
|
|
let node = this._querySelector(selector);
|
|
node[property] = value;
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string"),
|
|
property: Arg(1, "string"),
|
|
value: Arg(2, "string")
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Get a JS property on a DOM Node.
|
|
* @param {String} selector The node selector
|
|
* @param {String} property The property name
|
|
* @return {String} value The attribute value
|
|
*/
|
|
getProperty: protocol.method(function (selector, property) {
|
|
let node = this._querySelector(selector);
|
|
return node[property];
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string"),
|
|
property: Arg(1, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("string")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Get an attribute on a DOM Node.
|
|
* @param {String} selector The node selector
|
|
* @param {String} attribute The attribute name
|
|
* @return {String} value The attribute value
|
|
*/
|
|
getAttribute: protocol.method(function (selector, attribute) {
|
|
let node = this._querySelector(selector);
|
|
return node.getAttribute(attribute);
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string"),
|
|
property: Arg(1, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("string")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Set an attribute on a DOM Node.
|
|
* @param {String} selector The node selector
|
|
* @param {String} attribute The attribute name
|
|
* @param {String} value The attribute value
|
|
*/
|
|
setAttribute: protocol.method(function (selector, attribute, value) {
|
|
let node = this._querySelector(selector);
|
|
node.setAttribute(attribute, value);
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string"),
|
|
property: Arg(1, "string"),
|
|
value: Arg(2, "string")
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Remove an attribute from a DOM Node.
|
|
* @param {String} selector The node selector
|
|
* @param {String} attribute The attribute name
|
|
*/
|
|
removeAttribute: protocol.method(function (selector, attribute) {
|
|
let node = this._querySelector(selector);
|
|
node.removeAttribute(attribute);
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string"),
|
|
property: Arg(1, "string")
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Reload the content window.
|
|
*/
|
|
reload: protocol.method(function () {
|
|
this.content.location.reload();
|
|
}, {
|
|
request: {},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Reload an iframe and wait for its load event.
|
|
* @param {String} selector The node selector
|
|
*/
|
|
reloadFrame: protocol.method(function (selector) {
|
|
let node = this._querySelector(selector);
|
|
|
|
let deferred = promise.defer();
|
|
|
|
let onLoad = function () {
|
|
node.removeEventListener("load", onLoad);
|
|
deferred.resolve();
|
|
};
|
|
node.addEventListener("load", onLoad);
|
|
|
|
node.contentWindow.location.reload();
|
|
return deferred.promise;
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string"),
|
|
},
|
|
response: {}
|
|
}),
|
|
|
|
/**
|
|
* Evaluate a JS string in the context of the content document.
|
|
* @param {String} js JS string to evaluate
|
|
* @return {json} The evaluation result
|
|
*/
|
|
eval: protocol.method(function (js) {
|
|
// We have to use a sandbox, as CSP prevent us from using eval on apps...
|
|
let sb = Cu.Sandbox(this.content, { sandboxPrototype: this.content });
|
|
return Cu.evalInSandbox(js, sb);
|
|
}, {
|
|
request: {
|
|
js: Arg(0, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("nullable:json")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Scrolls the window to a particular set of coordinates in the document, or
|
|
* by the given amount if `relative` is set to `true`.
|
|
*
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @param {Boolean} relative
|
|
*
|
|
* @return {Object} An object with x / y properties, representing the number
|
|
* of pixels that the document has been scrolled horizontally and vertically.
|
|
*/
|
|
scrollWindow: protocol.method(function (x, y, relative) {
|
|
if (isNaN(x) || isNaN(y)) {
|
|
return {};
|
|
}
|
|
|
|
let deferred = promise.defer();
|
|
this.content.addEventListener("scroll", function onScroll(event) {
|
|
this.removeEventListener("scroll", onScroll);
|
|
|
|
let data = {x: this.content.scrollX, y: this.content.scrollY};
|
|
deferred.resolve(data);
|
|
});
|
|
|
|
this.content[relative ? "scrollBy" : "scrollTo"](x, y);
|
|
|
|
return deferred.promise;
|
|
}, {
|
|
request: {
|
|
x: Arg(0, "number"),
|
|
y: Arg(1, "number"),
|
|
relative: Arg(2, "nullable:boolean"),
|
|
},
|
|
response: {
|
|
value: RetVal("json")
|
|
}
|
|
}),
|
|
|
|
getNodeRect: protocol.method(Task.async(function* (selector) {
|
|
let node = this._querySelector(selector);
|
|
return getRect(this.content, node, this.content);
|
|
}), {
|
|
request: {
|
|
selector: Arg(0, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("json")
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Get information about a DOM element, identified by a selector.
|
|
* @param {String} selector The CSS selector to get the node (can be an array
|
|
* of selectors to get elements in an iframe).
|
|
* @return {Object} data Null if selector didn't match any node, otherwise:
|
|
* - {String} tagName.
|
|
* - {String} namespaceURI.
|
|
* - {Number} numChildren The number of children in the element.
|
|
* - {Array} attributes An array of {name, value, namespaceURI} objects.
|
|
* - {String} outerHTML.
|
|
* - {String} innerHTML.
|
|
* - {String} textContent.
|
|
*/
|
|
getNodeInfo: protocol.method(function(selector) {
|
|
let node = this._querySelector(selector);
|
|
let info = null;
|
|
|
|
if (node) {
|
|
info = {
|
|
tagName: node.tagName,
|
|
namespaceURI: node.namespaceURI,
|
|
numChildren: node.children.length,
|
|
attributes: [...node.attributes].map(({name, value, namespaceURI}) => {
|
|
return {name, value, namespaceURI};
|
|
}),
|
|
outerHTML: node.outerHTML,
|
|
innerHTML: node.innerHTML,
|
|
textContent: node.textContent
|
|
};
|
|
}
|
|
|
|
return info;
|
|
}, {
|
|
request: {
|
|
selector: Arg(0, "string")
|
|
},
|
|
response: {
|
|
value: RetVal("json")
|
|
}
|
|
})
|
|
});
|
|
|
|
var TestActorFront = exports.TestActorFront = protocol.FrontClass(TestActor, {
|
|
initialize: function(client, { testActor }, toolbox) {
|
|
protocol.Front.prototype.initialize.call(this, client, { actor: testActor });
|
|
this.manage(this);
|
|
this.toolbox = toolbox;
|
|
},
|
|
|
|
/**
|
|
* Zoom the current page to a given level.
|
|
* @param {Number} level The new zoom level.
|
|
* @return {Promise} The returned promise will only resolve when the
|
|
* highlighter has updated to the new zoom level.
|
|
*/
|
|
zoomPageTo: function(level) {
|
|
return this.changeZoomLevel(level, this.toolbox.highlighter.actorID);
|
|
},
|
|
|
|
changeHighlightedNodeWaitForUpdate: protocol.custom(function(name, value, highlighter) {
|
|
return this._changeHighlightedNodeWaitForUpdate(name, value, (highlighter || this.toolbox.highlighter).actorID);
|
|
}, {
|
|
impl: "_changeHighlightedNodeWaitForUpdate"
|
|
}),
|
|
|
|
/**
|
|
* Get the value of an attribute on one of the highlighter's node.
|
|
* @param {String} nodeID The Id of the node in the highlighter.
|
|
* @param {String} name The name of the attribute.
|
|
* @param {Object} highlighter Optional custom highlither to target
|
|
* @return {String} value
|
|
*/
|
|
getHighlighterNodeAttribute: function(nodeID, name, highlighter) {
|
|
return this.getHighlighterAttribute(nodeID, name, (highlighter || this.toolbox.highlighter).actorID);
|
|
},
|
|
|
|
getHighlighterNodeTextContent: protocol.custom(function(nodeID, highlighter) {
|
|
return this._getHighlighterNodeTextContent(nodeID, (highlighter || this.toolbox.highlighter).actorID);
|
|
}, {
|
|
impl: "_getHighlighterNodeTextContent"
|
|
}),
|
|
|
|
/**
|
|
* Is the highlighter currently visible on the page?
|
|
*/
|
|
isHighlighting: function() {
|
|
return this.getHighlighterNodeAttribute("box-model-elements", "hidden")
|
|
.then(value => value === null);
|
|
},
|
|
|
|
/**
|
|
* Assert that the box-model highlighter's current position corresponds to the
|
|
* given node boxquads.
|
|
* @param {String} selector The node selector to get the boxQuads from
|
|
* @param {Function} is assertion function to call for equality checks
|
|
* @param {String} prefix An optional prefix for logging information to the
|
|
* console.
|
|
*/
|
|
isNodeCorrectlyHighlighted: Task.async(function*(selector, is, prefix="") {
|
|
prefix += (prefix ? " " : "") + selector + " ";
|
|
|
|
let boxModel = yield this._getBoxModelStatus();
|
|
let regions = yield this.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,
|
|
prefix + boxType + " point " + point + " x coordinate is correct");
|
|
is(boxModel[boxType].points[point].y, quad[point].y,
|
|
prefix + boxType + " point " + point + " y coordinate is correct");
|
|
}
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Get the current rect of the border region of the box-model highlighter
|
|
*/
|
|
getSimpleBorderRect: Task.async(function*(toolbox) {
|
|
let {border} = yield this._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
|
|
};
|
|
}),
|
|
|
|
/**
|
|
* Get the current positions and visibility of the various box-model highlighter
|
|
* elements.
|
|
*/
|
|
_getBoxModelStatus: Task.async(function*() {
|
|
let isVisible = yield this.isHighlighting();
|
|
|
|
let ret = {
|
|
visible: isVisible
|
|
};
|
|
|
|
for (let region of ["margin", "border", "padding", "content"]) {
|
|
let points = yield this._getPointsForRegion(region);
|
|
let visible = yield this._isRegionHidden(region);
|
|
ret[region] = {points, visible};
|
|
}
|
|
|
|
ret.guides = {};
|
|
for (let guide of ["top", "right", "bottom", "left"]) {
|
|
ret.guides[guide] = yield this._getGuideStatus(guide);
|
|
}
|
|
|
|
return ret;
|
|
}),
|
|
|
|
assertHighlightedNode: Task.async(function* (selector) {
|
|
// Taken and tweaked from:
|
|
// https://github.com/iominh/point-in-polygon-extended/blob/master/src/index.js#L30-L85
|
|
function isLeft(p0, p1, p2) {
|
|
let l = ( (p1[0] - p0[0]) * (p2[1] - p0[1]) ) -
|
|
( (p2[0] - p0[0]) * (p1[1] - p0[1]) );
|
|
return l;
|
|
}
|
|
function isInside(point, polygon) {
|
|
if (polygon.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
var n = polygon.length;
|
|
var newPoints = polygon.slice(0);
|
|
newPoints.push(polygon[0]);
|
|
var wn = 0; // wn counter
|
|
|
|
// loop through all edges of the polygon
|
|
for (var i = 0; i < n; i++) {
|
|
// Accept points on the edges
|
|
let r = isLeft(newPoints[i], newPoints[i + 1], point);
|
|
if (r === 0) {
|
|
return true;
|
|
}
|
|
if (newPoints[i][1] <= point[1]) {
|
|
if (newPoints[i + 1][1] > point[1] && r > 0) {
|
|
wn++;
|
|
}
|
|
} else {
|
|
if (newPoints[i + 1][1] <= point[1] && r < 0) {
|
|
wn--;
|
|
}
|
|
}
|
|
}
|
|
if (wn === 0) {
|
|
dumpn(JSON.stringify(point) + " is outside of " + JSON.stringify(polygon));
|
|
}
|
|
// the point is outside only when this winding number wn===0, otherwise it's inside
|
|
return wn !== 0;
|
|
}
|
|
|
|
let {visible, border} = yield this._getBoxModelStatus();
|
|
let points = border.points;
|
|
if (visible) {
|
|
// Check that the node is within the box model
|
|
let { left, top, width, height } = yield this.getNodeRect(selector);
|
|
let right = left + width;
|
|
let bottom = top + height;
|
|
|
|
// Converts points dictionnary into an array
|
|
let list = [];
|
|
for(var i = 1; i <= 4; i++) {
|
|
let p = points["p" + i];
|
|
list.push([p.x, p.y]);
|
|
}
|
|
points = list;
|
|
|
|
// Check that each point of the node is within the box model
|
|
if (!isInside([left, top], points) ||
|
|
!isInside([right, top], points) ||
|
|
!isInside([right, bottom], points) ||
|
|
!isInside([left, bottom], points)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Get the coordinate (points attribute) from one of the polygon elements in the
|
|
* box model highlighter.
|
|
*/
|
|
_getPointsForRegion: Task.async(function*(region) {
|
|
let d = yield this.getHighlighterNodeAttribute("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 polygon element of the box-model highlighter currently
|
|
* hidden?
|
|
*/
|
|
_isRegionHidden: Task.async(function*(region) {
|
|
let value = yield this.getHighlighterNodeAttribute("box-model-" + region, "hidden");
|
|
return value !== null;
|
|
}),
|
|
|
|
_getGuideStatus: Task.async(function*(location) {
|
|
let id = "box-model-guide-" + location;
|
|
|
|
let hidden = yield this.getHighlighterNodeAttribute(id, "hidden");
|
|
let x1 = yield this.getHighlighterNodeAttribute(id, "x1");
|
|
let y1 = yield this.getHighlighterNodeAttribute(id, "y1");
|
|
let x2 = yield this.getHighlighterNodeAttribute(id, "x2");
|
|
let y2 = yield this.getHighlighterNodeAttribute(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.
|
|
* @return {Object} Null if at least one guide is hidden. Otherwise an object
|
|
* with p1, p2, p3, p4 properties being {x, y} objects.
|
|
*/
|
|
getGuidesRectangle: Task.async(function*() {
|
|
let tGuide = yield this._getGuideStatus("top");
|
|
let rGuide = yield this._getGuideStatus("right");
|
|
let bGuide = yield this._getGuideStatus("bottom");
|
|
let lGuide = yield this._getGuideStatus("left");
|
|
|
|
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}
|
|
};
|
|
}),
|
|
|
|
waitForHighlighterEvent: protocol.custom(function(event) {
|
|
return this._waitForHighlighterEvent(event, this.toolbox.highlighter.actorID);
|
|
}, {
|
|
impl: "_waitForHighlighterEvent"
|
|
}),
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
getHighlighterRegionPath: Task.async(function*(region, highlighter) {
|
|
let d = yield this.getHighlighterNodeAttribute("box-model-" + region, "d", highlighter);
|
|
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};
|
|
})
|
|
});
|