Bug 1694145 - [webdriver-bidi] Extract console api observer to dedicated listener class r=webdriver-reviewers,whimboo

Differential Revision: https://phabricator.services.mozilla.com/D132150
This commit is contained in:
Julian Descottes 2021-12-02 19:21:44 +00:00
Родитель 5d296cef69
Коммит a9e76a12f8
7 изменённых файлов: 219 добавлений и 30 удалений

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

@ -20,6 +20,7 @@ remote.jar:
content/shared/TabManager.jsm (shared/TabManager.jsm)
content/shared/WebSocketConnection.jsm (shared/WebSocketConnection.jsm)
content/shared/WindowManager.jsm (shared/WindowManager.jsm)
content/shared/listeners/ConsoleAPIListener.jsm (shared/listeners/ConsoleAPIListener.jsm)
content/shared/listeners/ConsoleListener.jsm (shared/listeners/ConsoleListener.jsm)
# shared modules (messagehandler architecture)

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

@ -0,0 +1,103 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EXPORTED_SYMBOLS = ["ConsoleAPIListener"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
EventEmitter: "resource://gre/modules/EventEmitter.jsm",
Services: "resource://gre/modules/Services.jsm",
});
/**
* The ConsoleAPIListener can be used to listen for messages coming from console
* API usage in a given windowGlobal, eg. console.log, console.error, ...
*
* Example:
* ```
* const listener = new ConsoleAPIListener(innerWindowId);
* listener.on("message", onConsoleAPIMessage);
* listener.startListening();
*
* const onConsoleAPIMessage = (eventName, data = {}) => {
* const { arguments: msgArguments, level, rawMessage, timeStamp } = data;
* ...
* };
* ```
*
* @emits message
* The ConsoleAPIListener emits "message" events, with the following object as
* payload:
* - `message` property pointing to the wrappedJSObject of the raw message
* - `rawMessage` property pointing to the raw message
*/
class ConsoleAPIListener {
#innerWindowId;
#listening;
/**
* Create a new ConsolerListener instance.
*
* @param {Number} innerWindowId
* The inner window id to filter the messages for.
*/
constructor(innerWindowId) {
EventEmitter.decorate(this);
this.#listening = false;
this.#innerWindowId = innerWindowId;
}
destroy() {
this.stopListening();
}
startListening() {
if (this.#listening) {
return;
}
// Bug 1731574: Retrieve cached messages first before registering the
// listener to avoid duplicated messages.
Services.obs.addObserver(
this.#onConsoleAPIMessage,
"console-api-log-event"
);
this.#listening = true;
}
stopListening() {
if (!this.#listening) {
return;
}
Services.obs.removeObserver(
this.#onConsoleAPIMessage,
"console-api-log-event"
);
this.#listening = false;
}
#onConsoleAPIMessage = message => {
const messageObject = message.wrappedJSObject;
if (messageObject.innerID !== this.#innerWindowId) {
// If the message doesn't match the innerWindowId of the current context
// ignore it.
return;
}
this.emit("message", {
arguments: messageObject.arguments,
level: messageObject.level,
rawMessage: message,
timeStamp: messageObject.timeStamp,
});
};
}

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

@ -118,7 +118,7 @@ class ConsoleListener {
level,
message: message.errorMessage,
rawMessage: message,
timestamp: message.timestamp || Date.now(),
timeStamp: message.timeStamp,
});
};

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

@ -4,5 +4,6 @@ subsuite = remote
prefs =
remote.messagehandler.modulecache.useBrowserTestRoot=true
[browser_ConsoleAPIListener.js]
[browser_ConsoleListener.js]

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

@ -0,0 +1,90 @@
/* 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/. */
const TESTS = [
{ method: "log", args: ["log1"] },
{ method: "log", args: ["log2", "log3"] },
{ method: "log", args: [[1, 2, 3], { someProperty: "someValue" }] },
{ method: "warn", args: ["warn1"] },
{ method: "error", args: ["error1"] },
{ method: "info", args: ["info1"] },
{ method: "debug", args: ["debug1"] },
{ method: "trace", args: ["trace1"] },
];
add_task(async function test_method_and_arguments() {
for (const { method, args } of TESTS) {
info(`Test ConsoleApiListener for ${JSON.stringify({ method, args })}`);
const listenerId = await listenToConsoleAPIMessage();
await useConsoleInContent(method, args);
const consoleMessage = await getConsoleAPIMessage(listenerId);
is(consoleMessage.level, method, "Message event has the expected level");
ok(
Number.isInteger(consoleMessage.timeStamp),
"Message event has a valid timestamp"
);
is(
consoleMessage.arguments.length,
args.length,
"Message event has the expected number of arguments"
);
for (let i = 0; i < args.length; i++) {
Assert.deepEqual(
consoleMessage.arguments[i],
args[i],
`Message event has the expected argument at index ${i}`
);
}
}
});
function useConsoleInContent(method, args) {
info(`Call console API: console.${method}("${args.join('", "')}");`);
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[method, args],
(_method, _args) => {
content.console[_method].apply(content.console, _args);
}
);
}
function listenToConsoleAPIMessage() {
info("Listen to a console api message in content");
return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
const innerWindowId = content.windowGlobalChild.innerWindowId;
const { ConsoleAPIListener } = ChromeUtils.import(
"chrome://remote/content/shared/listeners/ConsoleAPIListener.jsm"
);
const consoleAPIListener = new ConsoleAPIListener(innerWindowId);
const onMessage = consoleAPIListener.once("message");
consoleAPIListener.startListening();
const listenerId = Math.random();
content[listenerId] = { consoleAPIListener, onMessage };
return listenerId;
});
}
function getConsoleAPIMessage(listenerId) {
info("Retrieve the message event captured for listener: " + listenerId);
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[listenerId],
async _listenerId => {
const { consoleAPIListener, onMessage } = content[_listenerId];
const message = await onMessage;
consoleAPIListener.destroy();
// Note: we cannot return message directly here as it contains a
// `rawMessage` object which cannot be serialized by SpecialPowers.
return {
arguments: message.arguments,
level: message.level,
timeStamp: message.timeStamp,
};
}
);
}

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

@ -37,10 +37,10 @@ async function logMessage(options = {}) {
listener = (eventName, data) => {
is(eventName, level, "Expected event has been fired");
const { level: currentLevel, message, timestamp } = data;
const { level: currentLevel, message, timeStamp } = data;
is(typeof currentLevel, "string", "level is of type string");
is(typeof message, "string", "message is of type string");
is(typeof timestamp, "number", "timestamp is of type number");
is(typeof timeStamp, "number", "timeStamp is of type number");
resolve(data);
};

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

@ -11,8 +11,8 @@ const { XPCOMUtils } = ChromeUtils.import(
);
XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
ConsoleAPIListener:
"chrome://remote/content/shared/listeners/ConsoleAPIListener.jsm",
ConsoleListener:
"chrome://remote/content/shared/listeners/ConsoleListener.jsm",
Module: "chrome://remote/content/shared/messagehandler/Module.jsm",
@ -20,11 +20,18 @@ XPCOMUtils.defineLazyModuleGetters(this, {
});
class Log extends Module {
#consoleAPIListener;
#consoleMessageListener;
constructor(messageHandler) {
super(messageHandler);
// Create the console-api listener and listen on "message" events.
this.#consoleAPIListener = new ConsoleAPIListener(
this.messageHandler.innerWindowId
);
this.#consoleAPIListener.on("message", this.#onConsoleAPIMessage);
// Create the console listener and listen on error messages.
this.#consoleMessageListener = new ConsoleListener(
this.messageHandler.innerWindowId
@ -33,13 +40,10 @@ class Log extends Module {
}
destroy() {
this.#consoleAPIListener.off("message", this.#onConsoleAPIMessage);
this.#consoleAPIListener.destroy();
this.#consoleMessageListener.off("error", this.#onJavaScriptError);
this.#consoleMessageListener.destroy();
Services.obs.removeObserver(
this.#onConsoleAPILogEvent,
"console-api-log-event"
);
}
/**
@ -58,46 +62,36 @@ class Log extends Module {
_subscribeEvent(event) {
if (event === "log.entryAdded") {
this.#consoleAPIListener.startListening();
this.#consoleMessageListener.startListening();
Services.obs.addObserver(
this.#onConsoleAPILogEvent,
"console-api-log-event"
);
}
}
#onConsoleAPILogEvent = message => {
const messageObject = message.wrappedJSObject;
if (messageObject.innerID !== this.messageHandler.innerWindowId) {
// If the message doesn't match the innerWindowId of the current context
// ignore it.
return;
}
#onConsoleAPIMessage = (eventName, data = {}) => {
const { message } = data;
// Step numbers below refer to the specifications at
// https://w3c.github.io/webdriver-bidi/#event-log-entryAdded
// 1. The console method used to create the messageObject is stored in the
// 1. The console method used to create the message is stored in the
// `level` property. Translate it to a log.LogEntry level
const method = messageObject.level;
const method = message.level;
const level = this._getLogEntryLevelFromConsoleMethod(method);
// 2. Use the message's timeStamp or fallback on the current time value.
const timestamp = messageObject.timeStamp || Date.now();
const timestamp = message.timeStamp || Date.now();
// 3. Start assembling the text representation of the message.
let text = "";
// 4. Formatters have already been applied at this points.
// messageObject.arguments corresponds to the "formatted args" from the
// message.arguments corresponds to the "formatted args" from the
// specifications.
// 5. Concatenate all formatted arguments in text
// TODO: For m1 we only support string arguments, so we rely on the builtin
// toString for each argument which will be available in message.arguments.
const args = messageObject.arguments || [];
const args = message.arguments || [];
text += args.map(String).join(" ");
// Step 6 and 7: Serialize each arg as remote value.
@ -133,13 +127,13 @@ class Log extends Module {
};
#onJavaScriptError = (eventName, data = {}) => {
const { level, message, timestamp } = data;
const { level, message, timeStamp } = data;
const entry = {
type: "javascript",
level,
text: message,
timestamp,
timestamp: timeStamp || Date.now(),
// TODO: Bug 1731553
stackTrace: undefined,
};