# HG changeset patch # User Jan Odvarko # Date 1516642246 -3600 # Node ID e4631839df8cc2d0d380551bf9726a8aeceef1b4 # Parent 4594580ef04c2988dedc5ee71a1707622b237f7a Bug 1311177 - Implement the devtools.network.getHAR API method; r=jdescottes,rickychien,rpl MozReview-Commit-ID: gUtGjbr0FQ diff --git a/browser/components/extensions/ext-devtools-network.js b/browser/components/extensions/ext-devtools-network.js --- 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(); + }, }, }, }; } }; diff --git a/browser/components/extensions/schemas/devtools_network.json b/browser/components/extensions/schemas/devtools_network.json --- 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.", diff --git a/browser/components/extensions/test/browser/browser_ext_devtools_network.js b/browser/components/extensions/test/browser/browser_ext_devtools_network.js --- 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} = ChromeUtils.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": ` + + + + + + + + `, + "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": ` - - - - - - - - `, - "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); +}); diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -65,16 +65,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; }); @@ -2989,9 +2991,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(); + } }; diff --git a/devtools/client/netmonitor/initializer.js b/devtools/client/netmonitor/initializer.js --- 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) { diff --git a/devtools/client/netmonitor/src/har/har-builder-utils.js b/devtools/client/netmonitor/src/har/har-builder-utils.js 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; diff --git a/devtools/client/netmonitor/src/har/har-builder.js b/devtools/client/netmonitor/src/har/har-builder.js --- 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; diff --git a/devtools/client/netmonitor/src/har/har-exporter.js b/devtools/client/netmonitor/src/har/har-exporter.js --- 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 || diff --git a/devtools/client/netmonitor/src/har/moz.build b/devtools/client/netmonitor/src/har/moz.build --- 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']