зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1042082 - Chrome event bubbles are displayed when they shouldn't be r=ochameau
Differential Revision: https://phabricator.services.mozilla.com/D18358 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
bf8987e947
Коммит
11f246e18a
|
@ -12,6 +12,7 @@ support-files =
|
|||
doc_markup_events_02.html
|
||||
doc_markup_events_03.html
|
||||
doc_markup_events_04.html
|
||||
doc_markup_events_chrome_listeners.html
|
||||
doc_markup_events_jquery.html
|
||||
doc_markup_events_object_listener.html
|
||||
doc_markup_events-overflow.html
|
||||
|
@ -114,6 +115,8 @@ skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32
|
|||
[browser_markup_events_02.js]
|
||||
[browser_markup_events_03.js]
|
||||
[browser_markup_events_04.js]
|
||||
[browser_markup_events_chrome_blocked.js]
|
||||
[browser_markup_events_chrome_not_blocked.js]
|
||||
[browser_markup_events_click_to_close.js]
|
||||
[browser_markup_events_jquery_1.0.js]
|
||||
[browser_markup_events_jquery_1.1.js]
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* import-globals-from helper_events_test_runner.js */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that markup view event bubbles are hidden for <video> tags in the
|
||||
// content process when devtools.chrome.enabled=false.
|
||||
// <video> tags have 22 chrome listeners.
|
||||
|
||||
const TEST_URL = URL_ROOT + "doc_markup_events_chrome_listeners.html";
|
||||
|
||||
loadHelperScript("helper_events_test_runner.js");
|
||||
|
||||
const TEST_DATA = [
|
||||
{
|
||||
selector: "video",
|
||||
expected: [ ],
|
||||
},
|
||||
];
|
||||
|
||||
add_task(async function() {
|
||||
await pushPref("devtools.chrome.enabled", false);
|
||||
await runEventPopupTests(TEST_URL, TEST_DATA);
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* import-globals-from helper_events_test_runner.js */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that markup view event bubbles are shown for <video> tags in the
|
||||
// content process when devtools.chrome.enabled=true.
|
||||
|
||||
const TEST_URL = URL_ROOT + "doc_markup_events_chrome_listeners.html";
|
||||
|
||||
loadHelperScript("helper_events_test_runner.js");
|
||||
|
||||
const TEST_DATA = [
|
||||
{
|
||||
selector: "video",
|
||||
expected: [
|
||||
createEvent("canplay"),
|
||||
createEvent("canplaythrough"),
|
||||
createEvent("emptied"),
|
||||
createEvent("ended"),
|
||||
createEvent("error"),
|
||||
createEvent("keypress"),
|
||||
createEvent("loadeddata"),
|
||||
createEvent("loadedmetadata"),
|
||||
createEvent("loadstart"),
|
||||
createEvent("mozvideoonlyseekbegin"),
|
||||
createEvent("mozvideoonlyseekcompleted"),
|
||||
createEvent("pause"),
|
||||
createEvent("play"),
|
||||
createEvent("playing"),
|
||||
createEvent("progress"),
|
||||
createEvent("seeked"),
|
||||
createEvent("seeking"),
|
||||
createEvent("stalled"),
|
||||
createEvent("suspend"),
|
||||
createEvent("timeupdate"),
|
||||
createEvent("volumechange"),
|
||||
createEvent("waiting"),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function createEvent(type) {
|
||||
return {
|
||||
type: type,
|
||||
filename: "chrome://global/content/elements/videocontrols.js:437",
|
||||
attributes: [
|
||||
"Capturing",
|
||||
"DOM2",
|
||||
],
|
||||
handler: `
|
||||
${type === "play" ? "function" : "handleEvent"}(aEvent) {
|
||||
if (!aEvent.isTrusted) {
|
||||
this.log("Drop untrusted event ----> " + aEvent.type);
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("Got event ----> " + aEvent.type);
|
||||
|
||||
if (this.videoEvents.includes(aEvent.type)) {
|
||||
this.handleVideoEvent(aEvent);
|
||||
} else {
|
||||
this.handleControlEvent(aEvent);
|
||||
}
|
||||
}`,
|
||||
};
|
||||
}
|
||||
|
||||
add_task(async function() {
|
||||
await pushPref("devtools.chrome.enabled", true);
|
||||
await runEventPopupTests(TEST_URL, TEST_DATA);
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<video controls></video>
|
||||
</body>
|
||||
</html>
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
const { Cu } = require("chrome");
|
||||
const Services = require("Services");
|
||||
const makeDebugger = require("devtools/server/actors/utils/make-debugger");
|
||||
const {
|
||||
isAfterPseudoElement,
|
||||
isBeforePseudoElement,
|
||||
|
@ -194,6 +193,18 @@ const REACT_EVENT_NAMES = [
|
|||
* The base class that all the enent collectors should be based upon.
|
||||
*/
|
||||
class MainEventCollector {
|
||||
/**
|
||||
* We allow displaying chrome events if the page is chrome or if
|
||||
* `devtools.chrome.enabled = true`.
|
||||
*/
|
||||
get chromeEnabled() {
|
||||
if (typeof this._chromeEnabled === "undefined") {
|
||||
this._chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
|
||||
}
|
||||
|
||||
return this._chromeEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node has any event listeners attached. Please do not override
|
||||
* this method... your getListeners() implementation needs to have the
|
||||
|
@ -265,6 +276,16 @@ class MainEventCollector {
|
|||
unwrap(obj) {
|
||||
return Cu.isXrayWrapper(obj) ? obj.wrappedJSObject : obj;
|
||||
}
|
||||
|
||||
isChromeHandler(handler) {
|
||||
const handlerPrincipal = Cu.getObjectPrincipal(handler);
|
||||
|
||||
// Chrome codebase may register listeners on the page from a frame script or
|
||||
// JSM <video> tags may also report internal listeners, but they won't be
|
||||
// coming from the system principal. Instead, they will be using an expanded
|
||||
// principal.
|
||||
return handlerPrincipal.isSystemPrincipal || handlerPrincipal.isExpandedPrincipal;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -318,6 +339,12 @@ class DOMEventCollector extends MainEventCollector {
|
|||
continue;
|
||||
}
|
||||
|
||||
// If we shouldn't be showing chrome events due to context and this is a
|
||||
// chrome handler we can ignore it.
|
||||
if (!this.chromeEnabled && this.isChromeHandler(handler)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this is checking if a node has any listeners then we have found one
|
||||
// so return now.
|
||||
if (checkOnly) {
|
||||
|
@ -390,13 +417,20 @@ class JQueryEventCollector extends MainEventCollector {
|
|||
}
|
||||
|
||||
if (typeof event === "function" || typeof event === "object") {
|
||||
// If we shouldn't be showing chrome events due to context and this
|
||||
// is a chrome handler we can ignore it.
|
||||
const handler = event.handler || event;
|
||||
if (!this.chromeEnabled && this.isChromeHandler(handler)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const eventInfo = {
|
||||
type: type,
|
||||
handler: event.handler || event,
|
||||
handler: handler,
|
||||
tags: "jQuery",
|
||||
hide: {
|
||||
capturing: true,
|
||||
|
@ -470,12 +504,19 @@ class JQueryLiveEventCollector extends MainEventCollector {
|
|||
}
|
||||
|
||||
if (typeof event === "function" || typeof event === "object") {
|
||||
// If we shouldn't be showing chrome events due to context and this
|
||||
// is a chrome handler we can ignore it.
|
||||
const handler = event.handler || event;
|
||||
if (!this.chromeEnabled && this.isChromeHandler(handler)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
return true;
|
||||
}
|
||||
const eventInfo = {
|
||||
type: event.origType || event.type.substr(selector.length + 1),
|
||||
handler: event.handler || event,
|
||||
handler: handler,
|
||||
tags: "jQuery,Live",
|
||||
hide: {
|
||||
dom0: true,
|
||||
|
@ -577,6 +618,10 @@ class ReactEventCollector extends MainEventCollector {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!this.chromeEnabled && this.isChromeHandler(listener)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
return true;
|
||||
}
|
||||
|
@ -671,12 +716,6 @@ class EventCollector {
|
|||
new JQueryEventCollector(),
|
||||
new DOMEventCollector(),
|
||||
];
|
||||
|
||||
// Add a method to create a simple debugger.
|
||||
this.makeDebuggerForContent = makeDebugger.bind(null, {
|
||||
findDebuggees: dbg => [],
|
||||
shouldAddNewGlobalAsDebuggee: global => true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -684,7 +723,6 @@ class EventCollector {
|
|||
*/
|
||||
destroy() {
|
||||
this.eventCollectors = null;
|
||||
this.makeDebuggerForContent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -701,6 +739,7 @@ class EventCollector {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -713,7 +752,7 @@ class EventCollector {
|
|||
* {
|
||||
* type: type, // e.g. "click"
|
||||
* handler: handler, // The function called when event is triggered.
|
||||
* tags: "jQuery", // Comma seperated list of tags displayed
|
||||
* tags: "jQuery", // Comma separated list of tags displayed
|
||||
* // inside event bubble.
|
||||
* hide: { // Flags for hiding certain properties.
|
||||
* capturing: true,
|
||||
|
@ -723,9 +762,7 @@ class EventCollector {
|
|||
*/
|
||||
getEventListeners(node) {
|
||||
const listenerArray = [];
|
||||
const dbg = this.makeDebuggerForContent();
|
||||
const global = Cu.getGlobalForObject(node);
|
||||
const globalDO = dbg.addDebuggee(global);
|
||||
const dbg = new Debugger();
|
||||
|
||||
for (const collector of this.eventCollectors) {
|
||||
const listeners = collector.getListeners(node);
|
||||
|
@ -738,12 +775,10 @@ class EventCollector {
|
|||
if (collector.normalizeListener) {
|
||||
listener.normalizeListener = collector.normalizeListener;
|
||||
}
|
||||
this.processHandlerForEvent(listenerArray, listener, globalDO);
|
||||
this.processHandlerForEvent(listenerArray, listener, dbg);
|
||||
}
|
||||
}
|
||||
|
||||
dbg.removeDebuggee(globalDO);
|
||||
|
||||
listenerArray.sort((a, b) => {
|
||||
return a.type.localeCompare(b.type);
|
||||
});
|
||||
|
@ -757,9 +792,10 @@ class EventCollector {
|
|||
* @param {Array} listenerArray
|
||||
* listenerArray contains all event objects that we have gathered
|
||||
* so far.
|
||||
* @param {Object} eventInfo
|
||||
* See event-parsers.js.registerEventParser() for a description of the
|
||||
* eventInfo object.
|
||||
* @param {EventListener} listener
|
||||
* The event listener to process.
|
||||
* @param {Debugger} dbg
|
||||
* Debugger instance.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array of objects where a typical object looks like this:
|
||||
|
@ -776,128 +812,142 @@ class EventCollector {
|
|||
* native: false
|
||||
* }
|
||||
*/
|
||||
processHandlerForEvent(listenerArray, listener, globalDO) {
|
||||
const { capturing, handler } = listener;
|
||||
let listenerDO = globalDO.makeDebuggeeValue(handler);
|
||||
processHandlerForEvent(listenerArray, listener, dbg) {
|
||||
let globalDO;
|
||||
|
||||
const { normalizeListener } = listener;
|
||||
try {
|
||||
const { capturing, handler } = listener;
|
||||
const global = Cu.getGlobalForObject(handler);
|
||||
|
||||
if (normalizeListener) {
|
||||
listenerDO = normalizeListener(listenerDO, listener);
|
||||
}
|
||||
// It is important that we recreate the globalDO for each handler because
|
||||
// their global object can vary e.g. resource:// URLs on a video control. If
|
||||
// we don't do this then all chrome listeners simply display "native code."
|
||||
globalDO = dbg.addDebuggee(global);
|
||||
let listenerDO = globalDO.makeDebuggeeValue(handler);
|
||||
|
||||
const hide = listener.hide || {};
|
||||
const override = listener.override || {};
|
||||
const tags = listener.tags || "";
|
||||
const type = listener.type || "";
|
||||
let dom0 = false;
|
||||
let functionSource = handler.toString();
|
||||
let line = 0;
|
||||
let native = false;
|
||||
let url = "";
|
||||
const { normalizeListener } = listener;
|
||||
|
||||
// If the listener is an object with a 'handleEvent' method, use that.
|
||||
if (listenerDO.class === "Object" || /^XUL\w*Element$/.test(listenerDO.class)) {
|
||||
let desc;
|
||||
|
||||
while (!desc && listenerDO) {
|
||||
desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
|
||||
listenerDO = listenerDO.proto;
|
||||
if (normalizeListener) {
|
||||
listenerDO = normalizeListener(listenerDO, listener);
|
||||
}
|
||||
|
||||
if (desc && desc.value) {
|
||||
listenerDO = desc.value;
|
||||
}
|
||||
}
|
||||
const hide = listener.hide || {};
|
||||
const override = listener.override || {};
|
||||
const tags = listener.tags || "";
|
||||
const type = listener.type || "";
|
||||
let dom0 = false;
|
||||
let functionSource = handler.toString();
|
||||
let line = 0;
|
||||
let native = false;
|
||||
let url = "";
|
||||
|
||||
// If the listener is bound to a different context then we need to switch
|
||||
// to the bound function.
|
||||
if (listenerDO.isBoundFunction) {
|
||||
listenerDO = listenerDO.boundTargetFunction;
|
||||
}
|
||||
// If the listener is an object with a 'handleEvent' method, use that.
|
||||
if (listenerDO.class === "Object" || /^XUL\w*Element$/.test(listenerDO.class)) {
|
||||
let desc;
|
||||
|
||||
const { isArrowFunction, name, script, parameterNames } = listenerDO;
|
||||
while (!desc && listenerDO) {
|
||||
desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
|
||||
listenerDO = listenerDO.proto;
|
||||
}
|
||||
|
||||
if (script) {
|
||||
const scriptSource = script.source.text;
|
||||
|
||||
// Scripts are provided via script tags. If it wasn't provided by a
|
||||
// script tag it must be a DOM0 event.
|
||||
if (script.source.element) {
|
||||
dom0 = script.source.element.class !== "HTMLScriptElement";
|
||||
} else {
|
||||
dom0 = false;
|
||||
}
|
||||
|
||||
line = script.startLine;
|
||||
url = script.url;
|
||||
|
||||
// Checking for the string "[native code]" is the only way at this point
|
||||
// to check for native code. Even if this provides a false positive then
|
||||
// grabbing the source code a second time is harmless.
|
||||
if (functionSource === "[object Object]" ||
|
||||
functionSource === "[object XULElement]" ||
|
||||
functionSource.includes("[native code]")) {
|
||||
functionSource =
|
||||
scriptSource.substr(script.sourceStart, script.sourceLength);
|
||||
|
||||
// At this point the script looks like this:
|
||||
// () { ... }
|
||||
// We prefix this with "function" if it is not a fat arrow function.
|
||||
if (!isArrowFunction) {
|
||||
functionSource = "function " + functionSource;
|
||||
if (desc && desc.value) {
|
||||
listenerDO = desc.value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the listener is a native one (provided by C++ code) then we have no
|
||||
// access to the script. We use the native flag to prevent showing the
|
||||
// debugger button because the script is not available.
|
||||
native = true;
|
||||
}
|
||||
|
||||
// Arrow function text always contains the parameters. Function
|
||||
// parameters are often missing e.g. if Array.sort is used as a handler.
|
||||
// If they are missing we provide the parameters ourselves.
|
||||
if (parameterNames && parameterNames.length > 0) {
|
||||
const prefix = "function " + name + "()";
|
||||
const paramString = parameterNames.join(", ");
|
||||
// If the listener is bound to a different context then we need to switch
|
||||
// to the bound function.
|
||||
if (listenerDO.isBoundFunction) {
|
||||
listenerDO = listenerDO.boundTargetFunction;
|
||||
}
|
||||
|
||||
if (functionSource.startsWith(prefix)) {
|
||||
functionSource = functionSource.substr(prefix.length);
|
||||
const { isArrowFunction, name, script, parameterNames } = listenerDO;
|
||||
|
||||
functionSource = `function ${name} (${paramString})${functionSource}`;
|
||||
if (script) {
|
||||
const scriptSource = script.source.text;
|
||||
|
||||
// Scripts are provided via script tags. If it wasn't provided by a
|
||||
// script tag it must be a DOM0 event.
|
||||
if (script.source.element) {
|
||||
dom0 = script.source.element.class !== "HTMLScriptElement";
|
||||
} else {
|
||||
dom0 = false;
|
||||
}
|
||||
|
||||
line = script.startLine;
|
||||
url = script.url;
|
||||
|
||||
// Checking for the string "[native code]" is the only way at this point
|
||||
// to check for native code. Even if this provides a false positive then
|
||||
// grabbing the source code a second time is harmless.
|
||||
if (functionSource === "[object Object]" ||
|
||||
functionSource === "[object XULElement]" ||
|
||||
functionSource.includes("[native code]")) {
|
||||
functionSource =
|
||||
scriptSource.substr(script.sourceStart, script.sourceLength);
|
||||
|
||||
// At this point the script looks like this:
|
||||
// () { ... }
|
||||
// We prefix this with "function" if it is not a fat arrow function.
|
||||
if (!isArrowFunction) {
|
||||
functionSource = "function " + functionSource;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the listener is a native one (provided by C++ code) then we have no
|
||||
// access to the script. We use the native flag to prevent showing the
|
||||
// debugger button because the script is not available.
|
||||
native = true;
|
||||
}
|
||||
|
||||
// Arrow function text always contains the parameters. Function
|
||||
// parameters are often missing e.g. if Array.sort is used as a handler.
|
||||
// If they are missing we provide the parameters ourselves.
|
||||
if (parameterNames && parameterNames.length > 0) {
|
||||
const prefix = "function " + name + "()";
|
||||
const paramString = parameterNames.join(", ");
|
||||
|
||||
if (functionSource.startsWith(prefix)) {
|
||||
functionSource = functionSource.substr(prefix.length);
|
||||
|
||||
functionSource = `function ${name} (${paramString})${functionSource}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If the listener is native code we display the filename "[native code]."
|
||||
// This is the official string and should *not* be translated.
|
||||
let origin;
|
||||
if (native) {
|
||||
origin = "[native code]";
|
||||
} else {
|
||||
origin = url + ((dom0 || line === 0) ? "" : ":" + line);
|
||||
}
|
||||
|
||||
const eventObj = {
|
||||
type: override.type || type,
|
||||
handler: override.handler || functionSource.trim(),
|
||||
origin: override.origin || origin,
|
||||
tags: override.tags || tags,
|
||||
DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
|
||||
capturing: typeof override.capturing !== "undefined" ?
|
||||
override.capturing : capturing,
|
||||
hide: typeof override.hide !== "undefined" ? override.hide : hide,
|
||||
native,
|
||||
};
|
||||
|
||||
// Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
|
||||
// generated dynamically from e.g. an onclick="" attribute so the script
|
||||
// doesn't actually exist.
|
||||
if (native || dom0) {
|
||||
eventObj.hide.debugger = true;
|
||||
}
|
||||
listenerArray.push(eventObj);
|
||||
} finally {
|
||||
// Ensure that we always remove the debuggee.
|
||||
if (globalDO) {
|
||||
dbg.removeDebuggee(globalDO);
|
||||
}
|
||||
}
|
||||
|
||||
// If the listener is native code we display the filename "[native code]."
|
||||
// This is the official string and should *not* be translated.
|
||||
let origin;
|
||||
if (native) {
|
||||
origin = "[native code]";
|
||||
} else {
|
||||
origin = url + ((dom0 || line === 0) ? "" : ":" + line);
|
||||
}
|
||||
|
||||
const eventObj = {
|
||||
type: override.type || type,
|
||||
handler: override.handler || functionSource.trim(),
|
||||
origin: override.origin || origin,
|
||||
tags: override.tags || tags,
|
||||
DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
|
||||
capturing: typeof override.capturing !== "undefined" ?
|
||||
override.capturing : capturing,
|
||||
hide: typeof override.hide !== "undefined" ? override.hide : hide,
|
||||
native,
|
||||
};
|
||||
|
||||
// Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
|
||||
// generated dynamically from e.g. an onclick="" attribute so the script
|
||||
// doesn't actually exist.
|
||||
if (native || dom0) {
|
||||
eventObj.hide.debugger = true;
|
||||
}
|
||||
|
||||
listenerArray.push(eventObj);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -289,7 +289,10 @@ const NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
|
|||
* check if there are any event listeners.
|
||||
*/
|
||||
get _hasEventListeners() {
|
||||
return this._eventCollector.hasEventListeners(this.rawNode);
|
||||
// We need to pass a debugger instance from this compartment because
|
||||
// otherwise we can't make use of it inside the event-collector module.
|
||||
const dbg = this.parent().targetActor.makeDebugger();
|
||||
return this._eventCollector.hasEventListeners(this.rawNode, dbg);
|
||||
},
|
||||
|
||||
writeAttrs: function() {
|
||||
|
|
|
@ -15,6 +15,9 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1157469
|
|||
window.onload = function() {
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
const prevPrefValue = Services.prefs.getBoolPref("devtools.chrome.enabled");
|
||||
Services.prefs.setBoolPref("devtools.chrome.enabled", true);
|
||||
|
||||
let inspectee = null;
|
||||
let inspector = null;
|
||||
let walker = null;
|
||||
|
@ -148,6 +151,7 @@ window.onload = function() {
|
|||
is(mutations.length, 1, "expect only one mutation");
|
||||
isnot(mutations.type, "events", "mutation type should not be events");
|
||||
|
||||
Services.prefs.setBoolPref("devtools.chrome.enabled", prevPrefValue);
|
||||
runNextTest();
|
||||
});
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче