Bug 1311171 - Implement the devtools.network.onRequestFinished API event; r=jdescottes,rpl

MozReview-Commit-ID: IymuzcUg0VN

--HG--
extra : rebase_source : 5c262babe60132c9a73acc7dadf3b38f30133ecc
This commit is contained in:
Jan Odvarko 2018-02-14 11:32:10 +01:00
Родитель 75efe914ce
Коммит 96233200ee
9 изменённых файлов: 298 добавлений и 11 удалений

Просмотреть файл

@ -22,6 +22,13 @@ extensions.registerModules({
["devtools", "panels"],
],
},
devtools_network: {
url: "chrome://browser/content/ext-c-devtools-network.js",
scopes: ["devtools_child"],
paths: [
["devtools", "network"],
],
},
// Because of permissions, the module name must differ from both namespaces.
menusInternal: {
url: "chrome://browser/content/ext-c-menus.js",

Просмотреть файл

@ -0,0 +1,59 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
// The ext-* files are imported into the same scopes.
/* import-globals-from ../../../toolkit/components/extensions/ext-c-toolkit.js */
/**
* Responsible for fetching HTTP response content from the backend.
*
* @param {DevtoolsExtensionContext}
* A devtools extension context running in a child process.
* @param {object} options
*/
class ChildNetworkResponseLoader {
constructor(context, requestId) {
this.context = context;
this.requestId = requestId;
}
api() {
const {context, requestId} = this;
return {
getContent(callback) {
return context.childManager.callParentAsyncFunction(
"devtools.network.Request.getContent",
[requestId],
callback);
},
};
}
}
this.devtools_network = class extends ExtensionAPI {
getAPI(context) {
return {
devtools: {
network: {
onRequestFinished: new EventManager(context, "devtools.network.onRequestFinished", fire => {
let onFinished = (data) => {
const loader = new ChildNetworkResponseLoader(context, data.requestId);
const harEntry = {...data.harEntry, ...loader.api()};
const result = Cu.cloneInto(harEntry, context.cloneScope, {
cloneFunctions: true,
});
fire.asyncWithoutClone(result);
};
let parent = context.childManager.getParentEvent("devtools.network.onRequestFinished");
parent.addListener(onFinished);
return () => {
parent.removeListener(onFinished);
};
}).api(),
},
},
};
}
};

Просмотреть файл

@ -5,6 +5,10 @@
// The ext-* files are imported into the same scopes.
/* import-globals-from ext-devtools.js */
var {
SpreadArgs,
} = ExtensionCommon;
this.devtools_network = class extends ExtensionAPI {
getAPI(context) {
return {
@ -29,6 +33,35 @@ this.devtools_network = class extends ExtensionAPI {
getHAR: function() {
return context.devToolsToolbox.getHARFromNetMonitor();
},
onRequestFinished: new EventManager(context, "devtools.network.onRequestFinished", fire => {
const listener = (data) => {
fire.async(data);
};
const toolbox = context.devToolsToolbox;
toolbox.addRequestFinishedListener(listener);
return () => {
toolbox.removeRequestFinishedListener(listener);
};
}).api(),
// The following method is used internally to allow the request API
// piece that is running in the child process to ask the parent process
// to fetch response content from the back-end.
Request: {
async getContent(requestId) {
return context.devToolsToolbox.fetchResponseContent(requestId)
.then(({content}) => new SpreadArgs([content.text, content.mimeType]))
.catch(err => {
const debugName = context.extension.policy.debugName;
const errorMsg = "Unexpected error while fetching response content";
Cu.reportError(`${debugName}: ${errorMsg} for ${requestId}: ${err}`);
throw new ExtensionError(errorMsg);
});
},
},
},
},
};

Просмотреть файл

@ -37,6 +37,7 @@ browser.jar:
content/browser/ext-windows.js
content/browser/ext-c-browser.js
content/browser/ext-c-devtools-inspectedWindow.js
content/browser/ext-c-devtools-network.js
content/browser/ext-c-devtools-panels.js
content/browser/ext-c-devtools.js
content/browser/ext-c-menus.js

Просмотреть файл

@ -68,11 +68,14 @@
"events": [
{
"name": "onRequestFinished",
"unsupported": true,
"type": "function",
"description": "Fired when a network request is finished and all request data are available.",
"parameters": [
{ "name": "request", "$ref": "Request", "description": "Description of a network request in the form of a HAR entry. See HAR specification for details." }
{
"name": "request",
"$ref": "Request",
"description": "Description of a network request in the form of a HAR entry. See HAR specification for details."
}
]
},
{

Просмотреть файл

@ -55,6 +55,28 @@ function devtools_page() {
}
};
browser.test.onMessage.addListener(harListener);
let requestFinishedListener = async request => {
browser.test.assertTrue(request.request, "Request entry must exist");
browser.test.assertTrue(request.response, "Response entry must exist");
browser.test.sendMessage("onRequestFinished");
// Get response content using callback
request.getContent((content, encoding) => {
browser.test.sendMessage("onRequestFinished-callbackExecuted",
[content, encoding]);
});
// Get response content using returned promise
request.getContent().then(([content, encoding]) => {
browser.test.sendMessage("onRequestFinished-promiseResolved",
[content, encoding]);
});
browser.devtools.network.onRequestFinished.removeListener(requestFinishedListener);
};
browser.devtools.network.onRequestFinished.addListener(requestFinishedListener);
}
function waitForRequestAdded(toolbox) {
@ -157,6 +179,7 @@ add_task(async function test_devtools_network_get_har() {
await Promise.all([
extension.awaitMessage("tabUpdated"),
extension.awaitMessage("onNavigatedFired"),
extension.awaitMessage("onRequestFinished"),
waitForRequestAdded(toolbox),
]);
@ -175,3 +198,56 @@ add_task(async function test_devtools_network_get_har() {
await BrowserTestUtils.removeTab(tab);
});
/**
* Test for `chrome.devtools.network.onRequestFinished()` API
*/
add_task(async function test_devtools_network_on_request_finished() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
let extension = ExtensionTestUtils.loadExtension(extData);
await extension.startup();
await extension.awaitMessage("ready");
let target = gDevTools.getTargetForTab(tab);
// Open the Toolbox
let toolbox = await gDevTools.showToolbox(target, "netmonitor");
info("Developer toolbox opened.");
// Reload and wait for onRequestFinished event.
extension.sendMessage("navigate");
await Promise.all([
extension.awaitMessage("tabUpdated"),
extension.awaitMessage("onNavigatedFired"),
waitForRequestAdded(toolbox),
]);
await extension.awaitMessage("onRequestFinished");
// Wait for response content being fetched.
let [callbackRes, promiseRes] = await Promise.all([
extension.awaitMessage("onRequestFinished-callbackExecuted"),
extension.awaitMessage("onRequestFinished-promiseResolved"),
]);
ok(callbackRes[0].startsWith("<html>"),
"The expected content has been retrieved.");
is(callbackRes[1], "text/html; charset=utf-8",
"The expected content has been retrieved.");
is(promiseRes[0], callbackRes[0],
"The resolved value is equal to the one received in the callback API mode");
is(promiseRes[1], callbackRes[1],
"The resolved value is equal to the one received in the callback API mode");
// Shutdown
await gDevTools.closeToolbox(target);
await target.destroy();
await extension.unload();
await BrowserTestUtils.removeTab(tab);
});

Просмотреть файл

@ -115,6 +115,9 @@ function Toolbox(target, selectedTool, hostType, contentWindow, frameId) {
this.frameMap = new Map();
this.selectedFrameId = null;
// List of listeners for `devtools.network.onRequestFinished` WebExt API
this._requestFinishedListeners = new Set();
this._toolRegistered = this._toolRegistered.bind(this);
this._toolUnregistered = this._toolUnregistered.bind(this);
this._onWillNavigate = this._onWillNavigate.bind(this);
@ -2998,6 +3001,8 @@ Toolbox.prototype = {
return viewSource.viewSource(this, sourceURL, sourceLine);
},
// Support for WebExtensions API (`devtools.network.*`)
/**
* Returns data (HAR) collected by the Network panel.
*/
@ -3013,5 +3018,54 @@ Toolbox.prototype = {
// Use Netmonitor object to get the current HAR log.
return netPanel.panelWin.Netmonitor.getHar();
},
/**
* Add listener for `onRequestFinished` events.
*
* @param {Object} listener
* The listener to be called it's expected to be
* a function that takes ({harEntry, requestId})
* as first argument.
*/
addRequestFinishedListener: function (listener) {
// Log console message informing the extension developer
// that the Network panel needs to be selected at least
// once in order to receive `onRequestFinished` events.
let message = "The Network panel needs to be selected at least" +
" once in order to receive 'onRequestFinished' events.";
this.target.logErrorInPage(message, "har");
// Add the listener into internal list.
this._requestFinishedListeners.add(listener);
},
removeRequestFinishedListener: function (listener) {
this._requestFinishedListeners.delete(listener);
},
getRequestFinishedListeners: function () {
return this._requestFinishedListeners;
},
/**
* Used to lazily fetch HTTP response content within
* `onRequestFinished` event listener.
*
* @param {String} requestId
* Id of the request for which the response content
* should be fetched.
*/
fetchResponseContent: function (requestId) {
let netPanel = this.getPanel("netmonitor");
// The panel doesn't have to exist (it must be selected
// by the user at least once to be created).
// Return undefined content in such case.
if (!netPanel) {
return Promise.resolve({content: {}});
}
return netPanel.panelWin.Netmonitor.fetchResponseContent(requestId);
}
};

Просмотреть файл

@ -50,6 +50,7 @@ window.connector = connector;
window.Netmonitor = {
bootstrap({ toolbox, panel }) {
this.mount = document.querySelector("#mount");
this.toolbox = toolbox;
const connection = {
tabConnection: {
@ -66,6 +67,9 @@ window.Netmonitor = {
top.openUILinkIn(link, "tab");
};
this.onRequestAdded = this.onRequestAdded.bind(this);
window.on(EVENTS.REQUEST_ADDED, this.onRequestAdded);
// Render the root Application component.
const sourceMapService = toolbox.sourceMapURLService;
const app = App({ connector, openLink, sourceMapService });
@ -77,11 +81,14 @@ window.Netmonitor = {
destroy() {
unmountComponentAtNode(this.mount);
window.off(EVENTS.REQUEST_ADDED, this.onRequestAdded);
return connector.disconnect();
},
// Support for WebExtensions API
/**
* Returns list of requests currently available in the panel.
* Support for `devtools.network.getHAR` (get collected data as HAR)
*/
getHar() {
let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
@ -105,6 +112,46 @@ window.Netmonitor = {
return HarExporter.getHar(options);
},
/**
* Support for `devtools.network.onRequestFinished`. A hook for
* every finished HTTP request used by WebExtensions API.
*/
onRequestAdded(event, requestId) {
let listeners = this.toolbox.getRequestFinishedListeners();
if (!listeners.size) {
return;
}
let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
let { getLongString, getTabTarget, requestData } = connector;
let { form: { title, url } } = getTabTarget();
let options = {
getString: getLongString,
requestData,
title: title || url,
includeResponseBodies: false,
items: [getDisplayedRequestById(store.getState(), requestId)],
};
// Build HAR for specified request only.
HarExporter.getHar(options).then(har => {
let harEntry = har.log.entries[0];
delete harEntry.pageref;
listeners.forEach(listener => listener({
harEntry,
requestId,
}));
});
},
/**
* Support for `Request.getContent` WebExt API (lazy loading response body)
*/
fetchResponseContent(requestId) {
return connector.requestData(requestId, "responseContent");
},
/**
* Selects the specified request in the waterfall and opens the details view.
* This is a firefox toolbox specific API, which providing an ability to inspect

Просмотреть файл

@ -136,15 +136,22 @@ const HarExporter = {
options.id = options.id || uid++;
// Set default generic HAR export options.
options.jsonp = options.jsonp ||
Services.prefs.getBoolPref("devtools.netmonitor.har.jsonp");
options.includeResponseBodies = options.includeResponseBodies ||
Services.prefs.getBoolPref(
if (typeof options.jsonp != "boolean") {
options.jsonp = Services.prefs.getBoolPref(
"devtools.netmonitor.har.jsonp");
}
if (typeof options.includeResponseBodies != "boolean") {
options.includeResponseBodies = Services.prefs.getBoolPref(
"devtools.netmonitor.har.includeResponseBodies");
options.jsonpCallback = options.jsonpCallback ||
Services.prefs.getCharPref("devtools.netmonitor.har.jsonpCallback");
options.forceExport = options.forceExport ||
Services.prefs.getBoolPref("devtools.netmonitor.har.forceExport");
}
if (typeof options.jsonpCallback != "boolean") {
options.jsonpCallback = Services.prefs.getCharPref(
"devtools.netmonitor.har.jsonpCallback");
}
if (typeof options.forceExport != "boolean") {
options.forceExport = Services.prefs.getBoolPref(
"devtools.netmonitor.har.forceExport");
}
// Build HAR object.
return this.buildHarData(options).then(har => {