Merge mozilla-central to mozilla-inbound. r=merge a=merge CLOSED TREE

This commit is contained in:
Margareta Eliza Balazs 2017-11-30 12:56:57 +02:00
Родитель af3373dc1d 7f45cb7cc0
Коммит 8f1ff0908b
103 изменённых файлов: 2704 добавлений и 1083 удалений

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

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

6
servo/Cargo.lock сгенерированный
Просмотреть файл

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

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше