diff options
Diffstat (limited to 'devtools/client/shared/frame-script-utils.js')
-rw-r--r-- | devtools/client/shared/frame-script-utils.js | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/devtools/client/shared/frame-script-utils.js b/devtools/client/shared/frame-script-utils.js new file mode 100644 index 000000000..3db7ed9ab --- /dev/null +++ b/devtools/client/shared/frame-script-utils.js @@ -0,0 +1,206 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env browser */ +/* global addMessageListener, sendAsyncMessage, content */ +"use strict"; +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; +const {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const defer = require("devtools/shared/defer"); +const { Task } = require("devtools/shared/task"); + +loader.lazyGetter(this, "nsIProfilerModule", () => { + return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); +}); + +addMessageListener("devtools:test:history", function ({ data }) { + content.history[data.direction](); +}); + +addMessageListener("devtools:test:navigate", function ({ data }) { + content.location = data.location; +}); + +addMessageListener("devtools:test:reload", function ({ data }) { + data = data || {}; + content.location.reload(data.forceget); +}); + +addMessageListener("devtools:test:console", function ({ data }) { + let { method, args, id } = data; + content.console[method].apply(content.console, args); + sendAsyncMessage("devtools:test:console:response", { id }); +}); + +/** + * Performs a single XMLHttpRequest and returns a promise that resolves once + * the request has loaded. + * + * @param Object data + * { method: the request method (default: "GET"), + * url: the url to request (default: content.location.href), + * body: the request body to send (default: ""), + * nocache: append an unique token to the query string (default: true), + * requestHeaders: set request headers (default: none) + * } + * + * @return Promise A promise that's resolved with object + * { status: XMLHttpRequest.status, + * response: XMLHttpRequest.response } + * + */ +function promiseXHR(data) { + let xhr = new content.XMLHttpRequest(); + + let method = data.method || "GET"; + let url = data.url || content.location.href; + let body = data.body || ""; + + if (data.nocache) { + url += "?devtools-cachebust=" + Math.random(); + } + + let deferred = defer(); + xhr.addEventListener("loadend", function loadend(event) { + xhr.removeEventListener("loadend", loadend); + deferred.resolve({ status: xhr.status, response: xhr.response }); + }); + + xhr.open(method, url); + + // Set request headers + if (data.requestHeaders) { + data.requestHeaders.forEach(header => { + xhr.setRequestHeader(header.name, header.value); + }); + } + + xhr.send(body); + return deferred.promise; +} + +/** + * Performs XMLHttpRequest request(s) in the context of the page. The data + * parameter can be either a single object or an array of objects described + * below. The requests will be performed one at a time in the order they appear + * in the data. + * + * The objects should have following form (any of them can be omitted; defaults + * shown below): + * { + * method: "GET", + * url: content.location.href, + * body: "", + * nocache: true, // Adds a cache busting random token to the URL, + * requestHeaders: [{ + * name: "Content-Type", + * value: "application/json" + * }] + * } + * + * The handler will respond with devtools:test:xhr message after all requests + * have finished. Following data will be available for each requests + * (in the same order as requests): + * { + * status: XMLHttpRequest.status + * response: XMLHttpRequest.response + * } + */ +addMessageListener("devtools:test:xhr", Task.async(function* ({ data }) { + let requests = Array.isArray(data) ? data : [data]; + let responses = []; + + for (let request of requests) { + let response = yield promiseXHR(request); + responses.push(response); + } + + sendAsyncMessage("devtools:test:xhr", responses); +})); + +addMessageListener("devtools:test:profiler", function ({ data }) { + let { method, args, id } = data; + let result = nsIProfilerModule[method](...args); + sendAsyncMessage("devtools:test:profiler:response", { + data: result, + id: id + }); +}); + +// To eval in content, look at `evalInDebuggee` in the shared-head.js. +addMessageListener("devtools:test:eval", function ({ data }) { + sendAsyncMessage("devtools:test:eval:response", { + value: content.eval(data.script), + id: data.id + }); +}); + +addEventListener("load", function () { + sendAsyncMessage("devtools:test:load"); +}, true); + +/** + * Set a given style property value on a node. + * @param {Object} data + * - {String} selector The CSS selector to get the node (can be a "super" + * selector). + * - {String} propertyName The name of the property to set. + * - {String} propertyValue The value for the property. + */ +addMessageListener("devtools:test:setStyle", function (msg) { + let {selector, propertyName, propertyValue} = msg.data; + let node = superQuerySelector(selector); + if (!node) { + return; + } + + node.style[propertyName] = propertyValue; + + sendAsyncMessage("devtools:test:setStyle"); +}); + +/** + * Set a given attribute value on a node. + * @param {Object} data + * - {String} selector The CSS selector to get the node (can be a "super" + * selector). + * - {String} attributeName The name of the attribute to set. + * - {String} attributeValue The value for the attribute. + */ +addMessageListener("devtools:test:setAttribute", function (msg) { + let {selector, attributeName, attributeValue} = msg.data; + let node = superQuerySelector(selector); + if (!node) { + return; + } + + node.setAttribute(attributeName, attributeValue); + + sendAsyncMessage("devtools:test:setAttribute"); +}); + +/** + * Like document.querySelector but can go into iframes too. + * ".container iframe || .sub-container div" will first try to find the node + * matched by ".container iframe" in the root document, then try to get the + * content document inside it, and then try to match ".sub-container div" inside + * this document. + * Any selector coming before the || separator *MUST* match a frame node. + * @param {String} superSelector. + * @return {DOMNode} The node, or null if not found. + */ +function superQuerySelector(superSelector, root = content.document) { + let frameIndex = superSelector.indexOf("||"); + if (frameIndex === -1) { + return root.querySelector(superSelector); + } + let rootSelector = superSelector.substring(0, frameIndex).trim(); + let childSelector = superSelector.substring(frameIndex + 2).trim(); + root = root.querySelector(rootSelector); + if (!root || !root.contentWindow) { + return null; + } + + return superQuerySelector(childSelector, root.contentWindow.document); +} |