Merge mozilla-central to mozilla-inbound. a=merge

This commit is contained in:
Mihai Alexandru Michis 2019-07-18 18:46:47 +03:00
commit eb5c88a05d
72 changed files with 4659 additions and 8046 deletions

View File

@ -38,7 +38,7 @@ const SUITES = {
type: TEST_TYPES.JEST,
},
netmonitor: {
path: "../netmonitor",
path: "../netmonitor/test/node",
type: TEST_TYPES.JEST,
},
webconsole: {

View File

@ -3576,11 +3576,11 @@ Toolbox.prototype = {
settleAll(outstanding)
.catch(console.error)
.then(() => {
const api = this._netMonitorAPI;
this._netMonitorAPI = null;
return api ? api.destroy() : null;
}, console.error)
.then(() => {
if (this._netMonitorAPI) {
this._netMonitorAPI.destroy();
this._netMonitorAPI = null;
}
this._removeWindowListeners();
this._removeChromeEventHandlerEvents();

View File

@ -27,11 +27,8 @@ const ChangesApp = createFactory(require("./components/ChangesApp"));
const { getChangesStylesheet } = require("./selectors/changes");
const {
TELEMETRY_SCALAR_CONTEXTMENU,
TELEMETRY_SCALAR_CONTEXTMENU_COPY,
TELEMETRY_SCALAR_CONTEXTMENU_COPY_DECLARATION,
TELEMETRY_SCALAR_CONTEXTMENU_COPY_RULE,
TELEMETRY_SCALAR_COPY,
TELEMETRY_SCALAR_COPY_ALL_CHANGES,
TELEMETRY_SCALAR_COPY_RULE,
} = require("./constants");
@ -50,7 +47,6 @@ class ChangesView {
this.onClearChanges = this.onClearChanges.bind(this);
this.onChangesFront = this.onChangesFront.bind(this);
this.onContextMenu = this.onContextMenu.bind(this);
this.onCopy = this.onCopy.bind(this);
this.onCopyAllChanges = this.copyAllChanges.bind(this);
this.onCopyRule = this.copyRule.bind(this);
this.destroy = this.destroy.bind(this);
@ -69,7 +65,6 @@ class ChangesView {
init() {
const changesApp = ChangesApp({
onContextMenu: this.onContextMenu,
onCopy: this.onCopy,
onCopyAllChanges: this.onCopyAllChanges,
onCopyRule: this.onCopyRule,
});
@ -211,7 +206,6 @@ class ChangesView {
*/
copySelection() {
clipboardHelper.copyString(this.window.getSelection().toString());
this.telemetry.scalarAdd(TELEMETRY_SCALAR_CONTEXTMENU_COPY, 1);
}
onAddChange(change) {
@ -229,15 +223,6 @@ class ChangesView {
*/
onContextMenu(e) {
this.contextMenu.show(e);
this.telemetry.scalarAdd(TELEMETRY_SCALAR_CONTEXTMENU, 1);
}
/**
* Event handler for the "copy" event fired when content is copied to the clipboard.
* We don't change the default behavior. We only log the increment count of this action.
*/
onCopy() {
this.telemetry.scalarAdd(TELEMETRY_SCALAR_COPY, 1);
}
/**

View File

@ -24,8 +24,6 @@ class ChangesApp extends PureComponent {
changesTree: PropTypes.object.isRequired,
// Event handler for "contextmenu" event
onContextMenu: PropTypes.func.isRequired,
// Event handler for "copy" event
onCopy: PropTypes.func.isRequired,
// Event handler for click on "Copy All Changes" button
onCopyAllChanges: PropTypes.func.isRequired,
// Event handler for click on "Copy Rule" button
@ -221,7 +219,6 @@ class ChangesApp extends PureComponent {
className: "theme-sidebar inspector-tabpanel",
id: "sidebar-panel-changes",
onContextMenu: this.props.onContextMenu,
onCopy: this.props.onCopy,
},
!hasChanges && this.renderEmptyState(),
hasChanges && this.renderCopyAllChangesButton(),

View File

@ -7,13 +7,10 @@
"use strict";
module.exports = {
TELEMETRY_SCALAR_CONTEXTMENU: "devtools.changesview.contextmenu",
TELEMETRY_SCALAR_CONTEXTMENU_COPY: "devtools.changesview.contextmenu_copy",
TELEMETRY_SCALAR_CONTEXTMENU_COPY_DECLARATION:
"devtools.changesview.contextmenu_copy_declaration",
TELEMETRY_SCALAR_CONTEXTMENU_COPY_RULE:
"devtools.changesview.contextmenu_copy_rule",
TELEMETRY_SCALAR_COPY: "devtools.changesview.copy",
TELEMETRY_SCALAR_COPY_ALL_CHANGES: "devtools.changesview.copy_all_changes",
TELEMETRY_SCALAR_COPY_RULE: "devtools.changesview.copy_rule",
};

View File

@ -14,6 +14,11 @@ frame.unknownSource=(unknown)
# %S represents the URL to match in the debugger.
frame.viewsourceindebugger=View source in Debugger → %S
# LOCALIZATION NOTE (frame.viewsourceinstyleeditor): The label for the tooltip when hovering over
# a source link that links to the Style Editor.
# %S represents the URL to match in the style editor.
frame.viewsourceinstyleeditor=View source in Style Editor → %S
# LOCALIZATION NOTE (notificationBox.closeTooltip): The content of a tooltip that
# appears when hovering over the close button in a notification box.
notificationBox.closeTooltip=Close this message

View File

@ -1,7 +0,0 @@
{
"plugins": [
"transform-react-jsx",
"transform-object-rest-spread",
"transform-flow-strip-types",
],
}

View File

@ -14,18 +14,6 @@ Files used to run the Network Monitor inside of the DevTools toolbox.
* `index.html` panel UI and launch scripts.
* `src/connector/` wrap function call for Browser specific API. Current support Firefox and Chrome(experimental).
### Run in the browser tab (experimental)
Files used to run the Network Monitor in the browser tab
* `bin/` files to launch test server.
* `configs/` dev configs.
* `launchpad.js` the entry point, equivalent to `index.html`.
* `webpack.config.js` the webpack config file, including plenty of module alias map to shims and polyfills.
* `package.json` declare every required packages and available commands.
To run in the browser tab, the Network Monitor needs to get some dependencies from npm module. Check `package.json` to see all dependencies. Check `webpack.config.js` to find the module alias, and check [devtools-core](https://github.com/firefox-devtools/devtools-core) packages to dive into actual modules used by the Network Monitor and other Devtools.
### UI
The Network Monitor UI is built using [React](http://searchfox.org/mozilla-central/source/devtools/docs/frontend/react.md) components (in `src/components/`).

View File

@ -1,29 +0,0 @@
/* 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/. */
/* eslint-env node */
"use strict";
const fs = require("fs");
const path = require("path");
function getConfig() {
if (process.env.TARGET === "firefox-panel") {
return require("../configs/firefox-panel.json");
}
const developmentConfig = require("../configs/development.json");
let localConfig = {};
if (fs.existsSync(path.resolve(__dirname, "../configs/local.json"))) {
localConfig = require("../configs/local.json");
}
return Object.assign({}, developmentConfig, localConfig);
}
module.exports = {
getConfig,
};

View File

@ -1,19 +0,0 @@
/* 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/. */
/* eslint-env node */
"use strict";
const toolbox = require("devtools-launchpad/index");
const feature = require("devtools-config");
const { getConfig } = require("./configure");
const envConfig = getConfig();
feature.setConfig(envConfig);
const webpackConfig = require("../webpack.config");
toolbox.startDevServer(envConfig, webpackConfig, __dirname);

View File

@ -1,37 +0,0 @@
{
"title": "Netmonitor",
"environment": "development",
"baseWorkerURL": "http://localhost:8000/public/build/",
"defaultURL": "http://softwareishard.com/test/harexporttrigger/",
"host": "",
"theme": "light",
"dir": "ltr",
"logging": {
"client": false,
"firefoxProxy": false,
"actions": false
},
"features": {
},
"chrome": {
"debug": true,
"host": "localhost",
"port": 9222
},
"node": {
"debug": true,
"host": "localhost",
"port": 9229
},
"firefox": {
"webSocketConnection": false,
"host": "localhost",
"webSocketPort": 8116,
"tcpPort": 6080,
"mcPath": "./firefox"
},
"development": {
"serverPort": 8000,
"examplesPort": 7999
}
}

View File

@ -1,11 +0,0 @@
{
"environment": "firefox-panel",
"baseWorkerURL": "resource://devtools/client/netmonitor/",
"logging": false,
"clientLogging": false,
"firefox": {
"mcPath": "./firefox"
},
"features": {
}
}

View File

@ -1,87 +0,0 @@
/* 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";
/**
* This script is the entry point of devtools-launchpad. Make netmonitor possible
* to run on standalone browser tab without chrome privilege.
* See README.md for more information.
*/
const React = require("react");
const ReactDOM = require("react-dom");
const { bindActionCreators } = require("redux");
const { bootstrap, renderRoot } = require("devtools-launchpad");
const { Services: { appinfo, pref }} = require("devtools-modules");
// Initialize preferences as early as possible
pref("devtools.theme", "light");
pref("devtools.cache.disabled", false);
pref("devtools.netmonitor.enabled", true);
pref("devtools.netmonitor.filters", "[\"all\"]");
pref("devtools.netmonitor.visibleColumns",
"[\"status\",\"method\",\"domain,\"file\",\"domain\",\"cause\"," +
"\"type\",\"transferred\",\"contentSize\",\"waterfall\"]"
);
pref("devtools.netmonitor.panes-network-details-width", 550);
pref("devtools.netmonitor.panes-network-details-height", 450);
pref("devtools.netmonitor.har.defaultLogDir", "");
pref("devtools.netmonitor.har.defaultFileName", "Archive %date");
pref("devtools.netmonitor.har.jsonp", false);
pref("devtools.netmonitor.har.jsonpCallback", "");
pref("devtools.netmonitor.har.includeResponseBodies", true);
pref("devtools.netmonitor.har.compress", false);
pref("devtools.netmonitor.har.forceExport", false);
pref("devtools.netmonitor.har.pageLoadedTimeout", 1500);
pref("devtools.netmonitor.har.enableAutoExportToFile", false);
pref("devtools.netmonitor.persistlog", false);
pref("devtools.styleeditor.enabled", true);
require("./src/assets/styles/netmonitor.css");
const EventEmitter = require("devtools-modules/src/utils/event-emitter");
EventEmitter.decorate(window);
const { configureStore } = require("./src/create-store");
const App = require("./src/components/App");
const { Connector } = require("./src/connector/index");
const connector = new Connector();
const store = configureStore(connector);
const actions = bindActionCreators(require("./src/actions"), store.dispatch);
// Inject to global window for testing
window.store = store;
/**
* Stylesheet links in devtools xhtml files are using chrome or resource URLs.
* Rewrite the href attribute to remove the protocol. web-server.js contains redirects
* to map CSS urls to the proper file. Supports urls using:
* - devtools/client/
* - devtools/content/
* - skin/
* Will also add mandatory classnames and attributes to be compatible with devtools theme
* stylesheet.
*/
window.addEventListener("DOMContentLoaded", () => {
for (const link of document.head.querySelectorAll("link")) {
link.href = link.href.replace(/(resource|chrome)\:\/\//, "/");
}
if (appinfo.OS === "Darwin") {
document.documentElement.setAttribute("platform", "mac");
} else if (appinfo.OS === "Linux") {
document.documentElement.setAttribute("platform", "linux");
} else {
document.documentElement.setAttribute("platform", "win");
}
});
bootstrap(React, ReactDOM).then((connection) => {
if (!connection) {
return;
}
renderRoot(React, ReactDOM, App, store, {actions, connector});
connector.connect(connection, actions, store.getState);
});

View File

@ -1,33 +0,0 @@
{
"name": "netmonitor",
"version": "0.0.1",
"engines": {
"node": ">=7.10.0"
},
"description": "Network monitor in developer tools",
"dependencies": {
"babel-plugin-transform-flow-strip-types": "^6.22.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-jsx": "^6.24.1",
"codemirror": "^5.24.2",
"devtools-config": "=0.0.12",
"devtools-contextmenu": "=0.0.3",
"devtools-launchpad": "^0.0.119",
"devtools-modules": "=0.0.35",
"devtools-source-editor": "=0.0.3",
"file-loader": "^1.1.6",
"jszip": "^3.1.3",
"react": "=16.2.0",
"react-dom": "=16.2.0",
"react-prop-types": "=0.4.0",
"react-redux": "=5.0.6",
"redux": "^3.7.2",
"reselect": "^3.0.1"
},
"scripts": {
"start": "node bin/dev-server",
"test": "jest",
"test-ci": "jest --json"
},
"devDependencies": {}
}

View File

@ -29,10 +29,9 @@ NetMonitorPanel.prototype = {
return this;
},
async destroy() {
await this.panelWin.Netmonitor.destroy();
destroy() {
this.panelWin.Netmonitor.destroy();
this.emit("destroyed");
return this;
},
};

View File

@ -80,13 +80,13 @@ NetMonitorAPI.prototype = {
/**
* Clean up (unmount from DOM, remove listeners, disconnect).
*/
async destroy() {
destroy() {
this.off(EVENTS.PAYLOAD_READY, this.onPayloadReady);
await this.connector.disconnect();
this.connector.disconnect();
if (this.harExportConnector) {
await this.harExportConnector.disconnect();
this.harExportConnector.disconnect();
}
},

View File

@ -68,14 +68,14 @@ NetMonitorApp.prototype = {
/**
* Clean up (unmount from DOM, remove listeners, disconnect).
*/
async destroy() {
destroy() {
unmountComponentAtNode(this.mount);
// Make sure to destroy the API object. It's usually destroyed
// in the Toolbox destroy method, but we need it here for case
// where the Network panel is initialized without the toolbox
// and running in a tab (see initialize.js for details).
await this.api.destroy();
this.api.destroy();
},
/**

View File

@ -7,6 +7,12 @@
const { ACTIVITY_TYPE } = require("../constants");
const { CDPConnector } = require("./chrome/events");
/**
* DO NOT DELETE THIS FILE
*
* The ChromeConnector is currently not used, but is kept in tree to illustrate
* the Connector abstraction.
*/
class ChromeConnector {
constructor() {
// Internal properties
@ -31,7 +37,7 @@ class ChromeConnector {
this.connector.willNavigate(this.willNavigate);
}
async disconnect() {
disconnect() {
this.connector.disconnect();
}

View File

@ -88,12 +88,12 @@ class FirefoxConnector {
}
}
async disconnect() {
disconnect() {
if (this.actions) {
this.actions.batchReset();
}
await this.removeListeners();
this.removeListeners();
if (this.emulationFront) {
this.emulationFront.destroy();
@ -162,7 +162,7 @@ class FirefoxConnector {
await this.webConsoleClient.startListeners(["DocumentEvents"]);
}
async removeListeners() {
removeListeners() {
if (this.tabTarget) {
this.tabTarget.off("close", this.disconnect);
if (this.webSocketFront) {
@ -179,7 +179,6 @@ class FirefoxConnector {
this.dataProvider.onFrameReceived
);
this.webSocketFront.off("frameSent", this.dataProvider.onFrameSent);
this.webSocketFront.stopListening();
}
}
if (this.webConsoleClient) {

View File

@ -0,0 +1,14 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* global __dirname */
module.exports = {
verbose: true,
moduleNameMapper: {
// Map all require("devtools/...") to the real devtools root.
"^devtools\\/(.*)": `${__dirname}/../../../../$1`,
},
};

View File

@ -0,0 +1,15 @@
{
"name": "netmonitor-tests",
"license": "MPL-2.0",
"version": "0.0.1",
"engines": {
"node": ">=8.9.4"
},
"scripts": {
"test": "jest",
"test-ci": "jest --json"
},
"dependencies": {
"jest": "^24.6.0"
}
}

View File

@ -3,8 +3,11 @@
"use strict";
const { Sort, sortReducer } = require("../../src/reducers/sort");
const { SORT_BY } = require("../../src/constants");
const {
Sort,
sortReducer,
} = require("devtools/client/netmonitor/src/reducers/sort");
const { SORT_BY } = require("devtools/client/netmonitor/src/constants");
describe("sorting reducer", () => {
it("it should sort by sort type", () => {

File diff suppressed because it is too large Load Diff

View File

@ -1,187 +0,0 @@
/* 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/. */
/* eslint-env node */
/* eslint max-len: [0] */
"use strict";
const path = require("path");
const { NormalModuleReplacementPlugin } = require("webpack");
const { toolboxConfig } = require("./node_modules/devtools-launchpad/index");
const { getConfig } = require("./bin/configure");
const webpackConfig = {
entry: {
netmonitor: [path.join(__dirname, "launchpad.js")],
},
module: {
rules: [
{
test: /\.(png|svg)$/,
loader: "file-loader?name=[path][name].[ext]",
},
{
test: /\.js$/,
loaders: [
/**
* The version of webpack used in the launchpad seems to have trouble
* with the require("raw!${file}") that we use for the properties
* file in l10.js.
* This loader goes through the whole code and remove the "raw!" prefix
* so the raw-loader declared in devtools-launchpad config can load
* those files.
*/
"rewrite-raw",
// Replace all references to this.browserRequire() by require()
"rewrite-browser-require",
// Replace all references to loader.lazyRequire() by require()
"rewrite-lazy-require",
// Replace all references to loader.lazyGetter() by require()
"rewrite-lazy-getter",
],
},
],
},
resolveLoader: {
modules: ["node_modules", path.resolve("../shared/webpack")],
},
output: {
path: path.join(__dirname, "assets/build"),
filename: "[name].js",
publicPath: "/assets/build",
libraryTarget: "umd",
},
resolve: {
modules: [
// Make sure webpack is always looking for modules in
// `netmonitor/node_modules` directory first.
path.resolve(__dirname, "node_modules"),
"node_modules",
],
alias: {
Services: "devtools-modules/src/Services",
react: path.join(__dirname, "node_modules/react"),
"devtools/client/framework/devtools": path.join(
__dirname,
"../../client/shared/webpack/shims/framework-devtools-shim"
),
"devtools/client/framework/menu": "devtools-modules/src/menu",
"devtools/client/shared/components/menu/utils": "devtools-contextmenu",
"devtools/client/shared/vendor/react": "react",
"devtools/client/shared/vendor/react-dom": "react-dom",
"devtools/client/shared/vendor/react-redux": "react-redux",
"devtools/client/shared/vendor/redux": "redux",
"devtools/client/shared/vendor/reselect": "reselect",
"devtools/client/shared/vendor/jszip": "jszip",
"devtools/client/shared/sourceeditor/editor":
"devtools-source-editor/src/source-editor",
"devtools/shared/event-emitter":
"devtools-modules/src/utils/event-emitter",
"devtools/shared/platform/clipboard": path.join(
__dirname,
"../../client/shared/webpack/shims/platform-clipboard-stub"
),
"devtools/client/netmonitor/src/utils/firefox/open-request-in-tab": path.join(
__dirname,
"src/utils/open-request-in-tab"
),
"devtools/client/shared/unicode-url":
"./node_modules/devtools-modules/src/unicode-url",
// Locales need to be explicitly mapped to the en-US subfolder
"devtools/client/locales": path.join(
__dirname,
"../../client/locales/en-US"
),
"devtools/shared/locales": path.join(
__dirname,
"../../shared/locales/en-US"
),
"devtools/startup/locales": path.join(
__dirname,
"../../shared/locales/en-US"
),
"toolkit/locales": path.join(__dirname, "../../../toolkit/locales/en-US"),
// Unless a path explicitly needs to be rewritten or shimmed, all devtools paths can
// be mapped to ../../
devtools: path.join(__dirname, "../../"),
},
},
};
const mappings = [
[
/chrome:\/\/devtools\/skin/,
result => {
result.request = result.request.replace(
"./chrome://devtools/skin",
path.join(__dirname, "../themes")
);
},
],
[
/chrome:\/\/devtools\/content/,
result => {
result.request = result.request.replace(
"./chrome://devtools/content",
path.join(__dirname, "..")
);
},
],
[
/resource:\/\/devtools/,
result => {
result.request = result.request.replace(
"./resource://devtools/client",
path.join(__dirname, "..")
);
},
],
];
webpackConfig.plugins = mappings.map(
([regex, res]) => new NormalModuleReplacementPlugin(regex, res)
);
const basePath = path.join(__dirname, "../../").replace(/\\/g, "\\\\");
const baseName = path.basename(__dirname);
const config = toolboxConfig(webpackConfig, getConfig(), {
// Exclude to transpile all scripts in devtools/ but not for this folder
babelExcludes: new RegExp(`^${basePath}(.(?!${baseName}))*$`),
});
// Remove loaders from devtools-launchpad's webpack.config.js
// For svg-inline loader:
// Using file loader to bundle image assets instead of svg-inline-loader
config.module.rules = config.module.rules.filter(
rule => !["svg-inline-loader"].includes(rule.loader)
);
// For PostCSS loader:
// Disable PostCSS loader
config.module.rules.forEach(rule => {
if (Array.isArray(rule.use)) {
rule.use.some((use, idx) => {
if (use.loader === "postcss-loader") {
rule.use = rule.use.slice(0, idx);
return true;
}
return false;
});
}
});
module.exports = config;

File diff suppressed because it is too large Load Diff

View File

@ -290,6 +290,10 @@ pref("devtools.webconsole.features.editor", false);
// Saved editor mode state in the console.
pref("devtools.webconsole.input.editor", false);
// Editor width for webconsole and browserconsole
pref("devtools.webconsole.input.editorWidth", 0);
pref("devtools.browserconsole.input.editorWidth", 0);
// Disable the new performance recording panel by default
pref("devtools.performance.new-panel-enabled", false);

View File

@ -19,6 +19,7 @@ const {
getSourceMappedFile,
} = require("devtools/client/shared/source-utils");
const { LocalizationHelper } = require("devtools/shared/l10n");
const { MESSAGE_SOURCE } = require("devtools/client/webconsole/constants");
const l10n = new LocalizationHelper(
"devtools/client/locales/components.properties"
@ -51,6 +52,8 @@ class Frame extends Component {
showFullSourceUrl: PropTypes.bool,
// Service to enable the source map feature for console.
sourceMapService: PropTypes.object,
// The source of the message
messageSource: PropTypes.string,
};
}
@ -131,6 +134,7 @@ class Frame extends Component {
showHost,
showEmptyPathAsHost,
showFullSourceUrl,
messageSource,
} = this.props;
if (this.state && this.state.isSourceMapped && this.state.frame) {
@ -267,13 +271,21 @@ class Frame extends Component {
// Inner el is useful for achieving ellipsis on the left and correct LTR/RTL
// ordering. See CSS styles for frame-link-source-[inner] and bug 1290056.
let tooltipMessage;
if (messageSource && messageSource === MESSAGE_SOURCE.CSS) {
tooltipMessage = l10n.getFormatStr(
"frame.viewsourceinstyleeditor",
tooltip
);
} else {
tooltipMessage = l10n.getFormatStr("frame.viewsourceindebugger", tooltip);
}
const sourceInnerEl = dom.span(
{
key: "source-inner",
className: "frame-link-source-inner",
title: isLinkable
? l10n.getFormatStr("frame.viewsourceindebugger", tooltip)
: tooltip,
title: isLinkable ? tooltipMessage : tooltip,
},
sourceElements
);

View File

@ -35,7 +35,17 @@
color: var(--theme-toolbar-color);
background-color: var(--theme-body-background);
text-shadow: none;
border-bottom: 1px solid var(--theme-splitter-color);
border-color: var(--theme-splitter-color);
border-style: solid;
border-width: 0;
}
.notificationbox.border-top .notification {
border-top-width: 1px;
}
.notificationbox.border-bottom .notification {
border-bottom-width: 1px;
}
.notificationbox .notification[data-type="info"] {

View File

@ -76,12 +76,18 @@ class NotificationBox extends Component {
closeButtonTooltip: PropTypes.string,
// Wraps text when passed from console window as wrapping: true
wrapping: PropTypes.bool,
// Display a top border (default to false)
displayBorderTop: PropTypes.bool,
// Display a bottom border (default to true)
displayBorderBottom: PropTypes.bool,
};
}
static get defaultProps() {
return {
closeButtonTooltip: l10n.getStr("notificationBox.closeTooltip"),
displayBorderTop: false,
displayBorderBottom: true,
};
}
@ -251,10 +257,20 @@ class NotificationBox extends Component {
const notifications = this.props.notifications || this.state.notifications;
const notification = getHighestPriorityNotification(notifications);
const content = notification ? this.renderNotification(notification) : null;
const classNames = ["notificationbox"];
if (this.props.wrapping) {
classNames.push("wrapping");
}
if (this.props.displayBorderBottom) {
classNames.push("border-bottom");
}
if (this.props.displayBorderTop) {
classNames.push("border-top");
}
return div(
{
className: classNames.join(" "),

View File

@ -40,7 +40,7 @@ window.onload = async function () {
const newSizes = [];
function renderBox(props) {
async function renderBox(props) {
const boxProps = Object.assign({
start: "hello!",
end: "world!",
@ -49,10 +49,15 @@ window.onload = async function () {
newSizes.push(newSize);
}
}, props);
return ReactDOM.render(HSplitBox(boxProps), window.document.body);
const el = ReactDOM.render(HSplitBox(boxProps), window.document.body);
// wait until the element is rendered.
await SimpleTest.promiseWaitForCondition(
() => document.querySelector(".devtools-side-splitter")
);
return el;
}
const box = renderBox();
const box = await renderBox();
// Test that we properly rendered our two panes.
@ -65,7 +70,7 @@ window.onload = async function () {
// Now change the left width and assert that the changes are reflected.
renderBox({ startWidth: .25 });
await renderBox({ startWidth: .25 });
panes = document.querySelectorAll(".h-split-box-pane");
is(panes.length, 2, "Should still have two panes");
is(panes[0].style.flexGrow, "0.25", "First pane's width should be .25");

View File

@ -66,6 +66,21 @@ window.onload = async function () {
tooltip: "View source in Debugger → http://myfile.com/mahscripts.js:55",
});
// Check when there's an error in CSS (View source in Style Editor)
await checkFrameComponent({
frame: {
source: "http://myfile.com/cafebabe.css",
line: 13,
},
messageSource: "css",
}, {
file: "cafebabe.css",
line: 13,
shouldLink: true,
tooltip: "View source in Style Editor → http://myfile.com/cafebabe.css:13",
});
// Check when there's no parseable URL source;
// should not link but should render line/columns
await checkFrameComponent({

View File

@ -30,27 +30,20 @@ window.onload = async function () {
const renderedBox = shallowRenderComponent(NotificationBox, {});
is(renderedBox.type, "div", "NotificationBox is rendered as <div>");
// Test rendering
info("Test rendering NotificationBox with default props");
let boxElement = React.createElement(NotificationBox);
let notificationBox = TestUtils.renderIntoDocument(boxElement);
let notificationNode = ReactDOM.findDOMNode(notificationBox);
is(notificationNode.className, "notificationbox",
"NotificationBox has expected classname");
ok(notificationNode.classList.contains("notificationbox"),
"NotificationBox has expected class");
ok(notificationNode.classList.contains("border-bottom"),
"NotificationBox has expected class");
is(notificationNode.textContent, "",
"Empty NotificationBox has no text content");
checkNumberOfNotifications(notificationBox, 0);
// Append wrapping classname to the dom element when passing wrapping prop
const renderedBoxWrapped = shallowRenderComponent(NotificationBox, {});
let boxElementWrapped = React.createElement(NotificationBox, {wrapping: true});
let notificationBoxWrapped = TestUtils.renderIntoDocument(boxElementWrapped);
let wrappedNotificationNode = ReactDOM.findDOMNode(notificationBoxWrapped);
is(wrappedNotificationNode.className, "notificationbox wrapping",
"Wrapped notificationBox has expected classname");
// Append a notification
notificationBox.appendNotification(
"Info message",
@ -100,6 +93,28 @@ window.onload = async function () {
notificationBox.getNotificationWithValue("id3").close();
checkNumberOfNotifications(notificationBox, 0);
info(`Check "wrapping" prop works as expected`);
// Append wrapping classname to the dom element when passing wrapping prop
let boxElementWrapped = React.createElement(NotificationBox, {wrapping: true});
let notificationBoxWrapped = TestUtils.renderIntoDocument(boxElementWrapped);
let wrappedNotificationNode = ReactDOM.findDOMNode(notificationBoxWrapped);
ok(wrappedNotificationNode.classList.contains("wrapping"),
"Wrapped notificationBox has expected class");
info(`Check "displayBorderTop/displayBorderBottom" props work as expected`);
let element = React.createElement(NotificationBox, {
displayBorderTop: true,
displayBorderBottom: false,
});
let renderedElement = TestUtils.renderIntoDocument(element);
let elementNode = ReactDOM.findDOMNode(renderedElement);
ok(elementNode.classList.contains("border-top"),
"truthy displayBorderTop render a border-top className");
ok(!elementNode.classList.contains("border-bottom"),
"falsy displayBorderBottom does not render a border-bottom className");
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {

View File

@ -463,10 +463,16 @@ a {
html .jsterm-input-node-html,
html #webconsole-notificationbox {
flex: 0;
width: 100vw;
width: 100%;
}
.webconsole-output:empty ~ .notificationbox .notification {
border-top-width: 0;
border-bottom-width: 1px;
}
.jsterm-input-container {
max-width: 100%;
background-color: var(--console-input-background);
border-top: 1px solid var(--theme-splitter-color);
position: relative;
@ -846,7 +852,7 @@ a.learn-more-link.webconsole-learn-more-link {
}
/* console.table() */
.new-consoletable {
.message .new-consoletable {
width: 100%;
--consoletable-border: var(--theme-splitter-color);
margin-block-end: var(--attachment-margin-block-end);
@ -886,93 +892,6 @@ a.learn-more-link.webconsole-learn-more-link {
background-color: var(--table-zebra-background);
}
/* Layout */
.webconsole-output {
direction: ltr;
overflow: auto;
overflow-anchor: none;
-moz-user-select: text;
position: relative;
}
html,
body {
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
}
#app-wrapper {
height: 100vh;
max-height: 100vh;
}
/*
* Here's what the layout of the console looks like:
*
* +------------------------------+--------------+
* | | |
* | WEBCONSOLE WRAPPER | SIDEBAR |
* | | |
* +------------------------------+--------------+
*/
.webconsole-app {
--object-inspector-hover-background: transparent;
--attachment-margin-block-end: 3px;
--primary-toolbar-height: 29px;
display: grid;
grid-template-columns: minmax(200px, 1fr) auto;
max-height: 100vh !important;
height: 100vh !important;
width: 100vw;
overflow: hidden;
color: var(--console-output-color);
-moz-user-focus: normal;
}
.webconsole-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
max-height: 100vh;
overflow: hidden;
}
.webconsole-wrapper .webconsole-filteringbar-wrapper {
flex-shrink: 0;
overflow: hidden;
}
.webconsole-wrapper .webconsole-output {
flex-shrink: 100000;
overflow-x: hidden;
}
.webconsole-wrapper > .webconsole-output:not(:empty) {
min-height: 19px;
}
.webconsole-app #webconsole-notificationbox {
flex-shrink: 0;
}
.webconsole-app .jsterm-input-container {
min-height: 28px;
overflow-y: auto;
overflow-x: hidden;
flex-grow: 1;
}
.jsterm-cm .jsterm-input-container {
padding-block-start: 2px;
}
.webconsole-wrapper > .webconsole-output:empty ~ .jsterm-input-container {
border-top: none;
}
/* Object Inspector */
.webconsole-app .object-inspector.tree {
display: inline-block;
@ -1073,142 +992,6 @@ body {
fill: currentColor
}
/* Sidebar */
.sidebar {
display: flex;
grid-row: 1 / -1;
grid-column: -1 / -2;
background-color: var(--theme-sidebar-background);
border-inline-start: 1px solid var(--theme-splitter-color);
}
.sidebar .splitter {
/* Let the parent component handle the border. This is needed otherwise there is a visual
glitch between the input and the sidebar borders */
background-color: transparent;
}
.split-box.vert.sidebar {
/* Set to prevent the sidebar from extending past the right edge of the page */
width: unset;
height: 100vh;
}
.sidebar-wrapper {
display: grid;
grid-template-rows: auto 1fr;
width: 100%;
overflow: hidden;
}
.webconsole-sidebar-toolbar {
grid-row: 1 / 2;
min-height: var(--primary-toolbar-height);
display: flex;
justify-content: end;
padding: 0;
}
.sidebar-contents {
grid-row: 2 / 3;
overflow: auto;
}
.webconsole-sidebar-toolbar .sidebar-close-button {
padding: 4px 0;
margin: 0;
}
.sidebar-close-button::before {
background-image: url(chrome://devtools/skin/images/close.svg);
}
.sidebar-contents .object-inspector {
min-width: 100%;
}
/** EDITOR MODE */
.jsterm-editor .webconsole-wrapper {
display: grid;
/*
* Here's the design we want in editor mode
* +----------------------------------------------------+
* | Editor Toolbar | Filter bar primary |
* +----------------------------------------------------+
* | | (Filter bar secondary) |
* | +-------------------------+
* | Editor | |
* | | |
* | | |
* | | output |
* | | |
* | | |
* +--------------------------+ |
* | (Reverse search input) | |
* +----------------------------------------------------+
*
* Elements in parens may not be visible, so we set some rows to be auto to make them
* have no height when the element they contain is hidden.
*/
grid-template-rows: var(--primary-toolbar-height) auto 1fr auto;
/* The first column is set to auto so we can resize it */
grid-template-columns: auto 1fr;
}
.jsterm-editor .webconsole-filteringbar-wrapper {
grid-column: 2 / 3;
grid-row: 1 / 3;
grid-template-rows: subgrid;
}
.jsterm-editor .webconsole-filterbar-primary {
grid-row: 1 / 2;
}
/* Only put the filter buttons toolbar on its own row in narrow filterbar layout */
.jsterm-editor .narrow .devtools-toolbar.webconsole-filterbar-secondary {
grid-row: 2 / 3;
}
.jsterm-editor .webconsole-editor-toolbar {
grid-column: 1 / 2;
grid-row: 1 / 2;
border-inline-end: 1px solid var(--theme-splitter-color);
display: flex;
flex-direction: row;
justify-content: start;
height: unset;
}
.jsterm-editor .webconsole-output {
grid-column: 2 / 3;
grid-row: 3 / -1;
}
.jsterm-editor .jsterm-input-container {
grid-column: 1 / 2;
grid-row: 2 / 4;
width: 50vw;
border-inline-end: 1px solid var(--theme-splitter-color);
border-top: none;
}
.jsterm-editor #webconsole-notificationbox {
display: none;
}
.jsterm-cm.jsterm-editor .jsterm-input-container > .CodeMirror {
padding-inline-start: 0;
background-image: none;
}
.jsterm-editor .reverse-search {
grid-column: 1 / 2;
grid-row: -1 / -2;
border-top: 1px solid var(--theme-splitter-color);
}
/** Utils **/
.clipboard-only {
position: absolute;

View File

@ -23,6 +23,7 @@ const {
WARNING_GROUPS_TOGGLE,
FILTERBAR_DISPLAY_MODE_SET,
EDITOR_TOGGLE,
EDITOR_SET_WIDTH,
} = require("devtools/client/webconsole/constants");
function persistToggle() {
@ -98,6 +99,16 @@ function editorToggle() {
};
}
function setEditorWidth(width) {
return ({ dispatch, prefsService }) => {
dispatch({
type: EDITOR_SET_WIDTH,
width,
});
prefsService.setIntPref(PREFS.UI.EDITOR_WIDTH, width);
};
}
/**
* Dispatches a SHOW_OBJECT_IN_SIDEBAR action, with a grip property corresponding to the
* {actor} parameter in the {messageId} message.
@ -148,6 +159,7 @@ module.exports = {
persistToggle,
reverseSearchInputToggle,
selectNetworkMessageTab,
setEditorWidth,
showMessageObjectInSidebar,
showObjectInSidebar,
sidebarClose,

View File

@ -0,0 +1,264 @@
/* 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,
body {
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
}
#app-wrapper {
height: 100vh;
max-height: 100vh;
}
.webconsole-output {
direction: ltr;
overflow: auto;
overflow-anchor: none;
-moz-user-select: text;
position: relative;
}
.webconsole-app {
--object-inspector-hover-background: transparent;
--attachment-margin-block-end: 3px;
--primary-toolbar-height: 29px;
display: grid;
/*
* Here's the design we want in in-line mode
* +----------------------------------------------+
* | Filter bar primary |
* +---------------------------- |
* | [Filter bar secondary] |
* +---------------------------- |
* | |
* + +----------------------+ |
* | | | |
* | | Output | [sidebar] |
* | | | |
* | +----------------------+ |
* | | [NotificationBox] | |
* | +----------------------+ |
* | | | |
* | | JSTerm | |
* | | | |
* | +----------------------+ |
* | |
* +---------------------------- |
* | [Reverse search input] |
* +----------------------------------------------+
*
* - are width resizers
* - Elements inside brackets may not be visible, so we set
* rows/columns to "auto" to make them collapse when the element
* they contain is hidden.
*/
grid-template-rows: var(--primary-toolbar-height) auto 1fr auto;
grid-template-columns: minmax(200px, 1fr) minmax(0, auto);
max-height: 100vh !important;
height: 100vh !important;
width: 100vw;
overflow: hidden;
color: var(--console-output-color);
-moz-user-focus: normal;
}
.webconsole-filteringbar-wrapper {
grid-column: 1 / 2;
grid-row: 1 / 3;
grid-template-rows: subgrid;
}
.webconsole-filterbar-primary {
grid-row: 1 / 2;
}
/* Only put the filter buttons toolbar on its own row in narrow filterbar layout */
.narrow .devtools-toolbar.webconsole-filterbar-secondary {
grid-row: 2 / 3;
}
.flexible-output-input {
display: flex;
flex-direction: column;
grid-column: 1 / 2;
grid-row: 3 / 4;
/* Don't take more height than the grid allows to */
max-height: 100%;
overflow: hidden;
}
.flexible-output-input .webconsole-output {
flex-shrink: 100000;
overflow-x: hidden;
}
.flexible-output-input > .webconsole-output:not(:empty) {
min-height: 19px;
}
.webconsole-app .jsterm-input-container {
min-height: 28px;
overflow-y: auto;
overflow-x: hidden;
flex-grow: 1;
}
.jsterm-cm .jsterm-input-container {
padding-block-start: 2px;
}
.webconsole-app .webconsole-output:empty ~ .jsterm-input-container {
border-top: none;
}
.reverse-search {
grid-column: 1 / 2;
grid-row: -1 / -2;
border-top: 1px solid var(--theme-splitter-color);
}
.sidebar {
display: grid;
grid-row: 1 / -1;
grid-column: -1 / -2;
grid-template-rows: subgrid;
background-color: var(--theme-sidebar-background);
width: 200px;
min-width: 150px;
max-width: 100%;
}
.sidebar-resizer {
/* We want the splitter to cover the whole column (minus self-xss message) */
grid-row: 1 / -1;
grid-column: -1 / -2;
}
.webconsole-sidebar-toolbar {
grid-row: 1 / 2;
min-height: 100%;
display: flex;
justify-content: end;
margin: 0;
padding: 0;
}
.sidebar-contents {
grid-row: 2 / -1;
overflow: auto;
}
.webconsole-sidebar-toolbar .sidebar-close-button {
margin: 0;
}
.sidebar-close-button::before {
background-image: url(chrome://devtools/skin/images/close.svg);
}
.sidebar-contents .object-inspector {
min-width: 100%;
}
/** EDITOR MODE */
.webconsole-app.jsterm-editor {
display: grid;
/*
* Here's the design we want in editor mode
* +-----------------------------------------------------------------------+
* | [Notification Box (self XSS warning)] |
* +--------------------------+--------------------------+-----------------+
* | Editor Toolbar Filter bar primary |
* +---------------------------------------------------- |
* | [Filter bar secondary] |
* | -------------------------- |
* | |
* | Editor output [sidebar] |
* | |
* | |
* | |
* | |
* +-------------------------- |
* | [Reverse search input] |
* +-----------------------------------------------------+-----------------+
*
* - are width resizers
* - Elements inside brackets may not be visible, so we set
* rows/columns to "auto" to make them collapse when the element
* they contain is hidden.
*/
grid-template-rows:
auto
var(--primary-toolbar-height)
auto
1fr
auto;
grid-template-columns: minmax(150px, auto) minmax(200px, 1fr) minmax(0, auto);
}
.jsterm-editor .flexible-output-input {
/* This allow us to place the div children (jsterm, output, notification) on the grid */
display: contents;
}
.jsterm-editor .webconsole-filteringbar-wrapper {
grid-row: 2 / 4;
grid-column: 2 / 3;
}
.jsterm-editor .webconsole-filterbar-primary {
grid-row: 1 / 2;
}
.jsterm-editor .webconsole-editor-toolbar {
grid-column: 1 / 2;
grid-row: 2 / 3;
display: flex;
flex-direction: row;
justify-content: start;
height: unset;
}
.jsterm-editor .webconsole-output {
grid-column: 2 / 3;
grid-row: 4 / -1;
}
.jsterm-editor .jsterm-input-container {
grid-column: 1 / 2;
grid-row: 3 / 5;
width: 30vw;
min-width: 150px;
border-top: none;
}
.jsterm-editor #webconsole-notificationbox {
grid-row: 1 / 2;
grid-column: -1 / 1;
}
.jsterm-cm.jsterm-editor .jsterm-input-container > .CodeMirror {
padding-inline-start: 0;
background-image: none;
}
.jsterm-editor .sidebar {
grid-row: 2 / -1;
}
.jsterm-editor .editor-resizer {
grid-column: 1 / 2;
/* We want the splitter to cover the whole column (minus self-xss message) */
grid-row: 2 / -1;
}
.jsterm-editor .sidebar-resizer {
grid-column: -1 / -2;
/* We want the splitter to cover the whole column (minus self-xss message) */
grid-row: 2 / -1;
}

View File

@ -46,6 +46,9 @@ const ConfirmDialog = createFactory(
const NotificationBox = createFactory(
require("devtools/client/shared/components/NotificationBox").NotificationBox
);
const GridElementWidthResizer = createFactory(
require("devtools/client/shared/components/splitter/GridElementWidthResizer")
);
const l10n = require("devtools/client/webconsole/webconsole-l10n");
const { Utils: WebConsoleUtils } = require("devtools/client/webconsole/utils");
@ -82,6 +85,7 @@ class App extends Component {
reverseSearchInputVisible: PropTypes.bool,
reverseSearchInitialValue: PropTypes.string,
editorMode: PropTypes.bool,
editorWidth: PropTypes.number,
hideShowContentMessagesCheckbox: PropTypes.bool,
sidebarVisible: PropTypes.bool.isRequired,
filterBarDisplayMode: PropTypes.oneOf([
@ -231,40 +235,111 @@ class App extends Component {
input.addEventListener("keyup", pasteKeyUpHandler);
}
// Rendering
render() {
renderFilterBar() {
const {
webConsoleUI,
notifications,
onFirstMeaningfulPaint,
serviceContainer,
closeSplitConsole,
jstermCodeMirror,
autocomplete,
reverseSearchInitialValue,
editorMode,
filterBarDisplayMode,
sidebarVisible,
hideShowContentMessagesCheckbox,
} = this.props;
return FilterBar({
key: "filterbar",
hidePersistLogsCheckbox: webConsoleUI.isBrowserConsole,
hideShowContentMessagesCheckbox,
closeSplitConsole,
displayMode: filterBarDisplayMode,
});
}
renderConsoleOutput() {
const { onFirstMeaningfulPaint, serviceContainer } = this.props;
return ConsoleOutput({
key: "console-output",
serviceContainer,
onFirstMeaningfulPaint,
});
}
renderJsTerm() {
const {
webConsoleUI,
serviceContainer,
jstermCodeMirror,
autocomplete,
editorMode,
editorWidth,
} = this.props;
return JSTerm({
key: "jsterm",
webConsoleUI,
serviceContainer,
onPaste: this.onPaste,
codeMirrorEnabled: jstermCodeMirror,
autocomplete,
editorMode,
editorWidth,
});
}
renderReverseSearch() {
const { serviceContainer, reverseSearchInitialValue } = this.props;
return ReverseSearchInput({
key: "reverse-search-input",
setInputValue: serviceContainer.setInputValue,
focusInput: serviceContainer.focusInput,
evaluateInput: serviceContainer.evaluateInput,
initialValue: reverseSearchInitialValue,
});
}
renderSideBar() {
const { serviceContainer, sidebarVisible } = this.props;
return SideBar({
key: "sidebar",
serviceContainer,
visible: sidebarVisible,
});
}
renderNotificationBox() {
const { notifications, editorMode } = this.props;
return NotificationBox({
id: "webconsole-notificationbox",
key: "notification-box",
displayBorderTop: !editorMode,
displayBorderBottom: editorMode,
wrapping: true,
notifications,
});
}
renderConfirmDialog() {
const { webConsoleUI, serviceContainer, jstermCodeMirror } = this.props;
return ConfirmDialog({
webConsoleUI,
serviceContainer,
codeMirrorEnabled: jstermCodeMirror,
key: "confirm-dialog",
});
}
renderRootElement(children) {
const { jstermCodeMirror, editorMode } = this.props;
const classNames = ["webconsole-app"];
if (jstermCodeMirror) {
classNames.push("jsterm-cm");
}
if (editorMode) {
classNames.push("jsterm-editor");
}
if (jstermCodeMirror) {
classNames.push("jsterm-cm");
}
// Render the entire Console panel. The panel consists
// from the following parts:
// * FilterBar - Buttons & free text for content filtering
// * Content - List of logs & messages
// * NotificationBox - Notifications for JSTerm (self-xss warning at the moment)
// * JSTerm - Input command line.
// * ReverseSearchInput - Reverse search input.
// * SideBar - Object inspector
return div(
{
className: classNames.join(" "),
@ -274,53 +349,47 @@ class App extends Component {
this.node = node;
},
},
div(
{ className: "webconsole-wrapper" },
FilterBar({
hidePersistLogsCheckbox: webConsoleUI.isBrowserConsole,
hideShowContentMessagesCheckbox,
closeSplitConsole,
displayMode: filterBarDisplayMode,
}),
ConsoleOutput({
serviceContainer,
onFirstMeaningfulPaint,
}),
NotificationBox({
id: "webconsole-notificationbox",
wrapping: true,
notifications,
}),
EditorToolbar({
editorMode,
webConsoleUI,
}),
JSTerm({
webConsoleUI,
serviceContainer,
onPaste: this.onPaste,
codeMirrorEnabled: jstermCodeMirror,
autocomplete,
editorMode,
}),
ReverseSearchInput({
setInputValue: serviceContainer.setInputValue,
focusInput: serviceContainer.focusInput,
evaluateInput: serviceContainer.evaluateInput,
initialValue: reverseSearchInitialValue,
})
),
SideBar({
serviceContainer,
visible: sidebarVisible,
}),
ConfirmDialog({
webConsoleUI,
serviceContainer,
codeMirrorEnabled: jstermCodeMirror,
})
...children
);
}
render() {
const { webConsoleUI, editorMode, dispatch } = this.props;
const filterBar = this.renderFilterBar();
const consoleOutput = this.renderConsoleOutput();
const notificationBox = this.renderNotificationBox();
const jsterm = this.renderJsTerm();
const reverseSearch = this.renderReverseSearch();
const sidebar = this.renderSideBar();
const confirmDialog = this.renderConfirmDialog();
return this.renderRootElement([
filterBar,
editorMode
? EditorToolbar({
editorMode,
webConsoleUI,
})
: null,
dom.div(
{ className: "flexible-output-input" },
consoleOutput,
notificationBox,
jsterm
),
GridElementWidthResizer({
enabled: editorMode,
position: "end",
className: "editor-resizer",
getControlledElementNode: () => webConsoleUI.jsterm.node,
onResizeEnd: width => dispatch(actions.setEditorWidth(width)),
}),
reverseSearch,
sidebar,
confirmDialog,
]);
}
}
const mapStateToProps = state => ({
@ -328,6 +397,7 @@ const mapStateToProps = state => ({
reverseSearchInputVisible: state.ui.reverseSearchInputVisible,
reverseSearchInitialValue: state.ui.reverseSearchInitialValue,
editorMode: state.ui.editor,
editorWidth: state.ui.editorWidth,
sidebarVisible: state.ui.sidebarVisible,
filterBarDisplayMode: state.ui.filterBarDisplayMode,
});

View File

@ -114,6 +114,7 @@ class JSTerm extends Component {
autocompleteData: PropTypes.object.isRequired,
// Is the input in editor mode.
editorMode: PropTypes.bool,
editorWidth: PropTypes.number,
autocomplete: PropTypes.bool,
};
}
@ -154,6 +155,10 @@ class JSTerm extends Component {
}
componentDidMount() {
if (this.props.editorMode) {
this.setEditorWidth(this.props.editorWidth);
}
const autocompleteOptions = {
onSelect: this.onAutocompleteSelect.bind(this),
onClick: this.acceptProposedCompletion.bind(this),
@ -539,8 +544,33 @@ class JSTerm extends Component {
this.updateAutocompletionPopup(nextProps.autocompleteData);
}
if (this.editor && nextProps.editorMode !== this.props.editorMode) {
this.editor.setOption("lineNumbers", nextProps.editorMode);
if (nextProps.editorMode !== this.props.editorMode) {
if (this.editor) {
this.editor.setOption("lineNumbers", nextProps.editorMode);
}
if (nextProps.editorMode && nextProps.editorWidth) {
this.setEditorWidth(nextProps.editorWidth);
} else {
this.setEditorWidth(null);
}
}
}
/**
*
* @param {Number|null} editorWidth: The width to set the node to. If null, removes any
* `width` property on node style.
*/
setEditorWidth(editorWidth) {
if (!this.node) {
return;
}
if (editorWidth) {
this.node.style.width = `${editorWidth}px`;
} else {
this.node.style.removeProperty("width");
}
}
@ -1795,6 +1825,9 @@ class JSTerm extends Component {
key: "jsterm-container",
style: { direction: "ltr" },
"aria-live": "off",
ref: node => {
this.node = node;
},
},
dom.textarea({
className: "jsterm-complete-node devtools-monospace",

View File

@ -69,6 +69,7 @@ class ConsoleTable extends Component {
{
className: "new-consoletable-header",
role: "columnheader",
key,
},
value
)
@ -88,6 +89,7 @@ class ConsoleTable extends Component {
{
role: "gridcell",
className: index % 2 ? "odd" : "even",
key,
},
GripMessageBody({
grip: item[key],
@ -196,11 +198,11 @@ function getTableItems(data = {}, type, headers = null) {
const { preview } = property;
const entries = preview.ownProperties || preview.items;
if (entries) {
for (const key of Object.keys(entries)) {
const entry = entries[key];
item[key] = Object.prototype.hasOwnProperty.call(entry, "value")
? entry.value
: entry;
for (const [key, entry] of Object.entries(entries)) {
item[key] =
entry && Object.prototype.hasOwnProperty.call(entry, "value")
? entry.value
: entry;
}
} else {
if (preview.key) {

View File

@ -90,8 +90,8 @@ class Message extends Component {
openLink: PropTypes.func.isRequired,
sourceMapService: PropTypes.any,
canRewind: PropTypes.func.isRequired,
jumpToExecutionPoint: PropTypes.func.isRequired,
onMessageHover: PropTypes.func.isRequired,
jumpToExecutionPoint: PropTypes.func,
onMessageHover: PropTypes.func,
}),
notes: PropTypes.arrayOf(
PropTypes.shape({
@ -418,6 +418,7 @@ class Message extends Component {
sourceMapService: serviceContainer
? serviceContainer.sourceMapService
: undefined,
messageSource: source,
})
: null
);

View File

@ -5,10 +5,13 @@
const {
Component,
createElement,
createFactory,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const GridElementWidthResizer = createFactory(
require("devtools/client/shared/components/splitter/GridElementWidthResizer")
);
loader.lazyRequireGetter(
this,
"dom",
@ -30,11 +33,6 @@ loader.lazyRequireGetter(
"PropTypes",
"devtools/client/shared/vendor/react-prop-types"
);
loader.lazyRequireGetter(
this,
"SplitBox",
"devtools/client/shared/components/splitter/SplitBox"
);
loader.lazyRequireGetter(
this,
"reps",
@ -78,7 +76,7 @@ class SideBar extends Component {
return null;
}
const { grip, serviceContainer, onResized } = this.props;
const { grip, serviceContainer } = this.props;
const objectInspector = getObjectInspector(grip, serviceContainer, {
autoExpandDepth: 1,
@ -86,37 +84,40 @@ class SideBar extends Component {
autoFocusRoot: true,
});
const endPanel = dom.aside(
{
className: "sidebar-wrapper",
},
dom.header(
{
className: "devtools-toolbar webconsole-sidebar-toolbar",
},
dom.button({
className: "devtools-button sidebar-close-button",
title: l10n.getStr("webconsole.closeSidebarButton.tooltip"),
onClick: this.onClickSidebarClose,
})
),
return [
dom.aside(
{
className: "sidebar-contents",
className: "sidebar",
key: "sidebar",
ref: node => {
this.node = node;
},
},
objectInspector
)
);
return createElement(SplitBox, {
className: "sidebar",
endPanel,
endPanelControl: true,
initialSize: "200px",
minSize: "100px",
vert: true,
onControlledPanelResized: onResized,
});
dom.header(
{
className: "devtools-toolbar webconsole-sidebar-toolbar",
},
dom.button({
className: "devtools-button sidebar-close-button",
title: l10n.getStr("webconsole.closeSidebarButton.tooltip"),
onClick: this.onClickSidebarClose,
})
),
dom.aside(
{
className: "sidebar-contents",
},
objectInspector
)
),
GridElementWidthResizer({
key: "resizer",
enabled: true,
position: "start",
className: "sidebar-resizer",
getControlledElementNode: () => this.node,
}),
];
}
}

View File

@ -10,6 +10,7 @@ DIRS += [
]
DevToolsModules(
'App.css',
'App.js',
'SideBar.js',
)

View File

@ -47,6 +47,7 @@ const actionTypes = {
PAUSED_EXCECUTION_POINT: "PAUSED_EXCECUTION_POINT",
WARNING_GROUPS_TOGGLE: "WARNING_GROUPS_TOGGLE",
WILL_NAVIGATE: "WILL_NAVIGATE",
EDITOR_SET_WIDTH: "EDITOR_SET_WIDTH",
};
const prefs = {
@ -74,6 +75,8 @@ const prefs = {
CONTENT_MESSAGES: "devtools.browserconsole.contentMessages",
// Display timestamp in messages.
MESSAGE_TIMESTAMP: "devtools.webconsole.timestampMessages",
// Store the editor width.
EDITOR_WIDTH: "input.editorWidth",
},
FEATURES: {
// We use the same pref to enable the sidebar on webconsole and browser console.

View File

@ -18,13 +18,14 @@
<link rel="stylesheet" href="resource://devtools/client/shared/components/reps/reps.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/Tabs.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/NotificationBox.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/GridElementResizer.css"/>
<link rel="stylesheet" href="chrome://devtools/content/netmonitor/src/assets/styles/httpi.css"/>
<link rel="stylesheet" href="resource://devtools/client/webconsole/components/Input/ReverseSearchInput.css"/>
<link rel="stylesheet" href="resource://devtools/client/webconsole/components/App.css"/>
<link rel="stylesheet" href="chrome://devtools/content/netmonitor/src/assets/styles/StatusCode.css"/>
<link rel="stylesheet" href="chrome://devtools/content/netmonitor/src/assets/styles/RequestList.css"/>
<script src="chrome://devtools/content/shared/theme-switching.js"></script>
<script src="resource://devtools/client/webconsole/main.js"></script>
</head>
<body class="theme-sidebar" role="application">
<main id="app-wrapper" class="theme-body" role="document" aria-live="polite">

View File

@ -1,22 +0,0 @@
/* 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/. */
/* global BrowserLoader */
"use strict";
const { BrowserLoader } = ChromeUtils.import(
"resource://devtools/client/shared/browser-loader.js"
);
this.WebConsoleWrapper = function(parentNode, webConsoleUI, toolbox, document) {
// Initialize module loader and load all modules of the new inline
// preview feature. The entire code-base doesn't need any extra
// privileges and runs entirely in content scope.
const WebConsoleWrapper = BrowserLoader({
baseURI: "resource://devtools/client/webconsole/",
window,
}).require("./webconsole-wrapper");
return new WebConsoleWrapper(parentNode, webConsoleUI, toolbox, document);
};

View File

@ -18,7 +18,6 @@ DevToolsModules(
'browser-console.js',
'constants.js',
'hudservice.js',
'main.js',
'panel.js',
'store.js',
'types.js',

View File

@ -19,6 +19,7 @@ const {
FILTERBAR_DISPLAY_MODE_SET,
FILTERBAR_DISPLAY_MODES,
EDITOR_TOGGLE,
EDITOR_SET_WIDTH,
} = require("devtools/client/webconsole/constants");
const { PANELS } = require("devtools/client/netmonitor/src/constants");
@ -38,6 +39,7 @@ const UiState = overrides =>
reverseSearchInputVisible: false,
reverseSearchInitialValue: "",
editor: false,
editorWidth: null,
filterBarDisplayMode: FILTERBAR_DISPLAY_MODES.WIDE,
},
overrides
@ -87,6 +89,11 @@ function ui(state = UiState(), action) {
...state,
editor: !state.editor,
};
case EDITOR_SET_WIDTH:
return {
...state,
editorWidth: action.width,
};
}
return state;

View File

@ -78,6 +78,7 @@ function configureStore(webConsoleUI, options = {}) {
? getBoolPref(PREFS.UI.CONTENT_MESSAGES)
: true,
editor: getBoolPref(PREFS.UI.EDITOR),
editorWidth: getIntPref(PREFS.UI.EDITOR_WIDTH),
timestampsVisible: getBoolPref(PREFS.UI.MESSAGE_TIMESTAMP),
}),
};

View File

@ -29,6 +29,7 @@ pref("devtools.webconsole.input.editor", false);
pref("devtools.webconsole.input.autocomplete", true);
pref("devtools.browserconsole.contentMessages", true);
pref("devtools.webconsole.features.editor", true);
pref("devtools.webconsole.input.editorWidth", 800);
global.loader = {
lazyServiceGetter: () => {},

View File

@ -216,6 +216,7 @@ skip-if = os != 'mac' # The tested ctrl+key shortcuts are OSX only
[browser_jsterm_editor_execute.js]
[browser_jsterm_editor_gutter.js]
[browser_jsterm_editor_toggle_keyboard_shortcut.js]
[browser_jsterm_editor_resize.js]
[browser_jsterm_editor_toolbar.js]
[browser_jsterm_error_docs.js]
[browser_jsterm_error_outside_valid_range.js]

View File

@ -0,0 +1,88 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that the editor can be resized and that its width is persisted.
"use strict";
const TEST_URI =
"data:text/html;charset=utf-8,Web Console test for editor resize";
add_task(async function() {
await pushPref("devtools.webconsole.features.editor", true);
await pushPref("devtools.webconsole.input.editor", true);
// Run test with legacy JsTerm
await pushPref("devtools.webconsole.jsterm.codeMirror", false);
await performTests();
// And then run it with the CodeMirror-powered one.
await pushPref("devtools.webconsole.jsterm.codeMirror", true);
await performTests();
});
async function performTests() {
// Reset editorWidth pref so we have steady results when running multiple times.
await pushPref("devtools.webconsole.input.editorWidth", null);
let hud = await openNewTabAndConsole(TEST_URI);
const getEditorEl = () =>
hud.ui.outputNode.querySelector(".jsterm-input-container");
const resizerEl = hud.ui.outputNode.querySelector(".editor-resizer");
const editorBoundingRect = getEditorEl().getBoundingClientRect();
const delta = 100;
const originalWidth = editorBoundingRect.width;
const clientX = editorBoundingRect.right + delta;
await resize(resizerEl, clientX);
const newWidth = Math.floor(originalWidth + delta);
is(
Math.floor(getEditorEl().getBoundingClientRect().width),
newWidth,
"The editor element was resized as expected"
);
info("Close and re-open the console to check if editor width was persisted");
await closeConsole();
hud = await openConsole();
is(
Math.floor(getEditorEl().getBoundingClientRect().width),
newWidth,
"The editor element width was persisted"
);
await toggleLayout(hud);
ok(!getEditorEl().style.width, "The width isn't applied in in-line layout");
await toggleLayout(hud);
is(
getEditorEl().style.width,
`${newWidth}px`,
"The width is applied again when switching back to editor"
);
}
async function resize(resizer, clientX) {
const doc = resizer.ownerDocument;
const win = doc.defaultView;
info("Mouse down to start dragging");
EventUtils.synthesizeMouseAtCenter(
resizer,
{ button: 0, type: "mousedown" },
win
);
await waitFor(() => doc.querySelector(".dragging"));
const event = new MouseEvent("mousemove", { clientX });
resizer.ownerDocument.dispatchEvent(event);
info("Mouse up to stop resizing");
EventUtils.synthesizeMouseAtCenter(
doc.body,
{ button: 0, type: "mouseup" },
win
);
await waitFor(() => !doc.querySelector(".dragging"));
}

View File

@ -29,24 +29,31 @@ async function performTest() {
await pushPref(EDITOR_PREF, false);
let hud = await openNewTabAndConsole(TEST_URI);
const INPUT_VALUE = "hello";
setInputValue(hud, INPUT_VALUE);
is(isEditorModeEnabled(hud), false, "The console isn't in editor mode");
info("Enable the editor mode");
await toggleLayout(hud);
is(isEditorModeEnabled(hud), true, "Editor mode is enabled");
is(getInputValue(hud), INPUT_VALUE, "The input value wasn't cleared");
info("Close the console and reopen it");
await closeConsole();
hud = await openConsole();
is(isEditorModeEnabled(hud), true, "Editor mode is still enabled");
setInputValue(hud, INPUT_VALUE);
info("Disable the editor mode");
await toggleLayout(hud);
is(isEditorModeEnabled(hud), false, "Editor was disabled");
is(getInputValue(hud), INPUT_VALUE, "The input value wasn't cleared");
info("Enable the editor mode again");
await toggleLayout(hud);
is(isEditorModeEnabled(hud), true, "Editor mode was enabled again");
is(getInputValue(hud), INPUT_VALUE, "The input value wasn't cleared");
Services.prefs.clearUserPref(EDITOR_PREF);
}

View File

@ -50,6 +50,15 @@ add_task(async function() {
],
},
},
{
info: "Testing when data argument has holey array",
// eslint-disable-next-line no-sparse-arrays
input: [[1, , 2]],
expected: {
columns: ["(index)", "0", "1", "2"],
rows: [["0", "1", "undefined", "2"]],
},
},
{
info: "Testing when data argument is an object",
input: new Person("John", "Smith"),
@ -147,6 +156,15 @@ add_task(async function() {
rows: [["0", "apples"], ["1", "oranges"], ["2", "bananas"]],
},
},
{
info: "Testing overflow-y",
input: Array.from({ length: 50 }, (_, i) => `item-${i}`),
expected: {
columns: ["(index)", "Values"],
rows: Array.from({ length: 50 }, (_, i) => [i.toString(), `item-${i}`]),
overflow: true,
},
},
];
await ContentTask.spawn(gBrowser.selectedBrowser, testCases, function(tests) {
@ -199,6 +217,11 @@ function testItem(testCase, node) {
const rowCells = cells.slice(startIndex, startIndex + columnsNumber);
is(rowCells.map(x => x.textContent).join(" | "), expectedRow.join(" | "));
});
if (testCase.expected.overflow) {
ok(node.scrollHeight > node.clientHeight, "table overflows");
ok(getComputedStyle(node).overflowY !== "hidden", "table can be scrolled");
}
}
function findConsoleTable(node, index) {

View File

@ -28,9 +28,11 @@ async function performTests() {
const { ui } = hud;
const { document } = ui;
const appNode = document.querySelector(".webconsole-app");
const [filterBarNode, outputNode, , inputNode] = appNode.querySelector(
".webconsole-wrapper"
).childNodes;
const filterBarNode = appNode.querySelector(
".webconsole-filteringbar-wrapper"
);
const outputNode = appNode.querySelector(".webconsole-output");
const inputNode = appNode.querySelector(".jsterm-input-container");
testLayout(appNode);

View File

@ -35,6 +35,8 @@ function getPrefsService(hud) {
Services.prefs.getIntPref(getPrefName(pref), deflt),
setBoolPref: (pref, value) =>
Services.prefs.setBoolPref(getPrefName(pref), value),
setIntPref: (pref, value) =>
Services.prefs.setIntPref(getPrefName(pref), value),
clearUserPref: pref => Services.prefs.clearUserPref(getPrefName(pref)),
getPrefName,
};

View File

@ -17,6 +17,11 @@ const {
const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
const { l10n } = require("devtools/client/webconsole/utils/messages");
var ChromeUtils = require("ChromeUtils");
const { BrowserLoader } = ChromeUtils.import(
"resource://devtools/client/shared/browser-loader.js"
);
loader.lazyRequireGetter(
this,
"AppConstants",
@ -232,7 +237,14 @@ class WebConsoleUI {
const toolbox = gDevTools.getToolbox(this.hud.target);
this.wrapper = new this.window.WebConsoleWrapper(
// Initialize module loader and load all the WebConsoleWrapper. The entire code-base
// doesn't need any extra privileges and runs entirely in content scope.
const WebConsoleWrapper = BrowserLoader({
baseURI: "resource://devtools/client/webconsole/",
window: this.window,
}).require("./webconsole-wrapper");
this.wrapper = new WebConsoleWrapper(
this.outputNode,
this,
toolbox,

View File

@ -193,7 +193,7 @@ const previewers = {
value = ObjectUtils.makeDebuggeeValueIfNeeded(obj, value);
items.push(hooks.createValueGrip(value));
} else {
items.push(null);
items.push(hooks.createValueGrip(undefined));
}
} else {
// Workers do not have access to Cu, and when recording/replaying we

View File

@ -11,6 +11,14 @@
#include "prsystem.h"
// Short macro to get the size of a member of a
// given struct at compile time.
// t is the type of struct, m the name of the
// member:
// DOM_SIZEOF_MEMBER(struct mystruct, myint)
// will give you the size of the type of myint.
#define DOM_SIZEOF_MEMBER(t, m) sizeof(((t*)0)->m)
#if defined(XP_UNIX)
# include "unistd.h"
# include "dirent.h"
@ -602,6 +610,9 @@ static const dom::ConstantSpec gLibcProperties[] = {
{"OSFILE_SIZEOF_STATVFS", JS::Int32Value(sizeof(struct statvfs))},
// We have no guarantee how big "f_frsize" is, so we have to calculate that.
{"OSFILE_SIZEOF_STATVFS_F_FRSIZE",
JS::Int32Value(DOM_SIZEOF_MEMBER(struct statvfs, f_frsize))},
{"OSFILE_OFFSETOF_STATVFS_F_FRSIZE",
JS::Int32Value(offsetof(struct statvfs, f_frsize))},
{"OSFILE_OFFSETOF_STATVFS_F_BAVAIL",

View File

@ -122,15 +122,7 @@ void brush_vs(
vec2 f = (vi.local_pos - local_rect.p0) / local_rect.size;
#ifdef WR_FEATURE_ALPHA_PASS
int color_mode = prim_user_data.x & 0xffff;
int blend_mode = prim_user_data.x >> 16;
int raster_space = prim_user_data.y;
if (color_mode == COLOR_MODE_FROM_PASS) {
color_mode = uMode;
}
// Derive the texture coordinates for this image, based on
// whether the source image is a local-space or screen-space
// image.
@ -145,6 +137,14 @@ void brush_vs(
default:
break;
}
#ifdef WR_FEATURE_ALPHA_PASS
int color_mode = prim_user_data.x & 0xffff;
int blend_mode = prim_user_data.x >> 16;
if (color_mode == COLOR_MODE_FROM_PASS) {
color_mode = uMode;
}
#endif
// Offset and scale vUv here to avoid doing it in the fragment shader.

View File

@ -1226,7 +1226,7 @@ impl BatchBuilder {
textures,
[
ShaderColorMode::Image as i32 | ((AlphaType::PremultipliedAlpha as i32) << 16),
RasterizationSpace::Local as i32,
RasterizationSpace::Screen as i32,
get_shader_opacity(1.0),
0,
],

View File

@ -240,7 +240,7 @@ impl FrameBuilder {
retained_tiles: RetainedTiles,
globals: FrameGlobalResources,
) {
assert!(self.pending_retained_tiles.caches.is_empty());
debug_assert!(self.pending_retained_tiles.tiles.is_empty());
self.pending_retained_tiles = retained_tiles;
self.globals = globals;
}

View File

@ -111,35 +111,26 @@ struct PictureInfo {
_spatial_node_index: SpatialNodeIndex,
}
pub struct PictureCacheState {
/// The tiles retained by this picture cache.
pub tiles: FastHashMap<TileOffset, Tile>,
/// The current fractional offset of the cache transform root.
fract_offset: PictureVector2D,
}
/// Stores a list of cached picture tiles that are retained
/// between new scenes.
#[cfg_attr(feature = "capture", derive(Serialize))]
pub struct RetainedTiles {
/// The tiles retained between display lists.
#[cfg_attr(feature = "capture", serde(skip))] //TODO
pub caches: FastHashMap<usize, PictureCacheState>,
pub tiles: FastHashMap<TileKey, Tile>,
}
impl RetainedTiles {
pub fn new() -> Self {
RetainedTiles {
caches: FastHashMap::default(),
tiles: FastHashMap::default(),
}
}
/// Merge items from one retained tiles into another.
pub fn merge(&mut self, other: RetainedTiles) {
assert!(self.caches.is_empty() || other.caches.is_empty());
if self.caches.is_empty() {
self.caches = other.caches;
}
assert!(self.tiles.is_empty() || other.tiles.is_empty());
self.tiles.extend(other.tiles);
}
}
@ -207,6 +198,14 @@ impl From<PropertyBinding<f32>> for OpacityBinding {
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct TileId(usize);
#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
pub struct TileKey {
/// The picture cache slice that this belongs to.
slice: usize,
/// Offset (in tile coords) of the tile within this slice.
offset: TileOffset,
}
/// Information about a cached tile.
#[derive(Debug)]
pub struct Tile {
@ -505,7 +504,7 @@ pub struct TileCacheInstance {
/// The positioning node for this tile cache.
pub spatial_node_index: SpatialNodeIndex,
/// Hash of tiles present in this picture.
pub tiles: FastHashMap<TileOffset, Tile>,
pub tiles: FastHashMap<TileKey, Tile>,
/// A helper struct to map local rects into surface coords.
map_local_to_surface: SpaceMapper<LayoutPixel, PicturePixel>,
/// List of opacity bindings, with some extra information
@ -526,7 +525,7 @@ pub struct TileCacheInstance {
/// Local clip rect for this tile cache.
pub local_clip_rect: PictureRect,
/// A list of tiles that are valid and visible, which should be drawn to the main scene.
pub tiles_to_draw: Vec<TileOffset>,
pub tiles_to_draw: Vec<TileKey>,
/// The world space viewport that this tile cache draws into.
/// Any clips outside this viewport can be ignored (and must be removed so that
/// we can draw outside the bounds of the viewport).
@ -543,10 +542,6 @@ pub struct TileCacheInstance {
/// The allowed subpixel mode for this surface, which depends on the detected
/// opacity of the background.
pub subpixel_mode: SubpixelMode,
/// The current fractional offset of the cache transform root. If this changes,
/// all tiles need to be invalidated and redrawn, since snapping differences are
/// likely to occur.
fract_offset: PictureVector2D,
}
impl TileCacheInstance {
@ -577,7 +572,6 @@ impl TileCacheInstance {
background_color,
opaque_rect: PictureRect::zero(),
subpixel_mode: SubpixelMode::Allow,
fract_offset: PictureVector2D::zero(),
}
}
@ -645,35 +639,6 @@ impl TileCacheInstance {
frame_context.clip_scroll_tree,
);
// If there are pending retained state, retrieve it.
if let Some(prev_state) = frame_state.retained_tiles.caches.remove(&self.slice) {
self.tiles.extend(prev_state.tiles);
self.fract_offset = prev_state.fract_offset;
}
// Map an arbitrary point in picture space to world space, to work out
// what the fractional translation is that's applied by this scroll root.
// TODO(gw): I'm not 100% sure this is right. At least, in future, we should
// make a specific API for this, and/or enforce that the picture
// cache transform only includes scale and/or translation (we
// already ensure it doesn't have perspective).
let world_origin = pic_to_world_mapper
.map(&PictureRect::new(PicturePoint::zero(), PictureSize::new(1.0, 1.0)))
.expect("bug: unable to map origin to world space")
.origin;
let fract_offset = PictureVector2D::new(
world_origin.x.fract(),
world_origin.y.fract(),
);
// Determine if the fractional offset of the transform is different this frame
// from the currently cached tile set.
let fract_changed = (self.fract_offset.x - fract_offset.x).abs() > 0.001 ||
(self.fract_offset.y - fract_offset.y).abs() > 0.001;
if fract_changed {
self.fract_offset = fract_offset;
}
let spatial_node = &frame_context
.clip_scroll_tree
.spatial_nodes[self.spatial_node_index.0 as usize];
@ -786,16 +751,31 @@ impl TileCacheInstance {
self.tile_bounds_p0 = TileOffset::new(x0, y0);
self.tile_bounds_p1 = TileOffset::new(x1, y1);
let mut world_culling_rect = WorldRect::zero();
// TODO(gw): Tidy this up as we add better support for retaining
// slices and sub-grid dirty areas.
let mut keys = Vec::new();
for key in frame_state.retained_tiles.tiles.keys() {
if key.slice == self.slice {
keys.push(*key);
}
}
for key in keys {
self.tiles.insert(key, frame_state.retained_tiles.tiles.remove(&key).unwrap());
}
let mut old_tiles = mem::replace(
&mut self.tiles,
FastHashMap::default(),
);
let mut world_culling_rect = WorldRect::zero();
for y in y0 .. y1 {
for x in x0 .. x1 {
let key = TileOffset::new(x, y);
let key = TileKey {
offset: TileOffset::new(x, y),
slice: self.slice,
};
let mut tile = old_tiles
.remove(&key)
@ -804,13 +784,10 @@ impl TileCacheInstance {
Tile::new(next_id)
});
// Ensure each tile is offset by the appropriate amount from the
// origin, such that the content origin will be a whole number and
// the snapping will be consistent.
tile.rect = PictureRect::new(
PicturePoint::new(
x as f32 * self.tile_size.width - fract_offset.x,
y as f32 * self.tile_size.height - fract_offset.y,
x as f32 * self.tile_size.width,
y as f32 * self.tile_size.height,
),
self.tile_size,
);
@ -831,9 +808,8 @@ impl TileCacheInstance {
// Do tile invalidation for any dependencies that we know now.
for (_, tile) in &mut self.tiles {
// Start frame assuming that the tile has the same content,
// unless the fractional offset of the transform root changed.
tile.is_same_content = !fract_changed;
// Start frame assuming that the tile has the same content.
tile.is_same_content = true;
// Content has changed if any opacity bindings changed.
for binding in tile.descriptor.opacity_bindings.items() {
@ -1067,7 +1043,10 @@ impl TileCacheInstance {
for y in p0.y .. p1.y {
for x in p0.x .. p1.x {
// TODO(gw): Convert to 2d array temporarily to avoid hash lookups per-tile?
let key = TileOffset::new(x, y);
let key = TileKey {
slice: self.slice,
offset: TileOffset::new(x, y),
};
let tile = self.tiles.get_mut(&key).expect("bug: no tile");
// Mark if the tile is cacheable at all.
@ -1260,9 +1239,29 @@ impl TileCacheInstance {
TILE_SIZE_WIDTH,
TILE_SIZE_HEIGHT,
);
let content_origin_f = tile.world_rect.origin * frame_context.global_device_pixel_scale;
let content_origin_i = content_origin_f.floor();
// Calculate the UV coords for this tile. These are generally 0-1, but if the
// local rect of the cache has fractional coordinates, then the content origin
// of the tile is floor'ed, and so we need to adjust the UV rect in order to
// ensure a correct 1:1 texel:pixel mapping and correct snapping.
let s0 = (content_origin_f.x - content_origin_i.x) / tile.world_rect.size.width;
let t0 = (content_origin_f.y - content_origin_i.y) / tile.world_rect.size.height;
let s1 = 1.0;
let t1 = 1.0;
let uv_rect_kind = UvRectKind::Quad {
top_left: DeviceHomogeneousVector::new(s0, t0, 0.0, 1.0),
top_right: DeviceHomogeneousVector::new(s1, t0, 0.0, 1.0),
bottom_left: DeviceHomogeneousVector::new(s0, t1, 0.0, 1.0),
bottom_right: DeviceHomogeneousVector::new(s1, t1, 0.0, 1.0),
};
resource_cache.texture_cache.update_picture_cache(
tile_size,
&mut tile.handle,
uv_rect_kind,
gpu_cache,
);
}
@ -2049,15 +2048,7 @@ impl PicturePrimitive {
retained_tiles: &mut RetainedTiles,
) {
if let Some(tile_cache) = self.tile_cache.take() {
if !tile_cache.tiles.is_empty() {
retained_tiles.caches.insert(
tile_cache.slice,
PictureCacheState {
tiles: tile_cache.tiles,
fract_offset: tile_cache.fract_offset,
},
);
}
retained_tiles.tiles.extend(tile_cache.tiles);
}
}
@ -2443,10 +2434,10 @@ impl PicturePrimitive {
continue;
}
// The content origin for surfaces is always an integer value (this preserves
// the same snapping on a surface as would occur if drawn directly to parent).
let content_origin_f = tile.world_rect.origin * device_pixel_scale;
let content_origin = content_origin_f.round();
debug_assert!((content_origin_f.x - content_origin.x).abs() < 0.01);
debug_assert!((content_origin_f.y - content_origin.y).abs() < 0.01);
let content_origin = content_origin_f.floor().to_i32();
let cache_item = frame_state.resource_cache.texture_cache.get(&tile.handle);
@ -2458,7 +2449,7 @@ impl PicturePrimitive {
},
tile_size,
pic_index,
content_origin.to_i32(),
content_origin,
UvRectKind::Rect,
surface_spatial_node_index,
device_pixel_scale,

View File

@ -1316,6 +1316,7 @@ impl TextureCache {
&mut self,
tile_size: DeviceIntSize,
handle: &mut TextureCacheHandle,
uv_rect_kind: UvRectKind,
gpu_cache: &mut GpuCache,
) {
debug_assert!(self.now.is_valid());
@ -1347,10 +1348,11 @@ impl TextureCache {
}
// Upload the resource rect and texture array layer.
self.entries
let entry = self.entries
.get_opt_mut(handle)
.expect("BUG: handle must be valid now")
.update_gpu_cache(gpu_cache);
.expect("BUG: handle must be valid now");
entry.uv_rect_kind = uv_rect_kind;
entry.update_gpu_cache(gpu_cache);
}
}

View File

@ -480,7 +480,7 @@ pref("media.cubeb.logging_level", "");
// Cubeb sandbox (remoting) control
#if defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID)
pref("media.cubeb.sandbox", true);
pref("media.audioipc.pool_size", 2);
pref("media.audioipc.pool_size", 1);
// 64 * 4 kB stack per pool thread.
pref("media.audioipc.stack_size", 262144);
#else

View File

@ -12,6 +12,7 @@ class Domain {
this.name = this.constructor.name;
this.eventListeners_ = new Set();
this._requestCounter = 0;
}
destructor() {}
@ -30,6 +31,29 @@ class Domain {
}
}
/**
* Execute the provided method in the child domain that has the same domain
* name. eg. calling this.executeInChild from domains/parent/Input.jsm will
* attempt to execute the method in domains/content/Input.jsm.
*
* This can only be called from parent domains managed by a TabSession.
*
* @param {String} method
* Name of the method to call on the child domain.
* @param {Object} params
* Optional parameters. Must be serializable.
*/
executeInChild(method, params) {
if (!this.session.executeInChild) {
throw new Error(
"executeInChild can only be used in Domains managed by a TabSession"
);
}
this._requestCounter++;
const id = this.name + "-" + this._requestCounter;
return this.session.executeInChild(id, this.name, method, params);
}
addEventListener(listener) {
if (typeof listener != "function" && !isEventHandler(listener)) {
throw new TypeError();

View File

@ -10,4 +10,47 @@ const { ContentProcessDomain } = ChromeUtils.import(
"chrome://remote/content/domains/ContentProcessDomain.jsm"
);
class Input extends ContentProcessDomain {}
class Input extends ContentProcessDomain {
constructor(session) {
super(session);
// Map of event name -> event handler promise.
this._eventPromises = new Map();
}
/**
* Not a CDP method.
*
* Add an event listener in the content page for the provided eventName.
* To wait for the event, you should use `waitForContentEvent` with the same event name.
*
* Example usage from a parent process domain:
*
* await this.executeInChild("addContentEventListener", "click");
* // do something that triggers a click in content
* await this.executeInChild("waitForContentEvent", "click");
*/
addContentEventListener(eventName) {
const eventPromise = new Promise(r => {
this.chromeEventHandler.addEventListener(eventName, r, {
mozSystemGroup: true,
once: true,
});
});
this._eventPromises.set(eventName, eventPromise);
}
/**
* Not a CDP method.
*
* Wait for an event listener added via `addContentEventListener` to be fired.
*/
async waitForContentEvent(eventName) {
const eventPromise = this._eventPromises.get(eventName);
if (!eventPromise) {
throw new Error("No event promise available for " + eventName);
}
await eventPromise;
this._eventPromises.delete(eventName);
}
}

View File

@ -50,12 +50,7 @@ class Input extends Domain {
const browserWindow = browser.ownerGlobal;
const EventUtils = this._getEventUtils(browserWindow);
const onEvent = new Promise(r => {
browserWindow.addEventListener(domType, r, {
mozSystemGroup: true,
once: true,
});
});
await this.executeInChild("addContentEventListener", domType);
if (type == "char") {
// type == "char" is used when doing `await page.keyboard.type( 'Im a list' );`
@ -77,7 +72,8 @@ class Input extends Domain {
browserWindow
);
}
await onEvent;
await this.executeInChild("waitForContentEvent", domType);
}
async dispatchMouseEvent({ type, button, x, y, modifiers, clickCount }) {

View File

@ -4,6 +4,7 @@ subsuite = remote
prefs = remote.enabled=true
support-files =
chrome-remote-interface.js
doc_input_dispatchKeyEvent_race.html
doc_network_requestWillBeSent.html
doc_page_frameNavigated.html
doc_page_frameNavigated_iframe.html
@ -12,6 +13,7 @@ support-files =
[browser_cdp.js]
[browser_input_dispatchKeyEvent.js]
[browser_input_dispatchKeyEvent_race.js]
[browser_input_dispatchMouseEvent.js]
[browser_main_target.js]
[browser_network_requestWillBeSent.js]

View File

@ -59,13 +59,11 @@ add_task(async function() {
await checkInputContent("hH", 2);
info("Send char type event for char []");
await addInputEventListener("input");
await Input.dispatchKeyEvent({
type: "char",
modifiers: 0,
key: "",
});
await waitForInputEvent();
await checkInputContent("hH", 3);
info("Send Left");
@ -107,29 +105,14 @@ add_task(async function() {
await RemoteAgent.close();
});
/**
* Send a text key and wait for the input event to be fired.
*/
async function sendTextKey(Input, key) {
await addInputEventListener("input");
await dispatchKeyEvent(Input, key, "keyDown");
await dispatchKeyEvent(Input, key, "keyUp");
await waitForInputEvent();
}
/**
* Send an arrow key that will update the position of the cursor in the input and wait
* for the selectionchange event to be fired.
*/
async function sendArrowKey(Input, arrowKey, modifiers = 0) {
await addInputEventListener("selectionchange");
await dispatchKeyEvent(Input, arrowKey, "rawKeyDown", modifiers);
await dispatchKeyEvent(Input, arrowKey, "keyUp", modifiers);
await waitForInputEvent();
}
function dispatchKeyEvent(Input, key, type, modifiers = 0) {
@ -166,50 +149,8 @@ async function checkInputContent(expectedValue, expectedCaret) {
async function sendBackspace(Input, expected) {
info("Send Backspace");
await addInputEventListener("input");
await dispatchKeyEvent(Input, "Backspace", "rawKeyDown");
await dispatchKeyEvent(Input, "Backspace", "keyUp");
await waitForInputEvent();
await checkInputContent(expected, expected.length);
}
/**
* Add an event listener on the content page input. This will resolve when the listener is
* added.
*
* Since ContentTask.spawn is asynchronous make sure to await on this method before
* executing the code that should fire the event. Otherwise the test will be exposed to
* race conditions where the listener is added too late and is not triggered.
*
* Use `waitForInputEvent` afterwards to wait for the event to be fired.
*
* await addInputEventListener("click");
* await someCodeThatWillTriggerClick();
* await waitForInputEvent();
*/
function addInputEventListener(eventName) {
return ContentTask.spawn(
gBrowser.selectedBrowser,
eventName,
async _eventName => {
const input = content.document.querySelector("input");
this.__onInputEvent = new Promise(r =>
input.addEventListener(_eventName, r, { once: true })
);
}
);
}
/**
* See documentation for addInputEventListener.
*/
function waitForInputEvent() {
return ContentTask.spawn(
gBrowser.selectedBrowser,
null,
() => this.__onInputEvent
);
}

View File

@ -0,0 +1,88 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Here we test that the `dispatchKeyEvent` API resolves after all the synchronous event
// handlers from the content page have been flushed.
//
// Say the content page has an event handler such as:
//
// el.addEventListener("keyup", () => {
// doSomeVeryLongProcessing(); // <- takes a long time but is synchronous!
// window.myVariable = "newValue";
// });
//
// And imagine this is tested via:
//
// await Input.dispatchKeyEvent(...);
// const myVariable = await Runtime.evaluate({ expression: "window.myVariable" });
// equals(myVariable, "newValue");
//
// In order for this to work, we need to be sure that `await Input.dispatchKeyEvent`
// resolves only after the content page flushed the event handlers (and
// `window.myVariable = "newValue"` was executed).
//
// This can be racy because Input.dispatchKeyEvent and window.myVariable = "newValue" run
// in different processes.
const PAGE_URI =
"http://example.com/browser/remote/test/browser/doc_input_dispatchKeyEvent_race.html";
add_task(async function() {
const { client, tab } = await setupTestForUri(PAGE_URI);
is(gBrowser.selectedTab, tab, "Selected tab is the target tab");
const { Input, Runtime } = client;
// Need an enabled Runtime domain to run evaluate.
info("Enable the Runtime domain");
await Runtime.enable();
const { context } = await Runtime.executionContextCreated();
info("Focus the input on the page");
await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
const input = content.document.querySelector("input");
input.focus();
is(input, content.document.activeElement, "Input should be focused");
});
// See doc_input_dispatchKeyEvent_race.html
// The page listens to `input` events to update a property on window.
// We will check that the value is updated as soon dispatchKeyEvent has resolved.
await checkWindowTestValue("initial-value", context.id, Runtime);
info("Write 'hhhhhh' ('h' times 6)");
for (let i = 0; i < 6; i++) {
await dispatchKeyEvent(Input, "h", 72, "keyDown");
await dispatchKeyEvent(Input, "h", 72, "keyUp");
}
await checkWindowTestValue("hhhhhh", context.id, Runtime);
await client.close();
ok(true, "The client is closed");
BrowserTestUtils.removeTab(tab);
await RemoteAgent.close();
});
function dispatchKeyEvent(Input, key, keyCode, type, modifiers = 0) {
info(`Send ${type} for key ${key}`);
return Input.dispatchKeyEvent({
type,
modifiers,
windowsVirtualKeyCode: keyCode,
key,
});
}
async function checkWindowTestValue(expected, contextId, Runtime) {
info("Retrieve the value of `window.testValue` in the test page");
const { result } = await Runtime.evaluate({
contextId,
expression: "window.testValue",
});
is(result.value, expected, "Content window test value is correct");
}

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Test page for dispatchKeyEvent race test</title>
</head>
<body>
<input type="text">
<script type="text/javascript">
window.testValue = "initial-value";
// Small helper to synchronously pause for a given delay, in ms.
function sleep(delay) {
const start = Date.now();
while (Date.now() - start < delay) {
// Wait for the condition to fail.
}
}
document.querySelector("input").addEventListener("input", () => {
// Block the execution synchronously for one second.
sleep(1000);
// Update the value that will be checked by the test.
window.testValue = document.querySelector("input").value;
});
</script>
</body>
</html>

View File

@ -257,6 +257,16 @@
Type.fsblkcnt_t = Type.uintn_t(Const.OSFILE_SIZEOF_FSBLKCNT_T).withName(
"fsblkcnt_t"
);
// There is no guarantee of the size or order of members in sys-header structs
// It mostly is "unsigned long", but can be "unsigned int" as well.
// So it has its own "type".
// NOTE: This is still only partially correct, as signedness is also not guaranteed,
// so assuming an unsigned int might still be wrong here.
// But unsigned seems to have worked all those years, even though its signed
// on various platforms.
Type.statvfs_f_frsize = Type.uintn_t(
Const.OSFILE_SIZEOF_STATVFS_F_FRSIZE
).withName("statvfs_f_rsize");
// Structure |statvfs|
// Use an hollow structure
@ -269,7 +279,7 @@
statvfs.add_field_at(
Const.OSFILE_OFFSETOF_STATVFS_F_FRSIZE,
"f_frsize",
Type.unsigned_long.implementation
Type.statvfs_f_frsize.implementation
);
statvfs.add_field_at(
Const.OSFILE_OFFSETOF_STATVFS_F_BAVAIL,

View File

@ -2309,24 +2309,6 @@ devtools.changesview:
record_in_processes:
- 'main'
copy:
bug_numbers:
- 1522843
description: >
Number of times any text content from the Changes panel is copied to the clipboard.
expires: "69"
kind: uint
notification_emails:
- dev-developer-tools@lists.mozilla.org
- mbalfanz@mozilla.com
release_channel_collection: opt-out
products:
- 'firefox'
- 'fennec'
- 'geckoview'
record_in_processes:
- 'main'
copy_all_changes:
bug_numbers:
- 1532583
@ -2363,42 +2345,6 @@ devtools.changesview:
record_in_processes:
- 'main'
contextmenu:
bug_numbers:
- 1522843
description: >
Number of times the Changes panel context menu is shown.
expires: "69"
kind: uint
notification_emails:
- dev-developer-tools@lists.mozilla.org
- mbalfanz@mozilla.com
release_channel_collection: opt-out
products:
- 'firefox'
- 'fennec'
- 'geckoview'
record_in_processes:
- 'main'
contextmenu_copy:
bug_numbers:
- 1522843
description: >
Number of times the Copy option is picked from the Changes panel context menu.
expires: "69"
kind: uint
notification_emails:
- dev-developer-tools@lists.mozilla.org
- mbalfanz@mozilla.com
release_channel_collection: opt-out
products:
- 'firefox'
- 'fennec'
- 'geckoview'
record_in_processes:
- 'main'
contextmenu_copy_declaration:
bug_numbers:
- 1532583