зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1680504 - [remote] Implement Runtime.exceptionThrown. r=remote-protocol-reviewers,jgraham
Differential Revision: https://phabricator.services.mozilla.com/D100026
This commit is contained in:
Родитель
9e229babfa
Коммит
016c762add
|
@ -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",
|
||||
|
|
Загрузка…
Ссылка в новой задаче