зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound. r=merge a=merge CLOSED TREE
This commit is contained in:
Коммит
8f1ff0908b
|
@ -145,6 +145,11 @@ GPATH
|
|||
^testing/talos/talos/mitmproxy/mitmproxy
|
||||
^testing/talos/talos/mitmproxy/mitmweb
|
||||
|
||||
# Ignore talos speedometer files; source is copied from in-tree /third_party
|
||||
# into testing/talos/talos/tests/webkit/PerformanceTests/Speedometer when
|
||||
# talos speedometer test is run locally
|
||||
^testing/talos/talos/tests/webkit/PerformanceTests/Speedometer
|
||||
|
||||
# Ignore toolchains.json created by tooltool.
|
||||
^toolchains\.json
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
|
||||
"resource:///modules/RecentWindow.jsm");
|
||||
|
||||
var {
|
||||
ExtensionError,
|
||||
|
@ -173,6 +175,17 @@ class WindowTracker extends WindowTrackerBase {
|
|||
removeProgressListener(window, listener) {
|
||||
window.gBrowser.removeTabsProgressListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {DOMWindow|null} topNormalWindow
|
||||
* The currently active, or topmost, browser window, or null if no
|
||||
* browser window is currently open.
|
||||
* Will return the topmost "normal" (i.e., not popup) window.
|
||||
* @readonly
|
||||
*/
|
||||
get topNormalWindow() {
|
||||
return RecentWindow.getMostRecentBrowserWindow({allowPopups: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -328,7 +328,7 @@ this.tabs = class extends ExtensionAPI {
|
|||
return new Promise((resolve, reject) => {
|
||||
let window = createProperties.windowId !== null ?
|
||||
windowTracker.getWindow(createProperties.windowId, context) :
|
||||
windowTracker.topWindow;
|
||||
windowTracker.topNormalWindow;
|
||||
|
||||
if (!window.gBrowser) {
|
||||
let obs = (finishedWindow, topic, data) => {
|
||||
|
|
|
@ -13,9 +13,9 @@ const HOME_URI_2 = "http://example.com/";
|
|||
const HOME_URI_3 = "http://example.org/";
|
||||
const HOME_URI_4 = "http://example.net/";
|
||||
|
||||
const CONTROLLABLE = "controllable_by_this_extension";
|
||||
const CONTROLLED_BY_THIS = "controlled_by_this_extension";
|
||||
const CONTROLLED_BY_OTHER = "controlled_by_other_extensions";
|
||||
const NOT_CONTROLLABLE = "not_controllable";
|
||||
|
||||
const HOMEPAGE_URL_PREF = "browser.startup.homepage";
|
||||
|
||||
|
@ -35,11 +35,13 @@ add_task(async function test_multiple_extensions_overriding_home_page() {
|
|||
browser.test.sendMessage("homepage", homepage);
|
||||
break;
|
||||
case "trySet":
|
||||
await browser.browserSettings.homepageOverride.set({value: "foo"});
|
||||
let setResult = await browser.browserSettings.homepageOverride.set({value: "foo"});
|
||||
browser.test.assertFalse(setResult, "Calling homepageOverride.set returns false.");
|
||||
browser.test.sendMessage("homepageSet");
|
||||
break;
|
||||
case "tryClear":
|
||||
await browser.browserSettings.homepageOverride.clear({});
|
||||
let clearResult = await browser.browserSettings.homepageOverride.clear({});
|
||||
browser.test.assertFalse(clearResult, "Calling homepageOverride.clear returns false.");
|
||||
browser.test.sendMessage("homepageCleared");
|
||||
break;
|
||||
}
|
||||
|
@ -82,7 +84,7 @@ add_task(async function test_multiple_extensions_overriding_home_page() {
|
|||
|
||||
is(getHomePageURL(), defaultHomePage,
|
||||
"Home url should be the default");
|
||||
await checkHomepageOverride(ext1, getHomePageURL(), CONTROLLABLE);
|
||||
await checkHomepageOverride(ext1, getHomePageURL(), NOT_CONTROLLABLE);
|
||||
|
||||
// Because we are expecting the pref to change when we start or unload, we
|
||||
// need to wait on a pref change. This is because the pref management is
|
||||
|
@ -156,7 +158,7 @@ add_task(async function test_multiple_extensions_overriding_home_page() {
|
|||
"Home url should be reset to default");
|
||||
|
||||
await ext5.startup();
|
||||
await checkHomepageOverride(ext5, defaultHomePage, CONTROLLABLE);
|
||||
await checkHomepageOverride(ext5, defaultHomePage, NOT_CONTROLLABLE);
|
||||
await ext5.unload();
|
||||
});
|
||||
|
||||
|
|
|
@ -225,3 +225,27 @@ add_task(async function test_urlbar_focus() {
|
|||
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function test_create_with_popup() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
let normalWin = await browser.windows.create();
|
||||
let lastFocusedNormalWin = await browser.windows.getLastFocused({});
|
||||
browser.test.assertEq(lastFocusedNormalWin.id, normalWin.id, "The normal window is the last focused window.");
|
||||
let popupWin = await browser.windows.create({type: "popup"});
|
||||
let lastFocusedPopupWin = await browser.windows.getLastFocused({});
|
||||
browser.test.assertEq(lastFocusedPopupWin.id, popupWin.id, "The popup window is the last focused window.");
|
||||
let newtab = await browser.tabs.create({});
|
||||
browser.test.assertEq(normalWin.id, newtab.windowId, "New tab was created in last focused normal window.");
|
||||
await Promise.all([
|
||||
browser.windows.remove(normalWin.id),
|
||||
browser.windows.remove(popupWin.id),
|
||||
]);
|
||||
browser.test.sendMessage("complete");
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("complete");
|
||||
await extension.unload();
|
||||
});
|
||||
|
|
|
@ -44,9 +44,9 @@ add_task(async function test_multiple_extensions_overriding_newtab_page() {
|
|||
const EXT_2_ID = "ext2@tests.mozilla.org";
|
||||
const EXT_3_ID = "ext3@tests.mozilla.org";
|
||||
|
||||
const CONTROLLABLE = "controllable_by_this_extension";
|
||||
const CONTROLLED_BY_THIS = "controlled_by_this_extension";
|
||||
const CONTROLLED_BY_OTHER = "controlled_by_other_extensions";
|
||||
const NOT_CONTROLLABLE = "not_controllable";
|
||||
|
||||
function background() {
|
||||
browser.test.onMessage.addListener(async msg => {
|
||||
|
@ -56,11 +56,13 @@ add_task(async function test_multiple_extensions_overriding_newtab_page() {
|
|||
browser.test.sendMessage("newTabPage", newTabPage);
|
||||
break;
|
||||
case "trySet":
|
||||
await browser.browserSettings.newTabPageOverride.set({value: "foo"});
|
||||
let setResult = await browser.browserSettings.newTabPageOverride.set({value: "foo"});
|
||||
browser.test.assertFalse(setResult, "Calling newTabPageOverride.set returns false.");
|
||||
browser.test.sendMessage("newTabPageSet");
|
||||
break;
|
||||
case "tryClear":
|
||||
await browser.browserSettings.newTabPageOverride.clear({});
|
||||
let clearResult = await browser.browserSettings.newTabPageOverride.clear({});
|
||||
browser.test.assertFalse(clearResult, "Calling newTabPageOverride.clear returns false.");
|
||||
browser.test.sendMessage("newTabPageCleared");
|
||||
break;
|
||||
}
|
||||
|
@ -105,7 +107,7 @@ add_task(async function test_multiple_extensions_overriding_newtab_page() {
|
|||
equal(aboutNewTabService.newTabURL, DEFAULT_NEW_TAB_URL,
|
||||
"newTabURL is still set to the default.");
|
||||
|
||||
await checkNewTabPageOverride(ext1, aboutNewTabService.newTabURL, CONTROLLABLE);
|
||||
await checkNewTabPageOverride(ext1, aboutNewTabService.newTabURL, NOT_CONTROLLABLE);
|
||||
|
||||
await ext2.startup();
|
||||
ok(aboutNewTabService.newTabURL.endsWith(NEWTAB_URI_2),
|
||||
|
@ -128,7 +130,7 @@ add_task(async function test_multiple_extensions_overriding_newtab_page() {
|
|||
await disabledPromise;
|
||||
equal(aboutNewTabService.newTabURL, DEFAULT_NEW_TAB_URL,
|
||||
"newTabURL url is reset to the default after second extension is disabled.");
|
||||
await checkNewTabPageOverride(ext1, aboutNewTabService.newTabURL, CONTROLLABLE);
|
||||
await checkNewTabPageOverride(ext1, aboutNewTabService.newTabURL, NOT_CONTROLLABLE);
|
||||
|
||||
// Re-enable the second extension.
|
||||
let enabledPromise = awaitEvent("ready");
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
@import url("chrome://global/skin/in-content/common.css");
|
||||
|
||||
#errorPageContainer {
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
#errorTitle {
|
||||
background: url("chrome://global/skin/icons/info.svg") left 0 no-repeat;
|
||||
background-size: 2em;
|
||||
padding-inline-start: 3em;
|
||||
}
|
||||
|
||||
#button-box {
|
||||
text-align: center;
|
||||
width: 75%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
button {
|
||||
width: auto !important;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 300px) {
|
||||
body {
|
||||
padding: 0px 10px;
|
||||
}
|
||||
#errorPageContainer {
|
||||
min-width: 100%;
|
||||
}
|
||||
#errorTitle {
|
||||
background: none;
|
||||
padding-inline-start: 0 !important;
|
||||
}
|
||||
button {
|
||||
width: auto !important;
|
||||
min-width: auto !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/client/ca
|
|||
loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/client/webaudioeditor/panel").WebAudioEditorPanel);
|
||||
loader.lazyGetter(this, "MemoryPanel", () => require("devtools/client/memory/panel").MemoryPanel);
|
||||
loader.lazyGetter(this, "PerformancePanel", () => require("devtools/client/performance/panel").PerformancePanel);
|
||||
loader.lazyGetter(this, "NewPerformancePanel", () => require("devtools/client/performance-new/panel").PerformancePanel);
|
||||
loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/client/netmonitor/panel").NetMonitorPanel);
|
||||
loader.lazyGetter(this, "StoragePanel", () => require("devtools/client/storage/panel").StoragePanel);
|
||||
loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/client/scratchpad/scratchpad-panel").ScratchpadPanel);
|
||||
|
@ -253,29 +254,49 @@ Tools.canvasDebugger = {
|
|||
};
|
||||
|
||||
Tools.performance = {
|
||||
id: "performance",
|
||||
ordinal: 7,
|
||||
icon: "chrome://devtools/skin/images/tool-profiler.svg",
|
||||
url: "chrome://devtools/content/performance/performance.xul",
|
||||
visibilityswitch: "devtools.performance.enabled",
|
||||
label: l10n("performance.label"),
|
||||
panelLabel: l10n("performance.panelLabel"),
|
||||
get tooltip() {
|
||||
return l10n("performance.tooltip", "Shift+" +
|
||||
functionkey(l10n("performance.commandkey")));
|
||||
},
|
||||
accesskey: l10n("performance.accesskey"),
|
||||
inMenu: true,
|
||||
|
||||
isTargetSupported: function (target) {
|
||||
return target.hasActor("performance");
|
||||
},
|
||||
|
||||
build: function (frame, target) {
|
||||
return new PerformancePanel(frame, target);
|
||||
}
|
||||
id: "performance",
|
||||
ordinal: 7,
|
||||
icon: "chrome://devtools/skin/images/tool-profiler.svg",
|
||||
visibilityswitch: "devtools.performance.enabled",
|
||||
label: l10n("performance.label"),
|
||||
panelLabel: l10n("performance.panelLabel"),
|
||||
get tooltip() {
|
||||
return l10n("performance.tooltip", "Shift+" +
|
||||
functionkey(l10n("performance.commandkey")));
|
||||
},
|
||||
accesskey: l10n("performance.accesskey"),
|
||||
inMenu: true,
|
||||
};
|
||||
|
||||
function switchPerformancePanel() {
|
||||
if (Services.prefs.getBoolPref("devtools.performance.new-panel-enabled", false)) {
|
||||
Tools.performance.url = "chrome://devtools/content/performance-new/perf.xhtml";
|
||||
Tools.performance.build = function (frame, target) {
|
||||
return new NewPerformancePanel(frame, target);
|
||||
};
|
||||
Tools.performance.isTargetSupported = function (target) {
|
||||
// Root actors are lazily initialized, so we can't check if the target has
|
||||
// the perf actor yet. Also this function is not async, so we can't initialize
|
||||
// the actor yet.
|
||||
return true;
|
||||
};
|
||||
} else {
|
||||
Tools.performance.url = "chrome://devtools/content/performance/performance.xul";
|
||||
Tools.performance.build = function (frame, target) {
|
||||
return new PerformancePanel(frame, target);
|
||||
};
|
||||
Tools.performance.isTargetSupported = function (target) {
|
||||
return target.hasActor("performance");
|
||||
};
|
||||
}
|
||||
}
|
||||
switchPerformancePanel();
|
||||
|
||||
Services.prefs.addObserver(
|
||||
"devtools.performance.new-panel-enabled",
|
||||
{ observe: switchPerformancePanel }
|
||||
);
|
||||
|
||||
Tools.memory = {
|
||||
id: "memory",
|
||||
ordinal: 8,
|
||||
|
|
|
@ -328,6 +328,11 @@ OptionsPanel.prototype = {
|
|||
label: L10N.getStr("toolbox.options.enableNewDebugger.label"),
|
||||
id: "devtools-new-debugger",
|
||||
parentId: "debugger-options"
|
||||
}, {
|
||||
pref: "devtools.performance.new-panel-enabled",
|
||||
label: "Enable new performance recorder (then re-open DevTools)",
|
||||
id: "devtools-new-performance",
|
||||
parentId: "context-options"
|
||||
}];
|
||||
|
||||
let createPreferenceOption = ({pref, label, id}) => {
|
||||
|
|
|
@ -77,6 +77,7 @@ function setPrefDefaults() {
|
|||
Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
|
||||
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
|
||||
Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
|
||||
Services.prefs.setBoolPref("devtools.preference.new-panel-enabled", false);
|
||||
}
|
||||
window.addEventListener("load", function () {
|
||||
let cmdClose = document.getElementById("toolbox-cmd-close");
|
||||
|
|
|
@ -63,6 +63,9 @@ devtools.jar:
|
|||
content/performance/performance.xul (performance/performance.xul)
|
||||
content/performance/performance-controller.js (performance/performance-controller.js)
|
||||
content/performance/performance-view.js (performance/performance-view.js)
|
||||
content/performance-new/perf.xhtml (performance-new/perf.xhtml)
|
||||
content/performance-new/frame-script.js (performance-new/frame-script.js)
|
||||
content/performance-new/initializer.js (performance-new/initializer.js)
|
||||
content/performance/views/overview.js (performance/views/overview.js)
|
||||
content/performance/views/toolbar.js (performance/views/toolbar.js)
|
||||
content/performance/views/details.js (performance/views/details.js)
|
||||
|
@ -155,6 +158,7 @@ devtools.jar:
|
|||
skin/animationinspector.css (themes/animationinspector.css)
|
||||
skin/canvasdebugger.css (themes/canvasdebugger.css)
|
||||
skin/debugger.css (themes/debugger.css)
|
||||
skin/perf.css (themes/perf.css)
|
||||
skin/performance.css (themes/performance.css)
|
||||
skin/memory.css (themes/memory.css)
|
||||
skin/scratchpad.css (themes/scratchpad.css)
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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";
|
||||
|
||||
define(function (require, exports, module) {
|
||||
const { Component } = require("devtools/client/shared/vendor/react");
|
||||
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
||||
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
|
||||
const { pre } = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
|
||||
/**
|
||||
* This object represents a live DOM text node in a <pre>.
|
||||
*/
|
||||
class LiveText extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
data: PropTypes.instanceOf(Text),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
let el = findDOMNode(this);
|
||||
if (el.firstChild === this.props.data) {
|
||||
return;
|
||||
}
|
||||
el.textContent = "";
|
||||
el.append(this.props.data);
|
||||
}
|
||||
|
||||
render() {
|
||||
return pre({className: "data"});
|
||||
}
|
||||
}
|
||||
|
||||
// Exports from this module
|
||||
exports.LiveText = LiveText;
|
||||
});
|
|
@ -22,7 +22,7 @@ define(function (require, exports, module) {
|
|||
class MainTabbedArea extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
jsonText: PropTypes.instanceOf(Text),
|
||||
jsonText: PropTypes.string,
|
||||
tabActive: PropTypes.number,
|
||||
actions: PropTypes.object,
|
||||
headers: PropTypes.object,
|
||||
|
@ -42,8 +42,8 @@ define(function (require, exports, module) {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
json: props.json,
|
||||
expandedNodes: props.expandedNodes,
|
||||
json: {},
|
||||
headers: {},
|
||||
jsonText: props.jsonText,
|
||||
tabActive: props.tabActive
|
||||
};
|
||||
|
@ -64,7 +64,7 @@ define(function (require, exports, module) {
|
|||
className: "json",
|
||||
title: JSONView.Locale.$STR("jsonViewer.tab.JSON")},
|
||||
JsonPanel({
|
||||
data: this.state.json,
|
||||
data: this.props.json,
|
||||
expandedNodes: this.props.expandedNodes,
|
||||
actions: this.props.actions,
|
||||
searchFilter: this.state.searchFilter
|
||||
|
@ -74,8 +74,7 @@ define(function (require, exports, module) {
|
|||
className: "rawdata",
|
||||
title: JSONView.Locale.$STR("jsonViewer.tab.RawData")},
|
||||
TextPanel({
|
||||
isValidJson: !(this.state.json instanceof Error) &&
|
||||
document.readyState != "loading",
|
||||
isValidJson: !(this.props.json instanceof Error),
|
||||
data: this.state.jsonText,
|
||||
actions: this.props.actions
|
||||
})
|
||||
|
|
|
@ -12,8 +12,8 @@ define(function (require, exports, module) {
|
|||
const dom = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
const { createFactories } = require("devtools/client/shared/react-utils");
|
||||
const { TextToolbar } = createFactories(require("./TextToolbar"));
|
||||
const { LiveText } = createFactories(require("./LiveText"));
|
||||
const { div } = dom;
|
||||
|
||||
const { div, pre } = dom;
|
||||
|
||||
/**
|
||||
* This template represents the 'Raw Data' panel displaying
|
||||
|
@ -24,7 +24,7 @@ define(function (require, exports, module) {
|
|||
return {
|
||||
isValidJson: PropTypes.bool,
|
||||
actions: PropTypes.object,
|
||||
data: PropTypes.instanceOf(Text),
|
||||
data: PropTypes.string
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,9 @@ define(function (require, exports, module) {
|
|||
isValidJson: this.props.isValidJson
|
||||
}),
|
||||
div({className: "panelContent"},
|
||||
LiveText({data: this.props.data})
|
||||
pre({className: "data"},
|
||||
this.props.data
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -14,7 +14,6 @@ DevToolsModules(
|
|||
'HeadersToolbar.js',
|
||||
'JsonPanel.js',
|
||||
'JsonToolbar.js',
|
||||
'LiveText.js',
|
||||
'MainTabbedArea.js',
|
||||
'SearchBox.js',
|
||||
'TextPanel.js',
|
||||
|
|
|
@ -168,8 +168,6 @@ function exportData(win, request) {
|
|||
|
||||
data.json = new win.Text();
|
||||
|
||||
data.readyState = "uninitialized";
|
||||
|
||||
let Locale = {
|
||||
$STR: key => {
|
||||
try {
|
||||
|
@ -246,6 +244,7 @@ function initialHTML(doc) {
|
|||
element("script", {
|
||||
src: baseURI + "lib/require.js",
|
||||
"data-main": baseURI + "viewer-config.js",
|
||||
defer: true,
|
||||
})
|
||||
]),
|
||||
element("body", {}, [
|
||||
|
|
|
@ -19,26 +19,41 @@ define(function (require, exports, module) {
|
|||
|
||||
// Application state object.
|
||||
let input = {
|
||||
jsonText: JSONView.json,
|
||||
jsonText: JSONView.json.textContent,
|
||||
jsonPretty: null,
|
||||
headers: JSONView.headers,
|
||||
tabActive: 0,
|
||||
prettified: false
|
||||
};
|
||||
|
||||
try {
|
||||
input.json = JSON.parse(input.jsonText);
|
||||
} catch (err) {
|
||||
input.json = err;
|
||||
}
|
||||
|
||||
// Expand the document by default if its size isn't bigger than 100KB.
|
||||
if (!(input.json instanceof Error) && input.jsonText.length <= AUTO_EXPAND_MAX_SIZE) {
|
||||
input.expandedNodes = TreeViewClass.getExpandedNodes(
|
||||
input.json,
|
||||
{maxLevel: AUTO_EXPAND_MAX_LEVEL}
|
||||
);
|
||||
} else {
|
||||
input.expandedNodes = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Application actions/commands. This list implements all commands
|
||||
* available for the JSON viewer.
|
||||
*/
|
||||
input.actions = {
|
||||
onCopyJson: function () {
|
||||
let text = input.prettified ? input.jsonPretty : input.jsonText;
|
||||
copyString(text.textContent);
|
||||
copyString(input.prettified ? input.jsonPretty : input.jsonText);
|
||||
},
|
||||
|
||||
onSaveJson: function () {
|
||||
if (input.prettified && !prettyURL) {
|
||||
prettyURL = URL.createObjectURL(new window.Blob([input.jsonPretty.textContent]));
|
||||
prettyURL = URL.createObjectURL(new window.Blob([input.jsonPretty]));
|
||||
}
|
||||
dispatchEvent("save", input.prettified ? prettyURL : null);
|
||||
},
|
||||
|
@ -78,7 +93,7 @@ define(function (require, exports, module) {
|
|||
theApp.setState({jsonText: input.jsonText});
|
||||
} else {
|
||||
if (!input.jsonPretty) {
|
||||
input.jsonPretty = new Text(JSON.stringify(input.json, null, " "));
|
||||
input.jsonPretty = JSON.stringify(input.json, null, " ");
|
||||
}
|
||||
theApp.setState({jsonText: input.jsonPretty});
|
||||
}
|
||||
|
@ -124,52 +139,11 @@ define(function (require, exports, module) {
|
|||
* at the top of the window. This component also represents ReacJS root.
|
||||
*/
|
||||
let content = document.getElementById("content");
|
||||
let promise = (async function parseJSON() {
|
||||
if (document.readyState == "loading") {
|
||||
// If the JSON has not been loaded yet, render the Raw Data tab first.
|
||||
input.json = {};
|
||||
input.expandedNodes = new Set();
|
||||
input.tabActive = 1;
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener("DOMContentLoaded", resolve, {once: true});
|
||||
}).then(parseJSON).then(() => {
|
||||
// Now update the state and switch to the JSON tab.
|
||||
theApp.setState({
|
||||
tabActive: 0,
|
||||
json: input.json,
|
||||
expandedNodes: input.expandedNodes,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If the JSON has been loaded, parse it immediately before loading the app.
|
||||
let jsonString = input.jsonText.textContent;
|
||||
try {
|
||||
input.json = JSON.parse(jsonString);
|
||||
} catch (err) {
|
||||
input.json = err;
|
||||
}
|
||||
|
||||
// Expand the document by default if its size isn't bigger than 100KB.
|
||||
if (!(input.json instanceof Error) && jsonString.length <= AUTO_EXPAND_MAX_SIZE) {
|
||||
input.expandedNodes = TreeViewClass.getExpandedNodes(
|
||||
input.json,
|
||||
{maxLevel: AUTO_EXPAND_MAX_LEVEL}
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
let theApp = render(MainTabbedArea(input), content);
|
||||
|
||||
// Send readyState change notification event to the window. Can be useful for
|
||||
// Send notification event to the window. Can be useful for
|
||||
// tests as well as extensions.
|
||||
JSONView.readyState = "interactive";
|
||||
window.dispatchEvent(new CustomEvent("AppReadyStateChange"));
|
||||
|
||||
promise.then(() => {
|
||||
// Another readyState change notification event.
|
||||
JSONView.readyState = "complete";
|
||||
window.dispatchEvent(new CustomEvent("AppReadyStateChange"));
|
||||
});
|
||||
let event = new CustomEvent("JSONViewInitialized", {});
|
||||
JSONView.initialized = true;
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
|
|
|
@ -22,11 +22,8 @@ support-files =
|
|||
!/devtools/client/framework/test/head.js
|
||||
!/devtools/client/framework/test/shared-head.js
|
||||
|
||||
[browser_json_refresh.js]
|
||||
[browser_jsonview_bug_1380828.js]
|
||||
[browser_jsonview_chunked_json.js]
|
||||
support-files =
|
||||
chunked_json.sjs
|
||||
[browser_jsonview_ignore_charset.js]
|
||||
[browser_jsonview_content_type.js]
|
||||
[browser_jsonview_copy_headers.js]
|
||||
subsuite = clipboard
|
||||
|
@ -41,7 +38,6 @@ skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32
|
|||
[browser_jsonview_empty_object.js]
|
||||
[browser_jsonview_encoding.js]
|
||||
[browser_jsonview_filter.js]
|
||||
[browser_jsonview_ignore_charset.js]
|
||||
[browser_jsonview_invalid_json.js]
|
||||
[browser_jsonview_manifest.js]
|
||||
[browser_jsonview_nojs.js]
|
||||
|
@ -51,7 +47,8 @@ skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32
|
|||
[browser_jsonview_save_json.js]
|
||||
support-files =
|
||||
!/toolkit/content/tests/browser/common/mockTransfer.js
|
||||
[browser_jsonview_serviceworker.js]
|
||||
[browser_jsonview_slash.js]
|
||||
[browser_jsonview_theme.js]
|
||||
[browser_jsonview_slash.js]
|
||||
[browser_jsonview_valid_json.js]
|
||||
[browser_json_refresh.js]
|
||||
[browser_jsonview_serviceworker.js]
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const TEST_JSON_URL = URL_ROOT + "chunked_json.sjs";
|
||||
|
||||
add_task(async function () {
|
||||
info("Test chunked JSON started");
|
||||
|
||||
await addJsonViewTab(TEST_JSON_URL, {
|
||||
appReadyState: "interactive",
|
||||
docReadyState: "loading",
|
||||
});
|
||||
|
||||
is(await getElementCount(".rawdata.is-active"), 1,
|
||||
"The Raw Data tab is selected.");
|
||||
|
||||
// Write some text and check that it is displayed.
|
||||
await write("[");
|
||||
await checkText();
|
||||
|
||||
// Repeat just in case.
|
||||
await write("1,");
|
||||
await checkText();
|
||||
|
||||
is(await getElementCount("button.prettyprint"), 0,
|
||||
"There is no pretty print button during load");
|
||||
|
||||
await selectJsonViewContentTab("json");
|
||||
is(await getElementText(".jsonPanelBox > .panelContent"), "", "There is no JSON tree");
|
||||
|
||||
await selectJsonViewContentTab("headers");
|
||||
ok(await getElementText(".headersPanelBox .netInfoHeadersTable"),
|
||||
"The headers table has been filled.");
|
||||
|
||||
// Write some text without being in Raw Data, then switch tab and check.
|
||||
await write("2");
|
||||
await selectJsonViewContentTab("rawdata");
|
||||
await checkText();
|
||||
|
||||
// Another text check.
|
||||
await write("]");
|
||||
await checkText();
|
||||
|
||||
// Close the connection.
|
||||
await server("close");
|
||||
|
||||
is(await getElementCount(".json.is-active"), 1, "The JSON tab is selected.");
|
||||
|
||||
is(await getElementCount(".jsonPanelBox .treeTable .treeRow"), 2,
|
||||
"There is a tree with 2 rows.");
|
||||
|
||||
await selectJsonViewContentTab("rawdata");
|
||||
await checkText();
|
||||
|
||||
is(await getElementCount("button.prettyprint"), 1, "There is a pretty print button.");
|
||||
await clickJsonNode("button.prettyprint");
|
||||
await checkText(JSON.stringify(JSON.parse(data), null, 2));
|
||||
});
|
||||
|
||||
let data = " ";
|
||||
async function write(text) {
|
||||
data += text;
|
||||
await server("write", text);
|
||||
}
|
||||
async function checkText(text = data) {
|
||||
is(await getElementText(".textPanelBox .data"), text, "Got the right text.");
|
||||
}
|
||||
|
||||
function server(action, value) {
|
||||
return new Promise(resolve => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", TEST_JSON_URL + "?" + action + "=" + value);
|
||||
xhr.addEventListener("load", resolve, {once: true});
|
||||
xhr.send();
|
||||
});
|
||||
}
|
|
@ -5,21 +5,21 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
add_task(async function () {
|
||||
add_task(function* () {
|
||||
info("Test JSON without JavaScript started.");
|
||||
|
||||
let oldPref = SpecialPowers.getBoolPref("javascript.enabled");
|
||||
SpecialPowers.setBoolPref("javascript.enabled", false);
|
||||
|
||||
const TEST_JSON_URL = "data:application/json,[1,2,3]";
|
||||
|
||||
// "uninitialized" will be the last app readyState because JS is disabled.
|
||||
await addJsonViewTab(TEST_JSON_URL, {appReadyState: "uninitialized"});
|
||||
|
||||
info("Checking visible text contents.");
|
||||
let {text} = await executeInContent("Test:JsonView:GetElementVisibleText",
|
||||
{selector: "html"});
|
||||
is(text, "[1,2,3]", "The raw source should be visible.");
|
||||
yield addJsonViewTab(TEST_JSON_URL, 0).catch(() => {
|
||||
info("JSON Viewer did not load");
|
||||
return executeInContent("Test:JsonView:GetElementVisibleText", {selector: "html"})
|
||||
.then(result => {
|
||||
info("Checking visible text contents.");
|
||||
is(result.text, "[1,2,3]", "The raw source should be visible.");
|
||||
});
|
||||
});
|
||||
|
||||
SpecialPowers.setBoolPref("javascript.enabled", oldPref);
|
||||
});
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const key = "json-viewer-chunked-response";
|
||||
function setResponse(response) {
|
||||
setObjectState(key, response);
|
||||
}
|
||||
function getResponse() {
|
||||
let response;
|
||||
getObjectState(key, v => { response = v });
|
||||
return response;
|
||||
}
|
||||
|
||||
function handleRequest(request, response) {
|
||||
let {queryString} = request;
|
||||
if (!queryString) {
|
||||
response.processAsync();
|
||||
setResponse(response);
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
// Write something so that the JSON viewer app starts loading.
|
||||
response.write(" ");
|
||||
return;
|
||||
}
|
||||
let [command, value] = queryString.split('=');
|
||||
switch (command) {
|
||||
case "write":
|
||||
getResponse().write(value);
|
||||
break;
|
||||
case "close":
|
||||
getResponse().finish();
|
||||
setResponse(null);
|
||||
break;
|
||||
}
|
||||
response.setHeader("Content-Type", "text/plain");
|
||||
response.write("ok");
|
||||
}
|
|
@ -25,19 +25,17 @@ Services.scriptloader.loadSubScript(
|
|||
"chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
|
||||
|
||||
/**
|
||||
* When the ready state of the JSON View app changes, it triggers custom event
|
||||
* "AppReadyStateChange", then the "Test:JsonView:AppReadyStateChange" message
|
||||
* will be sent to the parent process for tests to wait for this event if needed.
|
||||
* When the JSON View is done rendering it triggers custom event
|
||||
* "JSONViewInitialized", then the Test:TestPageProcessingDone message
|
||||
* will be sent to the parent process for tests to wait for this event
|
||||
* if needed.
|
||||
*/
|
||||
content.addEventListener("AppReadyStateChange", () => {
|
||||
sendAsyncMessage("Test:JsonView:AppReadyStateChange");
|
||||
content.addEventListener("JSONViewInitialized", () => {
|
||||
sendAsyncMessage("Test:JsonView:JSONViewInitialized");
|
||||
});
|
||||
|
||||
/**
|
||||
* Analogous for the standard "readystatechange" event of the document.
|
||||
*/
|
||||
content.document.addEventListener("readystatechange", () => {
|
||||
sendAsyncMessage("Test:JsonView:DocReadyStateChange");
|
||||
content.addEventListener("load", () => {
|
||||
sendAsyncMessage("Test:JsonView:load");
|
||||
});
|
||||
|
||||
addMessageListener("Test:JsonView:GetElementCount", function (msg) {
|
||||
|
|
|
@ -26,52 +26,17 @@ registerCleanupFunction(() => {
|
|||
* Add a new test tab in the browser and load the given url.
|
||||
* @param {String} url
|
||||
* The url to be loaded in the new tab.
|
||||
*
|
||||
* @param {Object} [optional]
|
||||
* An object with the following optional properties:
|
||||
* - appReadyState: The readyState of the JSON Viewer app that you want to
|
||||
* wait for. Its value can be one of:
|
||||
* - "uninitialized": The converter has started the request.
|
||||
* If JavaScript is disabled, there will be no more readyState changes.
|
||||
* - "loading": RequireJS started loading the scripts for the JSON Viewer.
|
||||
* If the load timeouts, there will be no more readyState changes.
|
||||
* - "interactive": The JSON Viewer app loaded, but possibly not all the JSON
|
||||
* data has been received.
|
||||
* - "complete" (default): The app is fully loaded with all the JSON.
|
||||
* - docReadyState: The standard readyState of the document that you want to
|
||||
* wait for. Its value can be one of:
|
||||
* - "loading": The JSON data has not been completely loaded (but the app might).
|
||||
* - "interactive": All the JSON data has been received.
|
||||
* - "complete" (default): Since there aren't sub-resources like images,
|
||||
* behaves as "interactive". Note the app might not be loaded yet.
|
||||
* @param {Number} timeout [optional]
|
||||
* The maximum number of milliseconds allowed before the initialization of the
|
||||
* JSON Viewer once the tab has been loaded. If exceeded, the initialization
|
||||
* will be considered to have failed, and the returned promise will be rejected.
|
||||
* If this parameter is not passed or is negative, it will be ignored.
|
||||
*/
|
||||
async function addJsonViewTab(url, {
|
||||
appReadyState = "complete",
|
||||
docReadyState = "complete",
|
||||
} = {}) {
|
||||
let docReadyStates = ["loading", "interactive", "complete"];
|
||||
let docReadyIndex = docReadyStates.indexOf(docReadyState);
|
||||
let appReadyStates = ["uninitialized", ...docReadyStates];
|
||||
let appReadyIndex = appReadyStates.indexOf(appReadyState);
|
||||
if (docReadyIndex < 0 || appReadyIndex < 0) {
|
||||
throw new Error("Invalid app or doc readyState parameter.");
|
||||
}
|
||||
|
||||
async function addJsonViewTab(url, timeout = -1) {
|
||||
info("Adding a new JSON tab with URL: '" + url + "'");
|
||||
let tabLoaded = addTab(url);
|
||||
let tab = gBrowser.selectedTab;
|
||||
|
||||
let tab = await addTab(url);
|
||||
let browser = tab.linkedBrowser;
|
||||
await Promise.race([tabLoaded, new Promise(resolve => {
|
||||
browser.webProgress.addProgressListener({
|
||||
QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener",
|
||||
"nsISupportsWeakReference"]),
|
||||
onLocationChange(webProgress) {
|
||||
// Fires when the tab is ready but before completely loaded.
|
||||
webProgress.removeProgressListener(this);
|
||||
resolve();
|
||||
},
|
||||
}, Ci.nsIWebProgress.NOTIFY_LOCATION);
|
||||
})]);
|
||||
|
||||
// Load devtools/shared/frame-script-utils.js
|
||||
getFrameScript();
|
||||
|
@ -82,23 +47,32 @@ async function addJsonViewTab(url, {
|
|||
browser.messageManager.loadFrameScript(frameScriptUrl, false);
|
||||
|
||||
// Check if there is a JSONView object.
|
||||
let JSONView = content.window.wrappedJSObject.JSONView;
|
||||
if (!JSONView) {
|
||||
throw new Error("The JSON Viewer did not load.");
|
||||
if (!content.window.wrappedJSObject.JSONView) {
|
||||
throw new Error("JSON Viewer did not load.");
|
||||
}
|
||||
|
||||
// Wait until the document readyState suffices.
|
||||
let {document} = content.window;
|
||||
while (docReadyStates.indexOf(document.readyState) < docReadyIndex) {
|
||||
await waitForContentMessage("Test:JsonView:DocReadyStateChange");
|
||||
// Resolve if the JSONView is fully loaded.
|
||||
if (content.window.wrappedJSObject.JSONView.initialized) {
|
||||
return tab;
|
||||
}
|
||||
|
||||
// Wait until the app readyState suffices.
|
||||
while (appReadyStates.indexOf(JSONView.readyState) < appReadyIndex) {
|
||||
await waitForContentMessage("Test:JsonView:AppReadyStateChange");
|
||||
// Otherwise wait for an initialization event, possibly with a time limit.
|
||||
const onJSONViewInitialized =
|
||||
waitForContentMessage("Test:JsonView:JSONViewInitialized")
|
||||
.then(() => tab);
|
||||
|
||||
if (!(timeout >= 0)) {
|
||||
return onJSONViewInitialized;
|
||||
}
|
||||
|
||||
return tab;
|
||||
if (content.window.document.readyState !== "complete") {
|
||||
await waitForContentMessage("Test:JsonView:load");
|
||||
}
|
||||
|
||||
let onTimeout = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("JSON Viewer did not load.")), timeout));
|
||||
|
||||
return Promise.race([onJSONViewInitialized, onTimeout]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
// Send readyState change notification event to the window. It's useful for tests.
|
||||
JSONView.readyState = "loading";
|
||||
window.dispatchEvent(new CustomEvent("AppReadyStateChange"));
|
||||
|
||||
/**
|
||||
* RequireJS configuration for JSON Viewer.
|
||||
*
|
||||
|
|
|
@ -20,6 +20,7 @@ DIRS += [
|
|||
'memory',
|
||||
'netmonitor',
|
||||
'performance',
|
||||
'performance-new',
|
||||
'preferences',
|
||||
'responsive.html',
|
||||
'scratchpad',
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { Component } = require("devtools/client/shared/vendor/react");
|
||||
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
||||
const dom = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
||||
|
@ -30,219 +31,255 @@ const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
|
|||
const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
|
||||
const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
|
||||
|
||||
function CustomRequestPanel({
|
||||
removeSelectedCustomRequest,
|
||||
request = {},
|
||||
sendCustomRequest,
|
||||
updateRequest,
|
||||
}) {
|
||||
let {
|
||||
method,
|
||||
customQueryValue,
|
||||
requestHeaders,
|
||||
requestPostData,
|
||||
url,
|
||||
} = request;
|
||||
|
||||
let headers = "";
|
||||
if (requestHeaders) {
|
||||
headers = requestHeaders.customHeadersValue ?
|
||||
requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
|
||||
}
|
||||
let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
|
||||
let params = customQueryValue;
|
||||
if (!params) {
|
||||
params = queryArray ?
|
||||
queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
|
||||
}
|
||||
let postData = requestPostData && requestPostData.postData.text ?
|
||||
requestPostData.postData.text : "";
|
||||
|
||||
return (
|
||||
div({ className: "custom-request-panel" },
|
||||
div({ className: "tabpanel-summary-container custom-request" },
|
||||
div({ className: "custom-request-label custom-header" },
|
||||
CUSTOM_NEW_REQUEST
|
||||
),
|
||||
button({
|
||||
className: "devtools-button",
|
||||
id: "custom-request-send-button",
|
||||
onClick: sendCustomRequest,
|
||||
},
|
||||
CUSTOM_SEND
|
||||
),
|
||||
button({
|
||||
className: "devtools-button",
|
||||
id: "custom-request-close-button",
|
||||
onClick: removeSelectedCustomRequest,
|
||||
},
|
||||
CUSTOM_CANCEL
|
||||
),
|
||||
),
|
||||
div({
|
||||
className: "tabpanel-summary-container custom-method-and-url",
|
||||
id: "custom-method-and-url",
|
||||
},
|
||||
input({
|
||||
className: "custom-method-value",
|
||||
id: "custom-method-value",
|
||||
onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
|
||||
value: method || "GET",
|
||||
}),
|
||||
input({
|
||||
className: "custom-url-value",
|
||||
id: "custom-url-value",
|
||||
onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
|
||||
value: url || "http://",
|
||||
}),
|
||||
),
|
||||
// Hide query field when there is no params
|
||||
params ? div({
|
||||
className: "tabpanel-summary-container custom-section",
|
||||
id: "custom-query",
|
||||
},
|
||||
div({ className: "custom-request-label" }, CUSTOM_QUERY),
|
||||
textarea({
|
||||
className: "tabpanel-summary-input",
|
||||
id: "custom-query-value",
|
||||
onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
|
||||
rows: 4,
|
||||
value: params,
|
||||
wrap: "off",
|
||||
})
|
||||
) : null,
|
||||
div({
|
||||
id: "custom-headers",
|
||||
className: "tabpanel-summary-container custom-section",
|
||||
},
|
||||
div({ className: "custom-request-label" }, CUSTOM_HEADERS),
|
||||
textarea({
|
||||
className: "tabpanel-summary-input",
|
||||
id: "custom-headers-value",
|
||||
onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
|
||||
rows: 8,
|
||||
value: headers,
|
||||
wrap: "off",
|
||||
})
|
||||
),
|
||||
div({
|
||||
id: "custom-postdata",
|
||||
className: "tabpanel-summary-container custom-section",
|
||||
},
|
||||
div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
|
||||
textarea({
|
||||
className: "tabpanel-summary-input",
|
||||
id: "custom-postdata-value",
|
||||
onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
|
||||
rows: 6,
|
||||
value: postData,
|
||||
wrap: "off",
|
||||
})
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
CustomRequestPanel.displayName = "CustomRequestPanel";
|
||||
|
||||
CustomRequestPanel.propTypes = {
|
||||
connector: PropTypes.object.isRequired,
|
||||
removeSelectedCustomRequest: PropTypes.func.isRequired,
|
||||
request: PropTypes.object,
|
||||
sendCustomRequest: PropTypes.func.isRequired,
|
||||
updateRequest: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a text representation of a name[divider]value list with
|
||||
* the given name regex and divider character.
|
||||
*
|
||||
* @param {string} text - Text of list
|
||||
* @return {array} array of headers info {name, value}
|
||||
/*
|
||||
* Custom request panel component
|
||||
* A network request editor which simply provide edit and resend interface
|
||||
* for netowrk development.
|
||||
*/
|
||||
function parseRequestText(text, namereg, divider) {
|
||||
let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
|
||||
let pairs = [];
|
||||
class CustomRequestPanel extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
connector: PropTypes.object.isRequired,
|
||||
removeSelectedCustomRequest: PropTypes.func.isRequired,
|
||||
request: PropTypes.object,
|
||||
sendCustomRequest: PropTypes.func.isRequired,
|
||||
updateRequest: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
for (let line of text.split("\n")) {
|
||||
let matches = regex.exec(line);
|
||||
if (matches) {
|
||||
let [, name, value] = matches;
|
||||
pairs.push({ name, value });
|
||||
componentDidMount() {
|
||||
this.maybeFetchPostData(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.maybeFetchPostData(nextProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* When switching to another request, lazily fetch request post data
|
||||
* from the backend. The panel will first be empty and then display the content.
|
||||
*/
|
||||
maybeFetchPostData(props) {
|
||||
if (props.request.requestPostDataAvailable &&
|
||||
(!props.request.requestPostData ||
|
||||
!props.request.requestPostData.postData.text)) {
|
||||
// This method will set `props.request.requestPostData`
|
||||
// asynchronously and force another render.
|
||||
props.connector.requestData(props.request.id, "requestPostData");
|
||||
}
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Custom Request Fields
|
||||
*
|
||||
* @param {Object} evt click event
|
||||
* @param {Object} request current request
|
||||
* @param {updateRequest} updateRequest action
|
||||
*/
|
||||
function updateCustomRequestFields(evt, request, updateRequest) {
|
||||
const val = evt.target.value;
|
||||
let data;
|
||||
switch (evt.target.id) {
|
||||
case "custom-headers-value":
|
||||
let customHeadersValue = val || "";
|
||||
// Parse text representation of multiple HTTP headers
|
||||
let headersArray = parseRequestText(customHeadersValue, "\\S+?", ":");
|
||||
// Remove temp customHeadersValue while query string is parsable
|
||||
if (customHeadersValue === "" ||
|
||||
headersArray.length === customHeadersValue.split("\n").length) {
|
||||
customHeadersValue = null;
|
||||
/**
|
||||
* Parse a text representation of a name[divider]value list with
|
||||
* the given name regex and divider character.
|
||||
*
|
||||
* @param {string} text - Text of list
|
||||
* @return {array} array of headers info {name, value}
|
||||
*/
|
||||
parseRequestText(text, namereg, divider) {
|
||||
let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
|
||||
let pairs = [];
|
||||
|
||||
for (let line of text.split("\n")) {
|
||||
let matches = regex.exec(line);
|
||||
if (matches) {
|
||||
let [, name, value] = matches;
|
||||
pairs.push({ name, value });
|
||||
}
|
||||
data = {
|
||||
requestHeaders: {
|
||||
customHeadersValue,
|
||||
headers: headersArray,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case "custom-method-value":
|
||||
data = { method: val.trim() };
|
||||
break;
|
||||
case "custom-postdata-value":
|
||||
data = {
|
||||
requestPostData: {
|
||||
postData: { text: val },
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "custom-query-value":
|
||||
let customQueryValue = val || "";
|
||||
// Parse readable text list of a query string
|
||||
let queryArray = customQueryValue ?
|
||||
parseRequestText(customQueryValue, ".+?", "=") : [];
|
||||
// Write out a list of query params into a query string
|
||||
let queryString = queryArray.map(
|
||||
({ name, value }) => name + "=" + value).join("&");
|
||||
let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
|
||||
request.url.split("?")[0];
|
||||
// Remove temp customQueryValue while query string is parsable
|
||||
if (customQueryValue === "" ||
|
||||
queryArray.length === customQueryValue.split("\n").length) {
|
||||
customQueryValue = null;
|
||||
}
|
||||
data = {
|
||||
customQueryValue,
|
||||
url,
|
||||
};
|
||||
break;
|
||||
case "custom-url-value":
|
||||
data = {
|
||||
customQueryValue: null,
|
||||
url: val
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
if (data) {
|
||||
// All updateRequest batch mode should be disabled to make UI editing in sync
|
||||
updateRequest(request.id, data, false);
|
||||
|
||||
/**
|
||||
* Update Custom Request Fields
|
||||
*
|
||||
* @param {Object} evt click event
|
||||
* @param {Object} request current request
|
||||
* @param {updateRequest} updateRequest action
|
||||
*/
|
||||
updateCustomRequestFields(evt, request, updateRequest) {
|
||||
const val = evt.target.value;
|
||||
let data;
|
||||
|
||||
switch (evt.target.id) {
|
||||
case "custom-headers-value":
|
||||
let customHeadersValue = val || "";
|
||||
// Parse text representation of multiple HTTP headers
|
||||
let headersArray = this.parseRequestText(customHeadersValue, "\\S+?", ":");
|
||||
// Remove temp customHeadersValue while query string is parsable
|
||||
if (customHeadersValue === "" ||
|
||||
headersArray.length === customHeadersValue.split("\n").length) {
|
||||
customHeadersValue = null;
|
||||
}
|
||||
data = {
|
||||
requestHeaders: {
|
||||
customHeadersValue,
|
||||
headers: headersArray,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case "custom-method-value":
|
||||
data = { method: val.trim() };
|
||||
break;
|
||||
case "custom-postdata-value":
|
||||
data = {
|
||||
requestPostData: {
|
||||
postData: { text: val },
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "custom-query-value":
|
||||
let customQueryValue = val || "";
|
||||
// Parse readable text list of a query string
|
||||
let queryArray = customQueryValue ?
|
||||
this.parseRequestText(customQueryValue, ".+?", "=") : [];
|
||||
// Write out a list of query params into a query string
|
||||
let queryString = queryArray.map(
|
||||
({ name, value }) => name + "=" + value).join("&");
|
||||
let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
|
||||
request.url.split("?")[0];
|
||||
// Remove temp customQueryValue while query string is parsable
|
||||
if (customQueryValue === "" ||
|
||||
queryArray.length === customQueryValue.split("\n").length) {
|
||||
customQueryValue = null;
|
||||
}
|
||||
data = {
|
||||
customQueryValue,
|
||||
url,
|
||||
};
|
||||
break;
|
||||
case "custom-url-value":
|
||||
data = {
|
||||
customQueryValue: null,
|
||||
url: val
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (data) {
|
||||
// All updateRequest batch mode should be disabled to make UI editing in sync
|
||||
updateRequest(request.id, data, false);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
removeSelectedCustomRequest,
|
||||
request = {},
|
||||
sendCustomRequest,
|
||||
updateRequest,
|
||||
} = this.props;
|
||||
let {
|
||||
method,
|
||||
customQueryValue,
|
||||
requestHeaders,
|
||||
requestPostData,
|
||||
url,
|
||||
} = request;
|
||||
|
||||
let headers = "";
|
||||
if (requestHeaders) {
|
||||
headers = requestHeaders.customHeadersValue ?
|
||||
requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
|
||||
}
|
||||
let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
|
||||
let params = customQueryValue;
|
||||
if (!params) {
|
||||
params = queryArray ?
|
||||
queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
|
||||
}
|
||||
let postData = requestPostData && requestPostData.postData.text ?
|
||||
requestPostData.postData.text : "";
|
||||
|
||||
return (
|
||||
div({ className: "custom-request-panel" },
|
||||
div({ className: "tabpanel-summary-container custom-request" },
|
||||
div({ className: "custom-request-label custom-header" },
|
||||
CUSTOM_NEW_REQUEST,
|
||||
),
|
||||
button({
|
||||
className: "devtools-button",
|
||||
id: "custom-request-send-button",
|
||||
onClick: sendCustomRequest,
|
||||
},
|
||||
CUSTOM_SEND,
|
||||
),
|
||||
button({
|
||||
className: "devtools-button",
|
||||
id: "custom-request-close-button",
|
||||
onClick: removeSelectedCustomRequest,
|
||||
},
|
||||
CUSTOM_CANCEL,
|
||||
),
|
||||
),
|
||||
div({
|
||||
className: "tabpanel-summary-container custom-method-and-url",
|
||||
id: "custom-method-and-url",
|
||||
},
|
||||
input({
|
||||
className: "custom-method-value",
|
||||
id: "custom-method-value",
|
||||
onChange: (evt) =>
|
||||
this.updateCustomRequestFields(evt, request, updateRequest),
|
||||
value: method || "GET",
|
||||
}),
|
||||
input({
|
||||
className: "custom-url-value",
|
||||
id: "custom-url-value",
|
||||
onChange: (evt) =>
|
||||
this.updateCustomRequestFields(evt, request, updateRequest),
|
||||
value: url || "http://",
|
||||
}),
|
||||
),
|
||||
// Hide query field when there is no params
|
||||
params ? div({
|
||||
className: "tabpanel-summary-container custom-section",
|
||||
id: "custom-query",
|
||||
},
|
||||
div({ className: "custom-request-label" }, CUSTOM_QUERY),
|
||||
textarea({
|
||||
className: "tabpanel-summary-input",
|
||||
id: "custom-query-value",
|
||||
onChange: (evt) =>
|
||||
this.updateCustomRequestFields(evt, request, updateRequest),
|
||||
rows: 4,
|
||||
value: params,
|
||||
wrap: "off",
|
||||
}),
|
||||
) : null,
|
||||
div({
|
||||
id: "custom-headers",
|
||||
className: "tabpanel-summary-container custom-section",
|
||||
},
|
||||
div({ className: "custom-request-label" }, CUSTOM_HEADERS),
|
||||
textarea({
|
||||
className: "tabpanel-summary-input",
|
||||
id: "custom-headers-value",
|
||||
onChange: (evt) =>
|
||||
this.updateCustomRequestFields(evt, request, updateRequest),
|
||||
rows: 8,
|
||||
value: headers,
|
||||
wrap: "off",
|
||||
}),
|
||||
),
|
||||
div({
|
||||
id: "custom-postdata",
|
||||
className: "tabpanel-summary-container custom-section",
|
||||
},
|
||||
div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
|
||||
textarea({
|
||||
className: "tabpanel-summary-input",
|
||||
id: "custom-postdata-value",
|
||||
onChange: (evt) =>
|
||||
this.updateCustomRequestFields(evt, request, updateRequest),
|
||||
rows: 6,
|
||||
value: postData,
|
||||
wrap: "off",
|
||||
}),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ const SUMMARY_VERSION = L10N.getStr("netmonitor.summary.version");
|
|||
class HeadersPanel extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
connector: PropTypes.object.isRequired,
|
||||
cloneSelectedRequest: PropTypes.func.isRequired,
|
||||
request: PropTypes.object.isRequired,
|
||||
renderValue: PropTypes.func,
|
||||
|
@ -67,6 +68,30 @@ class HeadersPanel extends Component {
|
|||
this.toggleRawHeaders = this.toggleRawHeaders.bind(this);
|
||||
this.renderSummary = this.renderSummary.bind(this);
|
||||
this.renderValue = this.renderValue.bind(this);
|
||||
this.maybeFetchPostData = this.maybeFetchPostData.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.maybeFetchPostData(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.maybeFetchPostData(nextProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* When switching to another request, lazily fetch request post data
|
||||
* from the backend. The panel will first be empty and then display the content.
|
||||
* Fetching post data is used for updating requestHeadersFromUploadStream section,
|
||||
*/
|
||||
maybeFetchPostData(props) {
|
||||
if (props.request.requestPostDataAvailable &&
|
||||
(!props.request.requestPostData ||
|
||||
!props.request.requestPostData.postData.text)) {
|
||||
// This method will set `props.request.requestPostData`
|
||||
// asynchronously and force another render.
|
||||
props.connector.requestData(props.request.id, "requestPostData");
|
||||
}
|
||||
}
|
||||
|
||||
getProperties(headers, title) {
|
||||
|
|
|
@ -46,14 +46,58 @@ class ParamsPanel extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.maybeFetchPostData(this.props);
|
||||
updateFormDataSections(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.maybeFetchPostData(nextProps);
|
||||
updateFormDataSections(nextProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* When switching to another request, lazily fetch request post data
|
||||
* from the backend. The panel will first be empty and then display the content.
|
||||
*/
|
||||
maybeFetchPostData(props) {
|
||||
if (props.request.requestPostDataAvailable &&
|
||||
(!props.request.requestPostData ||
|
||||
!props.request.requestPostData.postData.text)) {
|
||||
// This method will set `props.request.requestPostData`
|
||||
// asynchronously and force another render.
|
||||
props.connector.requestData(props.request.id, "requestPostData");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping array to dict for TreeView usage.
|
||||
* Since TreeView only support Object(dict) format.
|
||||
* This function also deal with duplicate key case
|
||||
* (for multiple selection and query params with same keys)
|
||||
*
|
||||
* @param {Object[]} arr - key-value pair array like query or form params
|
||||
* @returns {Object} Rep compatible object
|
||||
*/
|
||||
getProperties(arr) {
|
||||
return sortObjectKeys(arr.reduce((map, obj) => {
|
||||
let value = map[obj.name];
|
||||
if (value) {
|
||||
if (typeof value !== "object") {
|
||||
map[obj.name] = [value];
|
||||
}
|
||||
map[obj.name].push(obj.value);
|
||||
} else {
|
||||
map[obj.name] = obj.value;
|
||||
}
|
||||
return map;
|
||||
}, {}));
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
openLink,
|
||||
|
@ -68,7 +112,7 @@ class ParamsPanel extends Component {
|
|||
let postData = requestPostData ? requestPostData.postData.text : null;
|
||||
let query = getUrlQuery(url);
|
||||
|
||||
if (!formDataSections && !postData && !query) {
|
||||
if ((!formDataSections || formDataSections.length === 0) && !postData && !query) {
|
||||
return div({ className: "empty-notice" },
|
||||
PARAMS_EMPTY_TEXT
|
||||
);
|
||||
|
@ -79,13 +123,13 @@ class ParamsPanel extends Component {
|
|||
|
||||
// Query String section
|
||||
if (query) {
|
||||
object[PARAMS_QUERY_STRING] = getProperties(parseQueryString(query));
|
||||
object[PARAMS_QUERY_STRING] = this.getProperties(parseQueryString(query));
|
||||
}
|
||||
|
||||
// Form Data section
|
||||
if (formDataSections && formDataSections.length > 0) {
|
||||
let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
|
||||
object[PARAMS_FORM_DATA] = getProperties(parseFormData(sections));
|
||||
object[PARAMS_FORM_DATA] = this.getProperties(parseFormData(sections));
|
||||
}
|
||||
|
||||
// Request payload section
|
||||
|
@ -123,30 +167,6 @@ class ParamsPanel extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping array to dict for TreeView usage.
|
||||
* Since TreeView only support Object(dict) format.
|
||||
* This function also deal with duplicate key case
|
||||
* (for multiple selection and query params with same keys)
|
||||
*
|
||||
* @param {Object[]} arr - key-value pair array like query or form params
|
||||
* @returns {Object} Rep compatible object
|
||||
*/
|
||||
function getProperties(arr) {
|
||||
return sortObjectKeys(arr.reduce((map, obj) => {
|
||||
let value = map[obj.name];
|
||||
if (value) {
|
||||
if (typeof value !== "object") {
|
||||
map[obj.name] = [value];
|
||||
}
|
||||
map[obj.name].push(obj.value);
|
||||
} else {
|
||||
map[obj.name] = obj.value;
|
||||
}
|
||||
return map;
|
||||
}, {}));
|
||||
}
|
||||
|
||||
module.exports = connect(null,
|
||||
(dispatch) => ({
|
||||
updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
|
||||
|
|
|
@ -13,6 +13,7 @@ const Actions = require("../actions/index");
|
|||
const { setTooltipImageContent } = require("../request-list-tooltip");
|
||||
const {
|
||||
getDisplayedRequests,
|
||||
getSelectedRequest,
|
||||
getWaterfallScale,
|
||||
} = require("../selectors/index");
|
||||
|
||||
|
@ -36,7 +37,7 @@ class RequestListContent extends Component {
|
|||
return {
|
||||
connector: PropTypes.object.isRequired,
|
||||
columns: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
cloneSelectedRequest: PropTypes.func.isRequired,
|
||||
displayedRequests: PropTypes.array.isRequired,
|
||||
firstRequestStartedMillis: PropTypes.number.isRequired,
|
||||
fromCache: PropTypes.bool,
|
||||
|
@ -45,8 +46,9 @@ class RequestListContent extends Component {
|
|||
onSecurityIconMouseDown: PropTypes.func.isRequired,
|
||||
onSelectDelta: PropTypes.func.isRequired,
|
||||
onWaterfallMouseDown: PropTypes.func.isRequired,
|
||||
openStatistics: PropTypes.func.isRequired,
|
||||
scale: PropTypes.number,
|
||||
selectedRequestId: PropTypes.string,
|
||||
selectedRequest: PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -61,13 +63,11 @@ class RequestListContent extends Component {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { dispatch, connector } = this.props;
|
||||
const { connector, cloneSelectedRequest, openStatistics } = this.props;
|
||||
this.contextMenu = new RequestListContextMenu({
|
||||
cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
|
||||
getTabTarget: connector.getTabTarget,
|
||||
getLongString: connector.getLongString,
|
||||
openStatistics: (open) => dispatch(Actions.openStatistics(connector, open)),
|
||||
requestData: connector.requestData,
|
||||
connector,
|
||||
cloneSelectedRequest,
|
||||
openStatistics,
|
||||
});
|
||||
this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
|
||||
}
|
||||
|
@ -78,7 +78,6 @@ class RequestListContent extends Component {
|
|||
toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
|
||||
interactive: true
|
||||
});
|
||||
|
||||
// Install event handler to hide the tooltip on scroll
|
||||
this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
|
||||
}
|
||||
|
@ -220,18 +219,18 @@ class RequestListContent extends Component {
|
|||
onSecurityIconMouseDown,
|
||||
onWaterfallMouseDown,
|
||||
scale,
|
||||
selectedRequestId,
|
||||
selectedRequest,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
div({ className: "requests-list-wrapper"},
|
||||
div({ className: "requests-list-table"},
|
||||
div({ className: "requests-list-wrapper" },
|
||||
div({ className: "requests-list-table" },
|
||||
div({
|
||||
ref: "contentEl",
|
||||
className: "requests-list-contents",
|
||||
tabIndex: 0,
|
||||
onKeyDown: this.onKeyDown,
|
||||
style: {"--timings-scale": scale, "--timings-rev-scale": 1 / scale}
|
||||
style: { "--timings-scale": scale, "--timings-rev-scale": 1 / scale }
|
||||
},
|
||||
RequestListHeader(),
|
||||
displayedRequests.map((item, index) => RequestListItem({
|
||||
|
@ -240,7 +239,7 @@ class RequestListContent extends Component {
|
|||
columns,
|
||||
item,
|
||||
index,
|
||||
isSelected: item.id === selectedRequestId,
|
||||
isSelected: item.id === (selectedRequest && selectedRequest.id),
|
||||
key: item.id,
|
||||
onContextMenu: this.onContextMenu,
|
||||
onFocusedNodeChange: this.onFocusedNodeChange,
|
||||
|
@ -261,11 +260,12 @@ module.exports = connect(
|
|||
columns: state.ui.columns,
|
||||
displayedRequests: getDisplayedRequests(state),
|
||||
firstRequestStartedMillis: state.requests.firstStartedMillis,
|
||||
selectedRequestId: state.requests.selectedId,
|
||||
selectedRequest: getSelectedRequest(state),
|
||||
scale: getWaterfallScale(state),
|
||||
}),
|
||||
(dispatch) => ({
|
||||
dispatch,
|
||||
(dispatch, props) => ({
|
||||
cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
|
||||
openStatistics: (open) => dispatch(Actions.openStatistics(props.connector, open)),
|
||||
/**
|
||||
* A handler that opens the stack trace tab when a stack trace is available
|
||||
*/
|
||||
|
|
|
@ -56,17 +56,10 @@ class ResponsePanel extends Component {
|
|||
this.isJSON = this.isJSON.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* `componentDidMount` is called when opening the ResponsePanel for the first time
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.maybeFetchResponseContent(this.props);
|
||||
}
|
||||
|
||||
/**
|
||||
* `componentWillReceiveProps` is the only method called when switching between two
|
||||
* requests while the response panel is displayed.
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.maybeFetchResponseContent(nextProps);
|
||||
}
|
||||
|
|
|
@ -59,17 +59,10 @@ class SecurityPanel extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `componentDidMount` is called when opening the SecurityPanel for the first time
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.maybeFetchSecurityInfo(this.props);
|
||||
}
|
||||
|
||||
/**
|
||||
* `componentWillReceiveProps` is the only method called when switching between two
|
||||
* requests while the security panel is displayed.
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.maybeFetchSecurityInfo(nextProps);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
const { createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
||||
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
||||
const { L10N } = require("../utils/l10n");
|
||||
const { PANELS } = require("../constants");
|
||||
|
||||
|
@ -58,7 +57,12 @@ function TabboxPanel({
|
|||
id: PANELS.HEADERS,
|
||||
title: HEADERS_TITLE,
|
||||
},
|
||||
HeadersPanel({ request, cloneSelectedRequest, openLink }),
|
||||
HeadersPanel({
|
||||
cloneSelectedRequest,
|
||||
connector,
|
||||
openLink,
|
||||
request,
|
||||
}),
|
||||
),
|
||||
TabPanel({
|
||||
id: PANELS.COOKIES,
|
||||
|
@ -118,4 +122,4 @@ TabboxPanel.propTypes = {
|
|||
sourceMapService: PropTypes.object,
|
||||
};
|
||||
|
||||
module.exports = connect()(TabboxPanel);
|
||||
module.exports = TabboxPanel;
|
||||
|
|
|
@ -35,6 +35,7 @@ class FirefoxDataProvider {
|
|||
|
||||
// Fetching data from the backend
|
||||
this.getLongString = this.getLongString.bind(this);
|
||||
this.getRequestFromQueue = this.getRequestFromQueue.bind(this);
|
||||
|
||||
// Event handlers
|
||||
this.onNetworkEvent = this.onNetworkEvent.bind(this);
|
||||
|
@ -260,18 +261,15 @@ class FirefoxDataProvider {
|
|||
|
||||
let { payload } = this.getRequestFromQueue(id);
|
||||
|
||||
// The payload is ready when all values in the record are true. (i.e. all data
|
||||
// received, but the lazy one. responseContent is the only one for now).
|
||||
// The payload is ready when all values in the record are true.
|
||||
// Note that we never fetch response header/cookies for request with security issues.
|
||||
// Bug 1404917 should simplify this heuristic by making all these field be lazily
|
||||
// fetched, only on-demand.
|
||||
return record.requestHeaders &&
|
||||
record.requestCookies &&
|
||||
record.eventTimings &&
|
||||
return record.requestHeaders && record.requestCookies && record.eventTimings &&
|
||||
(
|
||||
(record.responseHeaders && record.responseCookies) ||
|
||||
payload.securityState == "broken" ||
|
||||
(payload.responseContentAvailable && !payload.status)
|
||||
(record.responseHeaders && record.responseCookies) ||
|
||||
payload.securityState === "broken" ||
|
||||
(!payload.status && payload.responseContentAvailable)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -384,13 +382,17 @@ class FirefoxDataProvider {
|
|||
switch (updateType) {
|
||||
case "requestHeaders":
|
||||
case "requestCookies":
|
||||
case "requestPostData":
|
||||
case "responseHeaders":
|
||||
case "responseCookies":
|
||||
this.requestPayloadData(actor, updateType);
|
||||
break;
|
||||
// (Be careful, securityState can be undefined, for example for WebSocket requests)
|
||||
// Also note that service worker don't have security info set.
|
||||
case "requestPostData":
|
||||
this.updateRequest(actor, {
|
||||
// This field helps knowing when/if requestPostData property is available
|
||||
// and can be requested via `requestData`
|
||||
requestPostDataAvailable: true
|
||||
});
|
||||
break;
|
||||
case "securityInfo":
|
||||
this.updateRequest(actor, { securityState: networkInfo.securityInfo });
|
||||
break;
|
||||
|
@ -411,7 +413,6 @@ class FirefoxDataProvider {
|
|||
contentSize: networkInfo.response.bodySize,
|
||||
transferredSize: networkInfo.response.transferredSize,
|
||||
mimeType: networkInfo.response.content.mimeType,
|
||||
|
||||
// This field helps knowing when/if responseContent property is available
|
||||
// and can be requested via `requestData`
|
||||
responseContentAvailable: true,
|
||||
|
@ -471,9 +472,8 @@ class FirefoxDataProvider {
|
|||
this.cleanUpQueue(actor);
|
||||
this.rdpRequestMap.delete(actor);
|
||||
|
||||
let { updateRequest } = this.actions;
|
||||
if (updateRequest) {
|
||||
await updateRequest(actor, payloadFromQueue, true);
|
||||
if (this.actions.updateRequest) {
|
||||
await this.actions.updateRequest(actor, payloadFromQueue, true);
|
||||
}
|
||||
|
||||
// This event is fired only once per request, once all the properties are fetched
|
||||
|
@ -498,7 +498,7 @@ class FirefoxDataProvider {
|
|||
requestData(actor, method) {
|
||||
// Key string used in `lazyRequestData`. We use this Map to prevent requesting
|
||||
// the same data twice at the same time.
|
||||
let key = actor + "-" + method;
|
||||
let key = `${actor}-${method}`;
|
||||
let promise = this.lazyRequestData.get(key);
|
||||
// If a request is pending, reuse it.
|
||||
if (promise) {
|
||||
|
@ -512,10 +512,12 @@ class FirefoxDataProvider {
|
|||
// data again.
|
||||
this.lazyRequestData.delete(key, promise);
|
||||
|
||||
let payloadFromQueue = this.getRequestFromQueue(actor).payload;
|
||||
let { updateRequest } = this.actions;
|
||||
if (updateRequest) {
|
||||
await updateRequest(actor, payloadFromQueue, true);
|
||||
if (this.actions.updateRequest) {
|
||||
await this.actions.updateRequest(
|
||||
actor,
|
||||
this.getRequestFromQueue(actor).payload,
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
|
@ -548,7 +550,12 @@ class FirefoxDataProvider {
|
|||
let response = await new Promise((resolve, reject) => {
|
||||
// Do a RDP request to fetch data from the actor.
|
||||
if (typeof this.webConsoleClient[clientMethodName] === "function") {
|
||||
this.webConsoleClient[clientMethodName](actor, (res) => {
|
||||
// Make sure we fetch the real actor data instead of cloned actor
|
||||
// e.g. CustomRequestPanel will clone a request with additional '-clone' actor id
|
||||
this.webConsoleClient[clientMethodName](actor.replace("-clone", ""), (res) => {
|
||||
if (res.error) {
|
||||
console.error(res.error);
|
||||
}
|
||||
resolve(res);
|
||||
});
|
||||
} else {
|
||||
|
@ -556,6 +563,12 @@ class FirefoxDataProvider {
|
|||
}
|
||||
});
|
||||
|
||||
// Restore clone actor id
|
||||
if (actor.includes("-clone")) {
|
||||
// Because response's properties are read-only, we create a new response
|
||||
response = { ...response, from: `${response.from}-clone` };
|
||||
}
|
||||
|
||||
// Call data processing method.
|
||||
return this[callbackMethodName](response);
|
||||
}
|
||||
|
@ -591,12 +604,12 @@ class FirefoxDataProvider {
|
|||
*
|
||||
* @param {object} response the message received from the server.
|
||||
*/
|
||||
onRequestPostData(response) {
|
||||
return this.updateRequest(response.from, {
|
||||
async onRequestPostData(response) {
|
||||
let payload = await this.updateRequest(response.from, {
|
||||
requestPostData: response
|
||||
}).then(() => {
|
||||
emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
|
||||
});
|
||||
emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -124,6 +124,7 @@ const UPDATE_PROPS = [
|
|||
"requestHeadersFromUploadStream",
|
||||
"requestCookies",
|
||||
"requestPostData",
|
||||
"requestPostDataAvailable",
|
||||
"responseHeaders",
|
||||
"responseCookies",
|
||||
"responseContent",
|
||||
|
|
|
@ -120,7 +120,7 @@ HarBuilder.prototype = {
|
|||
entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
|
||||
entry.time = file.endedMillis - file.startedMillis;
|
||||
|
||||
entry.request = this.buildRequest(file);
|
||||
entry.request = await this.buildRequest(file);
|
||||
entry.response = await this.buildResponse(file);
|
||||
entry.cache = this.buildCache(file);
|
||||
entry.timings = file.eventTimings ? file.eventTimings.timings : {};
|
||||
|
@ -152,7 +152,7 @@ HarBuilder.prototype = {
|
|||
return timings;
|
||||
},
|
||||
|
||||
buildRequest: function (file) {
|
||||
buildRequest: async function (file) {
|
||||
let request = {
|
||||
bodySize: 0
|
||||
};
|
||||
|
@ -160,25 +160,15 @@ HarBuilder.prototype = {
|
|||
request.method = file.method;
|
||||
request.url = file.url;
|
||||
request.httpVersion = file.httpVersion || "";
|
||||
|
||||
request.headers = this.buildHeaders(file.requestHeaders);
|
||||
request.headers = this.appendHeadersPostData(request.headers, file);
|
||||
request.cookies = this.buildCookies(file.requestCookies);
|
||||
|
||||
request.queryString = parseQueryString(getUrlQuery(file.url)) || [];
|
||||
|
||||
if (file.requestPostData) {
|
||||
request.postData = this.buildPostData(file);
|
||||
}
|
||||
|
||||
request.headersSize = file.requestHeaders.headersSize;
|
||||
request.postData = await this.buildPostData(file);
|
||||
|
||||
// Set request body size, but make sure the body is fetched
|
||||
// from the backend.
|
||||
if (file.requestPostData) {
|
||||
this.fetchData(file.requestPostData.postData.text).then(value => {
|
||||
request.bodySize = value.length;
|
||||
});
|
||||
if (request.postData && request.postData.text) {
|
||||
request.bodySize = request.postData.text.length;
|
||||
}
|
||||
|
||||
return request;
|
||||
|
@ -243,47 +233,57 @@ HarBuilder.prototype = {
|
|||
return result;
|
||||
},
|
||||
|
||||
buildPostData: function (file) {
|
||||
let postData = {
|
||||
mimeType: findValue(file.requestHeaders.headers, "content-type"),
|
||||
params: [],
|
||||
text: ""
|
||||
};
|
||||
buildPostData: async function (file) {
|
||||
// When using HarAutomation, HarCollector will automatically fetch requestPostData,
|
||||
// but when we use it from netmonitor, FirefoxDataProvider should fetch it itself
|
||||
// lazily, via requestData.
|
||||
let requestPostData = file.requestPostData;
|
||||
let requestHeaders = file.requestHeaders;
|
||||
let requestHeadersFromUploadStream;
|
||||
|
||||
if (!file.requestPostData) {
|
||||
return postData;
|
||||
if (!requestPostData && this._options.requestData) {
|
||||
let payload = await this._options.requestData(file.id, "requestPostData");
|
||||
requestPostData = payload.requestPostData;
|
||||
requestHeadersFromUploadStream = payload.requestHeadersFromUploadStream;
|
||||
}
|
||||
|
||||
if (file.requestPostData.postDataDiscarded) {
|
||||
if (!requestPostData.postData.text) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let postData = {
|
||||
mimeType: findValue(requestHeaders.headers, "content-type"),
|
||||
params: [],
|
||||
text: requestPostData.postData.text,
|
||||
};
|
||||
|
||||
if (requestPostData.postDataDiscarded) {
|
||||
postData.comment = L10N.getStr("har.requestBodyNotIncluded");
|
||||
return postData;
|
||||
}
|
||||
|
||||
// Load request body from the backend.
|
||||
this.fetchData(file.requestPostData.postData.text).then(postDataText => {
|
||||
postData.text = postDataText;
|
||||
// If we are dealing with URL encoded body, parse parameters.
|
||||
if (CurlUtils.isUrlEncodedRequest({
|
||||
headers: requestHeaders.headers,
|
||||
postDataText: postData.text,
|
||||
})) {
|
||||
postData.mimeType = "application/x-www-form-urlencoded";
|
||||
|
||||
// If we are dealing with URL encoded body, parse parameters.
|
||||
let { headers } = file.requestHeaders;
|
||||
if (CurlUtils.isUrlEncodedRequest({ headers, postDataText })) {
|
||||
postData.mimeType = "application/x-www-form-urlencoded";
|
||||
// Extract form parameters and produce nice HAR array.
|
||||
let formDataSections = await getFormDataSections(
|
||||
requestHeaders,
|
||||
requestHeadersFromUploadStream,
|
||||
requestPostData,
|
||||
this._options.getString,
|
||||
);
|
||||
|
||||
// Extract form parameters and produce nice HAR array.
|
||||
getFormDataSections(
|
||||
file.requestHeaders,
|
||||
file.requestHeadersFromUploadStream,
|
||||
file.requestPostData,
|
||||
this._options.getString,
|
||||
).then(formDataSections => {
|
||||
formDataSections.forEach(section => {
|
||||
let paramsArray = parseQueryString(section);
|
||||
if (paramsArray) {
|
||||
postData.params = [...postData.params, ...paramsArray];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
formDataSections.forEach((section) => {
|
||||
let paramsArray = parseQueryString(section);
|
||||
if (paramsArray) {
|
||||
postData.params = [...postData.params, ...paramsArray];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return postData;
|
||||
},
|
||||
|
|
|
@ -19,7 +19,8 @@ add_task(function* () {
|
|||
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
|
||||
let RequestListContextMenu = windowRequire(
|
||||
"devtools/client/netmonitor/src/request-list-context-menu");
|
||||
let { getLongString, getTabTarget, requestData } = connector;
|
||||
let { getSortedRequests } = windowRequire(
|
||||
"devtools/client/netmonitor/src/selectors/index");
|
||||
|
||||
store.dispatch(Actions.batchEnable(false));
|
||||
|
||||
|
@ -27,10 +28,9 @@ add_task(function* () {
|
|||
tab.linkedBrowser.reload();
|
||||
yield wait;
|
||||
|
||||
let contextMenu = new RequestListContextMenu({
|
||||
getTabTarget, getLongString, requestData });
|
||||
let contextMenu = new RequestListContextMenu({ connector });
|
||||
|
||||
yield contextMenu.copyAllAsHar();
|
||||
yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
|
||||
|
||||
let jsonString = SpecialPowers.getClipboardData("text/unicode");
|
||||
let har = JSON.parse(jsonString);
|
||||
|
|
|
@ -16,7 +16,8 @@ add_task(function* () {
|
|||
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
|
||||
let RequestListContextMenu = windowRequire(
|
||||
"devtools/client/netmonitor/src/request-list-context-menu");
|
||||
let { getLongString, getTabTarget, requestData } = connector;
|
||||
let { getSortedRequests } = windowRequire(
|
||||
"devtools/client/netmonitor/src/selectors/index");
|
||||
|
||||
store.dispatch(Actions.batchEnable(false));
|
||||
|
||||
|
@ -28,9 +29,8 @@ add_task(function* () {
|
|||
yield wait;
|
||||
|
||||
// Copy HAR into the clipboard (asynchronous).
|
||||
let contextMenu = new RequestListContextMenu({
|
||||
getTabTarget, getLongString, requestData });
|
||||
let jsonString = yield contextMenu.copyAllAsHar();
|
||||
let contextMenu = new RequestListContextMenu({ connector });
|
||||
let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
|
||||
let har = JSON.parse(jsonString);
|
||||
|
||||
// Check out the HAR log.
|
||||
|
|
|
@ -16,7 +16,8 @@ add_task(function* () {
|
|||
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
|
||||
let RequestListContextMenu = windowRequire(
|
||||
"devtools/client/netmonitor/src/request-list-context-menu");
|
||||
let { getLongString, getTabTarget, requestData } = connector;
|
||||
let { getSortedRequests } = windowRequire(
|
||||
"devtools/client/netmonitor/src/selectors/index");
|
||||
|
||||
store.dispatch(Actions.batchEnable(false));
|
||||
|
||||
|
@ -28,9 +29,8 @@ add_task(function* () {
|
|||
yield wait;
|
||||
|
||||
// Copy HAR into the clipboard (asynchronous).
|
||||
let contextMenu = new RequestListContextMenu({
|
||||
getTabTarget, getLongString, requestData });
|
||||
let jsonString = yield contextMenu.copyAllAsHar();
|
||||
let contextMenu = new RequestListContextMenu({ connector });
|
||||
let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
|
||||
let har = JSON.parse(jsonString);
|
||||
|
||||
// Check out the HAR log.
|
||||
|
@ -39,8 +39,8 @@ add_task(function* () {
|
|||
is(har.log.entries.length, 1, "There must be one request");
|
||||
|
||||
let entry = har.log.entries[0];
|
||||
is(entry.request.postData, undefined,
|
||||
"Check post data is not present");
|
||||
|
||||
is(entry.request.postData, undefined, "Check post data is not present");
|
||||
|
||||
// Clean up
|
||||
return teardown(monitor);
|
||||
|
|
|
@ -20,7 +20,8 @@ function* throttleUploadTest(actuallyThrottle) {
|
|||
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
|
||||
let RequestListContextMenu = windowRequire(
|
||||
"devtools/client/netmonitor/src/request-list-context-menu");
|
||||
let { getLongString, getTabTarget, setPreferences, requestData } = connector;
|
||||
let { getSortedRequests } = windowRequire(
|
||||
"devtools/client/netmonitor/src/selectors/index");
|
||||
|
||||
store.dispatch(Actions.batchEnable(false));
|
||||
|
||||
|
@ -40,7 +41,7 @@ function* throttleUploadTest(actuallyThrottle) {
|
|||
|
||||
info("sending throttle request");
|
||||
yield new Promise((resolve) => {
|
||||
setPreferences(request, (response) => {
|
||||
connector.setPreferences(request, (response) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
|
@ -53,9 +54,8 @@ function* throttleUploadTest(actuallyThrottle) {
|
|||
yield wait;
|
||||
|
||||
// Copy HAR into the clipboard (asynchronous).
|
||||
let contextMenu = new RequestListContextMenu({
|
||||
getTabTarget, getLongString, requestData });
|
||||
let jsonString = yield contextMenu.copyAllAsHar();
|
||||
let contextMenu = new RequestListContextMenu({ connector });
|
||||
let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
|
||||
let har = JSON.parse(jsonString);
|
||||
|
||||
// Check out the HAR log.
|
||||
|
|
|
@ -134,6 +134,7 @@ function requestsReducer(state = Requests(), action) {
|
|||
urlDetails: clonedRequest.urlDetails,
|
||||
requestHeaders: clonedRequest.requestHeaders,
|
||||
requestPostData: clonedRequest.requestPostData,
|
||||
requestPostDataAvailable: clonedRequest.requestPostDataAvailable,
|
||||
isCustom: true
|
||||
};
|
||||
|
||||
|
|
|
@ -9,79 +9,72 @@ const { Curl } = require("devtools/client/shared/curl");
|
|||
const { gDevTools } = require("devtools/client/framework/devtools");
|
||||
const { saveAs } = require("devtools/client/shared/file-saver");
|
||||
const { copyString } = require("devtools/shared/platform/clipboard");
|
||||
const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
|
||||
const { HarExporter } = require("./har/har-exporter");
|
||||
const {
|
||||
getSelectedRequest,
|
||||
getSortedRequests,
|
||||
} = require("./selectors/index");
|
||||
const { L10N } = require("./utils/l10n");
|
||||
const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
|
||||
const {
|
||||
getUrlQuery,
|
||||
parseQueryString,
|
||||
getUrlBaseName,
|
||||
formDataURI,
|
||||
getUrlQuery,
|
||||
getUrlBaseName,
|
||||
parseQueryString,
|
||||
} = require("./utils/request-utils");
|
||||
|
||||
function RequestListContextMenu({
|
||||
cloneSelectedRequest,
|
||||
getLongString,
|
||||
getTabTarget,
|
||||
openStatistics,
|
||||
requestData,
|
||||
}) {
|
||||
this.cloneSelectedRequest = cloneSelectedRequest;
|
||||
this.getLongString = getLongString;
|
||||
this.getTabTarget = getTabTarget;
|
||||
this.openStatistics = openStatistics;
|
||||
this.requestData = requestData;
|
||||
}
|
||||
class RequestListContextMenu {
|
||||
constructor(props) {
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
RequestListContextMenu.prototype = {
|
||||
get selectedRequest() {
|
||||
open(event) {
|
||||
// FIXME: Bug 1336382 - Implement RequestListContextMenu React component
|
||||
// Remove window.store
|
||||
return getSelectedRequest(window.store.getState());
|
||||
},
|
||||
// Remove window.store.getState()
|
||||
let selectedRequest = getSelectedRequest(window.store.getState());
|
||||
let sortedRequests = getSortedRequests(window.store.getState());
|
||||
|
||||
get sortedRequests() {
|
||||
// FIXME: Bug 1336382 - Implement RequestListContextMenu React component
|
||||
// Remove window.store
|
||||
return getSortedRequests(window.store.getState());
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the context menu opening. Hide items if no request is selected.
|
||||
* Since visible attribute only accept boolean value but the method call may
|
||||
* return undefined, we use !! to force convert any object to boolean
|
||||
*/
|
||||
open(event = {}) {
|
||||
let selectedRequest = this.selectedRequest;
|
||||
let menu = [];
|
||||
let copySubmenu = [];
|
||||
let {
|
||||
id,
|
||||
isCustom,
|
||||
method,
|
||||
mimeType,
|
||||
httpVersion,
|
||||
requestHeaders,
|
||||
requestPostDataAvailable,
|
||||
responseHeaders,
|
||||
responseContentAvailable,
|
||||
url,
|
||||
} = selectedRequest || {};
|
||||
let {
|
||||
cloneSelectedRequest,
|
||||
openStatistics,
|
||||
} = this.props;
|
||||
|
||||
copySubmenu.push({
|
||||
id: "request-list-context-copy-url",
|
||||
label: L10N.getStr("netmonitor.context.copyUrl"),
|
||||
accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
|
||||
visible: !!selectedRequest,
|
||||
click: () => this.copyUrl(),
|
||||
click: () => this.copyUrl(url),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
id: "request-list-context-copy-url-params",
|
||||
label: L10N.getStr("netmonitor.context.copyUrlParams"),
|
||||
accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
|
||||
visible: !!(selectedRequest && getUrlQuery(selectedRequest.url)),
|
||||
click: () => this.copyUrlParams(),
|
||||
visible: !!(selectedRequest && getUrlQuery(url)),
|
||||
click: () => this.copyUrlParams(url),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
id: "request-list-context-copy-post-data",
|
||||
label: L10N.getStr("netmonitor.context.copyPostData"),
|
||||
accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
|
||||
visible: !!(selectedRequest && selectedRequest.requestPostData),
|
||||
click: () => this.copyPostData(),
|
||||
visible: !!(selectedRequest && requestPostDataAvailable),
|
||||
click: () => this.copyPostData(id),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
|
@ -89,59 +82,57 @@ RequestListContextMenu.prototype = {
|
|||
label: L10N.getStr("netmonitor.context.copyAsCurl"),
|
||||
accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
|
||||
visible: !!selectedRequest,
|
||||
click: () => this.copyAsCurl(),
|
||||
click: () => this.copyAsCurl(id, url, method, requestHeaders, httpVersion),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
type: "separator",
|
||||
visible: !!selectedRequest,
|
||||
visible: copySubmenu.slice(0, 4).some((subMenu) => subMenu.visible),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
id: "request-list-context-copy-request-headers",
|
||||
label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
|
||||
accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
|
||||
visible: !!(selectedRequest && selectedRequest.requestHeaders),
|
||||
click: () => this.copyRequestHeaders(),
|
||||
visible: !!(selectedRequest && requestHeaders && requestHeaders.rawHeaders),
|
||||
click: () => this.copyRequestHeaders(requestHeaders.rawHeaders.trim()),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
id: "response-list-context-copy-response-headers",
|
||||
label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
|
||||
accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
|
||||
visible: !!(selectedRequest && selectedRequest.responseHeaders),
|
||||
click: () => this.copyResponseHeaders(),
|
||||
visible: !!(selectedRequest && responseHeaders && responseHeaders.rawHeaders),
|
||||
click: () => this.copyResponseHeaders(responseHeaders.rawHeaders.trim()),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
id: "request-list-context-copy-response",
|
||||
label: L10N.getStr("netmonitor.context.copyResponse"),
|
||||
accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
|
||||
visible: !!(selectedRequest && selectedRequest.responseContentAvailable),
|
||||
click: () => this.copyResponse(),
|
||||
visible: !!(selectedRequest && responseContentAvailable),
|
||||
click: () => this.copyResponse(id),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
id: "request-list-context-copy-image-as-data-uri",
|
||||
label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
|
||||
accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
|
||||
visible: !!(selectedRequest &&
|
||||
selectedRequest.mimeType &&
|
||||
selectedRequest.mimeType.includes("image/")),
|
||||
click: () => this.copyImageAsDataUri(),
|
||||
visible: !!(selectedRequest && mimeType && mimeType.includes("image/")),
|
||||
click: () => this.copyImageAsDataUri(id, mimeType),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
type: "separator",
|
||||
visible: !!selectedRequest,
|
||||
visible: copySubmenu.slice(5, 9).some((subMenu) => subMenu.visible),
|
||||
});
|
||||
|
||||
copySubmenu.push({
|
||||
id: "request-list-context-copy-all-as-har",
|
||||
label: L10N.getStr("netmonitor.context.copyAllAsHar"),
|
||||
accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
|
||||
visible: this.sortedRequests.size > 0,
|
||||
click: () => this.copyAllAsHar(),
|
||||
visible: sortedRequests.size > 0,
|
||||
click: () => this.copyAllAsHar(sortedRequests),
|
||||
});
|
||||
|
||||
menu.push({
|
||||
|
@ -155,36 +146,34 @@ RequestListContextMenu.prototype = {
|
|||
id: "request-list-context-save-all-as-har",
|
||||
label: L10N.getStr("netmonitor.context.saveAllAsHar"),
|
||||
accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
|
||||
visible: this.sortedRequests.size > 0,
|
||||
click: () => this.saveAllAsHar(),
|
||||
visible: sortedRequests.size > 0,
|
||||
click: () => this.saveAllAsHar(sortedRequests),
|
||||
});
|
||||
|
||||
menu.push({
|
||||
id: "request-list-context-save-image-as",
|
||||
label: L10N.getStr("netmonitor.context.saveImageAs"),
|
||||
accesskey: L10N.getStr("netmonitor.context.saveImageAs.accesskey"),
|
||||
visible: !!(selectedRequest &&
|
||||
selectedRequest.mimeType &&
|
||||
selectedRequest.mimeType.includes("image/")),
|
||||
click: () => this.saveImageAs(),
|
||||
visible: !!(selectedRequest && mimeType && mimeType.includes("image/")),
|
||||
click: () => this.saveImageAs(id, url),
|
||||
});
|
||||
|
||||
menu.push({
|
||||
type: "separator",
|
||||
visible: !!(selectedRequest && !selectedRequest.isCustom),
|
||||
visible: copySubmenu.slice(10, 14).some((subMenu) => subMenu.visible),
|
||||
});
|
||||
|
||||
menu.push({
|
||||
id: "request-list-context-resend",
|
||||
label: L10N.getStr("netmonitor.context.editAndResend"),
|
||||
accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
|
||||
visible: !!(selectedRequest && !selectedRequest.isCustom),
|
||||
click: this.cloneSelectedRequest,
|
||||
visible: !!(selectedRequest && !isCustom),
|
||||
click: cloneSelectedRequest,
|
||||
});
|
||||
|
||||
menu.push({
|
||||
type: "separator",
|
||||
visible: !!selectedRequest,
|
||||
visible: copySubmenu.slice(15, 16).some((subMenu) => subMenu.visible),
|
||||
});
|
||||
|
||||
menu.push({
|
||||
|
@ -192,17 +181,15 @@ RequestListContextMenu.prototype = {
|
|||
label: L10N.getStr("netmonitor.context.newTab"),
|
||||
accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
|
||||
visible: !!selectedRequest,
|
||||
click: () => this.openRequestInTab()
|
||||
click: () => this.openRequestInTab(url),
|
||||
});
|
||||
|
||||
menu.push({
|
||||
id: "request-list-context-open-in-debugger",
|
||||
label: L10N.getStr("netmonitor.context.openInDebugger"),
|
||||
accesskey: L10N.getStr("netmonitor.context.openInDebugger.accesskey"),
|
||||
visible: !!(selectedRequest &&
|
||||
selectedRequest.mimeType &&
|
||||
selectedRequest.mimeType.includes("javascript")),
|
||||
click: () => this.openInDebugger()
|
||||
visible: !!(selectedRequest && mimeType && mimeType.includes("javascript")),
|
||||
click: () => this.openInDebugger(url),
|
||||
});
|
||||
|
||||
menu.push({
|
||||
|
@ -210,71 +197,71 @@ RequestListContextMenu.prototype = {
|
|||
label: L10N.getStr("netmonitor.context.openInStyleEditor"),
|
||||
accesskey: L10N.getStr("netmonitor.context.openInStyleEditor.accesskey"),
|
||||
visible: !!(selectedRequest &&
|
||||
Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
|
||||
selectedRequest.mimeType &&
|
||||
selectedRequest.mimeType.includes("css")),
|
||||
click: () => this.openInStyleEditor()
|
||||
Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
|
||||
mimeType && mimeType.includes("css")),
|
||||
click: () => this.openInStyleEditor(url),
|
||||
});
|
||||
|
||||
menu.push({
|
||||
id: "request-list-context-perf",
|
||||
label: L10N.getStr("netmonitor.context.perfTools"),
|
||||
accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
|
||||
visible: this.sortedRequests.size > 0,
|
||||
click: () => this.openStatistics(true)
|
||||
visible: sortedRequests.size > 0,
|
||||
click: () => openStatistics(true),
|
||||
});
|
||||
|
||||
return showMenu(event, menu);
|
||||
},
|
||||
showMenu(event, menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens selected item in a new tab.
|
||||
*/
|
||||
openRequestInTab() {
|
||||
openRequestInTab(url) {
|
||||
let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
|
||||
win.openUILinkIn(this.selectedRequest.url, "tab", { relatedToCurrent: true });
|
||||
},
|
||||
win.openUILinkIn(url, "tab", { relatedToCurrent: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens selected item in the debugger
|
||||
*/
|
||||
openInDebugger() {
|
||||
let toolbox = gDevTools.getToolbox(this.getTabTarget());
|
||||
toolbox.viewSourceInDebugger(this.selectedRequest.url, 0);
|
||||
},
|
||||
openInDebugger(url) {
|
||||
let toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
|
||||
toolbox.viewSourceInDebugger(url, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens selected item in the style editor
|
||||
*/
|
||||
openInStyleEditor() {
|
||||
let toolbox = gDevTools.getToolbox(this.getTabTarget());
|
||||
toolbox.viewSourceInStyleEditor(this.selectedRequest.url, 0);
|
||||
},
|
||||
openInStyleEditor(url) {
|
||||
let toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
|
||||
toolbox.viewSourceInStyleEditor(url, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the request url from the currently selected item.
|
||||
*/
|
||||
copyUrl() {
|
||||
copyString(this.selectedRequest.url);
|
||||
},
|
||||
copyUrl(url) {
|
||||
copyString(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the request url query string parameters from the currently
|
||||
* selected item.
|
||||
*/
|
||||
copyUrlParams() {
|
||||
let params = getUrlQuery(this.selectedRequest.url).split("&");
|
||||
copyUrlParams(url) {
|
||||
let params = getUrlQuery(url).split("&");
|
||||
copyString(params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n"));
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the request form data parameters (or raw payload) from
|
||||
* the currently selected item.
|
||||
*/
|
||||
copyPostData() {
|
||||
let { formDataSections, requestPostData } = this.selectedRequest;
|
||||
async copyPostData(id) {
|
||||
// FIXME: Bug 1336382 - Implement RequestListContextMenu React component
|
||||
// Remove window.store.getState()
|
||||
let { formDataSections } = getSelectedRequest(window.store.getState());
|
||||
let params = [];
|
||||
|
||||
// Try to extract any form data parameters.
|
||||
formDataSections.forEach(section => {
|
||||
let paramsArray = parseQueryString(section);
|
||||
|
@ -289,72 +276,69 @@ RequestListContextMenu.prototype = {
|
|||
|
||||
// Fall back to raw payload.
|
||||
if (!string) {
|
||||
let { requestPostData } = await this.props.connector
|
||||
.requestData(id, "requestPostData");
|
||||
string = requestPostData.postData.text;
|
||||
if (Services.appinfo.OS !== "WINNT") {
|
||||
string = string.replace(/\r/g, "");
|
||||
}
|
||||
}
|
||||
copyString(string);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a cURL command from the currently selected item.
|
||||
*/
|
||||
copyAsCurl() {
|
||||
let selected = this.selectedRequest;
|
||||
async copyAsCurl(id, url, method, requestHeaders, httpVersion) {
|
||||
let { requestPostData } = await this.props.connector
|
||||
.requestData(id, "requestPostData");
|
||||
// Create a sanitized object for the Curl command generator.
|
||||
let data = {
|
||||
url: selected.url,
|
||||
method: selected.method,
|
||||
headers: selected.requestHeaders.headers,
|
||||
httpVersion: selected.httpVersion,
|
||||
postDataText: selected.requestPostData && selected.requestPostData.postData.text,
|
||||
url,
|
||||
method,
|
||||
headers: requestHeaders.headers,
|
||||
httpVersion: httpVersion,
|
||||
postDataText: requestPostData ? requestPostData.postData.text : "",
|
||||
};
|
||||
copyString(Curl.generateCommand(data));
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the raw request headers from the currently selected item.
|
||||
*/
|
||||
copyRequestHeaders() {
|
||||
let rawHeaders = this.selectedRequest.requestHeaders.rawHeaders.trim();
|
||||
copyRequestHeaders(rawHeaders) {
|
||||
if (Services.appinfo.OS !== "WINNT") {
|
||||
rawHeaders = rawHeaders.replace(/\r/g, "");
|
||||
}
|
||||
copyString(rawHeaders);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the raw response headers from the currently selected item.
|
||||
*/
|
||||
copyResponseHeaders() {
|
||||
let rawHeaders = this.selectedRequest.responseHeaders.rawHeaders.trim();
|
||||
copyResponseHeaders(rawHeaders) {
|
||||
if (Services.appinfo.OS !== "WINNT") {
|
||||
rawHeaders = rawHeaders.replace(/\r/g, "");
|
||||
}
|
||||
copyString(rawHeaders);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy image as data uri.
|
||||
*/
|
||||
async copyImageAsDataUri() {
|
||||
let responseContent = await this.requestData(this.selectedRequest.id,
|
||||
"responseContent");
|
||||
let { mimeType } = this.selectedRequest;
|
||||
async copyImageAsDataUri(id, mimeType) {
|
||||
let responseContent = await this.props.connector.requestData(id, "responseContent");
|
||||
let { encoding, text } = responseContent.content;
|
||||
let src = formDataURI(mimeType, encoding, text);
|
||||
copyString(src);
|
||||
},
|
||||
copyString(formDataURI(mimeType, encoding, text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save image as.
|
||||
*/
|
||||
async saveImageAs() {
|
||||
let responseContent = await this.requestData(this.selectedRequest.id,
|
||||
"responseContent");
|
||||
async saveImageAs(id, url) {
|
||||
let responseContent = await this.props.connector.requestData(id, "responseContent");
|
||||
let { encoding, text } = responseContent.content;
|
||||
let fileName = getUrlBaseName(this.selectedRequest.url);
|
||||
let fileName = getUrlBaseName(url);
|
||||
let data;
|
||||
if (encoding === "base64") {
|
||||
let decoded = atob(text);
|
||||
|
@ -366,46 +350,45 @@ RequestListContextMenu.prototype = {
|
|||
data = text;
|
||||
}
|
||||
saveAs(new Blob([data]), fileName, document);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy response data as a string.
|
||||
*/
|
||||
async copyResponse() {
|
||||
let responseContent = await this.requestData(this.selectedRequest.id,
|
||||
"responseContent");
|
||||
async copyResponse(id) {
|
||||
let responseContent = await this.props.connector.requestData(id, "responseContent");
|
||||
copyString(responseContent.content.text);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy HAR from the network panel content to the clipboard.
|
||||
*/
|
||||
copyAllAsHar() {
|
||||
return HarExporter.copy(this.getDefaultHarOptions());
|
||||
},
|
||||
copyAllAsHar(sortedRequests) {
|
||||
return HarExporter.copy(this.getDefaultHarOptions(sortedRequests));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save HAR from the network panel content to a file.
|
||||
*/
|
||||
saveAllAsHar() {
|
||||
// FIXME: This will not work in launchpad
|
||||
saveAllAsHar(sortedRequests) {
|
||||
// This will not work in launchpad
|
||||
// document.execCommand(‘cut’/‘copy’) was denied because it was not called from
|
||||
// inside a short running user-generated event handler.
|
||||
// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
|
||||
return HarExporter.save(this.getDefaultHarOptions());
|
||||
},
|
||||
return HarExporter.save(this.getDefaultHarOptions(sortedRequests));
|
||||
}
|
||||
|
||||
getDefaultHarOptions() {
|
||||
let form = this.getTabTarget().form;
|
||||
let title = form.title || form.url;
|
||||
getDefaultHarOptions(sortedRequests) {
|
||||
let { getLongString, getTabTarget, requestData } = this.props.connector;
|
||||
let { form: { title, url } } = getTabTarget();
|
||||
|
||||
return {
|
||||
requestData: this.requestData,
|
||||
getString: this.getLongString,
|
||||
items: this.sortedRequests,
|
||||
title: title
|
||||
getString: getLongString,
|
||||
items: sortedRequests,
|
||||
requestData,
|
||||
title: title || url,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = RequestListContextMenu;
|
||||
|
|
|
@ -18,7 +18,10 @@ add_task(function* () {
|
|||
let {
|
||||
getSortedRequests,
|
||||
} = windowRequire("devtools/client/netmonitor/src/selectors/index");
|
||||
let { getLongString } = connector;
|
||||
let {
|
||||
getLongString,
|
||||
requestData,
|
||||
} = connector;
|
||||
|
||||
store.dispatch(Actions.batchEnable(false));
|
||||
|
||||
|
@ -35,22 +38,22 @@ add_task(function* () {
|
|||
multipartForm: getSortedRequests(store.getState()).get(3),
|
||||
};
|
||||
|
||||
let data = yield createCurlData(requests.get, getLongString);
|
||||
let data = yield createCurlData(requests.get, getLongString, requestData);
|
||||
testFindHeader(data);
|
||||
|
||||
data = yield createCurlData(requests.post, getLongString);
|
||||
data = yield createCurlData(requests.post, getLongString, requestData);
|
||||
testIsUrlEncodedRequest(data);
|
||||
testWritePostDataTextParams(data);
|
||||
testWriteEmptyPostDataTextParams(data);
|
||||
testDataArgumentOnGeneratedCommand(data);
|
||||
|
||||
data = yield createCurlData(requests.multipart, getLongString);
|
||||
data = yield createCurlData(requests.multipart, getLongString, requestData);
|
||||
testIsMultipartRequest(data);
|
||||
testGetMultipartBoundary(data);
|
||||
testMultiPartHeaders(data);
|
||||
testRemoveBinaryDataFromMultipartText(data);
|
||||
|
||||
data = yield createCurlData(requests.multipartForm, getLongString);
|
||||
data = yield createCurlData(requests.multipartForm, getLongString, requestData);
|
||||
testMultiPartHeaders(data);
|
||||
|
||||
testGetHeadersFromMultipartText({
|
||||
|
@ -231,7 +234,7 @@ function testEscapeStringWin() {
|
|||
"Newlines should be escaped.");
|
||||
}
|
||||
|
||||
function* createCurlData(selected, getLongString) {
|
||||
function* createCurlData(selected, getLongString, requestData) {
|
||||
let { url, method, httpVersion } = selected;
|
||||
|
||||
// Create a sanitized object for the Curl command generator.
|
||||
|
@ -249,9 +252,10 @@ function* createCurlData(selected, getLongString) {
|
|||
data.headers.push({ name: name, value: text });
|
||||
}
|
||||
|
||||
let { requestPostData } = yield requestData(selected.id, "requestPostData");
|
||||
// Fetch the request payload.
|
||||
if (selected.requestPostData) {
|
||||
let postData = selected.requestPostData.postData.text;
|
||||
if (requestPostData) {
|
||||
let postData = requestPostData.postData.text;
|
||||
data.postDataText = yield getLongString(postData);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ add_task(function* () {
|
|||
info("Starting test... ");
|
||||
|
||||
let { document, store, windowRequire } = monitor.panelWin;
|
||||
|
||||
let contextMenuDoc = monitor.panelWin.parent.document;
|
||||
// Avoid async processing
|
||||
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
|
||||
store.dispatch(Actions.batchEnable(false));
|
||||
|
@ -23,14 +23,15 @@ add_task(function* () {
|
|||
});
|
||||
yield wait;
|
||||
|
||||
wait = waitForDOM(contextMenuDoc, "#request-list-context-open-in-debugger");
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
document.querySelectorAll(".request-list-item")[2]);
|
||||
EventUtils.sendMouseEvent({ type: "contextmenu" },
|
||||
document.querySelectorAll(".request-list-item")[2]);
|
||||
yield wait;
|
||||
|
||||
let onDebuggerReady = toolbox.once("jsdebugger-ready");
|
||||
monitor.panelWin.parent.document
|
||||
.querySelector("#request-list-context-open-in-debugger").click();
|
||||
contextMenuDoc.querySelector("#request-list-context-open-in-debugger").click();
|
||||
yield onDebuggerReady;
|
||||
|
||||
ok(true, "Debugger has been open");
|
||||
|
|
|
@ -12,7 +12,7 @@ add_task(function* () {
|
|||
info("Starting test... ");
|
||||
|
||||
let { document, store, windowRequire } = monitor.panelWin;
|
||||
|
||||
let contextMenuDoc = monitor.panelWin.parent.document;
|
||||
// Avoid async processing
|
||||
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
|
||||
store.dispatch(Actions.batchEnable(false));
|
||||
|
@ -23,10 +23,12 @@ add_task(function* () {
|
|||
});
|
||||
yield wait;
|
||||
|
||||
wait = waitForDOM(contextMenuDoc, "#request-list-context-open-in-style-editor");
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
document.querySelectorAll(".request-list-item")[1]);
|
||||
EventUtils.sendMouseEvent({ type: "contextmenu" },
|
||||
document.querySelectorAll(".request-list-item")[1]);
|
||||
yield wait;
|
||||
|
||||
let onStyleEditorReady = toolbox.once("styleeditor-ready");
|
||||
monitor.panelWin.parent.document
|
||||
|
|
|
@ -12,6 +12,7 @@ add_task(function* () {
|
|||
info("Starting test...");
|
||||
|
||||
let { document, store, windowRequire } = monitor.panelWin;
|
||||
let contextMenuDoc = monitor.panelWin.parent.document;
|
||||
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
|
||||
|
||||
store.dispatch(Actions.batchEnable(false));
|
||||
|
@ -22,10 +23,12 @@ add_task(function* () {
|
|||
});
|
||||
yield wait;
|
||||
|
||||
wait = waitForDOM(contextMenuDoc, "#request-list-context-newtab");
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
document.querySelectorAll(".request-list-item")[0]);
|
||||
EventUtils.sendMouseEvent({ type: "contextmenu" },
|
||||
document.querySelectorAll(".request-list-item")[0]);
|
||||
yield wait;
|
||||
|
||||
let onTabOpen = once(gBrowser.tabContainer, "TabOpen", false);
|
||||
monitor.panelWin.parent.document
|
||||
|
|
|
@ -26,7 +26,7 @@ add_task(function* () {
|
|||
yield wait;
|
||||
|
||||
// Wait for all tree view updated by react
|
||||
wait = waitForDOM(document, "#headers-panel");
|
||||
wait = waitForDOM(document, "#headers-panel .tree-section .treeLabel", 3);
|
||||
EventUtils.sendMouseEvent({ type: "click" },
|
||||
document.querySelector(".network-details-panel-toggle"));
|
||||
EventUtils.sendMouseEvent({ type: "click" },
|
||||
|
@ -34,7 +34,6 @@ add_task(function* () {
|
|||
yield wait;
|
||||
|
||||
let tabpanel = document.querySelector("#headers-panel");
|
||||
|
||||
is(tabpanel.querySelectorAll(".tree-section .treeLabel").length, 3,
|
||||
"There should be 3 header sections displayed in this tabpanel.");
|
||||
|
||||
|
|
|
@ -30,9 +30,11 @@ add_task(function* () {
|
|||
document.querySelectorAll(".request-list-item")[0]);
|
||||
yield wait;
|
||||
|
||||
let onRequestPostData = monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_POST_DATA);
|
||||
wait = waitForDOM(document, ".raw-headers-container textarea", 2);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersButton());
|
||||
yield wait;
|
||||
yield onRequestPostData;
|
||||
|
||||
testRawHeaderButtonStyle(true);
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ add_task(function* () {
|
|||
info("Starting test... ");
|
||||
|
||||
let { document, store, windowRequire, connector } = monitor.panelWin;
|
||||
let { requestData } = connector;
|
||||
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
|
||||
let {
|
||||
getSelectedRequest,
|
||||
|
@ -36,8 +37,8 @@ add_task(function* () {
|
|||
store.dispatch(Actions.selectRequest(origItem.id));
|
||||
|
||||
// add a new custom request cloned from selected request
|
||||
store.dispatch(Actions.cloneSelectedRequest());
|
||||
|
||||
store.dispatch(Actions.cloneSelectedRequest());
|
||||
testCustomForm(origItem);
|
||||
|
||||
let customItem = getSelectedRequest(store.getState());
|
||||
|
@ -45,6 +46,7 @@ add_task(function* () {
|
|||
|
||||
// edit the custom request
|
||||
yield editCustomForm();
|
||||
|
||||
// FIXME: reread the customItem, it's been replaced by a new object (immutable!)
|
||||
customItem = getSelectedRequest(store.getState());
|
||||
testCustomItemChanged(customItem, origItem);
|
||||
|
@ -55,7 +57,8 @@ add_task(function* () {
|
|||
yield wait;
|
||||
|
||||
let sentItem = getSelectedRequest(store.getState());
|
||||
testSentRequest(sentItem, origItem);
|
||||
|
||||
yield testSentRequest(sentItem, origItem);
|
||||
|
||||
// Ensure the UI shows the new request, selected, and that the detail panel was closed.
|
||||
is(getSortedRequests(store.getState()).length, 3, "There are 3 requests shown");
|
||||
|
@ -142,14 +145,15 @@ add_task(function* () {
|
|||
postData.focus();
|
||||
yield postFocus;
|
||||
|
||||
// add to POST data
|
||||
// add to POST data once textarea has updated
|
||||
yield waitUntil(() => postData.textContent !== "");
|
||||
type(ADD_POSTDATA);
|
||||
}
|
||||
|
||||
/*
|
||||
* Make sure newly created event matches expected request
|
||||
*/
|
||||
function testSentRequest(data, origData) {
|
||||
function* testSentRequest(data, origData) {
|
||||
is(data.method, origData.method, "correct method in sent request");
|
||||
is(data.url, origData.url + "&" + ADD_QUERY, "correct url in sent request");
|
||||
|
||||
|
@ -160,9 +164,14 @@ add_task(function* () {
|
|||
let hasUAHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_UA_HEADER);
|
||||
ok(hasUAHeader, "User-Agent header added to sent request");
|
||||
|
||||
is(data.requestPostData.postData.text,
|
||||
origData.requestPostData.postData.text + ADD_POSTDATA,
|
||||
"post data added to sent request");
|
||||
let { requestPostData: clonedRequestPostData } = yield requestData(data.id,
|
||||
"requestPostData");
|
||||
let { requestPostData: origRequestPostData } = yield requestData(origData.id,
|
||||
"requestPostData");
|
||||
|
||||
is(clonedRequestPostData.postData.text,
|
||||
origRequestPostData.postData.text + ADD_POSTDATA,
|
||||
"post data added to sent request");
|
||||
}
|
||||
|
||||
function type(string) {
|
||||
|
|
|
@ -63,10 +63,11 @@ add_task(function* () {
|
|||
is(item.status, 200, `The ${item.method} response has the right status`);
|
||||
|
||||
if (item.method === "POST") {
|
||||
// Force fetching response content
|
||||
// Force fetching lazy load data
|
||||
let responseContent = yield connector.requestData(item.id, "responseContent");
|
||||
let { requestPostData } = yield connector.requestData(item.id, "requestPostData");
|
||||
|
||||
is(item.requestPostData.postData.text, "post-data",
|
||||
is(requestPostData.postData.text, "post-data",
|
||||
"The POST request has the right POST data");
|
||||
// eslint-disable-next-line mozilla/no-cpows-in-tests
|
||||
is(responseContent.content.text, "Access-Control-Allow-Origin: *",
|
||||
|
|
|
@ -303,15 +303,12 @@ function waitForNetworkEvents(monitor, getRequests, postRequests = 0) {
|
|||
let { getNetworkRequest } = panel.connector;
|
||||
let progress = {};
|
||||
let genericEvents = 0;
|
||||
let postEvents = 0;
|
||||
let payloadReady = 0;
|
||||
let awaitedEventsToListeners = [
|
||||
["UPDATING_REQUEST_HEADERS", onGenericEvent],
|
||||
["RECEIVED_REQUEST_HEADERS", onGenericEvent],
|
||||
["UPDATING_REQUEST_COOKIES", onGenericEvent],
|
||||
["RECEIVED_REQUEST_COOKIES", onGenericEvent],
|
||||
["UPDATING_REQUEST_POST_DATA", onPostEvent],
|
||||
["RECEIVED_REQUEST_POST_DATA", onPostEvent],
|
||||
["UPDATING_RESPONSE_HEADERS", onGenericEvent],
|
||||
["RECEIVED_RESPONSE_HEADERS", onGenericEvent],
|
||||
["UPDATING_RESPONSE_COOKIES", onGenericEvent],
|
||||
|
@ -322,8 +319,6 @@ function waitForNetworkEvents(monitor, getRequests, postRequests = 0) {
|
|||
];
|
||||
let expectedGenericEvents = awaitedEventsToListeners
|
||||
.filter(([, listener]) => listener == onGenericEvent).length;
|
||||
let expectedPostEvents = awaitedEventsToListeners
|
||||
.filter(([, listener]) => listener == onPostEvent).length;
|
||||
|
||||
function initProgressForURL(url) {
|
||||
if (progress[url]) {
|
||||
|
@ -351,17 +346,6 @@ function waitForNetworkEvents(monitor, getRequests, postRequests = 0) {
|
|||
maybeResolve(event, actor, networkInfo);
|
||||
}
|
||||
|
||||
function onPostEvent(event, actor) {
|
||||
let networkInfo = getNetworkRequest(actor);
|
||||
if (!networkInfo) {
|
||||
// Must have been related to reloading document to disable cache.
|
||||
// Ignore the event.
|
||||
return;
|
||||
}
|
||||
postEvents++;
|
||||
maybeResolve(event, actor, networkInfo);
|
||||
}
|
||||
|
||||
function onPayloadReady(event, actor) {
|
||||
let networkInfo = getNetworkRequest(actor);
|
||||
if (!networkInfo) {
|
||||
|
@ -379,7 +363,6 @@ function waitForNetworkEvents(monitor, getRequests, postRequests = 0) {
|
|||
"Payload: " + payloadReady + "/" + (getRequests + postRequests) + ", " +
|
||||
"Generic: " + genericEvents + "/" +
|
||||
((getRequests + postRequests) * expectedGenericEvents) + ", " +
|
||||
"Post: " + postEvents + "/" + (postRequests * expectedPostEvents) + ", " +
|
||||
"got " + event + " for " + actor);
|
||||
|
||||
let url = networkInfo.request.url;
|
||||
|
@ -392,8 +375,7 @@ function waitForNetworkEvents(monitor, getRequests, postRequests = 0) {
|
|||
// to be considered finished. The "requestPostData" packet isn't fired for non-POST
|
||||
// requests.
|
||||
if (payloadReady >= (getRequests + postRequests) &&
|
||||
genericEvents >= (getRequests + postRequests) * expectedGenericEvents &&
|
||||
postEvents >= postRequests * expectedPostEvents) {
|
||||
genericEvents >= (getRequests + postRequests) * expectedGenericEvents) {
|
||||
awaitedEventsToListeners.forEach(([e, l]) => panel.off(EVENTS[e], l));
|
||||
executeSoon(resolve);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,356 @@
|
|||
/* 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";
|
||||
|
||||
const { PureComponent } = require("devtools/client/shared/vendor/react");
|
||||
const { div, button } = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
||||
|
||||
/**
|
||||
* The recordingState is one of the following:
|
||||
**/
|
||||
|
||||
// The initial state before we've queried the PerfActor
|
||||
const NOT_YET_KNOWN = "not-yet-known";
|
||||
// The profiler is available, we haven't started recording yet.
|
||||
const AVAILABLE_TO_RECORD = "available-to-record";
|
||||
// An async request has been sent to start the profiler.
|
||||
const REQUEST_TO_START_RECORDING = "request-to-start-recording";
|
||||
// An async request has been sent to get the profile and stop the profiler.
|
||||
const REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER =
|
||||
"request-to-get-profile-and-stop-profiler";
|
||||
// An async request has been sent to stop the profiler.
|
||||
const REQUEST_TO_STOP_PROFILER = "request-to-stop-profiler";
|
||||
// The profiler notified us that our request to start it actually started it.
|
||||
const RECORDING = "recording";
|
||||
// Some other code with access to the profiler started it.
|
||||
const OTHER_IS_RECORDING = "other-is-recording";
|
||||
// Profiling is not available when in private browsing mode.
|
||||
const LOCKED_BY_PRIVATE_BROWSING = "locked-by-private-browsing";
|
||||
|
||||
class Perf extends PureComponent {
|
||||
static get propTypes() {
|
||||
return {
|
||||
perfFront: PropTypes.object.isRequired,
|
||||
receiveProfile: PropTypes.func.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
recordingState: NOT_YET_KNOWN,
|
||||
recordingUnexpectedlyStopped: false,
|
||||
// The following is either "null" for unknown, or a boolean value.
|
||||
isSupportedPlatform: null
|
||||
};
|
||||
this.startRecording = this.startRecording.bind(this);
|
||||
this.getProfileAndStopProfiler = this.getProfileAndStopProfiler.bind(this);
|
||||
this.stopProfilerAndDiscardProfile = this.stopProfilerAndDiscardProfile.bind(this);
|
||||
this.handleProfilerStarting = this.handleProfilerStarting.bind(this);
|
||||
this.handleProfilerStopping = this.handleProfilerStopping.bind(this);
|
||||
this.handlePrivateBrowsingStarting = this.handlePrivateBrowsingStarting.bind(this);
|
||||
this.handlePrivateBrowsingEnding = this.handlePrivateBrowsingEnding.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { perfFront } = this.props;
|
||||
|
||||
// Ask for the initial state of the profiler.
|
||||
Promise.all([
|
||||
perfFront.isActive(),
|
||||
perfFront.isSupportedPlatform(),
|
||||
perfFront.isLockedForPrivateBrowsing(),
|
||||
]).then((results) => {
|
||||
const [
|
||||
isActive,
|
||||
isSupportedPlatform,
|
||||
isLockedForPrivateBrowsing
|
||||
] = results;
|
||||
|
||||
let recordingState = this.state.recordingState;
|
||||
// It's theoretically possible we got an event that already let us know about
|
||||
// the current state of the profiler.
|
||||
if (recordingState === NOT_YET_KNOWN && isSupportedPlatform) {
|
||||
if (isLockedForPrivateBrowsing) {
|
||||
recordingState = LOCKED_BY_PRIVATE_BROWSING;
|
||||
} else {
|
||||
recordingState = isActive
|
||||
? OTHER_IS_RECORDING
|
||||
: AVAILABLE_TO_RECORD;
|
||||
}
|
||||
}
|
||||
this.setState({ isSupportedPlatform, recordingState });
|
||||
});
|
||||
|
||||
// Handle when the profiler changes state. It might be us, it might be someone else.
|
||||
this.props.perfFront.on("profiler-started", this.handleProfilerStarting);
|
||||
this.props.perfFront.on("profiler-stopped", this.handleProfilerStopping);
|
||||
this.props.perfFront.on("profile-locked-by-private-browsing",
|
||||
this.handlePrivateBrowsingStarting);
|
||||
this.props.perfFront.on("profile-unlocked-from-private-browsing",
|
||||
this.handlePrivateBrowsingEnding);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
switch (this.state.recordingState) {
|
||||
case NOT_YET_KNOWN:
|
||||
case AVAILABLE_TO_RECORD:
|
||||
case REQUEST_TO_STOP_PROFILER:
|
||||
case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
|
||||
case LOCKED_BY_PRIVATE_BROWSING:
|
||||
case OTHER_IS_RECORDING:
|
||||
// Do nothing for these states.
|
||||
break;
|
||||
|
||||
case RECORDING:
|
||||
case REQUEST_TO_START_RECORDING:
|
||||
this.props.perfFront.stopProfilerAndDiscardProfile();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unhandled recording state.");
|
||||
}
|
||||
}
|
||||
|
||||
getRecordingStateForTesting() {
|
||||
return this.state.recordingState;
|
||||
}
|
||||
|
||||
handleProfilerStarting() {
|
||||
switch (this.state.recordingState) {
|
||||
case NOT_YET_KNOWN:
|
||||
// We couldn't have started it yet, so it must have been someone
|
||||
// else. (fallthrough)
|
||||
case AVAILABLE_TO_RECORD:
|
||||
// We aren't recording, someone else started it up. (fallthrough)
|
||||
case REQUEST_TO_STOP_PROFILER:
|
||||
// We requested to stop the profiler, but someone else already started
|
||||
// it up. (fallthrough)
|
||||
case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
|
||||
// Someone re-started the profiler while we were asking for the completed
|
||||
// profile.
|
||||
|
||||
this.setState({
|
||||
recordingState: OTHER_IS_RECORDING,
|
||||
recordingUnexpectedlyStopped: false
|
||||
});
|
||||
break;
|
||||
|
||||
case REQUEST_TO_START_RECORDING:
|
||||
// Wait for the profiler to tell us that it has started.
|
||||
this.setState({
|
||||
recordingState: RECORDING,
|
||||
recordingUnexpectedlyStopped: false
|
||||
});
|
||||
break;
|
||||
|
||||
case LOCKED_BY_PRIVATE_BROWSING:
|
||||
case OTHER_IS_RECORDING:
|
||||
case RECORDING:
|
||||
// These state cases don't make sense to happen, and means we have a logical
|
||||
// fallacy somewhere.
|
||||
throw new Error(
|
||||
"The profiler started recording, when it shouldn't have " +
|
||||
`been able to. Current state: "${this.state.recordingState}"`);
|
||||
default:
|
||||
throw new Error("Unhandled recording state");
|
||||
}
|
||||
}
|
||||
|
||||
handleProfilerStopping() {
|
||||
switch (this.state.recordingState) {
|
||||
case NOT_YET_KNOWN:
|
||||
case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
|
||||
case REQUEST_TO_STOP_PROFILER:
|
||||
case OTHER_IS_RECORDING:
|
||||
this.setState({
|
||||
recordingState: AVAILABLE_TO_RECORD,
|
||||
recordingUnexpectedlyStopped: false
|
||||
});
|
||||
break;
|
||||
|
||||
case REQUEST_TO_START_RECORDING:
|
||||
// Highly unlikely, but someone stopped the recorder, this is fine.
|
||||
// Do nothing (fallthrough).
|
||||
case LOCKED_BY_PRIVATE_BROWSING:
|
||||
// The profiler is already locked, so we know about this already.
|
||||
break;
|
||||
|
||||
case RECORDING:
|
||||
this.setState({
|
||||
recordingState: AVAILABLE_TO_RECORD,
|
||||
recordingUnexpectedlyStopped: true
|
||||
});
|
||||
break;
|
||||
|
||||
case AVAILABLE_TO_RECORD:
|
||||
throw new Error(
|
||||
"The profiler stopped recording, when it shouldn't have been able to.");
|
||||
default:
|
||||
throw new Error("Unhandled recording state");
|
||||
}
|
||||
}
|
||||
|
||||
handlePrivateBrowsingStarting() {
|
||||
switch (this.state.recordingState) {
|
||||
case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
|
||||
// This one is a tricky case. Go ahead and act like nothing went wrong, maybe
|
||||
// it will resolve correctly? (fallthrough)
|
||||
case REQUEST_TO_STOP_PROFILER:
|
||||
case AVAILABLE_TO_RECORD:
|
||||
case OTHER_IS_RECORDING:
|
||||
case NOT_YET_KNOWN:
|
||||
this.setState({
|
||||
recordingState: LOCKED_BY_PRIVATE_BROWSING,
|
||||
recordingUnexpectedlyStopped: false
|
||||
});
|
||||
break;
|
||||
|
||||
case REQUEST_TO_START_RECORDING:
|
||||
case RECORDING:
|
||||
this.setState({
|
||||
recordingState: LOCKED_BY_PRIVATE_BROWSING,
|
||||
recordingUnexpectedlyStopped: true
|
||||
});
|
||||
break;
|
||||
|
||||
case LOCKED_BY_PRIVATE_BROWSING:
|
||||
// Do nothing
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unhandled recording state");
|
||||
}
|
||||
}
|
||||
|
||||
handlePrivateBrowsingEnding() {
|
||||
// No matter the state, go ahead and set this as ready to record. This should
|
||||
// be the only logical state to go into.
|
||||
this.setState({
|
||||
recordingState: AVAILABLE_TO_RECORD,
|
||||
recordingUnexpectedlyStopped: false
|
||||
});
|
||||
}
|
||||
|
||||
startRecording() {
|
||||
this.setState({
|
||||
recordingState: REQUEST_TO_START_RECORDING,
|
||||
// Reset this error state since it's no longer valid.
|
||||
recordingUnexpectedlyStopped: false,
|
||||
});
|
||||
this.props.perfFront.startProfiler();
|
||||
}
|
||||
|
||||
async getProfileAndStopProfiler() {
|
||||
this.setState({ recordingState: REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER });
|
||||
const profile = await this.props.perfFront.getProfileAndStopProfiler();
|
||||
this.setState({ recordingState: AVAILABLE_TO_RECORD });
|
||||
console.log("getProfileAndStopProfiler");
|
||||
this.props.receiveProfile(profile);
|
||||
}
|
||||
|
||||
stopProfilerAndDiscardProfile() {
|
||||
this.setState({ recordingState: REQUEST_TO_STOP_PROFILER });
|
||||
this.props.perfFront.stopProfilerAndDiscardProfile();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { recordingState, isSupportedPlatform } = this.state;
|
||||
|
||||
// Handle the cases of platform support.
|
||||
switch (isSupportedPlatform) {
|
||||
case null:
|
||||
// We don't know yet if this is a supported platform, wait for a response.
|
||||
return null;
|
||||
case false:
|
||||
return renderButton({
|
||||
label: "Start recording",
|
||||
disabled: true,
|
||||
additionalMessage: "Your platform is not supported. The Gecko Profiler only " +
|
||||
"supports Tier-1 platforms."
|
||||
});
|
||||
case true:
|
||||
// Continue on and render the panel.
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO - L10N all of the messages. Bug 1418056
|
||||
switch (recordingState) {
|
||||
case NOT_YET_KNOWN:
|
||||
return null;
|
||||
|
||||
case AVAILABLE_TO_RECORD:
|
||||
return renderButton({
|
||||
onClick: this.startRecording,
|
||||
label: "Start recording",
|
||||
additionalMessage: this.state.recordingUnexpectedlyStopped
|
||||
? div(null, "The recording was stopped by another tool.")
|
||||
: null
|
||||
});
|
||||
|
||||
case REQUEST_TO_STOP_PROFILER:
|
||||
return renderButton({
|
||||
label: "Stopping the recording",
|
||||
disabled: true
|
||||
});
|
||||
|
||||
case REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER:
|
||||
return renderButton({
|
||||
label: "Stopping the recording, and capturing the profile",
|
||||
disabled: true
|
||||
});
|
||||
|
||||
case REQUEST_TO_START_RECORDING:
|
||||
case RECORDING:
|
||||
return renderButton({
|
||||
label: "Stop and grab the recording",
|
||||
onClick: this.getProfileAndStopProfiler,
|
||||
disabled: this.state.recordingState === REQUEST_TO_START_RECORDING
|
||||
});
|
||||
|
||||
case OTHER_IS_RECORDING:
|
||||
return renderButton({
|
||||
label: "Stop and discard the other recording",
|
||||
onClick: this.stopProfilerAndDiscardProfile,
|
||||
additionalMessage: "Another tool is currently recording."
|
||||
});
|
||||
|
||||
case LOCKED_BY_PRIVATE_BROWSING:
|
||||
return renderButton({
|
||||
label: "Start recording",
|
||||
disabled: true,
|
||||
additionalMessage: `The profiler is disabled when Private Browsing is enabled.
|
||||
Close all Private Windows to re-enable the profiler`
|
||||
});
|
||||
|
||||
default:
|
||||
throw new Error("Unhandled recording state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Perf;
|
||||
|
||||
function renderButton(props) {
|
||||
const { disabled, label, onClick, additionalMessage } = props;
|
||||
const nbsp = "\u00A0";
|
||||
|
||||
return div(
|
||||
{ className: "perf" },
|
||||
div({ className: "perf-additional-message" }, additionalMessage || nbsp),
|
||||
div(
|
||||
null,
|
||||
button(
|
||||
{
|
||||
className: "devtools-button perf-button",
|
||||
"data-standalone": true,
|
||||
disabled,
|
||||
onClick
|
||||
},
|
||||
label
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
# vim: set filetype=python:
|
||||
# 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(
|
||||
'Perf.js',
|
||||
)
|
|
@ -0,0 +1,106 @@
|
|||
/* 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";
|
||||
/* global addMessageListener, addEventListener, content */
|
||||
|
||||
/**
|
||||
* This frame script injects itself into perf-html.io and injects the profile
|
||||
* into the page. It is mostly taken from the Gecko Profiler Addon implementation.
|
||||
*/
|
||||
|
||||
const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
|
||||
|
||||
let gProfile = null;
|
||||
|
||||
addMessageListener(TRANSFER_EVENT, e => {
|
||||
gProfile = e.data;
|
||||
// Eagerly try and see if the framescript was evaluated after perf loaded its scripts.
|
||||
connectToPage();
|
||||
// If not try again at DOMContentLoaded which should be called after the script
|
||||
// tag was synchronously loaded in.
|
||||
addEventListener("DOMContentLoaded", connectToPage);
|
||||
});
|
||||
|
||||
function connectToPage() {
|
||||
const unsafeWindow = content.wrappedJSObject;
|
||||
if (unsafeWindow.connectToGeckoProfiler) {
|
||||
unsafeWindow.connectToGeckoProfiler(makeAccessibleToPage({
|
||||
getProfile: () => Promise.resolve(gProfile),
|
||||
getSymbolTable: (debugName, breakpadId) => getSymbolTable(debugName, breakpadId),
|
||||
}, unsafeWindow));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For now, do not try to symbolicate. Reject any attempt.
|
||||
*/
|
||||
function getSymbolTable(debugName, breakpadId) {
|
||||
// Errors will not properly clone into the content page as they bring privileged
|
||||
// stacks information into the page. In this case provide a mock object to maintain
|
||||
// the Error type object shape.
|
||||
const error = {
|
||||
message: `The DevTools' "perf" actor does not support symbolication.`
|
||||
};
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// The following functions handle the security of cloning the object into the page.
|
||||
// The code was taken from the original Gecko Profiler Add-on to maintain
|
||||
// compatibility with the existing profile importing mechanism:
|
||||
// See: https://github.com/devtools-html/Gecko-Profiler-Addon/blob/78138190b42565f54ce4022a5b28583406489ed2/data/tab-framescript.js
|
||||
|
||||
/**
|
||||
* Create a promise that can be used in the page.
|
||||
*/
|
||||
function createPromiseInPage(fun, contentGlobal) {
|
||||
function funThatClonesObjects(resolve, reject) {
|
||||
return fun(result => resolve(Components.utils.cloneInto(result, contentGlobal)),
|
||||
error => reject(Components.utils.cloneInto(error, contentGlobal)));
|
||||
}
|
||||
return new contentGlobal.Promise(Components.utils.exportFunction(funThatClonesObjects,
|
||||
contentGlobal));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that calls the original function and tries to make the
|
||||
* return value available to the page.
|
||||
*/
|
||||
function wrapFunction(fun, contentGlobal) {
|
||||
return function () {
|
||||
let result = fun.apply(this, arguments);
|
||||
if (typeof result === "object") {
|
||||
if (("then" in result) && (typeof result.then === "function")) {
|
||||
// fun returned a promise.
|
||||
return createPromiseInPage((resolve, reject) =>
|
||||
result.then(resolve, reject), contentGlobal);
|
||||
}
|
||||
return Components.utils.cloneInto(result, contentGlobal);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass a simple object containing values that are objects or functions.
|
||||
* The objects or functions are wrapped in such a way that they can be
|
||||
* consumed by the page.
|
||||
*/
|
||||
function makeAccessibleToPage(obj, contentGlobal) {
|
||||
let result = Components.utils.createObjectIn(contentGlobal);
|
||||
for (let field in obj) {
|
||||
switch (typeof obj[field]) {
|
||||
case "function":
|
||||
Components.utils.exportFunction(
|
||||
wrapFunction(obj[field], contentGlobal), result, { defineAs: field });
|
||||
break;
|
||||
case "object":
|
||||
Components.utils.cloneInto(obj[field], result, { defineAs: field });
|
||||
break;
|
||||
default:
|
||||
result[field] = obj[field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/* 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";
|
||||
|
||||
/* exported gInit, gDestroy */
|
||||
|
||||
const BrowserLoaderModule = {};
|
||||
Components.utils.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
|
||||
const { require } = BrowserLoaderModule.BrowserLoader({
|
||||
baseURI: "resource://devtools/client/memory/",
|
||||
window
|
||||
});
|
||||
const Perf = require("devtools/client/performance-new/components/Perf");
|
||||
const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
|
||||
const { createElement } = require("devtools/client/shared/vendor/react");
|
||||
|
||||
/**
|
||||
* Perform a simple initialization on the panel. Hook up event listeners.
|
||||
*
|
||||
* @param perfFront - The Perf actor's front. Used to start and stop recordings.
|
||||
*/
|
||||
function gInit(perfFront) {
|
||||
const props = {
|
||||
perfFront,
|
||||
receiveProfile: profile => {
|
||||
// Open up a new tab and send a message with the profile.
|
||||
const browser = top.gBrowser;
|
||||
const tab = browser.addTab("https://perf-html.io/from-addon");
|
||||
browser.selectedTab = tab;
|
||||
const mm = tab.linkedBrowser.messageManager;
|
||||
mm.loadFrameScript(
|
||||
"chrome://devtools/content/performance-new/frame-script.js",
|
||||
false
|
||||
);
|
||||
mm.sendAsyncMessage("devtools:perf-html-transfer-profile", profile);
|
||||
}
|
||||
};
|
||||
render(createElement(Perf, props), document.querySelector("#root"));
|
||||
}
|
||||
|
||||
function gDestroy() {
|
||||
unmountComponentAtNode(document.querySelector("#root"));
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DIRS += [
|
||||
'components',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'panel.js',
|
||||
)
|
||||
|
||||
MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
|
|
@ -0,0 +1,59 @@
|
|||
/* 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";
|
||||
|
||||
const { PerfFront } = require("devtools/shared/fronts/perf");
|
||||
|
||||
loader.lazyRequireGetter(this, "EventEmitter",
|
||||
"devtools/shared/old-event-emitter");
|
||||
|
||||
class PerformancePanel {
|
||||
constructor(iframeWindow, toolbox) {
|
||||
this.panelWin = iframeWindow;
|
||||
this.toolbox = toolbox;
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open is effectively an asynchronous constructor.
|
||||
* @return {Promise} Resolves when the Perf tool completes opening.
|
||||
*/
|
||||
open() {
|
||||
if (!this._opening) {
|
||||
this._opening = this._doOpen();
|
||||
}
|
||||
return this._opening;
|
||||
}
|
||||
|
||||
async _doOpen() {
|
||||
this.panelWin.gToolbox = this.toolbox;
|
||||
this.panelWin.gTarget = this.target;
|
||||
|
||||
const rootForm = await this.target.root;
|
||||
const perfFront = new PerfFront(this.target.client, rootForm);
|
||||
|
||||
this.isReady = true;
|
||||
this.emit("ready");
|
||||
this.panelWin.gInit(perfFront);
|
||||
return this;
|
||||
}
|
||||
|
||||
// DevToolPanel API:
|
||||
|
||||
get target() {
|
||||
return this.toolbox.target;
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
// Make sure this panel is not already destroyed.
|
||||
if (this._destroyed) {
|
||||
return;
|
||||
}
|
||||
this.panelWin.gDestroy();
|
||||
this.emit("destroyed");
|
||||
this._destroyed = true;
|
||||
}
|
||||
}
|
||||
exports.PerformancePanel = PerformancePanel;
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % htmlDTD
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"DTD/xhtml1-strict.dtd">
|
||||
%htmlDTD;
|
||||
]>
|
||||
|
||||
<!-- 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/. -->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" dir="">
|
||||
<head>
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/perf.css" type="text/css"/>
|
||||
</head>
|
||||
<body class="theme-body">
|
||||
<div id="root"></div>
|
||||
<script type="application/javascript" src="initializer.js"></script>
|
||||
<script type="application/javascript"
|
||||
src="chrome://devtools/content/shared/theme-switching.js"
|
||||
defer="true">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for mochitests.
|
||||
"extends": "../../../.eslintrc.mochitests.js"
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
[DEFAULT]
|
||||
support-files =
|
||||
head.js
|
||||
|
||||
[test_perf-state-01.html]
|
||||
[test_perf-state-02.html]
|
||||
[test_perf-state-03.html]
|
||||
[test_perf-state-04.html]
|
|
@ -0,0 +1,133 @@
|
|||
/* 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";
|
||||
|
||||
/* exported addPerfTest, MockPerfFront */
|
||||
/* globals URL_ROOT */
|
||||
|
||||
const { BrowserLoader } = Components.utils.import("resource://devtools/client/shared/browser-loader.js", {});
|
||||
var { require } = BrowserLoader({
|
||||
baseURI: "resource://devtools/client/performance-new/",
|
||||
window
|
||||
});
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const { perfDescription } = require("devtools/shared/specs/perf");
|
||||
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
const flags = require("devtools/shared/flags");
|
||||
|
||||
flags.testing = true;
|
||||
let EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
|
||||
SimpleTest.registerCleanupFunction(function () {
|
||||
if (DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
|
||||
ok(false, "Should have had the expected number of DevToolsUtils.assert() failures." +
|
||||
"Expected " + EXPECTED_DTU_ASSERT_FAILURE_COUNT +
|
||||
", got " + DevToolsUtils.assertionFailureCount);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle test setup and teardown while catching errors.
|
||||
*/
|
||||
function addPerfTest(asyncTest) {
|
||||
window.onload = async () => {
|
||||
try {
|
||||
await asyncTest();
|
||||
} catch (e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The Gecko Profiler is a rather heavy-handed component that uses a lot of resources.
|
||||
* In order to get around that, and have quick component tests we provide a mock of
|
||||
* the performance front. It also has a method called flushAsyncQueue() that will
|
||||
* flush any queued async calls to deterministically run our tests.
|
||||
*/
|
||||
class MockPerfFront extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._isActive = false;
|
||||
this._asyncQueue = [];
|
||||
|
||||
// Tests can update these two values directly as needed.
|
||||
this.mockIsSupported = true;
|
||||
this.mockIsLocked = false;
|
||||
|
||||
// Wrap all async methods in a flushable queue, so that tests can control
|
||||
// when the responses come back.
|
||||
this.isActive = this._wrapInAsyncQueue(this.isActive);
|
||||
this.startProfiler = this._wrapInAsyncQueue(this.startProfiler);
|
||||
this.stopProfilerAndDiscardProfile = this._wrapInAsyncQueue(
|
||||
this.stopProfilerAndDiscardProfile);
|
||||
this.getProfileAndStopProfiler = this._wrapInAsyncQueue(
|
||||
this.getProfileAndStopProfiler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a flushable queue mechanism for all async work. The work piles up
|
||||
* and then is evaluated at once when _flushPendingQueue is called.
|
||||
*/
|
||||
_wrapInAsyncQueue(fn) {
|
||||
if (typeof fn !== "function") {
|
||||
throw new Error("_wrapInAsyncQueue requires a function");
|
||||
}
|
||||
return (...args) => {
|
||||
return new Promise(resolve => {
|
||||
this._asyncQueue.push(() => {
|
||||
resolve(fn.apply(this, args));
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
flushAsyncQueue() {
|
||||
const pending = this._asyncQueue;
|
||||
this._asyncQueue = [];
|
||||
pending.forEach(fn => fn());
|
||||
// Ensure this is async.
|
||||
return new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
startProfiler() {
|
||||
this._isActive = true;
|
||||
this.emit("profiler-started");
|
||||
}
|
||||
|
||||
getProfileAndStopProfiler() {
|
||||
this._isActive = false;
|
||||
this.emit("profiler-stopped");
|
||||
// Return a fake profile.
|
||||
return {};
|
||||
}
|
||||
|
||||
stopProfilerAndDiscardProfile() {
|
||||
this._isActive = false;
|
||||
this.emit("profiler-stopped");
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this._isActive;
|
||||
}
|
||||
|
||||
isSupportedPlatform() {
|
||||
return this.mockIsSupported;
|
||||
}
|
||||
|
||||
isLockedForPrivateBrowsing() {
|
||||
return this.mockIsLocked;
|
||||
}
|
||||
}
|
||||
|
||||
// Do a quick validation to make sure that our Mock has the same methods as a spec.
|
||||
const mockKeys = Object.getOwnPropertyNames(MockPerfFront.prototype);
|
||||
Object.getOwnPropertyNames(perfDescription.methods).forEach(methodName => {
|
||||
if (!mockKeys.includes(methodName)) {
|
||||
throw new Error(`The MockPerfFront is missing the method "${methodName}" from the ` +
|
||||
"actor's spec. It should be added to the mock.");
|
||||
}
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!-- 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/. -->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Perf component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript"></script>
|
||||
<script type="application/javascript">
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test the normal workflow of starting and stopping the profiler through the
|
||||
* Perf component.
|
||||
*/
|
||||
addPerfTest(async () => {
|
||||
const Perf = require("devtools/client/performance-new/components/Perf");
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const perfFront = new MockPerfFront();
|
||||
const container = document.querySelector("#container");
|
||||
|
||||
// Inject a function which will allow us to receive the profile.
|
||||
let profile;
|
||||
function receiveProfile(profileIn) {
|
||||
profile = profileIn;
|
||||
}
|
||||
|
||||
const element = React.createElement(Perf, { perfFront, receiveProfile });
|
||||
const perfComponent = ReactDOM.render(element, container);
|
||||
is(perfComponent.state.recordingState, "not-yet-known",
|
||||
"The component at first is in an unknown state.");
|
||||
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "available-to-record",
|
||||
"After talking to the actor, we're ready to record.");
|
||||
|
||||
const button = container.querySelector("button");
|
||||
ok(button, "Selected the button to click.");
|
||||
button.click();
|
||||
is(perfComponent.state.recordingState, "request-to-start-recording",
|
||||
"Sent in a request to start recording.");
|
||||
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "recording",
|
||||
"The actor has started its recording");
|
||||
|
||||
button.click();
|
||||
is(perfComponent.state.recordingState,
|
||||
"request-to-get-profile-and-stop-profiler",
|
||||
"We have requested to stop the profiler.");
|
||||
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "available-to-record",
|
||||
"The profiler is available to record again.");
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(typeof profile, "object", "Got a profile");
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,59 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!-- 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/. -->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Perf component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript"></script>
|
||||
<script type="application/javascript">
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test the perf component when the profiler is already started.
|
||||
*/
|
||||
addPerfTest(async () => {
|
||||
const Perf = require("devtools/client/performance-new/components/Perf");
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const perfFront = new MockPerfFront();
|
||||
const container = document.querySelector("#container");
|
||||
|
||||
ok(true, "Start the profiler before initiliazing the component, to simulate" +
|
||||
"the profiler being controlled by another tool.");
|
||||
|
||||
perfFront.startProfiler();
|
||||
await perfFront.flushAsyncQueue();
|
||||
|
||||
const receiveProfile = () => {};
|
||||
const element = React.createElement(Perf, { perfFront, receiveProfile });
|
||||
const perfComponent = ReactDOM.render(element, container);
|
||||
is(perfComponent.state.recordingState, "not-yet-known",
|
||||
"The component at first is in an unknown state.");
|
||||
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "other-is-recording",
|
||||
"The profiler is not available to record.");
|
||||
|
||||
const button = container.querySelector("button");
|
||||
ok(button, "Selected a button on the component");
|
||||
button.click();
|
||||
is(perfComponent.state.recordingState, "request-to-stop-profiler",
|
||||
"We can request to stop the profiler.");
|
||||
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "available-to-record",
|
||||
"The profiler is now available to record.");
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!-- 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/. -->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Perf component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript"></script>
|
||||
<script type="application/javascript">
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test the perf component for when the profiler is already started.
|
||||
*/
|
||||
addPerfTest(async () => {
|
||||
const Perf = require("devtools/client/performance-new/components/Perf");
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const perfFront = new MockPerfFront();
|
||||
const container = document.querySelector("#container");
|
||||
|
||||
const receiveProfile = () => {};
|
||||
const element = React.createElement(Perf, { perfFront, receiveProfile });
|
||||
const perfComponent = ReactDOM.render(element, container);
|
||||
|
||||
is(perfComponent.state.recordingState, "not-yet-known",
|
||||
"The component at first is in an unknown state.");
|
||||
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "available-to-record",
|
||||
"After talking to the actor, we're ready to record.");
|
||||
|
||||
document.querySelector("button").click();
|
||||
is(perfComponent.state.recordingState, "request-to-start-recording",
|
||||
"Sent in a request to start recording.");
|
||||
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "recording",
|
||||
"The actor has started its recording");
|
||||
|
||||
ok(true, "Simulate a third party stopping the profiler.");
|
||||
perfFront.stopProfilerAndDiscardProfile();
|
||||
await perfFront.flushAsyncQueue();
|
||||
|
||||
ok(perfComponent.state.recordingUnexpectedlyStopped,
|
||||
"The profiler unexpectedly stopped.");
|
||||
is(perfComponent.state.recordingState, "available-to-record",
|
||||
"However, the profiler is available to record again.");
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,64 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!-- 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/. -->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Perf component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript"></script>
|
||||
<script type="application/javascript">
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test that the profiler gets disabled during private browsing.
|
||||
*/
|
||||
addPerfTest(async () => {
|
||||
const Perf = require("devtools/client/performance-new/components/Perf");
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const perfFront = new MockPerfFront();
|
||||
const container = document.querySelector("#container");
|
||||
|
||||
perfFront.mockIsLocked = true;
|
||||
|
||||
const receiveProfile = () => {};
|
||||
const element = React.createElement(Perf, { perfFront, receiveProfile });
|
||||
const perfComponent = ReactDOM.render(element, container);
|
||||
|
||||
is(perfComponent.state.recordingState, "not-yet-known",
|
||||
"The component at first is in an unknown state.");
|
||||
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "locked-by-private-browsing",
|
||||
"After talking to the actor, it's locked for private browsing.");
|
||||
|
||||
perfFront.mockIsLocked = false;
|
||||
perfFront.emit("profile-unlocked-from-private-browsing");
|
||||
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "available-to-record",
|
||||
"After the profiler is unlocked, it's available to record.");
|
||||
|
||||
document.querySelector("button").click();
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "recording",
|
||||
"The actor has started its recording");
|
||||
|
||||
perfFront.mockIsLocked = true;
|
||||
perfFront.emit("profile-locked-by-private-browsing");
|
||||
await perfFront.flushAsyncQueue();
|
||||
is(perfComponent.state.recordingState, "locked-by-private-browsing",
|
||||
"The recording stops when going into private browsing mode.");
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -305,6 +305,9 @@ pref("devtools.webconsole.new-frontend-enabled", true);
|
|||
// Enable the webconsole sidebar toggle
|
||||
pref("devtools.webconsole.sidebarToggle", false);
|
||||
|
||||
// Disable the new performance recording panel by default
|
||||
pref("devtools.performance.new-panel-enabled", false);
|
||||
|
||||
// Enable client-side mapping service for source maps
|
||||
pref("devtools.source-map.client-service.enabled", true);
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/* 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/. */
|
||||
|
||||
.perf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.devtools-button.perf-button {
|
||||
padding: 5px;
|
||||
margin: auto;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.perf-additional-message {
|
||||
margin: 10px;
|
||||
margin-top: 65px;
|
||||
}
|
|
@ -85,8 +85,6 @@ support-files =
|
|||
test-bug-782653-css-errors.html
|
||||
test-bug-837351-security-errors.html
|
||||
test-bug-859170-longstring-hang.html
|
||||
test-bug-869003-iframe.html
|
||||
test-bug-869003-top-window.html
|
||||
test-bug-952277-highlight-nodes-in-vview.html
|
||||
test-iframe-child.html
|
||||
test-iframe-parent.html
|
||||
|
@ -132,6 +130,8 @@ support-files =
|
|||
test-iframe2.html
|
||||
test-iframe3.html
|
||||
test-image.png
|
||||
test-inspect-cross-domain-objects-frame.html
|
||||
test-inspect-cross-domain-objects-top.html
|
||||
test-jsterm-dollar.html
|
||||
test-location-debugger-link-console-log.js
|
||||
test-location-debugger-link-errors.js
|
||||
|
@ -319,7 +319,6 @@ skip-if = true # Bug 1404884
|
|||
skip-if = true # Bug 1404888
|
||||
# old console skip-if = true # Bug 1110500 - mouse event failure in test
|
||||
[browser_webconsole_inspect_cross_domain_object.js]
|
||||
skip-if = true # Bug 1401548
|
||||
[browser_webconsole_iterators_generators.js]
|
||||
skip-if = true # Bug 1404849
|
||||
# old console skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
|
||||
|
|
|
@ -9,75 +9,67 @@
|
|||
"use strict";
|
||||
|
||||
const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/test-bug-869003-top-window.html";
|
||||
"new-console-output/test/mochitest/" +
|
||||
"test-inspect-cross-domain-objects-top.html";
|
||||
|
||||
add_task(function* () {
|
||||
// This test is slightly more involved: it opens the web console, then the
|
||||
// variables view for a given object, it updates a property in the view and
|
||||
// checks the result. We can get a timeout with debug builds on slower
|
||||
// machines.
|
||||
add_task(async function () {
|
||||
requestLongerTimeout(2);
|
||||
|
||||
yield loadTab("data:text/html;charset=utf8,<p>hello");
|
||||
let hud = yield openConsole();
|
||||
let hud = await openNewTabAndConsole("data:text/html;charset=utf8,<p>hello");
|
||||
|
||||
info("Wait for the 'foobar' message to be logged by the frame");
|
||||
let onMessage = waitForMessage(hud, "foobar");
|
||||
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
|
||||
let {node} = await onMessage;
|
||||
|
||||
let [result] = yield waitForMessages({
|
||||
webconsole: hud,
|
||||
messages: [{
|
||||
name: "console.log message",
|
||||
text: "foobar",
|
||||
category: CATEGORY_WEBDEV,
|
||||
severity: SEVERITY_LOG,
|
||||
objects: true,
|
||||
}],
|
||||
});
|
||||
const objectInspectors = [...node.querySelectorAll(".tree")];
|
||||
is(objectInspectors.length, 2, "There is the expected number of object inspectors");
|
||||
|
||||
let msg = [...result.matched][0];
|
||||
ok(msg, "message element");
|
||||
const [oi1, oi2] = objectInspectors;
|
||||
|
||||
let body = msg.querySelector(".message-body");
|
||||
ok(body, "message body");
|
||||
ok(body.textContent.includes('{ hello: "world!",'), "message text check");
|
||||
ok(body.textContent.includes('function func()'), "message text check");
|
||||
info("Expanding the first object inspector");
|
||||
await expandObjectInspector(oi1);
|
||||
|
||||
yield testClickable(result.clickableElements[0], [
|
||||
{ name: "hello", value: "world!" },
|
||||
{ name: "bug", value: 869003 },
|
||||
], hud);
|
||||
yield testClickable(result.clickableElements[1], [
|
||||
{ name: "hello", value: "world!" },
|
||||
{ name: "name", value: "func" },
|
||||
{ name: "length", value: 1 },
|
||||
], hud);
|
||||
// The first object inspector now looks like:
|
||||
// ▼ {…}
|
||||
// | bug: 869003
|
||||
// | hello: "world!"
|
||||
// | ▶︎ __proto__: Object { … }
|
||||
|
||||
let oi1Nodes = oi1.querySelectorAll(".node");
|
||||
is(oi1Nodes.length, 4, "There is the expected number of nodes in the tree");
|
||||
ok(oi1.textContent.includes("bug: 869003"), "Expected content");
|
||||
ok(oi1.textContent.includes('hello: "world!"'), "Expected content");
|
||||
|
||||
info("Expanding the second object inspector");
|
||||
await expandObjectInspector(oi2);
|
||||
|
||||
// The second object inspector now looks like:
|
||||
// ▼ func()
|
||||
// | arguments: null
|
||||
// | bug: 869003
|
||||
// | caller: null
|
||||
// | hello: "world!"
|
||||
// | length: 1
|
||||
// | name: "func"
|
||||
// | ▶︎ prototype: Object { … }
|
||||
// | ▶︎ __proto__: function ()
|
||||
|
||||
let oi2Nodes = oi2.querySelectorAll(".node");
|
||||
is(oi2Nodes.length, 9, "There is the expected number of nodes in the tree");
|
||||
ok(oi2.textContent.includes("arguments: null"), "Expected content");
|
||||
ok(oi2.textContent.includes("bug: 869003"), "Expected content");
|
||||
ok(oi2.textContent.includes("caller: null"), "Expected content");
|
||||
ok(oi2.textContent.includes('hello: "world!"'), "Expected content");
|
||||
ok(oi2.textContent.includes("length: 1"), "Expected content");
|
||||
ok(oi2.textContent.includes('name: "func"'), "Expected content");
|
||||
});
|
||||
|
||||
function* testClickable(clickable, props, hud) {
|
||||
ok(clickable, "clickable object found");
|
||||
|
||||
executeSoon(() => {
|
||||
EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
|
||||
function expandObjectInspector(oi) {
|
||||
let onMutation = waitForNodeMutation(oi, {
|
||||
childList: true
|
||||
});
|
||||
|
||||
let aVar = yield hud.jsterm.once("variablesview-fetched");
|
||||
ok(aVar, "variables view fetched");
|
||||
ok(aVar._variablesView, "variables view object");
|
||||
|
||||
let [result] = yield findVariableViewProperties(aVar, props, { webconsole: hud });
|
||||
let prop = result.matchedProp;
|
||||
ok(prop, "matched the |" + props[0].name + "| property in the variables view");
|
||||
|
||||
// Check that property value updates work.
|
||||
aVar = yield updateVariablesViewProperty({
|
||||
property: prop,
|
||||
field: "value",
|
||||
string: "'omgtest'",
|
||||
webconsole: hud,
|
||||
});
|
||||
|
||||
info("onFetchAfterUpdate");
|
||||
|
||||
props[0].value = "omgtest";
|
||||
yield findVariableViewProperties(aVar, props, { webconsole: hud });
|
||||
oi.querySelector(".arrow").click();
|
||||
return onMutation;
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
<script type="text/javascript"><!--
|
||||
window.onload = function testConsoleLogging()
|
||||
{
|
||||
var o = { hello: "world!", bug: 869003 };
|
||||
var f = Object.assign(function func(arg){}, o);
|
||||
console.log("foobar", o, f);
|
||||
var obj1 = { hello: "world!", bug: 869003 };
|
||||
var obj2 = Object.assign(function func(arg){}, obj1);
|
||||
console.log("foobar", obj1, obj2);
|
||||
};
|
||||
// --></script>
|
||||
</head>
|
|
@ -9,6 +9,6 @@
|
|||
<body>
|
||||
<p>Make sure users can inspect objects from cross-domain iframes.</p>
|
||||
<p>Top window.</p>
|
||||
<iframe src="http://example.org/browser/devtools/client/webconsole/test/test-bug-869003-iframe.html"></iframe>
|
||||
<iframe src="http://example.org/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-inspect-cross-domain-objects-frame.html"></iframe>
|
||||
</body>
|
||||
</html>
|
|
@ -42,6 +42,7 @@ DevToolsModules(
|
|||
'memory.js',
|
||||
'monitor.js',
|
||||
'object.js',
|
||||
'perf.js',
|
||||
'performance-recording.js',
|
||||
'performance.js',
|
||||
'preference.js',
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/* 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";
|
||||
|
||||
const protocol = require("devtools/shared/protocol");
|
||||
const { ActorClassWithSpec, Actor } = protocol;
|
||||
const { perfSpec } = require("devtools/shared/specs/perf");
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const Services = require("Services");
|
||||
|
||||
loader.lazyGetter(this, "geckoProfiler", () => {
|
||||
return Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
|
||||
});
|
||||
|
||||
loader.lazyImporter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
// Some platforms are built without the Gecko Profiler.
|
||||
const IS_SUPPORTED_PLATFORM = "nsIProfiler" in Ci;
|
||||
|
||||
/**
|
||||
* The PerfActor wraps the Gecko Profiler interface
|
||||
*/
|
||||
exports.PerfActor = ActorClassWithSpec(perfSpec, {
|
||||
initialize(conn) {
|
||||
Actor.prototype.initialize.call(this, conn);
|
||||
|
||||
// Only setup the observers on a supported platform.
|
||||
if (IS_SUPPORTED_PLATFORM) {
|
||||
this._observer = {
|
||||
observe: this._observe.bind(this)
|
||||
};
|
||||
Services.obs.addObserver(this._observer, "profiler-started");
|
||||
Services.obs.addObserver(this._observer, "profiler-stopped");
|
||||
Services.obs.addObserver(this._observer, "chrome-document-global-created");
|
||||
Services.obs.addObserver(this._observer, "last-pb-context-exited");
|
||||
}
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (!IS_SUPPORTED_PLATFORM) {
|
||||
return;
|
||||
}
|
||||
Services.obs.removeObserver(this._observer, "profiler-started");
|
||||
Services.obs.removeObserver(this._observer, "profiler-stopped");
|
||||
Services.obs.removeObserver(this._observer, "chrome-document-global-created");
|
||||
Services.obs.removeObserver(this._observer, "last-pb-context-exited");
|
||||
Actor.prototype.destroy.call(this);
|
||||
},
|
||||
|
||||
startProfiler() {
|
||||
if (!IS_SUPPORTED_PLATFORM) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For a quick implementation, decide on some default values. These may need
|
||||
// to be tweaked or made configurable as needed.
|
||||
const settings = {
|
||||
entries: 1000000,
|
||||
interval: 1,
|
||||
features: ["js", "stackwalk", "threads", "leaf"],
|
||||
threads: ["GeckoMain", "Compositor"]
|
||||
};
|
||||
|
||||
try {
|
||||
// This can throw an error if the profiler is in the wrong state.
|
||||
geckoProfiler.StartProfiler(
|
||||
settings.entries,
|
||||
settings.interval,
|
||||
settings.features,
|
||||
settings.features.length,
|
||||
settings.threads,
|
||||
settings.threads.length
|
||||
);
|
||||
} catch (e) {
|
||||
// In case any errors get triggered, bailout with a false.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
stopProfilerAndDiscardProfile() {
|
||||
if (!IS_SUPPORTED_PLATFORM) {
|
||||
return;
|
||||
}
|
||||
geckoProfiler.StopProfiler();
|
||||
},
|
||||
|
||||
async getProfileAndStopProfiler() {
|
||||
if (!IS_SUPPORTED_PLATFORM) {
|
||||
return null;
|
||||
}
|
||||
let profile;
|
||||
try {
|
||||
// Attempt to pull out the data.
|
||||
profile = await geckoProfiler.getProfileDataAsync();
|
||||
|
||||
// Stop and discard the buffers.
|
||||
geckoProfiler.StopProfiler();
|
||||
} catch (e) {
|
||||
// If there was any kind of error, bailout with no profile.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Gecko Profiler errors can return an empty object, return null for this case
|
||||
// as well.
|
||||
if (Object.keys(profile).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return profile;
|
||||
},
|
||||
|
||||
isActive() {
|
||||
if (!IS_SUPPORTED_PLATFORM) {
|
||||
return false;
|
||||
}
|
||||
return geckoProfiler.IsActive();
|
||||
},
|
||||
|
||||
isSupportedPlatform() {
|
||||
return IS_SUPPORTED_PLATFORM;
|
||||
},
|
||||
|
||||
isLockedForPrivateBrowsing() {
|
||||
if (!IS_SUPPORTED_PLATFORM) {
|
||||
return false;
|
||||
}
|
||||
return !geckoProfiler.CanProfile();
|
||||
},
|
||||
|
||||
/**
|
||||
* Watch for events that happen within the browser. These can affect the current
|
||||
* availability and state of the Gecko Profiler.
|
||||
*/
|
||||
_observe(subject, topic, _data) {
|
||||
switch (topic) {
|
||||
case "chrome-document-global-created":
|
||||
if (PrivateBrowsingUtils.isWindowPrivate(subject)) {
|
||||
this.emit("profile-locked-by-private-browsing");
|
||||
}
|
||||
break;
|
||||
case "last-pb-context-exited":
|
||||
this.emit("profile-unlocked-from-private-browsing");
|
||||
break;
|
||||
case "profiler-started":
|
||||
case "profiler-stopped":
|
||||
this.emit(topic);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -443,6 +443,15 @@ var DebuggerServer = {
|
|||
constructor: "HeapSnapshotFileActor",
|
||||
type: { global: true }
|
||||
});
|
||||
// Always register this as a global module, even while there is a pref turning
|
||||
// on and off the other performance actor. This actor shouldn't conflict with
|
||||
// the other one. These are also lazily loaded so there shouldn't be a performance
|
||||
// impact.
|
||||
this.registerModule("devtools/server/actors/perf", {
|
||||
prefix: "perf",
|
||||
constructor: "PerfActor",
|
||||
type: { global: true }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -534,7 +543,8 @@ var DebuggerServer = {
|
|||
constructor: "TimelineActor",
|
||||
type: { tab: true }
|
||||
});
|
||||
if ("nsIProfiler" in Ci) {
|
||||
if ("nsIProfiler" in Ci &&
|
||||
!Services.prefs.getBoolPref("devtools.performance.new-panel-enabled", false)) {
|
||||
this.registerModule("devtools/server/actors/performance", {
|
||||
prefix: "performance",
|
||||
constructor: "PerformanceActor",
|
||||
|
|
|
@ -72,6 +72,9 @@ skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still di
|
|||
[browser_markers-timestamp.js]
|
||||
[browser_navigateEvents.js]
|
||||
skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
|
||||
[browser_perf-01.js]
|
||||
[browser_perf-02.js]
|
||||
[browser_perf-03.js]
|
||||
[browser_perf-allocation-data.js]
|
||||
skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S
|
||||
[browser_perf-profiler-01.js]
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Run through a series of basic recording actions for the perf actor.
|
||||
*/
|
||||
add_task(async function () {
|
||||
const {front, client} = await initPerfFront();
|
||||
|
||||
// Assert the initial state.
|
||||
is(await front.isSupportedPlatform(), true,
|
||||
"This test only runs on supported platforms.");
|
||||
is(await front.isLockedForPrivateBrowsing(), false,
|
||||
"The browser is not in private browsing mode.");
|
||||
is(await front.isActive(), false,
|
||||
"The profiler is not active yet.");
|
||||
|
||||
// Start the profiler.
|
||||
const profilerStarted = once(front, "profiler-started");
|
||||
await front.startProfiler();
|
||||
await profilerStarted;
|
||||
is(await front.isActive(), true, "The profiler was started.");
|
||||
|
||||
// Stop the profiler and assert the results.
|
||||
const profilerStopped1 = once(front, "profiler-stopped");
|
||||
const profile = await front.getProfileAndStopProfiler();
|
||||
await profilerStopped1;
|
||||
is(await front.isActive(), false, "The profiler was stopped.");
|
||||
ok("threads" in profile, "The actor was used to record a profile.");
|
||||
|
||||
// Restart the profiler.
|
||||
await front.startProfiler();
|
||||
is(await front.isActive(), true, "The profiler was re-started.");
|
||||
|
||||
// Stop and discard.
|
||||
const profilerStopped2 = once(front, "profiler-stopped");
|
||||
await front.stopProfilerAndDiscardProfile();
|
||||
await profilerStopped2;
|
||||
is(await front.isActive(), false,
|
||||
"The profiler was stopped and the profile discarded.");
|
||||
|
||||
// Clean up.
|
||||
await front.destroy();
|
||||
await client.close();
|
||||
});
|
|
@ -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";
|
||||
|
||||
/**
|
||||
* Test what happens when other tools control the profiler.
|
||||
*/
|
||||
add_task(async function () {
|
||||
const {front, client} = await initPerfFront();
|
||||
|
||||
// Simulate other tools by getting an independent handle on the Gecko Profiler.
|
||||
const geckoProfiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
|
||||
|
||||
is(await front.isActive(), false, "The profiler hasn't been started yet.");
|
||||
|
||||
// Start the profiler.
|
||||
await front.startProfiler();
|
||||
is(await front.isActive(), true, "The profiler was started.");
|
||||
|
||||
// Stop the profiler manually through the Gecko Profiler interface.
|
||||
const profilerStopped = once(front, "profiler-stopped");
|
||||
geckoProfiler.StopProfiler();
|
||||
await profilerStopped;
|
||||
is(await front.isActive(), false, "The profiler was stopped by another tool.");
|
||||
|
||||
// Clean up.
|
||||
await front.destroy();
|
||||
await client.close();
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Test that the profiler emits events when private browsing windows are opened
|
||||
* and closed.
|
||||
*/
|
||||
add_task(async function () {
|
||||
const {front, client} = await initPerfFront();
|
||||
|
||||
is(await front.isLockedForPrivateBrowsing(), false,
|
||||
"The profiler is not locked for private browsing.");
|
||||
|
||||
// Open up a new private browser window, and assert the correct events are fired.
|
||||
const profilerLocked = once(front, "profile-locked-by-private-browsing");
|
||||
const privateWindow = await BrowserTestUtils.openNewBrowserWindow({private: true});
|
||||
await profilerLocked;
|
||||
is(await front.isLockedForPrivateBrowsing(), true,
|
||||
"The profiler is now locked because of private browsing.");
|
||||
|
||||
// Close the private browser window, and assert the correct events are fired.
|
||||
const profilerUnlocked = once(front, "profile-unlocked-from-private-browsing");
|
||||
await BrowserTestUtils.closeWindow(privateWindow);
|
||||
await profilerUnlocked;
|
||||
is(await front.isLockedForPrivateBrowsing(), false,
|
||||
"The profiler is available again after closing the private browsing window.");
|
||||
|
||||
// Clean up.
|
||||
await front.destroy();
|
||||
await client.close();
|
||||
});
|
|
@ -108,6 +108,41 @@ function initDebuggerServer() {
|
|||
DebuggerServer.registerAllActors();
|
||||
}
|
||||
|
||||
async function initPerfFront() {
|
||||
const {PerfFront} = require("devtools/shared/fronts/perf");
|
||||
|
||||
initDebuggerServer();
|
||||
let client = new DebuggerClient(DebuggerServer.connectPipe());
|
||||
await waitUntilClientConnected(client);
|
||||
const rootForm = await getRootForm(client);
|
||||
const front = PerfFront(client, rootForm);
|
||||
return {front, client};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the RootActor form from a DebuggerClient.
|
||||
* @param {DebuggerClient} client
|
||||
* @return {RootActor} Resolves when connected.
|
||||
*/
|
||||
function getRootForm(client) {
|
||||
return new Promise(resolve => {
|
||||
client.listTabs(rootForm => {
|
||||
resolve(rootForm);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until a DebuggerClient is connected.
|
||||
* @param {DebuggerClient} client
|
||||
* @return {Promise} Resolves when connected.
|
||||
*/
|
||||
function waitUntilClientConnected(client) {
|
||||
return new Promise(resolve => {
|
||||
client.addOneTimeListener("connected", resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a debugger client.
|
||||
* @param {DebuggerClient}
|
||||
|
|
|
@ -23,6 +23,7 @@ DevToolsModules(
|
|||
'layout.js',
|
||||
'memory.js',
|
||||
'node.js',
|
||||
'perf.js',
|
||||
'performance-recording.js',
|
||||
'performance.js',
|
||||
'preference.js',
|
||||
|
|
|
@ -0,0 +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/. */
|
||||
"use strict";
|
||||
|
||||
const { FrontClassWithSpec, Front } = require("devtools/shared/protocol");
|
||||
const { perfSpec } = require("devtools/shared/specs/perf");
|
||||
|
||||
exports.PerfFront = FrontClassWithSpec(perfSpec, {
|
||||
initialize: function (client, form) {
|
||||
Front.prototype.initialize.call(this, client, form);
|
||||
this.actorID = form.perfActor;
|
||||
this.manage(this);
|
||||
}
|
||||
});
|
|
@ -132,6 +132,11 @@ const Types = exports.__TypesForTests = [
|
|||
spec: "devtools/shared/specs/node",
|
||||
front: "devtools/shared/fronts/node",
|
||||
},
|
||||
{
|
||||
types: ["perf"],
|
||||
spec: "devtools/shared/specs/perf",
|
||||
front: "devtools/shared/fronts/perf",
|
||||
},
|
||||
{
|
||||
types: ["performance"],
|
||||
spec: "devtools/shared/specs/performance",
|
||||
|
|
|
@ -28,6 +28,7 @@ DevToolsModules(
|
|||
'layout.js',
|
||||
'memory.js',
|
||||
'node.js',
|
||||
'perf.js',
|
||||
'performance-recording.js',
|
||||
'performance.js',
|
||||
'preference.js',
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/* 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";
|
||||
|
||||
const { RetVal, generateActorSpec } = require("devtools/shared/protocol");
|
||||
|
||||
const perfDescription = {
|
||||
typeName: "perf",
|
||||
|
||||
events: {
|
||||
"profiler-started": {
|
||||
type: "profiler-started"
|
||||
},
|
||||
"profiler-stopped": {
|
||||
type: "profiler-stopped"
|
||||
},
|
||||
"profile-locked-by-private-browsing": {
|
||||
type: "profile-locked-by-private-browsing"
|
||||
},
|
||||
"profile-unlocked-from-private-browsing": {
|
||||
type: "profile-unlocked-from-private-browsing"
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
startProfiler: {
|
||||
request: {},
|
||||
response: { value: RetVal("boolean") }
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns null when unable to return the profile.
|
||||
*/
|
||||
getProfileAndStopProfiler: {
|
||||
request: {},
|
||||
response: RetVal("nullable:json")
|
||||
},
|
||||
|
||||
stopProfilerAndDiscardProfile: {
|
||||
request: {},
|
||||
response: {}
|
||||
},
|
||||
|
||||
isActive: {
|
||||
request: {},
|
||||
response: { value: RetVal("boolean") }
|
||||
},
|
||||
|
||||
isSupportedPlatform: {
|
||||
request: {},
|
||||
response: { value: RetVal("boolean") }
|
||||
},
|
||||
|
||||
isLockedForPrivateBrowsing: {
|
||||
request: {},
|
||||
response: { value: RetVal("boolean") }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.perfDescription = perfDescription;
|
||||
|
||||
const perfSpec = generateActorSpec(perfDescription);
|
||||
|
||||
exports.perfSpec = perfSpec;
|
|
@ -670,11 +670,6 @@ void HTMLMediaElement::ReportLoadError(const char* aMsg,
|
|||
aParamCount);
|
||||
}
|
||||
|
||||
static bool IsAutoplayEnabled()
|
||||
{
|
||||
return Preferences::GetBool("media.autoplay.enabled");
|
||||
}
|
||||
|
||||
class HTMLMediaElement::AudioChannelAgentCallback final :
|
||||
public nsIAudioChannelAgentCallback
|
||||
{
|
||||
|
@ -2439,7 +2434,8 @@ void HTMLMediaElement::UpdatePreloadAction()
|
|||
PreloadAction nextAction = PRELOAD_UNDEFINED;
|
||||
// If autoplay is set, or we're playing, we should always preload data,
|
||||
// as we'll need it to play.
|
||||
if ((IsAutoplayEnabled() && HasAttr(kNameSpaceID_None, nsGkAtoms::autoplay)) ||
|
||||
if ((AutoplayPolicy::IsMediaElementAllowedToPlay(WrapNotNull(this)) &&
|
||||
HasAttr(kNameSpaceID_None, nsGkAtoms::autoplay)) ||
|
||||
!mPaused)
|
||||
{
|
||||
nextAction = HTMLMediaElement::PRELOAD_ENOUGH;
|
||||
|
@ -6192,7 +6188,7 @@ bool HTMLMediaElement::CanActivateAutoplay()
|
|||
// download is controlled by the script and there is no way to evaluate
|
||||
// MediaDecoder::CanPlayThrough().
|
||||
|
||||
if (!IsAutoplayEnabled()) {
|
||||
if (!AutoplayPolicy::IsMediaElementAllowedToPlay(WrapNotNull(this))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#include "AutoplayPolicy.h"
|
||||
|
||||
#include "mozilla/EventStateManager.h"
|
||||
#include "mozilla/NotNull.h"
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "mozilla/dom/HTMLMediaElement.h"
|
||||
#include "nsIDocument.h"
|
||||
|
@ -18,10 +17,6 @@ namespace dom {
|
|||
/* static */ bool
|
||||
AutoplayPolicy::IsDocumentAllowedToPlay(nsIDocument* aDoc)
|
||||
{
|
||||
if (!Preferences::GetBool("media.autoplay.enabled.user-gestures-needed")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return aDoc ? aDoc->HasBeenUserActivated() : false;
|
||||
}
|
||||
|
||||
|
@ -32,15 +27,27 @@ AutoplayPolicy::IsMediaElementAllowedToPlay(NotNull<HTMLMediaElement*> aElement)
|
|||
return true;
|
||||
}
|
||||
|
||||
if (Preferences::GetBool("media.autoplay.enabled.user-gestures-needed")) {
|
||||
return AutoplayPolicy::IsDocumentAllowedToPlay(aElement->OwnerDoc());
|
||||
}
|
||||
|
||||
// TODO : this old way would be removed when user-gestures-needed becomes
|
||||
// as a default option to block autoplay.
|
||||
// If elelement is blessed, it would always be allowed to play().
|
||||
return aElement->IsBlessed() ||
|
||||
EventStateManager::IsHandlingUserInput();
|
||||
if (!Preferences::GetBool("media.autoplay.enabled.user-gestures-needed", false)) {
|
||||
// If elelement is blessed, it would always be allowed to play().
|
||||
return aElement->IsBlessed() ||
|
||||
EventStateManager::IsHandlingUserInput();
|
||||
}
|
||||
|
||||
// Muted content
|
||||
if (aElement->Volume() == 0.0 || aElement->Muted()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Media has already loaded metadata and doesn't contain audio track
|
||||
if (aElement->IsVideo() &&
|
||||
aElement->ReadyState() >= nsIDOMHTMLMediaElement::HAVE_METADATA &&
|
||||
!aElement->HasAudio()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return AutoplayPolicy::IsDocumentAllowedToPlay(aElement->OwnerDoc());
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
|
|
|
@ -25,13 +25,14 @@ class HTMLMediaElement;
|
|||
* conditions is true.
|
||||
* 1) Owner document is activated by user gestures
|
||||
* We restrict user gestures to "mouse click", "keyboard press" and "touch".
|
||||
* 2) TODO...
|
||||
* 2) Muted media content or video without audio content
|
||||
*/
|
||||
class AutoplayPolicy
|
||||
{
|
||||
public:
|
||||
static bool IsDocumentAllowedToPlay(nsIDocument* aDoc);
|
||||
static bool IsMediaElementAllowedToPlay(NotNull<HTMLMediaElement*> aElement);
|
||||
private:
|
||||
static bool IsDocumentAllowedToPlay(nsIDocument* aDoc);
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "MediaSource.h"
|
||||
#include "MediaSourceDemuxer.h"
|
||||
#include "MediaSourceUtils.h"
|
||||
#include "SourceBuffer.h"
|
||||
#include "SourceBufferList.h"
|
||||
#include "VideoUtils.h"
|
||||
#include <algorithm>
|
||||
|
|
|
@ -679,6 +679,7 @@ skip-if = true # bug 475110 - disabled since we don't play Wave files standalone
|
|||
[test_autoplay.html]
|
||||
[test_autoplay_contentEditable.html]
|
||||
skip-if = android_version == '15' || android_version == '17' || android_version == '22' # android(bug 1232305, bug 1232318, bug 1372457)
|
||||
[test_autoplay_policy.html]
|
||||
[test_buffered.html]
|
||||
skip-if = android_version == '15' || android_version == '22' # bug 1308388, android(bug 1232305)
|
||||
[test_bug448534.html]
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Autoplay policy test</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<script type="text/javascript" src="manifest.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
|
||||
<script>
|
||||
|
||||
let manager = new MediaTestManager;
|
||||
|
||||
gTestPrefs.push(["media.autoplay.enabled", false],
|
||||
["media.autoplay.enabled.user-gestures-needed", true]);
|
||||
|
||||
window.info = function(msg, token) {
|
||||
SimpleTest.info(msg + ", token=" + token);
|
||||
}
|
||||
|
||||
window.is = function(valA, valB, msg, token) {
|
||||
SimpleTest.is(valA, valB, msg + ", token=" + token);
|
||||
}
|
||||
|
||||
window.ok = function(val, msg, token) {
|
||||
SimpleTest.ok(val, msg + ", token=" + token);
|
||||
}
|
||||
|
||||
/**
|
||||
* test files and paremeters
|
||||
*/
|
||||
var autoplayTests = [
|
||||
/* video */
|
||||
{ name:"gizmo.mp4", type:"video/mp4", hasAudio:true },
|
||||
{ name:"gizmo-noaudio.mp4", type:"video/mp4", hasAudio:false },
|
||||
{ name:"gizmo.webm", type:'video/webm', hasAudio:true },
|
||||
{ name:"gizmo-noaudio.webm", type:'video/webm', hasAudio:false },
|
||||
/* audio */
|
||||
{ name:"small-shot.ogg", type:"audio/ogg", hasAudio:true },
|
||||
{ name:"small-shot.m4a", type:"audio/mp4", hasAudio:true },
|
||||
{ name:"small-shot.mp3", type:"audio/mpeg", hasAudio:true },
|
||||
{ name:"small-shot.flac", type:"audio/flac", hasAudio:true }
|
||||
];
|
||||
|
||||
var autoplayParams = [
|
||||
{ volume: 1.0, muted: false, preload: "none" },
|
||||
{ volume: 1.0, muted: false, preload: "metadata" },
|
||||
{ volume: 0.0, muted: false, preload: "none" },
|
||||
{ volume: 0.0, muted: false, preload: "metadata" },
|
||||
{ volume: 1.0, muted: true, preload: "none"},
|
||||
{ volume: 1.0, muted: true, preload: "metadata" },
|
||||
{ volume: 0.0, muted: true, preload: "none"},
|
||||
{ volume: 0.0, muted: true, preload: "metadata" }
|
||||
];
|
||||
|
||||
function createTestArray()
|
||||
{
|
||||
var tests = [];
|
||||
for (let testIdx = 0; testIdx < autoplayTests.length; testIdx++) {
|
||||
for (let paramIdx = 0; paramIdx < autoplayParams.length; paramIdx++) {
|
||||
let test = autoplayTests[testIdx];
|
||||
let param = autoplayParams[paramIdx];
|
||||
let obj = new Object;
|
||||
obj.name = test.name;
|
||||
obj.type = test.type;
|
||||
obj.hasAudio = test.hasAudio;
|
||||
obj.volume = param.volume;
|
||||
obj.muted = param.muted;
|
||||
obj.preload = param.preload;
|
||||
tests.push(obj);
|
||||
}
|
||||
}
|
||||
return tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main test function for different autoplay cases without user interaction.
|
||||
*
|
||||
* When the pref "media.autoplay.enabled" is false and the pref
|
||||
* "media.autoplay.enabled.user-gestures-needed" is true, we would only allow
|
||||
* audible media to autoplay after the website has been activated by specific
|
||||
* user gestures. However, non-audible media won't be restricted.
|
||||
*
|
||||
* Audible means the volume is not zero, or muted is not true for the video with
|
||||
* audio track. For media without loading metadata, we can't know whether it has
|
||||
* audio track or not, so we would also regard it as audible media.
|
||||
*
|
||||
* Non-audible means the volume is zero, the muted is true, or the video without
|
||||
* audio track.
|
||||
*/
|
||||
async function runTest(test, token) {
|
||||
manager.started(token);
|
||||
|
||||
await testPlay(test, token);
|
||||
await testAutoplayKeyword(test, token);
|
||||
|
||||
manager.finished(token);
|
||||
}
|
||||
|
||||
manager.runTests(createTestArray(), runTest);
|
||||
|
||||
/**
|
||||
* Different test scenarios
|
||||
*/
|
||||
async function testPlay(test, token) {
|
||||
info("### start testPlay", token);
|
||||
info(`volume=${test.volume}, muted=${test.muted}, ` +
|
||||
`preload=${test.preload}, hasAudio=${test.hasAudio}`, token);
|
||||
|
||||
let element = document.createElement(getMajorMimeType(test.type));
|
||||
element.preload = test.preload;
|
||||
element.volume = test.volume;
|
||||
element.muted = test.muted;
|
||||
element.src = test.name;
|
||||
document.body.appendChild(element);
|
||||
|
||||
let preLoadNone = test.preload == "none";
|
||||
if (!preLoadNone) {
|
||||
info("### wait for loading metadata", token);
|
||||
await once(element, "loadedmetadata");
|
||||
}
|
||||
|
||||
let isAudible = (preLoadNone || test.hasAudio) &&
|
||||
test.volume != 0.0 &&
|
||||
!test.muted;
|
||||
let state = isAudible? "audible" : "non-audible";
|
||||
info(`### calling play() for ${state} media`, token);
|
||||
let promise = element.play();
|
||||
if (isAudible) {
|
||||
await promise.catch(function(error) {
|
||||
ok(element.paused, `${state} media fail to start via play()`, token);
|
||||
is(error.name, "NotAllowedError", "rejected play promise", token);
|
||||
});
|
||||
} else {
|
||||
// since we just want to check the value of 'paused', we don't need to wait
|
||||
// resolved play promise. (it's equal to wait for 'playing' event)
|
||||
await once(element, "play");
|
||||
ok(!element.paused, `${state} media start via play()`, token);
|
||||
}
|
||||
|
||||
removeNodeAndSource(element);
|
||||
}
|
||||
|
||||
async function testAutoplayKeyword(test, token) {
|
||||
info("### start testAutoplayKeyword", token);
|
||||
info(`volume=${test.volume}, muted=${test.muted}, ` +
|
||||
`preload=${test.preload}, hasAudio=${test.hasAudio}`, token);
|
||||
|
||||
let element = document.createElement(getMajorMimeType(test.type));
|
||||
element.autoplay = true;
|
||||
element.preload = test.preload;
|
||||
element.volume = test.volume;
|
||||
element.muted = test.muted;
|
||||
element.src = test.name;
|
||||
document.body.appendChild(element);
|
||||
|
||||
let preLoadNone = test.preload == "none";
|
||||
let isAudible = (preLoadNone || test.hasAudio) &&
|
||||
test.volume != 0.0 &&
|
||||
!test.muted;
|
||||
let state = isAudible? "audible" : "non-audible";
|
||||
info(`### wait to autoplay for ${state} media`, token);
|
||||
if (isAudible) {
|
||||
if (preLoadNone) {
|
||||
await once(element, "suspend");
|
||||
is(element.readyState, 0 /* HAVE_NOTHING */, "media won't start loading", token);
|
||||
} else {
|
||||
await once(element, "canplay");
|
||||
}
|
||||
ok(element.paused, `can not start with 'autoplay' keyword for ${state} media`, token);
|
||||
} else {
|
||||
await once(element, "play");
|
||||
ok(!element.paused, `start with 'autoplay' keyword for ${state} media`, token);
|
||||
}
|
||||
|
||||
removeNodeAndSource(element);
|
||||
}
|
||||
|
||||
</script>
|
|
@ -35,6 +35,7 @@ ScrollingLayersHelper::BeginBuild(WebRenderLayerManager* aManager,
|
|||
MOZ_ASSERT(!mBuilder);
|
||||
mBuilder = &aBuilder;
|
||||
MOZ_ASSERT(mCache.empty());
|
||||
MOZ_ASSERT(mScrollParents.empty());
|
||||
MOZ_ASSERT(mItemClipStack.empty());
|
||||
}
|
||||
|
||||
|
@ -44,6 +45,7 @@ ScrollingLayersHelper::EndBuild()
|
|||
mBuilder = nullptr;
|
||||
mManager = nullptr;
|
||||
mCache.clear();
|
||||
mScrollParents.clear();
|
||||
MOZ_ASSERT(mItemClipStack.empty());
|
||||
}
|
||||
|
||||
|
@ -352,22 +354,27 @@ ScrollingLayersHelper::RecurseAndDefineAsr(nsDisplayItem* aItem,
|
|||
if (mBuilder->HasExtraClip()) {
|
||||
ids.second = mBuilder->GetCacheOverride(aChain);
|
||||
} else {
|
||||
auto it = mCache.find(aChain);
|
||||
if (it == mCache.end()) {
|
||||
// Degenerate case, where there are two clip chain items that are
|
||||
// fundamentally the same but are different objects and so we can't
|
||||
// find it in the cache via hashing. Linear search for it instead.
|
||||
// XXX This shouldn't happen very often but it might still turn out
|
||||
// to be a performance cliff, so we should figure out a better way to
|
||||
// deal with this.
|
||||
for (it = mCache.begin(); it != mCache.end(); it++) {
|
||||
if (DisplayItemClipChain::Equal(aChain, it->first)) {
|
||||
break;
|
||||
}
|
||||
// Since the scroll layer was already defined, find the clip (if any)
|
||||
// that it was previously defined as a child of. If that clip is
|
||||
// equivalent to |aChain|, then we should use that one in the mCache
|
||||
// lookup as it is more likely to produce a result. This happens because
|
||||
// of how we can have two DisplayItemClipChain items that are ::Equal
|
||||
// but not ==, and mCache only does == checking. In the hunk below,
|
||||
// |canonicalChain| can be thought of as the clip chain instance that is
|
||||
// equivalent to |aChain| but has the best chance of being found in
|
||||
// mCache.
|
||||
const DisplayItemClipChain* canonicalChain = aChain;
|
||||
auto it = mScrollParents.find(scrollId);
|
||||
if (it != mScrollParents.end()) {
|
||||
const DisplayItemClipChain* scrollParent = it->second;
|
||||
if (DisplayItemClipChain::Equal(scrollParent, aChain)) {
|
||||
canonicalChain = scrollParent;
|
||||
}
|
||||
}
|
||||
|
||||
auto it2 = mCache.find(canonicalChain);
|
||||
// If |it == mCache.end()| here then we have run into a case where the
|
||||
// scroll layer was previously defined a specific parent clip, and
|
||||
// scroll layer was previously defined with a specific parent clip, and
|
||||
// now here it has a different parent clip. Gecko can create display
|
||||
// lists like this because it treats the ASR chain and clipping chain
|
||||
// more independently, but we can't yet represent this in WR. This is
|
||||
|
@ -376,8 +383,14 @@ ScrollingLayersHelper::RecurseAndDefineAsr(nsDisplayItem* aItem,
|
|||
// supports multiple ancestors on a scroll layer we can deal with this
|
||||
// better. The layout/reftests/text/wordwrap-08.html has a Text display
|
||||
// item that exercises this case.
|
||||
if (it != mCache.end()) {
|
||||
ids.second = Some(it->second);
|
||||
if (it2 == mCache.end()) {
|
||||
// leave ids.second as Nothing(). This should only happen if we didn't
|
||||
// pick up a better canonicalChain above, either because it didn't
|
||||
// exist, or because it was not ::Equal to aChain. Therefore
|
||||
// canonicalChain must still be equal to aChain here.
|
||||
MOZ_ASSERT(canonicalChain == aChain);
|
||||
} else {
|
||||
ids.second = Some(it2->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -423,6 +436,11 @@ ScrollingLayersHelper::RecurseAndDefineAsr(nsDisplayItem* aItem,
|
|||
// defining.
|
||||
MOZ_ASSERT(!(ancestorIds.first && ancestorIds.second));
|
||||
|
||||
if (ancestorIds.second) {
|
||||
MOZ_ASSERT(aChain);
|
||||
mScrollParents[scrollId] = aChain;
|
||||
}
|
||||
|
||||
LayoutDeviceRect contentRect =
|
||||
metrics.GetExpandedScrollableRect() * metrics.GetDevPixelsPerCSSPixel();
|
||||
// TODO: check coordinate systems are sane here
|
||||
|
|
|
@ -86,6 +86,9 @@ private:
|
|||
wr::DisplayListBuilder* mBuilder;
|
||||
ClipIdMap mCache;
|
||||
|
||||
typedef std::unordered_map<FrameMetrics::ViewID, const DisplayItemClipChain*> ScrollParentMap;
|
||||
ScrollParentMap mScrollParents;
|
||||
|
||||
struct ItemClips {
|
||||
ItemClips(const ActiveScrolledRoot* aAsr,
|
||||
const DisplayItemClipChain* aChain);
|
||||
|
|
|
@ -138,6 +138,7 @@ assertTypeFailInEval('function f({global}) { "use asm"; function g() {} return g
|
|||
assertTypeFailInEval('function f(global, {imports}) { "use asm"; function g() {} return g }');
|
||||
assertTypeFailInEval('function f(g = 2) { "use asm"; function g() {} return g }');
|
||||
assertTypeFailInEval('function *f() { "use asm"; function g() {} return g }');
|
||||
assertAsmTypeFail(USE_ASM + 'function *f(){}');
|
||||
assertTypeFailInEval('f => { "use asm"; function g() {} return g }');
|
||||
assertTypeFailInEval('var f = { method() {"use asm"; return {}} }');
|
||||
assertAsmTypeFail(USE_ASM + 'return {m() {}};');
|
||||
|
|
|
@ -7138,6 +7138,8 @@ ParseFunction(ModuleValidator& m, ParseNode** fnOut, unsigned* line)
|
|||
TokenKind tk;
|
||||
if (!tokenStream.getToken(&tk, TokenStream::Operand))
|
||||
return false;
|
||||
if (tk == TOK_MUL)
|
||||
return m.failCurrentOffset("unexpected generator function");
|
||||
if (!TokenKindIsPossibleIdentifier(tk))
|
||||
return false; // The regular parser will throw a SyntaxError, no need to m.fail.
|
||||
|
||||
|
|
|
@ -594,9 +594,13 @@ pref("media.recorder.video.frame_drops", true);
|
|||
|
||||
// Whether to autostart a media element with an |autoplay| attribute
|
||||
pref("media.autoplay.enabled", true);
|
||||
// If "media.autoplay.enabled" is off, and this pref is on, then autoplay could
|
||||
// be executed after website has been activated by specific user gestures.
|
||||
|
||||
// If "media.autoplay.enabled" is false, and this pref is true, then audible media
|
||||
// would only be allowed to autoplay after website has been activated by specific
|
||||
// user gestures, but the non-audible media won't be restricted.
|
||||
#ifdef NIGHTLY_BUILD
|
||||
pref("media.autoplay.enabled.user-gestures-needed", false);
|
||||
#endif
|
||||
|
||||
// The default number of decoded video frames that are enqueued in
|
||||
// MediaDecoderReader's mVideoQueue.
|
||||
|
|
|
@ -322,7 +322,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cbindgen"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"clap 2.28.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -1903,7 +1903,7 @@ version = "0.9.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cbindgen 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cbindgen 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mp4parse 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
@ -3789,7 +3789,7 @@ dependencies = [
|
|||
"checksum byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8"
|
||||
"checksum bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c129aff112dcc562970abb69e2508b40850dd24c274761bb50fb8a0067ba6c27"
|
||||
"checksum caseless 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8950b075cff75cdabadee97148a8b5816c7cf62e5948a6005b5255d564b42fe7"
|
||||
"checksum cbindgen 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d72afdc9e579e5102c2f80e84b0e2a7ee0d3652fe8dea040d9f114b76391eef"
|
||||
"checksum cbindgen 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "89e4b1f207d22a68f2d3d8778a4b83ed7399f81e96b8d0f6c316c945f1c7126f"
|
||||
"checksum cc 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2c674f0870e3dbd4105184ea035acb1c32c8ae69939c9e228d2b11bbfe29efad"
|
||||
"checksum cexpr 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "393a5f0088efbe41f9d1fcd062f24e83c278608420e62109feb2c8abee07de7d"
|
||||
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
|
||||
|
|
|
@ -164,6 +164,21 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
debug!("Collecting changes for: {:?}", element);
|
||||
debug!(" > state: {:?}", state_changes);
|
||||
debug!(
|
||||
" > id changed: {:?} -> +{:?} -{:?}",
|
||||
snapshot.id_changed(),
|
||||
id_added,
|
||||
id_removed
|
||||
);
|
||||
debug!(
|
||||
" > class changed: {:?} -> +{:?} -{:?}",
|
||||
snapshot.class_changed(),
|
||||
classes_added,
|
||||
classes_removed
|
||||
);
|
||||
|
||||
let lookup_element =
|
||||
if element.implemented_pseudo_element().is_some() {
|
||||
element.pseudo_element_originating_element().unwrap()
|
||||
|
|
|
@ -241,9 +241,9 @@ where
|
|||
);
|
||||
|
||||
debug!("Collected invalidations (self: {}): ", invalidated_self);
|
||||
debug!(" > self: {:?}", descendant_invalidations);
|
||||
debug!(" > descendants: {:?}", descendant_invalidations);
|
||||
debug!(" > siblings: {:?}", sibling_invalidations);
|
||||
debug!(" > self: {}, {:?}", self_invalidations.len(), self_invalidations);
|
||||
debug!(" > descendants: {}, {:?}", descendant_invalidations.len(), descendant_invalidations);
|
||||
debug!(" > siblings: {}, {:?}", sibling_invalidations.len(), sibling_invalidations);
|
||||
|
||||
let invalidated_self_from_collection = invalidated_self;
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
//! name, ids and hash.
|
||||
|
||||
use {Atom, LocalName};
|
||||
use applicable_declarations::ApplicableDeclarationBlock;
|
||||
use applicable_declarations::ApplicableDeclarationList;
|
||||
use context::QuirksMode;
|
||||
use dom::TElement;
|
||||
use fallible::FallibleVec;
|
||||
|
@ -18,7 +18,7 @@ use rule_tree::CascadeLevel;
|
|||
use selector_parser::SelectorImpl;
|
||||
use selectors::matching::{matches_selector, MatchingContext, ElementSelectorFlags};
|
||||
use selectors::parser::{Component, Combinator, SelectorIter};
|
||||
use smallvec::{SmallVec, VecLike};
|
||||
use smallvec::SmallVec;
|
||||
use std::hash::{BuildHasherDefault, Hash, Hasher};
|
||||
use stylist::Rule;
|
||||
|
||||
|
@ -146,11 +146,11 @@ impl SelectorMap<Rule> {
|
|||
///
|
||||
/// Extract matching rules as per element's ID, classes, tag name, etc..
|
||||
/// Sort the Rules at the end to maintain cascading order.
|
||||
pub fn get_all_matching_rules<E, V, F>(
|
||||
pub fn get_all_matching_rules<E, F>(
|
||||
&self,
|
||||
element: &E,
|
||||
rule_hash_target: &E,
|
||||
matching_rules_list: &mut V,
|
||||
matching_rules_list: &mut ApplicableDeclarationList,
|
||||
context: &mut MatchingContext<E::Impl>,
|
||||
quirks_mode: QuirksMode,
|
||||
flags_setter: &mut F,
|
||||
|
@ -158,7 +158,6 @@ impl SelectorMap<Rule> {
|
|||
)
|
||||
where
|
||||
E: TElement,
|
||||
V: VecLike<ApplicableDeclarationBlock>,
|
||||
F: FnMut(&E, ElementSelectorFlags),
|
||||
{
|
||||
if self.is_empty() {
|
||||
|
@ -210,17 +209,16 @@ impl SelectorMap<Rule> {
|
|||
}
|
||||
|
||||
/// Adds rules in `rules` that match `element` to the `matching_rules` list.
|
||||
fn get_matching_rules<E, V, F>(
|
||||
fn get_matching_rules<E, F>(
|
||||
element: &E,
|
||||
rules: &[Rule],
|
||||
matching_rules: &mut V,
|
||||
matching_rules: &mut ApplicableDeclarationList,
|
||||
context: &mut MatchingContext<E::Impl>,
|
||||
flags_setter: &mut F,
|
||||
cascade_level: CascadeLevel,
|
||||
)
|
||||
where
|
||||
E: TElement,
|
||||
V: VecLike<ApplicableDeclarationBlock>,
|
||||
F: FnMut(&E, ElementSelectorFlags),
|
||||
{
|
||||
for rule in rules {
|
||||
|
|
|
@ -32,13 +32,10 @@ use selectors::matching::{ElementSelectorFlags, matches_selector, MatchingContex
|
|||
use selectors::matching::VisitedHandlingMode;
|
||||
use selectors::parser::{AncestorHashes, Combinator, Component, Selector};
|
||||
use selectors::parser::{SelectorIter, SelectorMethods};
|
||||
use selectors::sink::Push;
|
||||
use selectors::visitor::SelectorVisitor;
|
||||
use servo_arc::{Arc, ArcBorrow};
|
||||
use shared_lock::{Locked, SharedRwLockReadGuard, StylesheetGuards};
|
||||
use smallbitvec::SmallBitVec;
|
||||
use smallvec::VecLike;
|
||||
use std::fmt::Debug;
|
||||
use std::ops;
|
||||
use std::sync::Mutex;
|
||||
use style_traits::viewport::ViewportConstraints;
|
||||
|
@ -1205,7 +1202,7 @@ impl Stylist {
|
|||
/// Returns the applicable CSS declarations for the given element.
|
||||
///
|
||||
/// This corresponds to `ElementRuleCollector` in WebKit.
|
||||
pub fn push_applicable_declarations<E, V, F>(
|
||||
pub fn push_applicable_declarations<E, F>(
|
||||
&self,
|
||||
element: &E,
|
||||
pseudo_element: Option<&PseudoElement>,
|
||||
|
@ -1213,13 +1210,12 @@ impl Stylist {
|
|||
smil_override: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>,
|
||||
animation_rules: AnimationRules,
|
||||
rule_inclusion: RuleInclusion,
|
||||
applicable_declarations: &mut V,
|
||||
applicable_declarations: &mut ApplicableDeclarationList,
|
||||
context: &mut MatchingContext<E::Impl>,
|
||||
flags_setter: &mut F,
|
||||
)
|
||||
where
|
||||
E: TElement,
|
||||
V: Push<ApplicableDeclarationBlock> + VecLike<ApplicableDeclarationBlock> + Debug,
|
||||
F: FnMut(&E, ElementSelectorFlags),
|
||||
{
|
||||
// Gecko definitely has pseudo-elements with style attributes, like
|
||||
|
@ -1343,8 +1339,7 @@ impl Stylist {
|
|||
if !only_default_rules {
|
||||
// Step 4: Normal style attributes.
|
||||
if let Some(sa) = style_attribute {
|
||||
Push::push(
|
||||
applicable_declarations,
|
||||
applicable_declarations.push(
|
||||
ApplicableDeclarationBlock::from_declarations(
|
||||
sa.clone_arc(),
|
||||
CascadeLevel::StyleAttributeNormal
|
||||
|
@ -1355,8 +1350,7 @@ impl Stylist {
|
|||
// Step 5: SMIL override.
|
||||
// Declarations from SVG SMIL animation elements.
|
||||
if let Some(so) = smil_override {
|
||||
Push::push(
|
||||
applicable_declarations,
|
||||
applicable_declarations.push(
|
||||
ApplicableDeclarationBlock::from_declarations(
|
||||
so.clone_arc(),
|
||||
CascadeLevel::SMILOverride
|
||||
|
@ -1368,8 +1362,7 @@ impl Stylist {
|
|||
// The animations sheet (CSS animations, script-generated animations,
|
||||
// and CSS transitions that are no longer tied to CSS markup)
|
||||
if let Some(anim) = animation_rules.0 {
|
||||
Push::push(
|
||||
applicable_declarations,
|
||||
applicable_declarations.push(
|
||||
ApplicableDeclarationBlock::from_declarations(
|
||||
anim.clone(),
|
||||
CascadeLevel::Animations
|
||||
|
@ -1389,8 +1382,7 @@ impl Stylist {
|
|||
// Step 11: Transitions.
|
||||
// The transitions sheet (CSS transitions that are tied to CSS markup)
|
||||
if let Some(anim) = animation_rules.1 {
|
||||
Push::push(
|
||||
applicable_declarations,
|
||||
applicable_declarations.push(
|
||||
ApplicableDeclarationBlock::from_declarations(
|
||||
anim.clone(),
|
||||
CascadeLevel::Transitions
|
||||
|
|
|
@ -417,6 +417,26 @@ class Talos(TestingMixin, MercurialScript, BlobUploadMixin, TooltoolMixin,
|
|||
else:
|
||||
self.info("Not downloading pageset because the no-download option was specified")
|
||||
|
||||
# if running speedometer locally, need to copy speedometer source into talos/tests
|
||||
if self.config.get('run_local') and 'speedometer' in self.suite:
|
||||
self.get_speedometer_source()
|
||||
|
||||
def get_speedometer_source(self):
|
||||
# in production the build system auto copies speedometer source into place;
|
||||
# but when run locally we need to do this manually, so that talos can find it
|
||||
src = os.path.join(self.repo_path, 'third_party', 'webkit',
|
||||
'PerformanceTests', 'Speedometer')
|
||||
dest = os.path.join(self.talos_path, 'talos', 'tests', 'webkit',
|
||||
'PerformanceTests', 'Speedometer')
|
||||
if not os.path.exists(dest):
|
||||
self.info("Copying speedometer source from %s to %s" % (src, dest))
|
||||
try:
|
||||
shutil.copytree(src, dest)
|
||||
except:
|
||||
self.critical("Error copying speedometer source from %s to %s" % (src, dest))
|
||||
else:
|
||||
self.info("Speedometer source already found at %s" % dest)
|
||||
|
||||
def setup_mitmproxy(self):
|
||||
"""Some talos tests require the use of mitmproxy to playback the pages,
|
||||
set it up here.
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче