merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2014-10-27 15:56:31 +01:00
commit 38668bd0ef
38 changed files with 1229 additions and 421 deletions

View File

@ -853,6 +853,7 @@ var gBrowserInit = {
let mm = window.getGroupMessageManager("browsers");
mm.loadFrameScript("chrome://browser/content/content.js", true);
mm.loadFrameScript("chrome://browser/content/content-UITour.js", true);
// initialize observers and listeners
// and give C++ access to gBrowser

View File

@ -0,0 +1,86 @@
let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const PREF_TEST_WHITELIST = "browser.uitour.testingOrigins";
const UITOUR_PERMISSION = "uitour";
let UITourListener = {
handleEvent: function (event) {
if (!Services.prefs.getBoolPref("browser.uitour.enabled")) {
return;
}
if (!this.ensureTrustedOrigin()) {
return;
}
addMessageListener("UITour:SendPageCallback", this);
sendAsyncMessage("UITour:onPageEvent", {detail: event.detail, type: event.type});
},
isTestingOrigin: function(aURI) {
if (Services.prefs.getPrefType(PREF_TEST_WHITELIST) != Services.prefs.PREF_STRING) {
return false;
}
// Add any testing origins (comma-seperated) to the whitelist for the session.
for (let origin of Services.prefs.getCharPref(PREF_TEST_WHITELIST).split(",")) {
try {
let testingURI = Services.io.newURI(origin, null, null);
if (aURI.prePath == testingURI.prePath) {
return true;
}
} catch (ex) {
Cu.reportError(ex);
}
}
return false;
},
// This function is copied from UITour.jsm.
isSafeScheme: function(aURI) {
let allowedSchemes = new Set(["https", "about"]);
if (!Services.prefs.getBoolPref("browser.uitour.requireSecure"))
allowedSchemes.add("http");
if (!allowedSchemes.has(aURI.scheme))
return false;
return true;
},
ensureTrustedOrigin: function() {
if (content.top != content)
return false;
let uri = content.document.documentURIObject;
if (uri.schemeIs("chrome"))
return true;
if (!this.isSafeScheme(uri))
return false;
let permission = Services.perms.testPermission(uri, UITOUR_PERMISSION);
if (permission == Services.perms.ALLOW_ACTION)
return true;
return this.isTestingOrigin(uri);
},
receiveMessage: function(aMessage) {
switch (aMessage.name) {
case "UITour:SendPageCallback":
this.sendPageCallback(aMessage.data);
break;
}
},
sendPageCallback: function (detail) {
let doc = content.document;
let event = new doc.defaultView.CustomEvent("mozUITourResponse", {
bubbles: true,
detail: Cu.cloneInto(detail, doc.defaultView)
});
doc.dispatchEvent(event);
}
};
addEventListener("mozUITour", UITourListener, false, true);

View File

@ -25,8 +25,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
"resource:///modules/PluginContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
"resource:///modules/UITour.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
"resource:///modules/FormSubmitObserver.jsm");
@ -122,15 +120,6 @@ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
.getService(Ci.nsIEventListenerService)
.addSystemEventListener(global, "contextmenu", handleContentContextMenu, true);
} else {
addEventListener("mozUITour", function(event) {
if (!Services.prefs.getBoolPref("browser.uitour.enabled"))
return;
let handled = UITour.onPageEvent(event);
if (handled)
addEventListener("pagehide", UITour);
}, false, true);
}
let AboutHomeListener = {

View File

@ -73,11 +73,25 @@ Sanitizer.prototype = {
if (openWindowsIndex != -1) {
itemsToClear.splice(openWindowsIndex, 1);
let item = this.items.openWindows;
if (!item.clear()) {
// When cancelled, reject the deferred and return the promise:
deferred.reject();
return deferred.promise;
function onWindowsCleaned() {
try {
let clearedPromise = this.sanitize(itemsToClear);
clearedPromise.then(deferred.resolve, deferred.reject);
} catch(e) {
let error = "Sanitizer threw after closing windows: " + e;
Cu.reportError(error);
deferred.reject(error);
}
}
let ok = item.clear(onWindowsCleaned.bind(this));
// When cancelled, reject immediately
if (!ok) {
deferred.reject("Sanitizer canceled closing windows");
}
return deferred.promise;
}
// Cache the range of times to clear
@ -465,11 +479,15 @@ Sanitizer.prototype = {
win.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow();
}
},
clear: function()
clear: function(aCallback)
{
// NB: this closes all *browser* windows, not other windows like the library, about window,
// browser console, etc.
if (!aCallback) {
throw "Sanitizer's openWindows clear() requires a callback.";
}
// Keep track of the time in case we get stuck in la-la-land because of onbeforeunload
// dialogs
let existingWindow = Services.appShell.hiddenDOMWindow;
@ -507,7 +525,39 @@ Sanitizer.prototype = {
let newWindow = existingWindow.openDialog("chrome://browser/content/", "_blank",
features, defaultArgs);
// Then close all those windows we checked:
// Window creation and destruction is asynchronous. We need to wait
// until all existing windows are fully closed, and the new window is
// fully open, before continuing. Otherwise the rest of the sanitizer
// could run too early (and miss new cookies being set when a page
// closes) and/or run too late (and not have a fully-formed window yet
// in existence). See bug 1088137.
let newWindowOpened = false;
function onWindowOpened(subject, topic, data) {
if (subject != newWindow)
return;
Services.obs.removeObserver(onWindowOpened, "browser-delayed-startup-finished");
newWindowOpened = true;
// If we're the last thing to happen, invoke callback.
if (numWindowsClosing == 0)
aCallback();
}
let numWindowsClosing = windowList.length;
function onWindowClosed() {
numWindowsClosing--;
if (numWindowsClosing == 0) {
Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
// If we're the last thing to happen, invoke callback.
if (newWindowOpened)
aCallback();
}
}
Services.obs.addObserver(onWindowOpened, "browser-delayed-startup-finished", false);
Services.obs.addObserver(onWindowClosed, "xul-window-destroyed", false);
// Start the process of closing windows
while (windowList.length) {
windowList.pop().close();
}

View File

@ -77,6 +77,7 @@ browser.jar:
* content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml)
* content/browser/chatWindow.xul (content/chatWindow.xul)
content/browser/content.js (content/content.js)
content/browser/content-UITour.js (content/content-UITour.js)
content/browser/defaultthemes/1.footer.jpg (content/defaultthemes/1.footer.jpg)
content/browser/defaultthemes/1.header.jpg (content/defaultthemes/1.header.jpg)
content/browser/defaultthemes/1.icon.jpg (content/defaultthemes/1.icon.jpg)

View File

@ -17,6 +17,9 @@ Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
"resource:///modules/AboutHome.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
"resource:///modules/UITour.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
@ -2553,3 +2556,12 @@ let E10SUINotification = {
var components = [BrowserGlue, ContentPermissionPrompt];
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
// Listen for UITour messages.
// Do it here instead of the UITour module itself so that the UITour module is lazy loaded
// when the first message is received.
let globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
globalMM.addMessageListener("UITour:onPageEvent", function(aMessage) {
UITour.onPageEvent(aMessage, aMessage.data);
});

View File

@ -76,6 +76,7 @@ support-files =
doc_pretty-print-2.html
doc_pretty-print-3.html
doc_pretty-print-on-paused.html
doc_promise.html
doc_random-javascript.html
doc_recursion-stack.html
doc_same-line-functions.html
@ -278,6 +279,7 @@ skip-if = os == "linux" || e10s # Bug 888811 & bug 891176
[browser_dbg_variables-view-03.js]
[browser_dbg_variables-view-04.js]
[browser_dbg_variables-view-05.js]
[browser_dbg_variables-view-06.js]
[browser_dbg_variables-view-accessibility.js]
[browser_dbg_variables-view-data.js]
[browser_dbg_variables-view-edit-cancel.js]

View File

@ -0,0 +1,122 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that Promises get their internal state added as psuedo properties.
*/
const TAB_URL = EXAMPLE_URL + "doc_promise.html";
const test = Task.async(function* () {
const [tab, debuggee, panel] = yield initDebugger(TAB_URL);
yield ensureSourceIs(panel, "doc_promise.html", true);
const scopes = waitForCaretAndScopes(panel, 21);
executeSoon(debuggee.doPause);
yield scopes;
const variables = panel.panelWin.DebuggerView.Variables;
ok(variables, "Should get the variables view.");
const scope = [...variables][0];
ok(scope, "Should get the current function's scope.");
const promiseVariables = [...scope].filter(([name]) =>
["p", "f", "r"].indexOf(name) !== -1);
is(promiseVariables.length, 3,
"Should have our 3 promise variables: p, f, r");
for (let [name, item] of promiseVariables) {
info("Expanding variable '" + name + "'");
let expanded = once(variables, "fetched");
item.expand();
yield expanded;
let foundState = false;
switch (name) {
case "p":
for (let [property, { value }] of item) {
if (property !== "<state>") {
isnot(property, "<value>",
"A pending promise shouldn't have a value");
isnot(property, "<reason>",
"A pending promise shouldn't have a reason");
continue;
}
foundState = true;
is(value, "pending", "The state should be pending.");
}
ok(foundState, "We should have found the <state> property.");
break;
case "f":
let foundValue = false;
for (let [property, value] of item) {
if (property === "<state>") {
foundState = true;
is(value.value, "fulfilled", "The state should be fulfilled.");
} else if (property === "<value>") {
foundValue = true;
let expanded = once(variables, "fetched");
value.expand();
yield expanded;
let expectedProps = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (let [prop, val] of value) {
if (prop === "__proto__") {
continue;
}
ok(expectedProps.has(prop), "The property should be expected.");
is(val.value, expectedProps.get(prop), "The property value should be correct.");
expectedProps.delete(prop);
}
is(Object.keys(expectedProps).length, 0,
"Should have found all of the expected properties.");
} else {
isnot(property, "<reason>",
"A fulfilled promise shouldn't have a reason");
}
}
ok(foundState, "We should have found the <state> property.");
ok(foundValue, "We should have found the <value> property.");
break;
case "r":
let foundReason = false;
for (let [property, value] of item) {
if (property === "<state>") {
foundState = true;
is(value.value, "rejected", "The state should be rejected.");
} else if (property === "<reason>") {
foundReason = true;
let expanded = once(variables, "fetched");
value.expand();
yield expanded;
let foundMessage = false;
for (let [prop, val] of value) {
if (prop !== "message") {
continue;
}
foundMessage = true;
is(val.value, "uh oh", "Should have the correct error message.");
}
ok(foundMessage, "Should have found the error's message");
} else {
isnot(property, "<value>",
"A rejected promise shouldn't have a value");
}
}
ok(foundState, "We should have found the <state> property.");
break;
}
}
debugger;
resumeDebuggerThenCloseAndFinish(panel);
});

View File

@ -0,0 +1,30 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Debugger + Promise test page</title>
</head>
<body>
<script>
window.pending = new Promise(function () {});
window.fulfilled = Promise.resolve({ a: 1, b: 2, c: 3 });
window.rejected = Promise.reject(new Error("uh oh"));
window.doPause = function () {
var p = window.pending;
var f = window.fulfilled;
var r = window.rejected;
debugger;
};
// Attach an error handler so that the logs don't have a warning about an
// unhandled, rejected promise.
window.rejected.then(null, function () {});
</script>
</body>
</html>

View File

@ -0,0 +1,75 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const Cu = Components.utils;
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { VariablesView } = Cu.import("resource:///modules/devtools/VariablesView.jsm", {});
const PENDING = {
"type": "object",
"class": "Promise",
"actor": "conn0.pausedobj35",
"extensible": true,
"frozen": false,
"sealed": false,
"promiseState": {
"state": "pending"
},
"preview": {
"kind": "Object",
"ownProperties": {},
"ownPropertiesLength": 0,
"safeGetterValues": {}
}
};
const FULFILLED = {
"type": "object",
"class": "Promise",
"actor": "conn0.pausedobj35",
"extensible": true,
"frozen": false,
"sealed": false,
"promiseState": {
"state": "fulfilled",
"value": 10
},
"preview": {
"kind": "Object",
"ownProperties": {},
"ownPropertiesLength": 0,
"safeGetterValues": {}
}
};
const REJECTED = {
"type": "object",
"class": "Promise",
"actor": "conn0.pausedobj35",
"extensible": true,
"frozen": false,
"sealed": false,
"promiseState": {
"state": "rejected",
"reason": 10
},
"preview": {
"kind": "Object",
"ownProperties": {},
"ownPropertiesLength": 0,
"safeGetterValues": {}
}
};
function run_test() {
equal(VariablesView.getString(PENDING, { concise: true }), "Promise");
equal(VariablesView.getString(PENDING), 'Promise {<state>: "pending"}');
equal(VariablesView.getString(FULFILLED, { concise: true }), "Promise");
equal(VariablesView.getString(FULFILLED), 'Promise {<state>: "fulfilled", <value>: 10}');
equal(VariablesView.getString(REJECTED, { concise: true }), "Promise");
equal(VariablesView.getString(REJECTED), 'Promise {<state>: "rejected", <reason>: 10}');
}

View File

@ -7,3 +7,4 @@ skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_bezierCanvas.js]
[test_cubicBezier.js]
[test_undoStack.js]
[test_VariablesView_getString_promise.js]

View File

@ -3575,6 +3575,17 @@ VariablesView.stringifiers.byObjectKind = {
let {preview} = aGrip;
let props = [];
if (aGrip.class == "Promise" && aGrip.promiseState) {
let { state, value, reason } = aGrip.promiseState;
props.push("<state>: " + VariablesView.getString(state));
if (state == "fulfilled") {
props.push("<value>: " + VariablesView.getString(value, { concise: true }));
} else if (state == "rejected") {
props.push("<reason>: " + VariablesView.getString(reason, { concise: true }));
}
}
for (let key of Object.keys(preview.ownProperties || {})) {
let value = preview.ownProperties[key];
let valueString = "";

View File

@ -170,6 +170,16 @@ VariablesViewController.prototype = {
_populateFromObject: function(aTarget, aGrip) {
let deferred = promise.defer();
if (aGrip.class === "Promise" && aGrip.promiseState) {
const { state, value, reason } = aGrip.promiseState;
aTarget.addItem("<state>", { value: state });
if (state === "fulfilled") {
this.addExpander(aTarget.addItem("<value>", { value }), value);
} else if (state === "rejected") {
this.addExpander(aTarget.addItem("<reason>", { value: reason }), reason);
}
}
let objectClient = this._getObjectClient(aGrip);
objectClient.getPrototypeAndProperties(aResponse => {
let { ownProperties, prototype } = aResponse;

View File

@ -1172,17 +1172,15 @@ Messages.Extended.prototype = Heritage.extend(Messages.Simple.prototype,
*/
_renderObjectActor: function(objectActor, options = {})
{
let widget = null;
let {preview} = objectActor;
let widget = Widgets.ObjectRenderers.byClass[objectActor.class];
if (preview && preview.kind) {
let { preview } = objectActor;
if ((!widget || (widget.canRender && !widget.canRender(objectActor)))
&& preview
&& preview.kind) {
widget = Widgets.ObjectRenderers.byKind[preview.kind];
}
if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
widget = Widgets.ObjectRenderers.byClass[objectActor.class];
}
if (!widget || (widget.canRender && !widget.canRender(objectActor))) {
widget = Widgets.JSObject;
}
@ -2299,6 +2297,118 @@ Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
this.element = this._anchor(str, { className: className });
},
/**
* Render a concise representation of an object.
*/
_renderConciseObject: function()
{
this.element = this._anchor(this.objectActor.class,
{ className: "cm-variable" });
},
/**
* Render the `<class> { ` prefix of an object.
*/
_renderObjectPrefix: function()
{
let { kind } = this.objectActor.preview;
this.element = this.el("span.kind-" + kind);
this._anchor(this.objectActor.class, { className: "cm-variable" });
this._text(" { ");
},
/**
* Render the ` }` suffix of an object.
*/
_renderObjectSuffix: function()
{
this._text(" }");
},
/**
* Render an object property.
*
* @param String key
* The property name.
* @param Object value
* The property value, as an RDP grip.
* @param nsIDOMNode container
* The container node to render to.
* @param Boolean needsComma
* True if there was another property before this one and we need to
* separate them with a comma.
* @param Boolean valueIsText
* Add the value as is, don't treat it as a grip and pass it to
* `_renderValueGrip`.
*/
_renderObjectProperty: function(key, value, container, needsComma, valueIsText = false)
{
if (needsComma) {
this._text(", ");
}
container.appendChild(this.el("span.cm-property", key));
this._text(": ");
if (valueIsText) {
this._text(value);
} else {
let valueElem = this.message._renderValueGrip(value, { concise: true });
container.appendChild(valueElem);
}
},
/**
* Render this object's properties.
*
* @param nsIDOMNode container
* The container node to render to.
* @param Boolean needsComma
* True if there was another property before this one and we need to
* separate them with a comma.
*/
_renderObjectProperties: function(container, needsComma)
{
let { preview } = this.objectActor;
let { ownProperties, safeGetterValues } = preview;
let shown = 0;
let getValue = desc => {
if (desc.get) {
return "Getter";
} else if (desc.set) {
return "Setter";
} else {
return desc.value;
}
};
for (let key of Object.keys(ownProperties || {})) {
this._renderObjectProperty(key, getValue(ownProperties[key]), container,
shown > 0 || needsComma,
ownProperties[key].get || ownProperties[key].set);
shown++;
}
let ownPropertiesShown = shown;
for (let key of Object.keys(safeGetterValues || {})) {
this._renderObjectProperty(key, safeGetterValues[key].getterValue,
container, shown > 0 || needsComma);
shown++;
}
if (typeof preview.ownPropertiesLength == "number" &&
ownPropertiesShown < preview.ownPropertiesLength) {
this._text(", ");
let n = preview.ownPropertiesLength - ownPropertiesShown;
let str = VariablesView.stringifiers._getNMoreString(n);
this._anchor(str);
}
},
/**
* Render an anchor with a given text content and link.
*
@ -3085,6 +3195,42 @@ Widgets.ObjectRenderers.add({
},
}); // Widgets.ObjectRenderers.byKind.DOMNode
/**
* The widget user for displaying Promise objects.
*/
Widgets.ObjectRenderers.add({
byClass: "Promise",
render: function()
{
let { ownProperties, safeGetterValues } = this.objectActor.preview;
if ((!ownProperties && !safeGetterValues) || this.options.concise) {
this._renderConciseObject();
return;
}
this._renderObjectPrefix();
let container = this.element;
let addedPromiseInternalProps = false;
if (this.objectActor.promiseState) {
const { state, value, reason } = this.objectActor.promiseState;
this._renderObjectProperty("<state>", state, container, false);
addedPromiseInternalProps = true;
if (state == "fulfilled") {
this._renderObjectProperty("<value>", value, container, true);
} else if (state == "rejected") {
this._renderObjectProperty("<reason>", reason, container, true);
}
}
this._renderObjectProperties(container, addedPromiseInternalProps);
this._renderObjectSuffix();
}
}); // Widgets.ObjectRenderers.byClass.Promise
/**
* The widget used for displaying generic JS object previews.
*/
@ -3093,73 +3239,15 @@ Widgets.ObjectRenderers.add({
render: function()
{
let {preview} = this.objectActor;
let {ownProperties, safeGetterValues} = preview;
let { ownProperties, safeGetterValues } = this.objectActor.preview;
if ((!ownProperties && !safeGetterValues) || this.options.concise) {
this.element = this._anchor(this.objectActor.class,
{ className: "cm-variable" });
this._renderConciseObject();
return;
}
let container = this.element = this.el("span.kind-" + preview.kind);
this._anchor(this.objectActor.class, { className: "cm-variable" });
this._text(" { ");
let addProperty = (str) => {
container.appendChild(this.el("span.cm-property", str));
};
let shown = 0;
for (let key of Object.keys(ownProperties || {})) {
if (shown > 0) {
this._text(", ");
}
let value = ownProperties[key];
addProperty(key);
this._text(": ");
if (value.get) {
addProperty("Getter");
} else if (value.set) {
addProperty("Setter");
} else {
let valueElem = this.message._renderValueGrip(value.value, { concise: true });
container.appendChild(valueElem);
}
shown++;
}
let ownPropertiesShown = shown;
for (let key of Object.keys(safeGetterValues || {})) {
if (shown > 0) {
this._text(", ");
}
addProperty(key);
this._text(": ");
let value = safeGetterValues[key].getterValue;
let valueElem = this.message._renderValueGrip(value, { concise: true });
container.appendChild(valueElem);
shown++;
}
if (typeof preview.ownPropertiesLength == "number" &&
ownPropertiesShown < preview.ownPropertiesLength) {
this._text(", ");
let n = preview.ownPropertiesLength - ownPropertiesShown;
let str = VariablesView.stringifiers._getNMoreString(n);
this._anchor(str);
}
this._text(" }");
this._renderObjectPrefix();
this._renderObjectProperties(this.element, false);
this._renderObjectSuffix();
},
}); // Widgets.ObjectRenderers.byKind.Object

View File

@ -84,6 +84,26 @@ let inputTests = [
inspectable: true,
variablesViewLabel: "String[5]"
},
// 9
{
// XXX: Can't test fulfilled and rejected promises, because promises get
// settled on the next tick of the event loop.
input: "new Promise(function () {})",
output: 'Promise { <state>: "pending" }',
printOutput: "[object Promise]",
inspectable: true,
variablesViewLabel: "Promise"
},
// 10
{
input: "(function () { var p = new Promise(function () {}); p.foo = 1; return p; }())",
output: 'Promise { <state>: "pending", foo: 1 }',
printOutput: "[object Promise]",
inspectable: true,
variablesViewLabel: "Promise"
}
];
function test() {

View File

@ -946,6 +946,8 @@ function openDebugger(aOptions = {})
*/
function waitForMessages(aOptions)
{
info("Waiting for messages...");
gPendingOutputTest++;
let webconsole = aOptions.webconsole;
let rules = WebConsoleUtils.cloneObject(aOptions.messages, true);
@ -1473,6 +1475,7 @@ function checkOutputForInputs(hud, inputTests)
function* checkConsoleLog(entry)
{
info("Logging: " + entry.input);
hud.jsterm.clearOutput();
hud.jsterm.execute("console.log(" + entry.input + ")");
@ -1494,6 +1497,7 @@ function checkOutputForInputs(hud, inputTests)
function checkPrintOutput(entry)
{
info("Printing: " + entry.input);
hud.jsterm.clearOutput();
hud.jsterm.execute("print(" + entry.input + ")");
@ -1511,6 +1515,7 @@ function checkOutputForInputs(hud, inputTests)
function* checkJSEval(entry)
{
info("Evaluating: " + entry.input);
hud.jsterm.clearOutput();
hud.jsterm.execute(entry.input);
@ -1534,6 +1539,7 @@ function checkOutputForInputs(hud, inputTests)
function* checkObjectClick(entry, msg)
{
info("Clicking: " + entry.input);
let body = msg.querySelector(".message-body a") ||
msg.querySelector(".message-body");
ok(body, "the message body");
@ -1569,6 +1575,7 @@ function checkOutputForInputs(hud, inputTests)
function checkLinkToInspector(entry, msg)
{
info("Checking Inspector Link: " + entry.input);
let elementNodeWidget = [...msg._messageObject.widgets][0];
if (!elementNodeWidget) {
ok(!entry.inspectorIcon, "The message has no ElementNode widget");
@ -1592,6 +1599,7 @@ function checkOutputForInputs(hud, inputTests)
function onVariablesViewOpen(entry, {resolve, reject}, event, view, options)
{
info("Variables view opened: " + entry.input);
let label = entry.variablesViewLabel || entry.output;
if (typeof label == "string" && options.label != label) {
return;

View File

@ -626,7 +626,7 @@ SessionStore.prototype = {
if (aBrowser.__SS_restore)
return;
let aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 };
aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 };
let tabData = {};
tabData.entries = aHistory.entries;

View File

@ -25,8 +25,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
"resource:///modules/BrowserUITelemetry.jsm");
const UITOUR_PERMISSION = "uitour";
const PREF_TEST_WHITELIST = "browser.uitour.testingOrigins";
const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs";
const MAX_BUTTONS = 4;
@ -222,18 +220,13 @@ this.UITour = {
JSON.stringify([...this.seenPageIDs]));
},
onPageEvent: function(aEvent) {
onPageEvent: function(aMessage, aEvent) {
let contentDocument = null;
if (aEvent.target instanceof Ci.nsIDOMHTMLDocument)
contentDocument = aEvent.target;
else if (aEvent.target instanceof Ci.nsIDOMHTMLElement)
contentDocument = aEvent.target.ownerDocument;
else
return false;
// Ignore events if they're not from a trusted origin.
if (!this.ensureTrustedOrigin(contentDocument))
return false;
let browser = aMessage.target;
let window = browser.ownerDocument.defaultView;
let tab = window.gBrowser.getTabForBrowser(browser);
let messageManager = browser.messageManager;
if (typeof aEvent.detail != "object")
return false;
@ -246,20 +239,22 @@ this.UITour = {
if (typeof data != "object")
return false;
let window = this.getChromeWindow(contentDocument);
// Do this before bailing if there's no tab, so later we can pick up the pieces:
window.gBrowser.tabContainer.addEventListener("TabSelect", this);
let tab = window.gBrowser._getTabForContentWindow(contentDocument.defaultView);
if (!tab) {
// This should only happen while detaching a tab:
if (this._detachingTab) {
this._queuedEvents.push(aEvent);
this._pendingDoc = Cu.getWeakReference(contentDocument);
if (!window.gMultiProcessBrowser) { // Non-e10s. See bug 1089000.
contentDocument = browser.contentWindow.document;
if (!tab) {
// This should only happen while detaching a tab:
if (this._detachingTab) {
this._queuedEvents.push(aEvent);
this._pendingDoc = Cu.getWeakReference(contentDocument);
return;
}
Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
"This shouldn't happen!");
return;
}
Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
"This shouldn't happen!");
return;
}
switch (action) {
@ -315,7 +310,7 @@ this.UITour = {
let iconURL = null;
if (typeof data.icon == "string")
iconURL = this.resolveURL(contentDocument, data.icon);
iconURL = this.resolveURL(browser, data.icon);
let buttons = [];
if (Array.isArray(data.buttons) && data.buttons.length > 0) {
@ -329,7 +324,7 @@ this.UITour = {
};
if (typeof buttonData.icon == "string")
button.iconURL = this.resolveURL(contentDocument, buttonData.icon);
button.iconURL = this.resolveURL(browser, buttonData.icon);
if (typeof buttonData.style == "string")
button.style = buttonData.style;
@ -349,7 +344,7 @@ this.UITour = {
if (typeof data.targetCallbackID == "string")
infoOptions.targetCallbackID = data.targetCallbackID;
this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons, infoOptions);
this.showInfo(messageManager, target, data.title, data.text, iconURL, buttons, infoOptions);
}).then(null, Cu.reportError);
break;
}
@ -382,7 +377,7 @@ this.UITour = {
case "showMenu": {
this.showMenu(window, data.name, () => {
if (typeof data.showCallbackID == "string")
this.sendPageCallback(contentDocument, data.showCallbackID);
this.sendPageCallback(messageManager, data.showCallbackID);
});
break;
}
@ -428,7 +423,7 @@ this.UITour = {
return false;
}
this.getConfiguration(contentDocument, data.configuration, data.callbackID);
this.getConfiguration(messageManager, window, data.configuration, data.callbackID);
break;
}
@ -450,19 +445,22 @@ this.UITour = {
// Add a widget to the toolbar
let targetPromise = this.getTarget(window, data.name);
targetPromise.then(target => {
this.addNavBarWidget(target, contentDocument, data.callbackID);
this.addNavBarWidget(target, messageManager, data.callbackID);
}).then(null, Cu.reportError);
break;
}
}
if (!this.originTabs.has(window))
this.originTabs.set(window, new Set());
if (!window.gMultiProcessBrowser) { // Non-e10s. See bug 1089000.
if (!this.originTabs.has(window)) {
this.originTabs.set(window, new Set());
}
this.originTabs.get(window).add(tab);
tab.addEventListener("TabClose", this);
tab.addEventListener("TabBecomingWindow", this);
window.addEventListener("SSWindowClosing", this);
this.originTabs.get(window).add(tab);
tab.addEventListener("TabClose", this);
tab.addEventListener("TabBecomingWindow", this);
window.addEventListener("SSWindowClosing", this);
}
return true;
},
@ -621,44 +619,7 @@ this.UITour = {
.wrappedJSObject;
},
isTestingOrigin: function(aURI) {
if (Services.prefs.getPrefType(PREF_TEST_WHITELIST) != Services.prefs.PREF_STRING) {
return false;
}
// Add any testing origins (comma-seperated) to the whitelist for the session.
for (let origin of Services.prefs.getCharPref(PREF_TEST_WHITELIST).split(",")) {
try {
let testingURI = Services.io.newURI(origin, null, null);
if (aURI.prePath == testingURI.prePath) {
return true;
}
} catch (ex) {
Cu.reportError(ex);
}
}
return false;
},
ensureTrustedOrigin: function(aDocument) {
if (aDocument.defaultView.top != aDocument.defaultView)
return false;
let uri = aDocument.documentURIObject;
if (uri.schemeIs("chrome"))
return true;
if (!this.isSafeScheme(uri))
return false;
let permission = Services.perms.testPermission(uri, UITOUR_PERMISSION);
if (permission == Services.perms.ALLOW_ACTION)
return true;
return this.isTestingOrigin(uri);
},
// This function is copied to UITourListener.
isSafeScheme: function(aURI) {
let allowedSchemes = new Set(["https", "about"]);
if (!Services.prefs.getBoolPref("browser.uitour.requireSecure"))
@ -670,9 +631,9 @@ this.UITour = {
return true;
},
resolveURL: function(aDocument, aURL) {
resolveURL: function(aBrowser, aURL) {
try {
let uri = Services.io.newURI(aURL, null, aDocument.documentURIObject);
let uri = Services.io.newURI(aURL, null, aBrowser.currentURI);
if (!this.isSafeScheme(uri))
return null;
@ -683,16 +644,9 @@ this.UITour = {
return null;
},
sendPageCallback: function(aDocument, aCallbackID, aData = {}) {
sendPageCallback: function(aMessageManager, aCallbackID, aData = {}) {
let detail = {data: aData, callbackID: aCallbackID};
detail = Cu.cloneInto(detail, aDocument.defaultView);
let event = new aDocument.defaultView.CustomEvent("mozUITourResponse", {
bubbles: true,
detail: detail
});
aDocument.dispatchEvent(event);
aMessageManager.sendAsyncMessage("UITour:SendPageCallback", detail);
},
isElementVisible: function(aElement) {
@ -966,7 +920,7 @@ this.UITour = {
/**
* Show an info panel.
*
* @param {Document} aContentDocument
* @param {nsIMessageSender} aMessageManager
* @param {Node} aAnchor
* @param {String} [aTitle=""]
* @param {String} [aDescription=""]
@ -975,7 +929,7 @@ this.UITour = {
* @param {Object} [aOptions={}]
* @param {String} [aOptions.closeButtonCallbackID]
*/
showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "",
showInfo: function(aMessageManager, aAnchor, aTitle = "", aDescription = "", aIconURL = "",
aButtons = [], aOptions = {}) {
function showInfoPanel(aAnchorEl) {
aAnchorEl.focus();
@ -1014,7 +968,7 @@ this.UITour = {
let callbackID = button.callbackID;
el.addEventListener("command", event => {
tooltip.hidePopup();
this.sendPageCallback(aContentDocument, callbackID);
this.sendPageCallback(aMessageManager, callbackID);
});
tooltipButtons.appendChild(el);
@ -1026,7 +980,7 @@ this.UITour = {
let closeButtonCallback = (event) => {
this.hideInfo(document.defaultView);
if (aOptions && aOptions.closeButtonCallbackID)
this.sendPageCallback(aContentDocument, aOptions.closeButtonCallbackID);
this.sendPageCallback(aMessageManager, aOptions.closeButtonCallbackID);
};
tooltipClose.addEventListener("command", closeButtonCallback);
@ -1035,7 +989,7 @@ this.UITour = {
target: aAnchor.targetName,
type: event.type,
};
this.sendPageCallback(aContentDocument, aOptions.targetCallbackID, details);
this.sendPageCallback(aMessageManager, aOptions.targetCallbackID, details);
};
if (aOptions.targetCallbackID && aAnchor.addTargetListener) {
aAnchor.addTargetListener(document, targetCallback);
@ -1214,13 +1168,13 @@ this.UITour = {
aWindow.gBrowser.selectedTab = tab;
},
getConfiguration: function(aContentDocument, aConfiguration, aCallbackID) {
getConfiguration: function(aMessageManager, aWindow, aConfiguration, aCallbackID) {
switch (aConfiguration) {
case "availableTargets":
this.getAvailableTargets(aContentDocument, aCallbackID);
this.getAvailableTargets(aMessageManager, aWindow, aCallbackID);
break;
case "sync":
this.sendPageCallback(aContentDocument, aCallbackID, {
this.sendPageCallback(aMessageManager, aCallbackID, {
setup: Services.prefs.prefHasUserValue("services.sync.username"),
});
break;
@ -1228,7 +1182,7 @@ this.UITour = {
let props = ["defaultUpdateChannel", "version"];
let appinfo = {};
props.forEach(property => appinfo[property] = Services.appinfo[property]);
this.sendPageCallback(aContentDocument, aCallbackID, appinfo);
this.sendPageCallback(aMessageManager, aCallbackID, appinfo);
break;
default:
Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration);
@ -1236,12 +1190,12 @@ this.UITour = {
}
},
getAvailableTargets: function(aContentDocument, aCallbackID) {
getAvailableTargets: function(aMessageManager, aChromeWindow, aCallbackID) {
Task.spawn(function*() {
let window = this.getChromeWindow(aContentDocument);
let window = aChromeWindow;
let data = this.availableTargetsCache.get(window);
if (data) {
this.sendPageCallback(aContentDocument, aCallbackID, data);
this.sendPageCallback(aMessageManager, aCallbackID, data);
return;
}
@ -1268,16 +1222,16 @@ this.UITour = {
targets: targetNames,
};
this.availableTargetsCache.set(window, data);
this.sendPageCallback(aContentDocument, aCallbackID, data);
this.sendPageCallback(aMessageManager, aCallbackID, data);
}.bind(this)).catch(err => {
Cu.reportError(err);
this.sendPageCallback(aContentDocument, aCallbackID, {
this.sendPageCallback(aMessageManager, aCallbackID, {
targets: [],
});
});
},
addNavBarWidget: function (aTarget, aContentDocument, aCallbackID) {
addNavBarWidget: function (aTarget, aMessageManager, aCallbackID) {
if (aTarget.node) {
Cu.reportError("UITour: can't add a widget already present: " + data.target);
return;
@ -1292,7 +1246,7 @@ this.UITour = {
}
CustomizableUI.addWidgetToArea(aTarget.widgetName, CustomizableUI.AREA_NAVBAR);
this.sendPageCallback(aContentDocument, aCallbackID);
this.sendPageCallback(aMessageManager, aCallbackID);
},
_addAnnotationPanelMutationObserver: function(aPanelEl) {

View File

@ -81,12 +81,16 @@ let tests = [
function test_highlight_2() {
let highlight = document.getElementById("UITourHighlight");
gContentAPI.hideHighlight();
waitForElementToBeHidden(highlight, test_highlight_3, "Highlight should be hidden after hideHighlight()");
}
function test_highlight_3() {
is_element_hidden(highlight, "Highlight should be hidden after hideHighlight()");
gContentAPI.showHighlight("urlbar");
waitForElementToBeVisible(highlight, test_highlight_3, "Highlight should be shown after showHighlight()");
waitForElementToBeVisible(highlight, test_highlight_4, "Highlight should be shown after showHighlight()");
}
function test_highlight_3() {
function test_highlight_4() {
let highlight = document.getElementById("UITourHighlight");
gContentAPI.showHighlight("backForward");
waitForElementToBeVisible(highlight, done, "Highlight should be shown after showHighlight()");
@ -302,33 +306,27 @@ let tests = [
gContentAPI.showInfo("urlbar", "test title", "test text");
},
function test_info_2(done) {
taskify(function* test_info_2() {
let popup = document.getElementById("UITourTooltip");
let title = document.getElementById("UITourTooltipTitle");
let desc = document.getElementById("UITourTooltipDescription");
let icon = document.getElementById("UITourTooltipIcon");
let buttons = document.getElementById("UITourTooltipButtons");
popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown);
is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar");
is(title.textContent, "urlbar title", "Popup should have correct title");
is(desc.textContent, "urlbar text", "Popup should have correct description text");
is(icon.src, "", "Popup should have no icon");
is(buttons.hasChildNodes(), false, "Popup should have no buttons");
yield showInfoPromise("urlbar", "urlbar title", "urlbar text");
gContentAPI.showInfo("search", "search title", "search text");
executeSoon(function() {
is(popup.popupBoxObject.anchorNode, document.getElementById("searchbar"), "Popup should be anchored to the searchbar");
is(title.textContent, "search title", "Popup should have correct title");
is(desc.textContent, "search text", "Popup should have correct description text");
is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar");
is(title.textContent, "urlbar title", "Popup should have correct title");
is(desc.textContent, "urlbar text", "Popup should have correct description text");
is(icon.src, "", "Popup should have no icon");
is(buttons.hasChildNodes(), false, "Popup should have no buttons");
done();
});
});
yield showInfoPromise("search", "search title", "search text");
gContentAPI.showInfo("urlbar", "urlbar title", "urlbar text");
},
is(popup.popupBoxObject.anchorNode, document.getElementById("searchbar"), "Popup should be anchored to the searchbar");
is(title.textContent, "search title", "Popup should have correct title");
is(desc.textContent, "search text", "Popup should have correct description text");
}),
function test_getConfigurationVersion(done) {
function callback(result) {
let props = ["defaultUpdateChannel", "version"];
@ -368,8 +366,9 @@ let tests = [
},
// Make sure this test is last in the file so the appMenu gets left open and done will confirm it got tore down.
function cleanupMenus(done) {
taskify(function* cleanupMenus() {
let shownPromise = promisePanelShown(window);
gContentAPI.showMenu("appMenu");
done();
},
yield shownPromise;
}),
];

View File

@ -56,19 +56,22 @@ let tests = [
waitForPopupAtAnchor(popup, target.node, function checkMenuIsStillOpen() {
isnot(PanelUI.panel.state, "closed",
"Menu should remain open since UITour didn't open it in the first place");
waitForElementToBeHidden(window.PanelUI.panel, () => {
ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up on close");
done();
});
gContentAPI.hideMenu("appMenu");
ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up on close");
done();
}, "Info should move to the appMenu button");
});
}, "Info should be shown after showInfo() for fixed menu panel items");
});
}).then(null, Components.utils.reportError);
},
function test_pinnedTab(done) {
taskify(function* test_pinnedTab() {
is(UITour.pinnedTabs.get(window), null, "Should not already have a pinned tab");
gContentAPI.addPinnedTab();
yield addPinnedTabPromise();
let tabInfo = UITour.pinnedTabs.get(window);
isnot(tabInfo, null, "Should have recorded data about a pinned tab after addPinnedTab()");
isnot(tabInfo.tab, null, "Should have added a pinned tab after addPinnedTab()");
@ -76,28 +79,29 @@ let tests = [
let tab = tabInfo.tab;
gContentAPI.removePinnedTab();
yield removePinnedTabPromise();
isnot(gBrowser.tabs[0], tab, "First tab should not be the pinned tab");
tabInfo = UITour.pinnedTabs.get(window);
is(tabInfo, null, "Should not have any data about the removed pinned tab after removePinnedTab()");
gContentAPI.addPinnedTab();
gContentAPI.addPinnedTab();
gContentAPI.addPinnedTab();
yield addPinnedTabPromise();
yield addPinnedTabPromise();
yield addPinnedTabPromise();
is(gBrowser.tabs[1].pinned, false, "After multiple calls of addPinnedTab, should still only have one pinned tab");
done();
},
function test_menu(done) {
}),
taskify(function* test_menu() {
let bookmarksMenuButton = document.getElementById("bookmarks-menu-button");
ise(bookmarksMenuButton.open, false, "Menu should initially be closed");
ise(bookmarksMenuButton.open, false, "Menu should initially be closed");
gContentAPI.showMenu("bookmarks");
ise(bookmarksMenuButton.open, true, "Menu should be shown after showMenu()");
yield waitForConditionPromise(() => {
return bookmarksMenuButton.open;
}, "Menu should be visible after showMenu()");
gContentAPI.hideMenu("bookmarks");
ise(bookmarksMenuButton.open, false, "Menu should be closed after hideMenu()");
done();
},
yield waitForConditionPromise(() => {
return !bookmarksMenuButton.open;
}, "Menu should be hidden after hideMenu()");
}),
];

View File

@ -16,7 +16,7 @@ function test() {
}
let tests = [
function test_info_icon(done) {
taskify(function* test_info_icon() {
let popup = document.getElementById("UITourTooltip");
let title = document.getElementById("UITourTooltipTitle");
let desc = document.getElementById("UITourTooltipDescription");
@ -27,139 +27,122 @@ let tests = [
// window during the transition instead of the buttons in the popup.
popup.setAttribute("animate", "false");
popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown);
yield showInfoPromise("urlbar", "a title", "some text", "image.png");
is(title.textContent, "a title", "Popup should have correct title");
is(desc.textContent, "some text", "Popup should have correct description text");
is(title.textContent, "a title", "Popup should have correct title");
is(desc.textContent, "some text", "Popup should have correct description text");
let imageURL = getRootDirectory(gTestPath) + "image.png";
imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
is(icon.src, imageURL, "Popup should have correct icon shown");
let imageURL = getRootDirectory(gTestPath) + "image.png";
imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
is(icon.src, imageURL, "Popup should have correct icon shown");
is(buttons.hasChildNodes(), false, "Popup should have no buttons");
is(buttons.hasChildNodes(), false, "Popup should have no buttons");
}),
done();
});
gContentAPI.showInfo("urlbar", "a title", "some text", "image.png");
},
function test_info_buttons_1(done) {
taskify(function* test_info_buttons_1() {
let popup = document.getElementById("UITourTooltip");
let title = document.getElementById("UITourTooltipTitle");
let desc = document.getElementById("UITourTooltipDescription");
let icon = document.getElementById("UITourTooltipIcon");
popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown);
is(title.textContent, "another title", "Popup should have correct title");
is(desc.textContent, "moar text", "Popup should have correct description text");
let imageURL = getRootDirectory(gTestPath) + "image.png";
imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
is(icon.src, imageURL, "Popup should have correct icon shown");
let buttons = document.getElementById("UITourTooltipButtons");
is(buttons.childElementCount, 2, "Popup should have two buttons");
is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
popup.addEventListener("popuphidden", function onPopupHidden() {
popup.removeEventListener("popuphidden", onPopupHidden);
ok(true, "Popup should close automatically");
executeSoon(function() {
is(gContentWindow.callbackResult, "button1", "Correct callback should have been called");
done();
});
});
EventUtils.synthesizeMouseAtCenter(buttons.childNodes[0], {}, window);
});
let buttons = gContentWindow.makeButtons();
gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons);
},
function test_info_buttons_2(done) {
yield showInfoPromise("urlbar", "another title", "moar text", "./image.png", buttons);
is(title.textContent, "another title", "Popup should have correct title");
is(desc.textContent, "moar text", "Popup should have correct description text");
let imageURL = getRootDirectory(gTestPath) + "image.png";
imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
is(icon.src, imageURL, "Popup should have correct icon shown");
buttons = document.getElementById("UITourTooltipButtons");
is(buttons.childElementCount, 2, "Popup should have two buttons");
is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
let promiseHidden = promisePanelElementHidden(window, popup);
EventUtils.synthesizeMouseAtCenter(buttons.childNodes[0], {}, window);
yield promiseHidden;
ok(true, "Popup should close automatically");
yield waitForCallbackResultPromise();
is(gContentWindow.callbackResult, "button1", "Correct callback should have been called");
}),
taskify(function* test_info_buttons_2() {
let popup = document.getElementById("UITourTooltip");
let title = document.getElementById("UITourTooltipTitle");
let desc = document.getElementById("UITourTooltipDescription");
let icon = document.getElementById("UITourTooltipIcon");
popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown);
is(title.textContent, "another title", "Popup should have correct title");
is(desc.textContent, "moar text", "Popup should have correct description text");
let imageURL = getRootDirectory(gTestPath) + "image.png";
imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
is(icon.src, imageURL, "Popup should have correct icon shown");
let buttons = document.getElementById("UITourTooltipButtons");
is(buttons.childElementCount, 2, "Popup should have two buttons");
is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
popup.addEventListener("popuphidden", function onPopupHidden() {
popup.removeEventListener("popuphidden", onPopupHidden);
ok(true, "Popup should close automatically");
executeSoon(function() {
is(gContentWindow.callbackResult, "button2", "Correct callback should have been called");
done();
});
});
EventUtils.synthesizeMouseAtCenter(buttons.childNodes[1], {}, window);
});
let buttons = gContentWindow.makeButtons();
gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons);
},
function test_info_close_button(done) {
yield showInfoPromise("urlbar", "another title", "moar text", "./image.png", buttons);
is(title.textContent, "another title", "Popup should have correct title");
is(desc.textContent, "moar text", "Popup should have correct description text");
let imageURL = getRootDirectory(gTestPath) + "image.png";
imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
is(icon.src, imageURL, "Popup should have correct icon shown");
buttons = document.getElementById("UITourTooltipButtons");
is(buttons.childElementCount, 2, "Popup should have two buttons");
is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
let promiseHidden = promisePanelElementHidden(window, popup);
EventUtils.synthesizeMouseAtCenter(buttons.childNodes[1], {}, window);
yield promiseHidden;
ok(true, "Popup should close automatically");
yield waitForCallbackResultPromise();
is(gContentWindow.callbackResult, "button2", "Correct callback should have been called");
}),
taskify(function* test_info_close_button() {
let popup = document.getElementById("UITourTooltip");
let closeButton = document.getElementById("UITourTooltipClose");
popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown);
EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
executeSoon(function() {
is(gContentWindow.callbackResult, "closeButton", "Close button callback called");
done();
});
});
let infoOptions = gContentWindow.makeInfoOptions();
gContentAPI.showInfo("urlbar", "Close me", "X marks the spot", null, null, infoOptions);
},
function test_info_target_callback(done) {
yield showInfoPromise("urlbar", "Close me", "X marks the spot", null, null, infoOptions);
EventUtils.synthesizeMouseAtCenter(closeButton, {}, window);
yield waitForCallbackResultPromise();
is(gContentWindow.callbackResult, "closeButton", "Close button callback called");
}),
taskify(function* test_info_target_callback() {
let popup = document.getElementById("UITourTooltip");
popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown);
PanelUI.show().then(() => {
is(gContentWindow.callbackResult, "target", "target callback called");
is(gContentWindow.callbackData.target, "appMenu", "target callback was from the appMenu");
is(gContentWindow.callbackData.type, "popupshown", "target callback was from the mousedown");
popup.removeAttribute("animate");
done();
});
});
let infoOptions = gContentWindow.makeInfoOptions();
gContentAPI.showInfo("appMenu", "I want to know when the target is clicked", "*click*", null, null, infoOptions);
},
yield showInfoPromise("appMenu", "I want to know when the target is clicked", "*click*", null, null, infoOptions);
yield PanelUI.show();
yield waitForCallbackResultPromise();
is(gContentWindow.callbackResult, "target", "target callback called");
is(gContentWindow.callbackData.target, "appMenu", "target callback was from the appMenu");
is(gContentWindow.callbackData.type, "popupshown", "target callback was from the mousedown");
// Cleanup.
yield hideInfoPromise();
popup.removeAttribute("animate");
}),
];

View File

@ -11,8 +11,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
let gContentDoc;
let highlight = document.getElementById("UITourHighlight");
let tooltip = document.getElementById("UITourTooltip");
Components.utils.import("resource:///modules/UITour.jsm");
@ -23,61 +21,69 @@ function test() {
UITourTest();
}
/**
* When tab is changed we're tearing the tour down. So the UITour client has to always be aware of this
* fact and therefore listens to visibilitychange events.
* In particular this scenario happens for detaching the tab (ie. moving it to a new window).
*/
let tests = [
function test_move_tab_to_new_window(done) {
let gOpenedWindow;
taskify(function* test_move_tab_to_new_window(done) {
let onVisibilityChange = (aEvent) => {
if (!document.hidden && window != UITour.getChromeWindow(aEvent.target)) {
gContentAPI.showHighlight("appMenu");
}
};
let onDOMWindowDestroyed = (aWindow, aTopic, aData) => {
if (gOpenedWindow && aWindow == gOpenedWindow) {
let highlight = document.getElementById("UITourHighlight");
let windowDestroyedDeferred = Promise.defer();
let onDOMWindowDestroyed = (aWindow) => {
if (gContentWindow && aWindow == gContentWindow) {
Services.obs.removeObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
done();
}
};
let onBrowserDelayedStartup = (aWindow, aTopic, aData) => {
gOpenedWindow = aWindow;
Services.obs.removeObserver(onBrowserDelayedStartup, "browser-delayed-startup-finished");
try {
let newWindowHighlight = gOpenedWindow.document.getElementById("UITourHighlight");
let selectedTab = aWindow.gBrowser.selectedTab;
is(selectedTab.linkedBrowser && selectedTab.linkedBrowser.contentDocument, gContentDoc, "Document should be selected in new window");
ok(UITour.originTabs && UITour.originTabs.has(aWindow), "Window should be known");
ok(UITour.originTabs.get(aWindow).has(selectedTab), "Tab should be known");
waitForElementToBeVisible(newWindowHighlight, function checkHighlightIsThere() {
let shownPromise = promisePanelShown(aWindow);
gContentAPI.showMenu("appMenu");
shownPromise.then(() => {
isnot(aWindow.PanelUI.panel.state, "closed", "Panel should be open");
ok(aWindow.PanelUI.contents.children.length > 0, "Panel contents should have children");
gContentAPI.hideHighlight();
gContentAPI.hideMenu("appMenu");
gTestTab = null;
aWindow.close();
}).then(null, Components.utils.reportError);
}, "Highlight should be shown in new window.");
} catch (ex) {
Cu.reportError(ex);
ok(false, "An error occurred running UITour tab detach test.");
} finally {
gContentDoc.removeEventListener("visibilitychange", onVisibilityChange, false);
Services.obs.addObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
windowDestroyedDeferred.resolve();
}
};
Services.obs.addObserver(onBrowserDelayedStartup, "browser-delayed-startup-finished", false);
let browserStartupDeferred = Promise.defer();
Services.obs.addObserver(function onBrowserDelayedStartup(aWindow) {
Services.obs.removeObserver(onBrowserDelayedStartup, "browser-delayed-startup-finished");
browserStartupDeferred.resolve(aWindow);
}, "browser-delayed-startup-finished", false);
// NB: we're using this rather than gContentWindow.document because the latter wouldn't
// have an XRayWrapper, and we need to compare this to the doc we get using this method
// later on...
gContentDoc = gBrowser.selectedTab.linkedBrowser.contentDocument;
gContentDoc.addEventListener("visibilitychange", onVisibilityChange, false);
gContentAPI.showHighlight("appMenu");
waitForElementToBeVisible(highlight, function checkForInitialHighlight() {
gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
});
},
yield elementVisiblePromise(highlight);
gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
gContentWindow = yield browserStartupDeferred.promise;
// This highlight should be shown thanks to the visibilitychange listener.
let newWindowHighlight = gContentWindow.document.getElementById("UITourHighlight");
yield elementVisiblePromise(newWindowHighlight);
let selectedTab = gContentWindow.gBrowser.selectedTab;
is(selectedTab.linkedBrowser && selectedTab.linkedBrowser.contentDocument, gContentDoc, "Document should be selected in new window");
ok(UITour.originTabs && UITour.originTabs.has(gContentWindow), "Window should be known");
ok(UITour.originTabs.get(gContentWindow).has(selectedTab), "Tab should be known");
let shownPromise = promisePanelShown(gContentWindow);
gContentAPI.showMenu("appMenu");
yield shownPromise;
isnot(gContentWindow.PanelUI.panel.state, "closed", "Panel should be open");
ok(gContentWindow.PanelUI.contents.children.length > 0, "Panel contents should have children");
gContentAPI.hideHighlight();
gContentAPI.hideMenu("appMenu");
gTestTab = null;
Services.obs.addObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
gContentWindow.close();
yield windowDestroyedDeferred.promise;
}),
];

View File

@ -66,9 +66,11 @@ let tests = [
done();
},
function test_seenPageIDs_set_1(done) {
taskify(function* test_seenPageIDs_set_1() {
gContentAPI.registerPageID("testpage1");
yield waitForConditionPromise(() => UITour.seenPageIDs.size == 3, "Waiting for page to be registered.");
checkExpectedSeenPageIDs(["savedID1", "savedID2", "testpage1"]);
const PREFIX = BrowserUITelemetry.BUCKET_PREFIX;
@ -85,11 +87,12 @@ let tests = [
gBrowser.removeTab(gBrowser.selectedTab);
gBrowser.selectedTab = gTestTab;
BrowserUITelemetry.setBucket(null);
done();
},
function test_seenPageIDs_set_2(done) {
}),
taskify(function* test_seenPageIDs_set_2() {
gContentAPI.registerPageID("testpage2");
yield waitForConditionPromise(() => UITour.seenPageIDs.size == 4, "Waiting for page to be registered.");
checkExpectedSeenPageIDs(["savedID1", "savedID2", "testpage1", "testpage2"]);
const PREFIX = BrowserUITelemetry.BUCKET_PREFIX;
@ -105,6 +108,5 @@ let tests = [
"After closing tab, bucket should be expiring");
BrowserUITelemetry.setBucket(null);
done();
},
}),
];

View File

@ -2,27 +2,51 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource:///modules/UITour.jsm");
Cu.import("resource://gre/modules/Task.jsm");
function waitForCondition(condition, nextTest, errorMsg) {
var tries = 0;
var interval = setInterval(function() {
if (tries >= 30) {
ok(false, errorMsg);
moveOn();
const SINGLE_TRY_TIMEOUT = 100;
const NUMBER_OF_TRIES = 30;
function waitForConditionPromise(condition, timeoutMsg) {
let defer = Promise.defer();
let tries = 0;
function checkCondition() {
if (tries >= NUMBER_OF_TRIES) {
defer.reject(timeoutMsg);
}
var conditionPassed;
try {
conditionPassed = condition();
} catch (e) {
ok(false, e + "\n" + e.stack);
conditionPassed = false;
return defer.reject(e);
}
if (conditionPassed) {
moveOn();
return defer.resolve();
}
tries++;
}, 100);
var moveOn = function() { clearInterval(interval); nextTest(); };
setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
}
setTimeout(checkCondition, SINGLE_TRY_TIMEOUT);
return defer.promise;
}
function waitForCondition(condition, nextTest, errorMsg) {
waitForConditionPromise(condition, errorMsg).then(nextTest, (reason) => {
ok(false, reason + (reason.stack ? "\n" + e.stack : ""));
});
}
/**
* Wrapper to partially transition tests to Task.
*/
function taskify(fun) {
return (done) => {
return Task.spawn(fun).then(done, (reason) => {
ok(false, reason);
done();
});
}
}
function is_hidden(element) {
@ -80,6 +104,14 @@ function waitForElementToBeHidden(element, nextTest, msg) {
"Timeout waiting for invisibility: " + msg);
}
function elementVisiblePromise(element, msg) {
return waitForConditionPromise(() => is_visible(element), "Timeout waiting for visibility: " + msg);
}
function elementHiddenPromise(element, msg) {
return waitForConditionPromise(() => is_hidden(element), "Timeout waiting for invisibility: " + msg);
}
function waitForPopupAtAnchor(popup, anchorNode, nextTest, msg) {
waitForCondition(() => is_visible(popup) && popup.popupBoxObject.anchorNode == anchorNode,
() => {
@ -90,24 +122,69 @@ function waitForPopupAtAnchor(popup, anchorNode, nextTest, msg) {
"Timeout waiting for popup at anchor: " + msg);
}
function hideInfoPromise(...args) {
let popup = document.getElementById("UITourTooltip");
gContentAPI.hideInfo.apply(gContentAPI, args);
return promisePanelElementHidden(window, popup);
}
function showInfoPromise(...args) {
let popup = document.getElementById("UITourTooltip");
gContentAPI.showInfo.apply(gContentAPI, args);
return promisePanelElementShown(window, popup);
}
function waitForCallbackResultPromise() {
return waitForConditionPromise(() => {
return gContentWindow.callbackResult;
}, "callback should be called");
}
function addPinnedTabPromise() {
gContentAPI.addPinnedTab();
return waitForConditionPromise(() => {
let tabInfo = UITour.pinnedTabs.get(window);
if (!tabInfo) {
return false;
}
return tabInfo.tab.pinned;
});
}
function removePinnedTabPromise() {
gContentAPI.removePinnedTab();
return waitForConditionPromise(() => {
let tabInfo = UITour.pinnedTabs.get(window);
return tabInfo == null;
});
}
function promisePanelShown(win) {
let panelEl = win.PanelUI.panel;
return promisePanelElementShown(win, panelEl);
}
function promisePanelElementShown(win, aPanel) {
function promisePanelElementEvent(win, aPanel, aEvent) {
let deferred = Promise.defer();
let timeoutId = win.setTimeout(() => {
deferred.reject("Panel did not show within 5 seconds.");
}, 5000);
aPanel.addEventListener("popupshown", function onPanelOpen(e) {
aPanel.removeEventListener("popupshown", onPanelOpen);
aPanel.addEventListener(aEvent, function onPanelEvent(e) {
aPanel.removeEventListener(aEvent, onPanelEvent);
win.clearTimeout(timeoutId);
deferred.resolve();
});
return deferred.promise;
}
function promisePanelElementShown(win, aPanel) {
return promisePanelElementEvent(win, aPanel, "popupshown");
}
function promisePanelElementHidden(win, aPanel) {
return promisePanelElementEvent(win, aPanel, "popuphidden");
}
function is_element_hidden(element, msg) {
isnot(element, null, "Element should not be null, when checking visibility");
ok(is_hidden(element), msg);

View File

@ -4704,7 +4704,7 @@ window > chatbox {
@media (-moz-mac-lion-theme) {
#TabsToolbar > .private-browsing-indicator {
transform: translateY(calc(0px - var(--negative-space)));
transform: translateY(calc(0px - var(--space-above-tabbar)));
/* We offset by 38px for mask graphic, plus 4px to account for the
* margin-left, which sums to 42px.
*/

View File

@ -2379,12 +2379,22 @@ public class GeckoAppShell
@WrapElementForJNI
public static void enableNetworkNotifications() {
GeckoNetworkManager.getInstance().enableNotifications();
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
GeckoNetworkManager.getInstance().enableNotifications();
}
});
}
@WrapElementForJNI
public static void disableNetworkNotifications() {
GeckoNetworkManager.getInstance().disableNotifications();
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
GeckoNetworkManager.getInstance().disableNotifications();
}
});
}
/**

View File

@ -37,7 +37,7 @@ import android.util.Log;
public class GeckoNetworkManager extends BroadcastReceiver implements NativeEventListener {
private static final String LOGTAG = "GeckoNetworkManager";
static private GeckoNetworkManager sInstance;
private static GeckoNetworkManager sInstance;
public static void destroy() {
if (sInstance != null) {
@ -75,7 +75,7 @@ public class GeckoNetworkManager extends BroadcastReceiver implements NativeEven
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Wifi:Enable");
}
private ConnectionType mConnectionType = ConnectionType.NONE;
private volatile ConnectionType mConnectionType = ConnectionType.NONE;
private final IntentFilter mNetworkFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
// Whether the manager should be listening to Network Information changes.
@ -88,6 +88,7 @@ public class GeckoNetworkManager extends BroadcastReceiver implements NativeEven
// The application context used for registering receivers, so
// we can unregister them again later.
private volatile Context mApplicationContext;
private boolean mIsListening;
public static GeckoNetworkManager getInstance() {
if (sInstance == null) {
@ -118,14 +119,23 @@ public class GeckoNetworkManager extends BroadcastReceiver implements NativeEven
}
private void startListening() {
if (mIsListening) {
Log.w(LOGTAG, "Already started!");
return;
}
final Context appContext = mApplicationContext;
if (appContext == null) {
Log.w(LOGTAG, "Not registering receiver: no context!");
return;
}
Log.v(LOGTAG, "Registering receiver.");
appContext.registerReceiver(this, mNetworkFilter);
// registerReceiver will return null if registering fails.
if (appContext.registerReceiver(this, mNetworkFilter) == null) {
Log.e(LOGTAG, "Registering receiver failed");
} else {
mIsListening = true;
}
}
public void stop() {
@ -158,7 +168,13 @@ public class GeckoNetworkManager extends BroadcastReceiver implements NativeEven
return;
}
if (!mIsListening) {
Log.w(LOGTAG, "Already stopped!");
return;
}
mApplicationContext.unregisterReceiver(this);
mIsListening = false;
}
private int wifiDhcpGatewayAddress() {
@ -188,23 +204,29 @@ public class GeckoNetworkManager extends BroadcastReceiver implements NativeEven
}
private void updateConnectionType() {
ConnectionType previousConnectionType = mConnectionType;
mConnectionType = getConnectionType();
final ConnectionType previousConnectionType = mConnectionType;
final ConnectionType newConnectionType = getConnectionType();
if (newConnectionType == previousConnectionType) {
return;
}
if (mConnectionType == previousConnectionType || !mShouldNotify) {
mConnectionType = newConnectionType;
if (!mShouldNotify) {
return;
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkEvent(
mConnectionType.value,
mConnectionType == ConnectionType.WIFI,
newConnectionType.value,
newConnectionType == ConnectionType.WIFI,
wifiDhcpGatewayAddress()));
}
public double[] getCurrentInformation() {
return new double[] { mConnectionType.value,
(mConnectionType == ConnectionType.WIFI) ? 1.0 : 0.0,
wifiDhcpGatewayAddress()};
final ConnectionType connectionType = mConnectionType;
return new double[] { connectionType.value,
connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
wifiDhcpGatewayAddress() };
}
public void enableNotifications() {

View File

@ -103,6 +103,7 @@ skip-if = android_version == "10"
[testDeviceSearchEngine]
[testJNI]
# [testMozPay] # see bug 945675
[testNetworkManager]
[testOrderedBroadcast]
[testOSLocale]
[testResourceSubstitutions]

View File

@ -0,0 +1,11 @@
/* 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/. */
package org.mozilla.gecko.tests;
public class testNetworkManager extends JavascriptTest {
public testNetworkManager() {
super("testNetworkManager.js");
}
}

View File

@ -0,0 +1,28 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
function ok(passed, text) {
do_report_result(passed, text, Components.stack.caller, false);
}
add_test(function check_linktype() {
// Let's exercise the interface. Even if the network is not up, we can make sure nothing blows up.
let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
if (network.isLinkUp) {
ok(network.linkType != Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN, "LinkType is not UNKNOWN");
} else {
ok(network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN, "LinkType is UNKNOWN");
}
run_next_test();
});
run_next_test();

View File

@ -64,6 +64,7 @@ let loaderModules = {
"toolkit/loader": loader,
"xpcInspector": xpcInspector,
"promise": promise,
"PromiseDebugging": PromiseDebugging
};
try {
let { indexedDB } = Cu.Sandbox(this, {wantGlobalProperties:["indexedDB"]});

View File

@ -14,6 +14,7 @@ const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const { dbg_assert, dumpn, update } = DevToolsUtils;
const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
const promise = require("promise");
const PromiseDebugging = require("PromiseDebugging");
const Debugger = require("Debugger");
const xpcInspector = require("xpcInspector");
const mapURIToAddonID = require("./utils/map-uri-to-addon-id");
@ -33,6 +34,33 @@ let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
// collections, etc.
let OBJECT_PREVIEW_MAX_ITEMS = 10;
/**
* Call PromiseDebugging.getState on this Debugger.Object's referent and wrap
* the resulting `value` or `reason` properties in a Debugger.Object instance.
*
* See dom/webidl/PromiseDebugging.webidl
*
* @returns Object
* An object of one of the following forms:
* - { state: "pending" }
* - { state: "fulfilled", value }
* - { state: "rejected", reason }
*/
Debugger.Object.prototype.getPromiseState = function () {
if (this.class != "Promise") {
throw new Error(
"Can't call `getPromiseState` on `Debugger.Object`s that don't " +
"refer to Promise objects.");
}
const state = PromiseDebugging.getState(this.unsafeDereference());
return {
state: state.state,
value: this.makeDebuggeeValue(state.value),
reason: this.makeDebuggeeValue(state.reason)
};
};
/**
* BreakpointStore objects keep track of all breakpoints that get set so that we
* can reset them when the same script is introduced to the thread again (such
@ -2986,8 +3014,15 @@ function stringify(aObj) {
DevToolsUtils.reportException("stringify", error);
return "<dead object>";
}
const stringifier = stringifiers[aObj.class] || stringifiers.Object;
return stringifier(aObj);
try {
return stringifier(aObj);
} catch (e) {
DevToolsUtils.reportException("stringify", e);
return "<failed to stringify object>";
}
}
// Used to prevent infinite recursion when an array is found inside itself.
@ -3056,7 +3091,18 @@ let stringifiers = {
return '[Exception... "' + message + '" ' +
'code: "' + code +'" ' +
'nsresult: "0x' + result + ' (' + name + ')"]';
}
},
Promise: obj => {
const { state, value, reason } = obj.getPromiseState();
let statePreview = state;
if (state != "pending") {
const settledValue = state === "fulfilled" ? value : reason;
statePreview += ": " + (typeof settledValue === "object" && settledValue !== null
? stringify(settledValue)
: settledValue);
}
return "Promise (" + statePreview + ")";
},
};
/**
@ -3093,6 +3139,17 @@ ObjectActor.prototype = {
};
if (this.obj.class != "DeadObject") {
// Expose internal Promise state.
if (this.obj.class == "Promise") {
const { state, value, reason } = this.obj.getPromiseState();
g.promiseState = { state };
if (state == "fulfilled") {
g.promiseState.value = this.threadActor.createValueGrip(value);
} else if (state == "rejected") {
g.promiseState.reason = this.threadActor.createValueGrip(reason);
}
}
let raw = this.obj.unsafeDereference();
// If Cu is not defined, we are running on a worker thread, where xrays
@ -3787,6 +3844,7 @@ DebuggerServer.ObjectActorPreviewers = {
return true;
}], // DOMStringMap
}; // DebuggerServer.ObjectActorPreviewers
/**

View File

@ -119,6 +119,18 @@ function test_display_string()
{
input: "new Proxy({}, {})",
output: "[object Object]"
},
{
input: "Promise.resolve(5)",
output: "Promise (fulfilled: 5)"
},
{
input: "Promise.reject(new Error())",
output: "Promise (rejected: Error)"
},
{
input: "new Promise(function () {})",
output: "Promise (pending)"
}
];

View File

@ -0,0 +1,40 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that the preview in a Promise's grip is correct when the Promise is
* pending.
*/
function run_test()
{
initTestDebuggerServer();
const debuggee = addTestGlobal("test-promise-state");
const client = new DebuggerClient(DebuggerServer.connectPipe());
client.connect(function() {
attachTestTabAndResume(client, "test-promise-state", function (response, tabClient, threadClient) {
Task.spawn(function* () {
const packet = yield executeOnNextTickAndWaitForPause(() => evalCode(debuggee), client);
const grip = packet.frame.environment.bindings.variables.p;
ok(grip.value.preview);
equal(grip.value.class, "Promise");
equal(grip.value.promiseState.state, "pending");
finishClient(client);
});
});
});
do_test_pending();
}
function evalCode(debuggee) {
Components.utils.evalInSandbox(
"doTest();\n" +
function doTest() {
var p = new Promise(function () {});
debugger;
},
debuggee
);
}

View File

@ -0,0 +1,45 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that the preview in a Promise's grip is correct when the Promise is
* fulfilled.
*/
function run_test()
{
initTestDebuggerServer();
const debuggee = addTestGlobal("test-promise-state");
const client = new DebuggerClient(DebuggerServer.connectPipe());
client.connect(function() {
attachTestTabAndResume(client, "test-promise-state", function (response, tabClient, threadClient) {
Task.spawn(function* () {
const packet = yield executeOnNextTickAndWaitForPause(() => evalCode(debuggee), client);
const grip = packet.frame.environment.bindings.variables.p;
ok(grip.value.preview);
equal(grip.value.class, "Promise");
equal(grip.value.promiseState.state, "fulfilled");
equal(grip.value.promiseState.value.actorID, packet.frame.arguments[0].actorID,
"The promise's fulfilled state value should be the same value passed to the then function");
finishClient(client);
});
});
});
do_test_pending();
}
function evalCode(debuggee) {
Components.utils.evalInSandbox(
"doTest();\n" +
function doTest() {
var resolved = Promise.resolve({});
resolved.then(() => {
var p = resolved;
debugger;
});
},
debuggee
);
}

View File

@ -0,0 +1,45 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that the preview in a Promise's grip is correct when the Promise is
* rejected.
*/
function run_test()
{
initTestDebuggerServer();
const debuggee = addTestGlobal("test-promise-state");
const client = new DebuggerClient(DebuggerServer.connectPipe());
client.connect(function() {
attachTestTabAndResume(client, "test-promise-state", function (response, tabClient, threadClient) {
Task.spawn(function* () {
const packet = yield executeOnNextTickAndWaitForPause(() => evalCode(debuggee), client);
const grip = packet.frame.environment.bindings.variables.p;
ok(grip.value.preview);
equal(grip.value.class, "Promise");
equal(grip.value.promiseState.state, "rejected");
equal(grip.value.promiseState.reason.actorID, packet.frame.arguments[0].actorID,
"The promise's rejected state reason should be the same value passed to the then function");
finishClient(client);
});
});
});
do_test_pending();
}
function evalCode(debuggee) {
Components.utils.evalInSandbox(
"doTest();\n" +
function doTest() {
var resolved = Promise.reject(new Error("uh oh"));
resolved.then(null, () => {
var p = resolved;
debugger;
});
},
debuggee
);
}

View File

@ -157,6 +157,9 @@ reason = bug 820380
[test_objectgrips-11.js]
[test_objectgrips-12.js]
[test_objectgrips-13.js]
[test_promise_state-01.js]
[test_promise_state-02.js]
[test_promise_state-03.js]
[test_interrupt.js]
[test_stepping-01.js]
[test_stepping-02.js]

View File

@ -320,7 +320,8 @@ if (typeof Components === "object") {
"promise": Promise,
"Debugger": Debugger,
"xpcInspector": xpcInspector,
"Timer": Object.create(Timer)
"Timer": Object.create(Timer),
"PromiseDebugging": PromiseDebugging
},
paths: {
"": "resource://gre/modules/commonjs/",