Bug 1311177 - Implement the devtools.network.getHAR API method; r=rpl,rickychien,jdescottes
MozReview-Commit-ID: gUtGjbr0FQ
--- a/browser/components/extensions/ext-devtools-network.js
+++ b/browser/components/extensions/ext-devtools-network.js
@@ -20,13 +20,17 @@ this.devtools_network = class extends Ex
target.on("navigate", listener);
});
return () => {
targetPromise.then(target => {
target.off("navigate", listener);
});
};
}).api(),
+
+ getHAR: function() {
+ return context.devToolsToolbox.getHARFromNetMonitor();
+ },
},
},
};
}
};
--- a/browser/components/extensions/schemas/devtools_network.json
+++ b/browser/components/extensions/schemas/devtools_network.json
@@ -40,17 +40,16 @@
]
}
]
}
],
"functions": [
{
"name": "getHAR",
- "unsupported": true,
"type": "function",
"description": "Returns HAR log that contains all known network requests.",
"async": "callback",
"parameters": [
{
"name": "callback",
"type": "function",
"description": "A function that receives the HAR log when the request completes.",
--- a/browser/components/extensions/test/browser/browser_ext_devtools_network.js
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_network.js
@@ -1,69 +1,102 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
const {gDevTools} = require("devtools/client/framework/devtools");
+function background() {
+ browser.test.onMessage.addListener(msg => {
+ let code;
+ if (msg === "navigate") {
+ code = "window.wrappedJSObject.location.href = 'http://example.com/';";
+ browser.tabs.executeScript({code});
+ } else if (msg === "reload") {
+ code = "window.wrappedJSObject.location.reload(true);";
+ browser.tabs.executeScript({code});
+ }
+ });
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (changeInfo.status === "complete" && tab.url === "http://example.com/") {
+ browser.test.sendMessage("tabUpdated");
+ }
+ });
+ browser.test.sendMessage("ready");
+}
+
+function devtools_page() {
+ let eventCount = 0;
+ let listener = url => {
+ eventCount++;
+ browser.test.assertEq("http://example.com/", url, "onNavigated received the expected url.");
+ browser.test.sendMessage("onNavigatedFired", eventCount);
+
+ if (eventCount === 2) {
+ eventCount = 0;
+ browser.devtools.network.onNavigated.removeListener(listener);
+ }
+ };
+ browser.devtools.network.onNavigated.addListener(listener);
+
+ let harLogCount = 0;
+ let harListener = async msg => {
+ if (msg !== "getHAR") {
+ return;
+ }
+
+ harLogCount++;
+
+ const harLog = await browser.devtools.network.getHAR();
+ browser.test.sendMessage("getHAR-result", harLog);
+
+ if (harLogCount === 2) {
+ harLogCount = 0;
+ browser.test.onMessage.removeListener(harListener);
+ }
+ };
+ browser.test.onMessage.addListener(harListener);
+}
+
+function waitForRequestAdded(toolbox) {
+ return new Promise(resolve => {
+ let netPanel = toolbox.getPanel("netmonitor");
+ netPanel.panelWin.once("NetMonitor:RequestAdded", () => {
+ resolve();
+ });
+ });
+}
+
+let extData = {
+ background,
+ manifest: {
+ permissions: ["tabs", "http://mochi.test/", "http://example.com/"],
+ devtools_page: "devtools_page.html",
+ },
+ files: {
+ "devtools_page.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="devtools_page.js"></script>
+ </head>
+ <body>
+ </body>
+ </html>`,
+ "devtools_page.js": devtools_page,
+ },
+};
+
+/**
+ * Test for `chrome.devtools.network.onNavigate()` API
+ */
add_task(async function test_devtools_network_on_navigated() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
-
- function background() {
- browser.test.onMessage.addListener(msg => {
- let code;
- if (msg === "navigate") {
- code = "window.wrappedJSObject.location.href = 'http://example.com/';";
- browser.tabs.executeScript({code});
- } else if (msg === "reload") {
- code = "window.wrappedJSObject.location.reload(true);";
- browser.tabs.executeScript({code});
- }
- });
- browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
- if (changeInfo.status === "complete" && tab.url === "http://example.com/") {
- browser.test.sendMessage("tabUpdated");
- }
- });
- browser.test.sendMessage("ready");
- }
-
- function devtools_page() {
- let eventCount = 0;
- let listener = url => {
- eventCount++;
- browser.test.assertEq("http://example.com/", url, "onNavigated received the expected url.");
- if (eventCount === 2) {
- browser.devtools.network.onNavigated.removeListener(listener);
- }
- browser.test.sendMessage("onNavigatedFired", eventCount);
- };
- browser.devtools.network.onNavigated.addListener(listener);
- }
-
- let extension = ExtensionTestUtils.loadExtension({
- background,
- manifest: {
- permissions: ["tabs", "http://mochi.test/", "http://example.com/"],
- devtools_page: "devtools_page.html",
- },
- files: {
- "devtools_page.html": `<!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <script src="devtools_page.js"></script>
- </head>
- <body>
- </body>
- </html>`,
- "devtools_page.js": devtools_page,
- },
- });
+ let extension = ExtensionTestUtils.loadExtension(extData);
await extension.startup();
await extension.awaitMessage("ready");
let target = gDevTools.getTargetForTab(tab);
await gDevTools.showToolbox(target, "webconsole");
info("Developer toolbox opened.");
@@ -85,8 +118,60 @@ add_task(async function test_devtools_ne
await gDevTools.closeToolbox(target);
await target.destroy();
await extension.unload();
await BrowserTestUtils.removeTab(tab);
});
+
+/**
+ * Test for `chrome.devtools.network.getHAR()` API
+ */
+add_task(async function test_devtools_network_get_har() {
+ 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, "webconsole");
+ info("Developer toolbox opened.");
+
+ // Get HAR, it should be empty since the Net panel wasn't selected.
+ const getHAREmptyPromise = extension.awaitMessage("getHAR-result");
+ extension.sendMessage("getHAR");
+ const getHAREmptyResult = await getHAREmptyPromise;
+ is(getHAREmptyResult.log.entries.length, 0, "HAR log should be empty");
+
+ // Select the Net panel.
+ await toolbox.selectTool("netmonitor");
+
+ // Reload the page to collect some HTTP requests.
+ extension.sendMessage("navigate");
+
+ // Wait till the navigation is complete and request
+ // added into the net panel.
+ await Promise.all([
+ extension.awaitMessage("tabUpdated"),
+ extension.awaitMessage("onNavigatedFired"),
+ waitForRequestAdded(toolbox),
+ ]);
+
+ // Get HAR, it should not be empty now.
+ const getHARPromise = extension.awaitMessage("getHAR-result");
+ extension.sendMessage("getHAR");
+ const getHARResult = await getHARPromise;
+ is(getHARResult.log.entries.length, 1, "HAR log should not be empty");
+
+ // Shutdown
+ await gDevTools.closeToolbox(target);
+
+ await target.destroy();
+
+ await extension.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -64,16 +64,18 @@ loader.lazyRequireGetter(this, "ToolboxB
loader.lazyRequireGetter(this, "SourceMapURLService",
"devtools/client/framework/source-map-url-service", true);
loader.lazyRequireGetter(this, "HUDService",
"devtools/client/webconsole/hudservice", true);
loader.lazyRequireGetter(this, "viewSource",
"devtools/client/shared/view-source");
loader.lazyRequireGetter(this, "StyleSheetsFront",
"devtools/shared/fronts/stylesheets", true);
+loader.lazyRequireGetter(this, "buildHarLog",
+ "devtools/client/netmonitor/src/har/har-builder-utils", true);
loader.lazyGetter(this, "domNodeConstants", () => {
return require("devtools/shared/dom-node-constants");
});
loader.lazyGetter(this, "registerHarOverlay", () => {
return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
});
@@ -3001,9 +3003,26 @@ Toolbox.prototype = {
/**
* Opens source in plain "view-source:".
* @see devtools/client/shared/source-utils.js
*/
viewSource: function (sourceURL, sourceLine) {
return viewSource.viewSource(this, sourceURL, sourceLine);
},
+
+ /**
+ * Returns data (HAR) collected by the Network panel.
+ */
+ getHARFromNetMonitor: function () {
+ 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 default empty HAR file in such case.
+ if (!netPanel) {
+ return Promise.resolve(buildHarLog(Services.appinfo));
+ }
+
+ // Use Netmonitor object to get the current HAR log.
+ return netPanel.panelWin.Netmonitor.getHar();
+ }
};
--- a/devtools/client/netmonitor/initializer.js
+++ b/devtools/client/netmonitor/initializer.js
@@ -19,35 +19,38 @@ const require = window.windowRequire = B
const EventEmitter = require("devtools/shared/old-event-emitter");
const { createFactory } = require("devtools/client/shared/vendor/react");
const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
const { Connector } = require("./src/connector/index");
const { configureStore } = require("./src/utils/create-store");
const App = createFactory(require("./src/components/App"));
-const { getDisplayedRequestById } = require("./src/selectors/index");
const { EVENTS } = require("./src/constants");
+const {
+ getDisplayedRequestById,
+ getSortedRequests
+} = require("./src/selectors/index");
// Inject EventEmitter into global window.
EventEmitter.decorate(window);
// Configure store/state object.
let connector = new Connector();
const store = configureStore(connector);
const actions = bindActionCreators(require("./src/actions/index"), store.dispatch);
// Inject to global window for testing
window.store = store;
window.connector = connector;
/**
* Global Netmonitor object in this panel. This object can be consumed
* by other panels (e.g. Console is using inspectRequest), by the
- * Launchpad (bootstrap), etc.
+ * Launchpad (bootstrap), WebExtension API (getHAR), etc.
*/
window.Netmonitor = {
bootstrap({ toolbox, panel }) {
this.mount = document.querySelector("#mount");
const connection = {
tabConnection: {
tabTarget: toolbox.target,
@@ -73,16 +76,35 @@ window.Netmonitor = {
},
destroy() {
unmountComponentAtNode(this.mount);
return connector.disconnect();
},
/**
+ * Returns list of requests currently available in the panel.
+ */
+ getHar() {
+ let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
+ let { getLongString, getTabTarget, requestData } = connector;
+ let { form: { title, url } } = getTabTarget();
+ let state = store.getState();
+
+ let options = {
+ getString: getLongString,
+ items: getSortedRequests(state),
+ requestData,
+ title: title || url,
+ };
+
+ return HarExporter.getHar(options);
+ },
+
+ /**
* 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
* a network request directly from other internal toolbox panel.
*
* @param {string} requestId The actor ID of the request to inspect.
* @return {object} A promise resolved once the task finishes.
*/
inspectRequest(requestId) {
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/har/har-builder-utils.js
@@ -0,0 +1,30 @@
+/* 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";
+
+/**
+ * Currently supported HAR version.
+ */
+const HAR_VERSION = "1.1";
+
+function buildHarLog(appInfo) {
+ return {
+ log: {
+ version: HAR_VERSION,
+ creator: {
+ name: appInfo.name,
+ version: appInfo.version
+ },
+ browser: {
+ name: appInfo.name,
+ version: appInfo.version
+ },
+ pages: [],
+ entries: [],
+ }
+ };
+}
+
+exports.buildHarLog = buildHarLog;
--- a/devtools/client/netmonitor/src/har/har-builder.js
+++ b/devtools/client/netmonitor/src/har/har-builder.js
@@ -8,19 +8,18 @@ const Services = require("Services");
const appInfo = Services.appinfo;
const { LocalizationHelper } = require("devtools/shared/l10n");
const { CurlUtils } = require("devtools/client/shared/curl");
const {
getFormDataSections,
getUrlQuery,
parseQueryString,
} = require("../utils/request-utils");
-
+const { buildHarLog } = require("./har-builder-utils");
const L10N = new LocalizationHelper("devtools/client/locales/har.properties");
-const HAR_VERSION = "1.1";
/**
* This object is responsible for building HAR file. See HAR spec:
* https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html
* http://www.softwareishard.com/blog/har-12-spec/
*
* @param {Object} options configuration object
*
@@ -50,48 +49,32 @@ HarBuilder.prototype = {
*
* @returns {Promise} A promise that resolves to the HAR object when
* the entire build process is done.
*/
build: async function () {
this.promises = [];
// Build basic structure for data.
- let log = this.buildLog();
+ let log = buildHarLog(appInfo);
// Build entries.
for (let file of this._options.items) {
- log.entries.push(await this.buildEntry(log, file));
+ log.log.entries.push(await this.buildEntry(log.log, file));
}
// Some data needs to be fetched from the backend during the
// build process, so wait till all is done.
await Promise.all(this.promises);
- return { log };
+ return log;
},
// Helpers
- buildLog: function () {
- return {
- version: HAR_VERSION,
- creator: {
- name: appInfo.name,
- version: appInfo.version
- },
- browser: {
- name: appInfo.name,
- version: appInfo.version
- },
- pages: [],
- entries: [],
- };
- },
-
buildPage: function (file) {
let page = {};
// Page start time is set when the first request is processed
// (see buildEntry)
page.startedDateTime = 0;
page.id = "page_" + this._options.id;
page.title = this._options.title;
--- a/devtools/client/netmonitor/src/har/har-exporter.js
+++ b/devtools/client/netmonitor/src/har/har-exporter.js
@@ -114,16 +114,26 @@ const HarExporter = {
*/
copy: function (options) {
return this.fetchHarData(options).then(jsonString => {
clipboardHelper.copyString(jsonString);
return jsonString;
});
},
+ /**
+ * Get HAR data as JSON object.
+ *
+ * @param Object options
+ * Configuration object, see save() for detailed description.
+ */
+ getHar: function (options) {
+ return this.fetchHarData(options).then(JSON.parse);
+ },
+
// Helpers
fetchHarData: function (options) {
// Generate page ID
options.id = options.id || uid++;
// Set default generic HAR export options.
options.jsonp = options.jsonp ||
--- a/devtools/client/netmonitor/src/har/moz.build
+++ b/devtools/client/netmonitor/src/har/moz.build
@@ -1,14 +1,15 @@
# 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/.
DevToolsModules(
'har-automation.js',
+ 'har-builder-utils.js',
'har-builder.js',
'har-collector.js',
'har-exporter.js',
'har-utils.js',
'toolbox-overlay.js',
)
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']