Bug 1073238 - Split UITour.jsm into chrome and content parts that communicate via messages. r=MattN

This commit is contained in:
Tomasz Kołodziejski 2014-10-24 17:19:00 +02:00
Родитель 64d2cea345
Коммит 1b9c50100d
12 изменённых файлов: 440 добавлений и 326 удалений

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

@ -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

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

@ -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);

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

@ -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 = {

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

@ -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)

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

@ -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);
});

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

@ -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) {

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

@ -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;
}),
];

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

@ -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()");
}),
];

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

@ -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");
}),
];

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

@ -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;
}),
];

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

@ -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();
},
}),
];

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

@ -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);