Bug 1311177 - Implement the devtools.network.getHAR API method; r=jdescottes,rickychien,rpl

MozReview-Commit-ID: I9F4tGSwBrt

--HG--
extra : rebase_source : ce3adb6ce47e62302b29651a05276d13621679cb
This commit is contained in:
Jan Odvarko 2018-01-17 13:32:42 +01:00
Родитель 6f469dc037
Коммит e1114707d4
9 изменённых файлов: 214 добавлений и 76 удалений

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

@ -25,6 +25,10 @@ this.devtools_network = class extends ExtensionAPI {
});
};
}).api(),
getHAR: function() {
return context.devToolsToolbox.getHARFromNetMonitor();
},
},
},
};

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

@ -45,7 +45,6 @@
"functions": [
{
"name": "getHAR",
"unsupported": true,
"type": "function",
"description": "Returns HAR log that contains all known network requests.",
"async": "callback",

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

@ -5,60 +5,84 @@
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);
}
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");
@ -90,3 +114,49 @@ add_task(async function test_devtools_network_on_navigated() {
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");
await extension.awaitMessage("tabUpdated");
await extension.awaitMessage("onNavigatedFired");
// 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);
});

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

@ -69,6 +69,8 @@ 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");
@ -3006,4 +3008,21 @@ Toolbox.prototype = {
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();
}
};

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

@ -24,8 +24,11 @@ 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);
@ -42,7 +45,7 @@ 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 }) {
@ -77,6 +80,25 @@ window.Netmonitor = {
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

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

@ -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;

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

@ -13,9 +13,8 @@ const {
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:
@ -55,38 +54,22 @@ HarBuilder.prototype = {
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 = {};

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

@ -119,6 +119,16 @@ const HarExporter = {
});
},
/**
* 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) {

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

@ -4,6 +4,7 @@
DevToolsModules(
'har-automation.js',
'har-builder-utils.js',
'har-builder.js',
'har-collector.js',
'har-exporter.js',