Bug 1680504 - [remote] Implement Runtime.exceptionThrown. r=remote-protocol-reviewers,jgraham

Differential Revision: https://phabricator.services.mozilla.com/D100026
This commit is contained in:
Henrik Skupin 2020-12-21 12:35:40 +00:00
Родитель 9e229babfa
Коммит 016c762add
12 изменённых файлов: 430 добавлений и 73 удалений

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

@ -46,29 +46,39 @@ class Log extends ContentProcessDomain {
}
}
_getLogCategory(category) {
if (category.startsWith("CORS")) {
return "network";
} else if (category.includes("javascript")) {
return "javascript";
}
return "other";
}
// nsIObserver
/**
* Takes all script error messages belonging to the current window.
* Takes all script error messages that do not have an exception attached,
* and emits a "Log.entryAdded" event.
*
* @param {nsIConsoleMessage} message
* Message originating from the nsIConsoleService.
*/
observe(message) {
// Console messages will be handled via Runtime.consoleAPICalled
if (
message instanceof Ci.nsIScriptError &&
message.flags == Ci.nsIScriptError.errorFlag
) {
if (message instanceof Ci.nsIScriptError && !message.hasException) {
let url;
if (message.sourceName !== "debugger eval code") {
url = message.sourceName;
}
const entry = {
source: "javascript",
level: CONSOLE_MESSAGE_LEVEL_MAP[message.flags],
lineNumber: message.lineNumber,
stacktrace: message.stack,
source: this._getLogCategory(message.category),
level: CONSOLE_MESSAGE_LEVEL_MAP[message.logLevel],
text: message.errorMessage,
timestamp: message.timeStamp,
url: message.sourceName,
url,
lineNumber: message.lineNumber,
};
this.emit("Log.entryAdded", { entry });

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

@ -25,6 +25,10 @@ addDebuggerToGlobal(Cu.getGlobalForObject(this));
const OBSERVER_CONSOLE_API = "console-api-log-event";
const CONSOLE_API_LEVEL_MAP = {
warn: "warning",
};
class SetMap extends Map {
constructor() {
super();
@ -297,6 +301,29 @@ class Runtime extends ContentProcessDomain {
return this.__debugger;
}
_buildStackTrace(stack) {
const callFrames = [];
while (
stack &&
stack.source !== "debugger eval code" &&
!stack.source.startsWith("chrome://")
) {
callFrames.push({
functionName: stack.functionDisplayName,
scriptId: stack.sourceId,
url: stack.source,
lineNumber: stack.line,
columnNumber: stack.column,
});
stack = stack.parent || stack.asyncParent;
}
return {
callFrames,
};
}
_getRemoteObject(objectId) {
for (const ctx of this.contexts.values()) {
const debuggerObj = ctx.getRemoteObject(objectId);
@ -380,7 +407,33 @@ class Runtime extends ContentProcessDomain {
executionContextId: context?.id || 0,
timestamp: payload.timestamp,
type: payload.type,
stackTrace: payload.stacktrace,
stackTrace: this._buildStackTrace(payload.stack),
});
}
_emitExceptionThrown(payload) {
// Filter out messages that aren't coming from a valid inner window, or from
// a different browser tab. Also messages of type "time", which are not
// getting reported by Chrome.
const curBrowserId = this.session.browsingContext.browserId;
const win = Services.wm.getCurrentInnerWindowWithId(payload.innerWindowId);
if (!win || BrowsingContext.getFromWindow(win).browserId != curBrowserId) {
return;
}
const context = this._getDefaultContextForWindow();
this.emit("Runtime.exceptionThrown", {
timestamp: payload.timestamp,
exceptionDetails: {
// Temporary placeholder to return a number.
exceptionId: 0,
text: payload.text,
lineNumber: payload.lineNumber,
columnNumber: payload.columnNumber,
url: payload.url,
stackTrace: this._buildStackTrace(payload.stack),
executionContextId: context?.id || undefined,
},
});
}
@ -515,8 +568,9 @@ class Runtime extends ContentProcessDomain {
// nsIObserver
/**
* Takes a console message belonging to the current window that is not
* a Javascript error, and emits a "consoleAPICalled" event.
* Takes a console message belonging to the current window and emits a
* "exceptionThrown" event if it's a Javascript error, otherwise a
* "consoleAPICalled" event.
*
* @param {nsIConsoleMessage} message
* Console message.
@ -528,20 +582,9 @@ class Runtime extends ContentProcessDomain {
const message = subject.wrappedJSObject;
entry = fromConsoleAPI(message);
this._emitConsoleAPICalled(entry);
return;
}
if (subject instanceof Ci.nsIConsoleMessage) {
// Script errors will be handled through Log.entryAdded
if (
subject instanceof Ci.nsIScriptError &&
subject.flags == Ci.nsIScriptError.errorFlag
) {
return;
}
entry = fromConsoleMessage(subject);
this._emitConsoleAPICalled(entry);
} else if (subject instanceof Ci.nsIScriptError && subject.hasException) {
entry = fromScriptError(subject);
this._emitExceptionThrown(entry);
}
}
@ -553,36 +596,26 @@ class Runtime extends ContentProcessDomain {
}
function fromConsoleAPI(message) {
const CONSOLE_API_LEVEL_MAP = {
warn: "warning",
};
// From sendConsoleAPIMessage (toolkit/modules/Console.jsm)
return {
arguments: message.arguments,
innerWindowId: message.innerID,
// TODO: Fetch the stack (Bug 1679981)
stacktrace: undefined,
stack: undefined,
timestamp: message.timeStamp,
type: CONSOLE_API_LEVEL_MAP[message.level] || message.level,
};
}
function fromConsoleMessage(message) {
const CONSOLE_MESSAGE_LEVEL_MAP = {
[Ci.nsIConsoleMessage.debug]: "verbose",
[Ci.nsIConsoleMessage.info]: "info",
[Ci.nsIConsoleMessage.warn]: "warning",
[Ci.nsIConsoleMessage.error]: "error",
};
// From xpcom/base/nsIConsoleMessage.idl and dom/bindings/nsIScriptError.idl
function fromScriptError(error) {
// From dom/bindings/nsIScriptError.idl
return {
arguments: [message.message],
innerWindowId: message.innerWindowID,
// TODO: Fetch the stack (Bug 1679981)
stacktrace: undefined,
timestamp: message.timeStamp,
type: CONSOLE_MESSAGE_LEVEL_MAP[message.logLevel] || message.logLevel,
innerWindowId: error.innerWindowID,
columnNumber: error.columnNumber,
lineNumber: error.lineNumber,
stack: error.stack,
text: error.errorMessage,
timestamp: error.timeStamp,
url: error.sourceName,
};
}

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

@ -1380,7 +1380,7 @@
"FAIL"
],
"Page Page.Events.PageError should fire (page.spec.ts)": [
"TIMEOUT"
"PASS"
],
"Page Page.setUserAgent should work (page.spec.ts)": [
"PASS"

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

@ -440,6 +440,43 @@ async function createFile(contents, options = {}) {
return { file, path };
}
async function throwScriptError(options = {}) {
const { inContent = true } = options;
const addScriptErrorInternal = ({ options }) => {
const {
flag = Ci.nsIScriptError.errorFlag,
innerWindowId = content.windowGlobalChild.innerWindowId,
} = options;
const scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
Ci.nsIScriptError
);
scriptError.initWithWindowID(
options.text,
options.sourceName || "sourceName",
null,
options.lineNumber || 0,
options.columnNumber || 0,
flag,
options.category || "javascript",
innerWindowId
);
Services.console.logMessage(scriptError);
};
if (inContent) {
ContentTask.spawn(
gBrowser.selectedBrowser,
{ options },
addScriptErrorInternal
);
} else {
options.innerWindowId = window.windowGlobalChild.innerWindowId;
addScriptErrorInternal({ options });
}
}
class RecordEvents {
/**
* A timeline of events chosen by calls to `addRecorder`.

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

@ -0,0 +1,11 @@
[DEFAULT]
tags = remote
subsuite = remote
prefs =
remote.enabled=true
support-files =
!/remote/test/browser/chrome-remote-interface.js
!/remote/test/browser/head.js
head.js
[browser_entryAdded.js]

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

@ -0,0 +1,138 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function noEventsWhenLogDomainDisabled({ client }) {
await runEntryAddedTest(client, 0, async () => {
await throwScriptError("foo");
});
});
add_task(async function noEventsAfterLogDomainDisabled({ client }) {
const { Log } = client;
await Log.enable();
await Log.disable();
await runEntryAddedTest(client, 0, async () => {
await throwScriptError("foo");
});
});
add_task(async function noEventsForConsoleMessageWithException({ client }) {
const { Log } = client;
await Log.enable();
const context = await enableRuntime(client);
await runEntryAddedTest(client, 0, async () => {
evaluate(client, context.id, () => {
const foo = {};
foo.bar();
});
});
});
add_task(async function eventsForScriptErrorWithoutException({ client }) {
const { Log } = client;
await Log.enable();
await enableRuntime(client);
const events = await runEntryAddedTest(client, 1, async () => {
throwScriptError({
text: "foo",
sourceName: "http://foo.bar",
lineNumber: 7,
category: "javascript",
});
});
is(events[0].source, "javascript", "Got expected source");
is(events[0].level, "error", "Got expected level");
is(events[0].text, "foo", "Got expected text");
is(events[0].url, "http://foo.bar", "Got expected url");
is(events[0].lineNumber, 7, "Got expected line number");
});
add_task(async function eventsForScriptErrorLevels({ client }) {
const { Log } = client;
await Log.enable();
const flags = {
info: Ci.nsIScriptError.infoFlag,
warning: Ci.nsIScriptError.warningFlag,
error: Ci.nsIScriptError.errorFlag,
};
await enableRuntime(client);
for (const [level, flag] of Object.entries(flags)) {
const events = await runEntryAddedTest(client, 1, async () => {
throwScriptError({ text: level, flag });
});
is(events[0].text, level, "Got expected text");
is(events[0].level, level, "Got expected level");
}
});
add_task(async function eventsForScriptErrorContent({ client }) {
const { Log } = client;
await Log.enable();
const context = await enableRuntime(client);
const events = await runEntryAddedTest(client, 1, async () => {
evaluate(client, context.id, () => {
document.execCommand("copy");
});
});
is(events[0].source, "other", "Got expected source");
is(events[0].level, "warning", "Got expected level");
ok(
events[0].text.includes("document.execCommand(cut/copy) was denied"),
"Got expected text"
);
is(events[0].url, undefined, "Got undefined url");
is(events[0].lineNumber, 2, "Got expected line number");
});
async function runEntryAddedTest(client, eventCount, callback, options = {}) {
const { Log } = client;
const EVENT_ENTRY_ADDED = "Log.entryAdded";
const history = new RecordEvents(eventCount);
history.addRecorder({
event: Log.entryAdded,
eventName: EVENT_ENTRY_ADDED,
messageFn: payload => `Received "${EVENT_ENTRY_ADDED}"`,
});
const timeBefore = Date.now();
await callback();
const entryAddedEvents = await history.record();
is(entryAddedEvents.length, eventCount, "Got expected amount of events");
if (eventCount == 0) {
return [];
}
const timeAfter = Date.now();
// Check basic details for entryAdded events
entryAddedEvents.forEach(event => {
const timestamp = event.payload.entry.timestamp;
ok(
timestamp >= timeBefore && timestamp <= timeAfter,
"Got valid timestamp"
);
});
return entryAddedEvents.map(event => event.payload.entry);
}

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

@ -0,0 +1,11 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* import-globals-from ../head.js */
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/remote/test/browser/head.js",
this
);

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

@ -13,6 +13,7 @@ support-files =
[browser_callFunctionOn_returnByValue.js]
[browser_consoleAPICalled.js]
[browser_evaluate.js]
[browser_exceptionThrown.js]
[browser_executionContextEvents.js]
skip-if = os == "mac" || os == "win" # bug 1586503,1590930
[browser_getProperties.js]

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

@ -23,6 +23,17 @@ add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) {
});
});
add_task(async function noEventsForJavascriptErrors({ client }) {
await loadURL(PAGE_CONSOLE_EVENTS);
const context = await enableRuntime(client);
await runConsoleTest(client, 0, async () => {
evaluate(client, context.id, () => {
document.getElementById("js-error").click();
});
});
});
add_task(async function consoleAPI({ client }) {
const context = await enableRuntime(client);
@ -138,29 +149,6 @@ add_task(async function consoleAPIByContentInteraction({ client }) {
);
});
add_task(async function consoleMessageByContent({ client }) {
await loadURL(PAGE_CONSOLE_EVENTS);
const context = await enableRuntime(client);
const events = await runConsoleTest(client, 1, async () => {
evaluate(client, context.id, () => {
document.execCommand("copy");
});
});
is(events[0].type, "warning", "Got expected type");
Assert.equal(events[0].args.length, 1, "Got expected amount of argumnets");
ok(
events[0].args[0].value.includes("document.execCommand"),
"Got expected argument value"
);
is(
events[0].executionContextId,
context.id,
"Got event from current execution context"
);
});
async function runConsoleTest(client, eventCount, callback, options = {}) {
const { Runtime } = client;

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

@ -0,0 +1,120 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const PAGE_CONSOLE_EVENTS =
"http://example.com/browser/remote/test/browser/runtime/doc_console_events.html";
add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) {
await runExceptionThrownTest(client, 0, async () => {
await throwScriptError({ text: "foo" });
});
});
add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) {
const { Runtime } = client;
await Runtime.enable();
await Runtime.disable();
await runExceptionThrownTest(client, 0, async () => {
await throwScriptError({ text: "foo" });
});
});
add_task(async function noEventsForScriptErrorWithoutException({ client }) {
const { Runtime } = client;
await Runtime.enable();
await runExceptionThrownTest(client, 0, async () => {
await throwScriptError({ text: "foo" });
});
});
add_task(async function eventsForScriptErrorWithException({ client }) {
await loadURL(PAGE_CONSOLE_EVENTS);
const context = await enableRuntime(client);
const events = await runExceptionThrownTest(client, 1, async () => {
evaluate(client, context.id, () => {
document.getElementById("js-error").click();
});
});
is(
typeof events[0].exceptionId,
"number",
"Got expected type for exception id"
);
is(
events[0].text,
"TypeError: foo.click is not a function",
"Got expected text"
);
is(events[0].lineNumber, 9, "Got expected line number");
is(events[0].columnNumber, 11, "Got expected column number");
is(events[0].url, PAGE_CONSOLE_EVENTS, "Got expected url");
is(
events[0].executionContextId,
context.id,
"Got event from current execution context"
);
const callFrames = events[0].stackTrace.callFrames;
is(callFrames.length, 2, "Got expected amount of call frames");
is(callFrames[0].functionName, "throwError", "Got expected function name");
is(callFrames[0].url, PAGE_CONSOLE_EVENTS, "Got expected url");
is(callFrames[0].lineNumber, 9, "Got expected line number");
is(callFrames[0].columnNumber, 11, "Got expected column number");
is(callFrames[1].functionName, "onclick", "Got expected function name");
is(callFrames[1].url, PAGE_CONSOLE_EVENTS, "Got expected url");
});
async function runExceptionThrownTest(
client,
eventCount,
callback,
options = {}
) {
const { Runtime } = client;
const EVENT_EXCEPTION_THROWN = "Runtime.exceptionThrown";
const history = new RecordEvents(eventCount);
history.addRecorder({
event: Runtime.exceptionThrown,
eventName: EVENT_EXCEPTION_THROWN,
messageFn: payload => `Received "${payload.name}"`,
});
const timeBefore = Date.now();
await callback();
const exceptionThrownEvents = await history.record();
is(exceptionThrownEvents.length, eventCount, "Got expected amount of events");
if (eventCount == 0) {
return [];
}
const timeAfter = Date.now();
// Check basic details for entryAdded events
exceptionThrownEvents.forEach(event => {
const details = event.payload.exceptionDetails;
const timestamp = event.payload.timestamp;
is(typeof details, "object", "Got expected 'exceptionDetails' property");
ok(
timestamp >= timeBefore && timestamp <= timeAfter,
"Got valid timestamp"
);
});
return exceptionThrownEvents.map(event => event.payload.exceptionDetails);
}

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

@ -2,10 +2,17 @@
<html>
<head>
<title>Empty page</title>
<script>
function throwError() {
let foo = {};
foo.click();
}
</script>
</head>
<body>
<a id="console-log" href="javascript:console.log('foo')">console.log()</a><br/>
<a id="console-error" href="javascript:console.error('foo')">console.error()</a><br/>
<a id="js-error" href="javascript:foo.click()">Javascript Error</a><br/>
<a id="js-error" onclick="throwError()">Javascript Error</a><br/>
</body>
</html>

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

@ -9,6 +9,7 @@ BROWSER_CHROME_MANIFESTS += [
"browser/emulation/browser.ini",
"browser/input/browser.ini",
"browser/io/browser.ini",
"browser/log/browser.ini",
"browser/network/browser.ini",
"browser/page/browser.ini",
"browser/runtime/browser.ini",