author | Matthew Noorenberghe <mozilla@noorenberghe.ca> |
Tue, 12 Jun 2012 18:16:00 -0700 | |
changeset 102284 | 59707ed19e48846651a27b53afbd308058024a4c |
parent 102283 | 2d595c62abcb188f208ad7bf6703a882a826fbb6 |
child 102285 | 13d2ad44ba747fd47058082294b768b4ac72a556 |
push id | 13402 |
push user | mozilla@noorenberghe.ca |
push date | Tue, 14 Aug 2012 10:01:39 +0000 |
treeherder | mozilla-inbound@59707ed19e48 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | dolske |
bugs | 764213 |
milestone | 17.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -454,28 +454,37 @@ window[chromehidden~="toolbar"] toolbar: .notification-anchor-icon { -moz-user-focus: normal; } .notification-anchor-icon:not([showing]) { display: none; } +#notification-popup .text-link.custom-link { + -moz-binding: url("chrome://global/content/bindings/text.xml#text-label"); + text-decoration: none; +} + #invalid-form-popup > description { max-width: 280px; } #geolocation-notification { -moz-binding: url("chrome://browser/content/urlbarBindings.xml#geolocation-notification"); } #addon-progress-notification { -moz-binding: url("chrome://browser/content/urlbarBindings.xml#addon-progress-notification"); } +#identity-request-notification { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#identity-request-notification"); +} + /* override hidden="true" for the status bar compatibility shim in case it was persisted for the real status bar */ #status-bar { display: -moz-box; } /* Remove the resizer from the statusbar compatibility shim */ #status-bar[hideresizer] > .statusbar-resizerpanel {
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -559,16 +559,17 @@ oninput="gBrowser.userTypedValue = this.value;" ontextentered="this.handleCommand(param);" ontextreverted="return this.handleRevert();" pageproxystate="invalid" onfocus="document.getElementById('identity-box').style.MozUserFocus= 'normal'" onblur="setTimeout(function() document.getElementById('identity-box').style.MozUserFocus = '', 0);"> <box id="notification-popup-box" hidden="true" align="center"> <image id="default-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="identity-notification-icon" class="notification-anchor-icon" role="button"/> <image id="geo-notification-icon" class="notification-anchor-icon" role="button"/> <image id="addons-notification-icon" class="notification-anchor-icon" role="button"/> <image id="indexedDB-notification-icon" class="notification-anchor-icon" role="button"/> <image id="password-notification-icon" class="notification-anchor-icon" role="button"/> <image id="webapps-notification-icon" class="notification-anchor-icon" role="button"/> <image id="plugins-notification-icon" class="notification-anchor-icon" role="button"/> </box> <!-- Use onclick instead of normal popup= syntax since the popup
--- a/browser/base/content/test/browser_popupNotification.js +++ b/browser/base/content/test/browser_popupNotification.js @@ -73,17 +73,17 @@ function runNextTest() { } let onHidden = onHiddenArray.shift(); info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)"); executeSoon(function () { onHidden.call(nextTest, this); if (!onHiddenArray.length) goNext(); - }); + }.bind(this)); }, onHiddenArray.length); info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen); } info("[Test #" + gTestIndex + "] running test"); nextTest.run(); }
--- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -1170,16 +1170,271 @@ <method name="onDownloadEnded"> <body><![CDATA[ this.updateProgress(); ]]></body> </method> </implementation> </binding> + <binding id="identity-request-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification"> + <content align="start"> + + <xul:image class="popup-notification-icon" + xbl:inherits="popupid,src=icon"/> + + <xul:vbox flex="1"> + <xul:vbox anonid="identity-deck"> + <xul:vbox flex="1" pack="center"> <!-- 1: add an email --> + <html:input type="email" anonid="email" required="required" size="30"/> + <xul:description anonid="newidentitydesc"/> + <xul:spacer flex="1"/> + <xul:label class="text-link custom-link small-margin" anonid="chooseemail" hidden="true"/> + </xul:vbox> + <xul:vbox flex="1" hidden="true"> <!-- 2: choose an email --> + <xul:description anonid="chooseidentitydesc"/> + <xul:radiogroup anonid="identities"> + </xul:radiogroup> + <xul:label class="text-link custom-link" anonid="newemail"/> + </xul:vbox> + </xul:vbox> + <xul:hbox class="popup-notification-button-container" + pack="end" align="center"> + <xul:label anonid="tos" class="text-link" hidden="true"/> + <xul:label anonid="privacypolicy" class="text-link" hidden="true"/> + <xul:spacer flex="1"/> + <xul:image anonid="throbber" src="chrome://browser/skin/tabbrowser/loading.png" + style="visibility:hidden" width="16" height="16"/> + <xul:button anonid="button" + type="menu-button" + class="popup-notification-menubutton" + xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey"> + <xul:menupopup anonid="menupopup" + xbl:inherits="oncommand=menucommand"> + <children/> + <xul:menuitem class="menuitem-iconic popup-notification-closeitem" + label="&closeNotificationItem.label;" + xbl:inherits="oncommand=closeitemcommand"/> + </xul:menupopup> + </xul:button> + </xul:hbox> + </xul:vbox> + <xul:vbox pack="start"> + <xul:toolbarbutton anonid="closebutton" + class="messageCloseButton popup-notification-closebutton tabbable" + xbl:inherits="oncommand=closebuttoncommand" + tooltiptext="&closeNotification.tooltip;"/> + </xul:vbox> + </content> + <implementation> + <constructor><![CDATA[ + // this.notification.options.identity is used to pass identity-specific info to the binding + let origin = this.identity.origin + + // Populate text + this.emailField.placeholder = gNavigatorBundle. + getString("identity.newIdentity.email.placeholder"); + this.newIdentityDesc.textContent = gNavigatorBundle.getFormattedString( + "identity.newIdentity.description", [origin]); + this.chooseIdentityDesc.textContent = gNavigatorBundle.getFormattedString( + "identity.chooseIdentity.description", [origin]); + + // Show optional terms of service and privacy policy links + this._populateLink(this.identity.termsOfService, "tos", "identity.termsOfService"); + this._populateLink(this.identity.privacyPolicy, "privacypolicy", "identity.privacyPolicy"); + + // Populate the list of identities to choose from. The origin is used to provide + // better suggestions. + let identities = this.SignInToWebsiteUX.getIdentitiesForSite(origin); + + this._populateIdentityList(identities); + + if (typeof this.step == "undefined") { + // First opening of this notification + // Show the add email pane (0) if there are no existing identities otherwise show the list + this.step = "result" in identities && identities.result.length ? 1 : 0; + } else { + // Already opened so restore previous state + if (this.identity.typedEmail) { + this.emailField.value = this.identity.typedEmail; + } + if (this.identity.selected) { + // If the user already chose an identity then update the UI to reflect that + this.onIdentitySelected(); + } + // Update the view for the step + this.step = this.step; + } + + // Fire notification with the chosen identity when main button is clicked + this.button.addEventListener("command", this._onButtonCommand.bind(this), true); + + // Do the same if enter is pressed in the email field + this.emailField.addEventListener("keypress", function emailFieldKeypress(aEvent) { + if (aEvent.keyCode != aEvent.DOM_VK_RETURN) + return; + this._onButtonCommand(aEvent); + }.bind(this)); + + this.addEmailLink.value = gNavigatorBundle.getString("identity.newIdentity.label"); + this.addEmailLink.accessKey = gNavigatorBundle.getString("identity.newIdentity.accessKey"); + this.addEmailLink.addEventListener("click", function addEmailClick(evt) { + this.step = 0; + }.bind(this)); + + this.chooseEmailLink.value = gNavigatorBundle.getString("identity.chooseIdentity.label"); + this.chooseEmailLink.hidden = !("result" in identities && identities.result.length); + this.chooseEmailLink.addEventListener("click", function chooseEmailClick(evt) { + this.step = 1; + }.bind(this)); + + this.emailField.addEventListener("blur", function onEmailBlur() { + this.identity.typedEmail = this.emailField.value; + }.bind(this)); + ]]></constructor> + + <field name="SignInToWebsiteUX" readonly="true"> + let sitw = {}; + Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw); + sitw.SignInToWebsiteUX; + </field> + + <field name="newIdentityDesc" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "newidentitydesc"); + </field> + + <field name="chooseIdentityDesc" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "chooseidentitydesc"); + </field> + + <field name="identityList" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "identities"); + </field> + + <field name="emailField" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "email"); + </field> + + <field name="addEmailLink" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "newemail"); + </field> + + <field name="chooseEmailLink" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "chooseemail"); + </field> + + <field name="throbber" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "throbber"); + </field> + + <field name="identity" readonly="true"> + this.notification.options.identity; + </field> + + <!-- persist the state on the identity object so we can re-create the + notification state upon re-opening --> + <property name="step"> + <getter> + return this.identity.step; + </getter> + <setter><![CDATA[ + let deck = document.getAnonymousElementByAttribute(this, "anonid", "identity-deck"); + for (let i = 0; i < deck.children.length; i++) { + deck.children[i].hidden = (val != i); + } + this.identity.step = val; + switch (val) { + case 0: + this.emailField.focus(); + break; + }]]> + </setter> + </property> + + <method name="onIdentitySelected"> + <body><![CDATA[ + this.throbber.style.visibility = "visible"; + this.button.disabled = true; + this.emailField.value = this.identity.selected + this.emailField.disabled = true; + this.identityList.disabled = true; + ]]></body> + </method> + + <method name="_populateLink"> + <parameter name="aURL"/> + <parameter name="aLinkId"/> + <parameter name="aStringId"/> + <body><![CDATA[ + if (aURL) { + // Show optional link to aURL + let link = document.getAnonymousElementByAttribute(this, "anonid", aLinkId); + link.value = gNavigatorBundle.getString(aStringId); + link.href = aURL; + link.hidden = false; + } + ]]></body> + </method> + + <method name="_populateIdentityList"> + <parameter name="aIdentities"/> + <body><![CDATA[ + let foundLastUsed = false; + let lastUsed = this.identity.selected || aIdentities.lastUsed; + for (let id in aIdentities.result) { + let label = aIdentities.result[id]; + let opt = this.identityList.appendItem(label); + if (label == lastUsed) { + this.identityList.selectedItem = opt; + foundLastUsed = true; + } + } + if (!foundLastUsed) { + this.identityList.selectedIndex = -1; + } + ]]></body> + </method> + + <method name="_onButtonCommand"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.target != aEvent.currentTarget) + return; + let chosenId; + switch (this.step) { + case 0: + aEvent.stopPropagation(); + if (!this.emailField.validity.valid) { + this.emailField.focus(); + return; + } + chosenId = this.emailField.value; + break; + case 1: + aEvent.stopPropagation(); + let selectedItem = this.identityList.selectedItem + chosenId = selectedItem ? selectedItem.label : null; + if (!chosenId) + return; + break; + default: + throw new Error("Unknown case"); + return; + } + // Actually select the identity + this.SignInToWebsiteUX.selectIdentity(this.identity.rpId, chosenId); + this.identity.selected = chosenId; + this.onIdentitySelected(); + ]]></body> + </method> + + </implementation> + </binding> + + <binding id="splitmenu"> <content> <xul:hbox anonid="menuitem" flex="1" class="splitmenu-menuitem" xbl:inherits="iconic,label,disabled,onclick=oncommand,_moz-menuactive=active"/> <xul:menu anonid="menu" class="splitmenu-menu" xbl:inherits="disabled,_moz-menuactive=active" oncommand="event.stopPropagation();">
--- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -7,30 +7,31 @@ const Ci = Components.interfaces; const Cc = Components.classes; const Cr = Components.results; const Cu = Components.utils; const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/SignInToWebsite.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils", "resource://gre/modules/BookmarkHTMLUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "webappsUI", +XPCOMUtils.defineLazyModuleGetter(this, "webappsUI", "resource:///modules/webappsUI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", "resource:///modules/PageThumbs.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PdfJs", "resource://pdf.js/PdfJs.jsm"); @@ -303,16 +304,17 @@ BrowserGlue.prototype = { this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME); if (this._isPlacesInitObserver) os.removeObserver(this, "places-init-complete"); if (this._isPlacesLockedObserver) os.removeObserver(this, "places-database-locked"); if (this._isPlacesShutdownObserver) os.removeObserver(this, "places-shutdown"); webappsUI.uninit(); + SignInToWebsiteUX.uninit(); }, _onAppDefaults: function BG__onAppDefaults() { // apply distribution customizations (prefs) // other customizations are applied in _onProfileStartup() this._distributionCustomizer.applyPrefDefaults(); }, @@ -332,16 +334,18 @@ BrowserGlue.prototype = { // handle any UI migration this._migrateUI(); // Initialize webapps UI webappsUI.init(); PageThumbs.init(); + SignInToWebsiteUX.init(); + PdfJs.init(); Services.obs.notifyObservers(null, "browser-ui-startup-complete", ""); }, // the first browser window has finished initializing _onFirstWindowLoaded: function BG__onFirstWindowLoaded() { #ifdef XP_WIN
--- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -378,8 +378,26 @@ social.enable.label=%S integration social.enable.accesskey=n # LOCALIZATION NOTE (social.remove.label): %S = brandShortName social.remove.label=Remove from %S social.remove.accesskey=R # LOCALIZATION NOTE (social.enabled.message): %1$S is the name of the social provider, %2$S is brandShortName (e.g. Firefox) social.activated.message=%1$S integration with %2$S has been activated. + +# Identity notifications popups +identity.termsOfService = Terms of Service +identity.privacyPolicy = Privacy Policy +identity.chooseIdentity.description = Sign in to %S +identity.chooseIdentity.label = Use an existing email +identity.newIdentity.label = Use a different email +identity.newIdentity.accessKey = e +identity.newIdentity.email.placeholder = Email +# LOCALIZATION NOTE (identity.newIdentity.description, identity.chooseIdentity.description): %S is the website origin (ie. https://www.mozilla.org) shown in popup notifications. +identity.newIdentity.description = Enter your email address to sign in to %S +identity.next.label = Next +identity.next.accessKey = n +# LOCALIZATION NOTE: shown in the popup notification when a user successfully logs into a website +# LOCALIZATION NOTE (identity.loggedIn.description): %S is the website origin (ie. https://www.mozilla.org) +identity.loggedIn.description = Signed in as: %S +identity.loggedIn.signOut.label = Sign Out +identity.loggedIn.signOut.accessKey = O
--- a/browser/modules/Makefile.in +++ b/browser/modules/Makefile.in @@ -13,21 +13,22 @@ include $(topsrcdir)/config/config.mk TEST_DIRS += test EXTRA_JS_MODULES = \ openLocationLastURL.jsm \ NetworkPrioritizer.jsm \ NewTabUtils.jsm \ offlineAppCache.jsm \ + SignInToWebsite.jsm \ TelemetryTimestamps.jsm \ Social.jsm \ webappsUI.jsm \ $(NULL) -ifeq ($(MOZ_WIDGET_TOOLKIT),windows) +ifeq ($(MOZ_WIDGET_TOOLKIT),windows) EXTRA_JS_MODULES += \ WindowsPreviewPerTab.jsm \ WindowsJumpLists.jsm \ $(NULL) endif include $(topsrcdir)/config/rules.mk
new file mode 100644 --- /dev/null +++ b/browser/modules/SignInToWebsite.jsm @@ -0,0 +1,235 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["SignInToWebsiteUX"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "IdentityService", + "resource://gre/modules/identity/Identity.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Logger", + "resource://gre/modules/identity/LogUtils.jsm"); + +function log(...aMessageArgs) { + Logger.log.apply(Logger, ["SignInToWebsiteUX"].concat(aMessageArgs)); +} + +let SignInToWebsiteUX = { + + init: function SignInToWebsiteUX_init() { + Services.obs.addObserver(this, "identity-request", false); + Services.obs.addObserver(this, "identity-auth", false); + Services.obs.addObserver(this, "identity-auth-complete", false); + Services.obs.addObserver(this, "identity-login-state-changed", false); + }, + + uninit: function SignInToWebsiteUX_uninit() { + Services.obs.removeObserver(this, "identity-request"); + Services.obs.removeObserver(this, "identity-auth"); + Services.obs.removeObserver(this, "identity-auth-complete"); + Services.obs.removeObserver(this, "identity-login-state-changed"); + }, + + observe: function SignInToWebsiteUX_observe(aSubject, aTopic, aData) { + log("observe: received", aTopic, "with", aData, "for", aSubject); + let options = null; + if (aSubject) { + options = aSubject.wrappedJSObject; + } + switch(aTopic) { + case "identity-request": + this.requestLogin(options); + break; + case "identity-auth": + this._openAuthenticationUI(aData, options); + break; + case "identity-auth-complete": + this._closeAuthenticationUI(aData); + break; + case "identity-login-state-changed": + let emailAddress = aData; + if (emailAddress) { + this._removeRequestUI(options); + this._showLoggedInUI(emailAddress, options); + } else { + this._removeLoggedInUI(options); + } + break; + default: + Logger.reportError("SignInToWebsiteUX", "Unknown observer notification:", aTopic); + break; + } + }, + + /** + * The website is requesting login so the user must choose an identity to use. + */ + requestLogin: function SignInToWebsiteUX_requestLogin(aOptions) { + let windowID = aOptions.rpId; + log("requestLogin", aOptions); + let [chromeWin, browserEl] = this._getUIForWindowID(windowID); + + // message is not shown in the UI but is required + let message = aOptions.origin; + let mainAction = { + label: chromeWin.gNavigatorBundle.getString("identity.next.label"), + accessKey: chromeWin.gNavigatorBundle.getString("identity.next.accessKey"), + callback: function() {}, // required + }; + let options = { + identity: { + origin: aOptions.origin, + }, + }; + let secondaryActions = []; + + // add some extra properties to the notification to store some identity-related state + for (let opt in aOptions) { + options.identity[opt] = aOptions[opt]; + } + log("requestLogin: rpId: ", options.identity.rpId); + + chromeWin.PopupNotifications.show(browserEl, "identity-request", message, + "identity-notification-icon", mainAction, + [], options); + }, + + /** + * Get the list of possible identities to login to the given origin. + */ + getIdentitiesForSite: function SignInToWebsiteUX_getIdentitiesForSite(aOrigin) { + return IdentityService.RP.getIdentitiesForSite(aOrigin); + }, + + /** + * User chose a new or existing identity from the doorhanger after a request() call + */ + selectIdentity: function SignInToWebsiteUX_selectIdentity(aRpId, aIdentity) { + log("selectIdentity: rpId: ", aRpId, " identity: ", aIdentity); + IdentityService.selectIdentity(aRpId, aIdentity); + }, + + // Private + + /** + * Return the chrome window and <browser> for the given outer window ID. + */ + _getUIForWindowID: function(aWindowID) { + let someWindow = Services.wm.getMostRecentWindow("navigator:browser"); + if (!someWindow) { + Logger.reportError("SignInToWebsiteUX", "no window"); + return [null, null]; + } + + let windowUtils = someWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let content = windowUtils.getOuterWindowWithId(aWindowID); + + if (content) { + let browser = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell).chromeEventHandler; + let chromeWin = browser.ownerDocument.defaultView; + return [chromeWin, browser]; + } + Logger.reportError("SignInToWebsiteUX", "no content"); + + return [null, null]; + }, + + /** + * Open UI with a content frame displaying aAuthURI so that the user can authenticate with their + * IDP. Then tell Identity.jsm the identifier for the window so that it knows that the DOM API + * calls are for this authentication flow. + */ + _openAuthenticationUI: function _openAuthenticationUI(aAuthURI, aContext) { + // Open a tab/window with aAuthURI with an identifier (aID) attached so that the DOM APIs know this is an auth. window. + let chromeWin = Services.wm.getMostRecentWindow('navigator:browser'); + let features = "chrome=false,width=640,height=480,centerscreen,location=yes,resizable=yes,scrollbars=yes,status=yes"; + log("aAuthURI: ", aAuthURI); + let authWin = Services.ww.openWindow(chromeWin, "about:blank", "", features, null); + let windowID = authWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + log("authWin outer id: ", windowID); + + let provId = aContext.provId; + // Tell the ID service about the id before loading the url + IdentityService.IDP.setAuthenticationFlow(windowID, provId); + + authWin.location = aAuthURI; + }, + + _closeAuthenticationUI: function _closeAuthenticationUI(aAuthId) { + log("_closeAuthenticationUI:", aAuthId); + let [chromeWin, browserEl] = this._getUIForWindowID(aAuthId); + if (chromeWin) + chromeWin.close(); + else + Logger.reportError("SignInToWebsite", "Could not close window with ID", aAuthId); + }, + + /** + * Show a doorhanger indicating the currently logged-in user. + */ + _showLoggedInUI: function _showLoggedInUI(aIdentity, aContext) { + let windowID = aContext.rpId; + log("_showLoggedInUI for ", windowID); + let [chromeWin, browserEl] = this._getUIForWindowID(windowID); + + let message = chromeWin.gNavigatorBundle.getFormattedString("identity.loggedIn.description", + [aIdentity]); + let mainAction = { + label: chromeWin.gNavigatorBundle.getString("identity.loggedIn.signOut.label"), + accessKey: chromeWin.gNavigatorBundle.getString("identity.loggedIn.signOut.accessKey"), + callback: function() { + log("sign out callback fired"); + IdentityService.RP.logout(windowID); + }, + }; + let secondaryActions = []; + let options = { + dismissed: true, + }; + let loggedInNot = chromeWin.PopupNotifications.show(browserEl, "identity-logged-in", message, + "identity-notification-icon", mainAction, + secondaryActions, options); + loggedInNot.rpId = windowID; + }, + + /** + * Remove the doorhanger indicating the currently logged-in user. + */ + _removeLoggedInUI: function _removeLoggedInUI(aContext) { + let windowID = aContext.rpId; + log("_removeLoggedInUI for ", windowID); + if (!windowID) + throw "_removeLoggedInUI: Invalid RP ID"; + let [chromeWin, browserEl] = this._getUIForWindowID(windowID); + + let loggedInNot = chromeWin.PopupNotifications.getNotification("identity-logged-in", browserEl); + if (loggedInNot) + chromeWin.PopupNotifications.remove(loggedInNot); + }, + + /** + * Remove the doorhanger indicating the currently logged-in user. + */ + _removeRequestUI: function _removeRequestUI(aContext) { + let windowID = aContext.rpId; + log("_removeRequestUI for ", windowID); + let [chromeWin, browserEl] = this._getUIForWindowID(windowID); + + let requestNot = chromeWin.PopupNotifications.getNotification("identity-request", browserEl); + if (requestNot) + chromeWin.PopupNotifications.remove(requestNot); + }, + +};
--- a/browser/modules/test/Makefile.in +++ b/browser/modules/test/Makefile.in @@ -9,18 +9,19 @@ VPATH = @srcdir@ relativesrcdir = @relativesrcdir@ include $(DEPTH)/config/autoconf.mk include $(topsrcdir)/config/rules.mk _BROWSER_FILES = \ browser_NetworkPrioritizer.js \ browser_TelemetryTimestamps.js \ + browser_SignInToWebsite.js \ $(NULL) -ifeq ($(MOZ_WIDGET_TOOLKIT),windows) +ifeq ($(MOZ_WIDGET_TOOLKIT),windows) _BROWSER_FILES += \ browser_taskbar_preview.js \ $(NULL) endif libs:: $(_BROWSER_FILES) $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644 --- /dev/null +++ b/browser/modules/test/browser_SignInToWebsite.js @@ -0,0 +1,549 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * TO TEST: + * - test state saved on doorhanger dismissal + * - links to switch steps + * - TOS and PP link clicks + * - identityList is populated correctly + */ + +Services.prefs.setBoolPref("toolkit.identity.debug", true); + +XPCOMUtils.defineLazyModuleGetter(this, "IdentityService", + "resource://gre/modules/identity/Identity.jsm"); + +const TEST_ORIGIN = "https://example.com"; +const TEST_EMAIL = "user@example.com"; + +let gTestIndex = 0; +let outerWinId = gBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + +function NotificationBase(aNotId) { + this.id = aNotId; +} +NotificationBase.prototype = { + message: TEST_ORIGIN, + mainAction: { + label: "", + callback: function() { + this.mainActionClicked = true; + }.bind(this), + }, + secondaryActions: [], + options: { + "identity": { + origin: TEST_ORIGIN, + rpId: outerWinId, + }, + }, +}; + +let tests = [ + { + name: "test_request_required_typed", + + run: function() { + setupRPFlow(); + this.notifyOptions = { + rpId: outerWinId, + origin: TEST_ORIGIN, + }; + this.notifyObj = new NotificationBase("identity-request"); + Services.obs.notifyObservers({wrappedJSObject: this.notifyOptions}, + "identity-request", null); + }, + + onShown: function(popup) { + checkPopup(popup, this.notifyObj); + let notification = popup.childNodes[0]; + + // Check identity popup state + let state = notification.identity; + ok(!state.typedEmail, "Nothing should be typed yet"); + ok(!state.selected, "Identity should not be selected yet"); + ok(!state.termsOfService, "No TOS specified"); + ok(!state.privacyPolicy, "No PP specified"); + is(state.step, 0, "Step should be persisted with default value"); + is(state.rpId, outerWinId, "Check rpId"); + is(state.origin, TEST_ORIGIN, "Check origin"); + + is(notification.step, 0, "Should be on the new email step"); + is(notification.chooseEmailLink.hidden, true, "Identity list is empty so link to list view should be hidden"); + is(notification.addEmailLink.parentElement.hidden, true, "We are already on the email input step so choose email pane should be hidden"); + is(notification.emailField.value, "", "Email field should default to empty on a new notification"); + let notifDoc = notification.ownerDocument; + ok(notifDoc.getAnonymousElementByAttribute(notification, "anonid", "tos").hidden, + "TOS link should be hidden"); + ok(notifDoc.getAnonymousElementByAttribute(notification, "anonid", "privacypolicy").hidden, + "PP link should be hidden"); + + // Try to continue with a missing email address + triggerMainCommand(popup); + is(notification.throbber.style.visibility, "hidden", "is throbber visible"); + ok(!notification.button.disabled, "Button should not be disabled"); + is(window.gIdentitySelected, null, "Check no identity selected"); + + // Fill in an invalid email address and try again + notification.emailField.value = "foo"; + triggerMainCommand(popup); + is(notification.throbber.style.visibility, "hidden", "is throbber visible"); + ok(!notification.button.disabled, "Button should not be disabled"); + is(window.gIdentitySelected, null, "Check no identity selected"); + + // Fill in an email address and try again + notification.emailField.value = TEST_EMAIL; + triggerMainCommand(popup); + is(window.gIdentitySelected.rpId, outerWinId, "Check identity selected rpId"); + is(window.gIdentitySelected.identity, TEST_EMAIL, "Check identity selected email"); + is(notification.identity.selected, TEST_EMAIL, "Check persisted email"); + is(notification.throbber.style.visibility, "visible", "is throbber visible"); + ok(notification.button.disabled, "Button should be disabled"); + ok(notification.emailField.disabled, "Email field should be disabled"); + ok(notification.identityList.disabled, "Identity list should be disabled"); + + PopupNotifications.getNotification("identity-request").remove(); + }, + + onHidden: function(popup) { }, + }, + { + name: "test_request_optional", + + run: function() { + this.notifyOptions = { + rpId: outerWinId, + origin: TEST_ORIGIN, + privacyPolicy: TEST_ORIGIN + "/pp.txt", + termsOfService: TEST_ORIGIN + "/tos.tzt", + }; + this.notifyObj = new NotificationBase("identity-request"); + Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions }, + "identity-request", null); + }, + + onShown: function(popup) { + checkPopup(popup, this.notifyObj); + let notification = popup.childNodes[0]; + + // Check identity popup state + let state = notification.identity; + ok(!state.typedEmail, "Nothing should be typed yet"); + ok(!state.selected, "Identity should not be selected yet"); + is(state.termsOfService, this.notifyOptions.termsOfService, "Check TOS URL"); + is(state.privacyPolicy, this.notifyOptions.privacyPolicy, "Check PP URL"); + is(state.step, 0, "Step should be persisted with default value"); + is(state.rpId, outerWinId, "Check rpId"); + is(state.origin, TEST_ORIGIN, "Check origin"); + + is(notification.step, 0, "Should be on the new email step"); + is(notification.chooseEmailLink.hidden, true, "Identity list is empty so link to list view should be hidden"); + is(notification.addEmailLink.parentElement.hidden, true, "We are already on the email input step so choose email pane should be hidden"); + is(notification.emailField.value, "", "Email field should default to empty on a new notification"); + let notifDoc = notification.ownerDocument; + let tosLink = notifDoc.getAnonymousElementByAttribute(notification, "anonid", "tos"); + ok(!tosLink.hidden, "TOS link should be visible"); + is(tosLink.href, this.notifyOptions.termsOfService, "Check TOS link URL"); + let ppLink = notifDoc.getAnonymousElementByAttribute(notification, "anonid", "privacypolicy"); + ok(!ppLink.hidden, "PP link should be visible"); + is(ppLink.href, this.notifyOptions.privacyPolicy, "Check PP link URL"); + + // Try to continue with a missing email address + triggerMainCommand(popup); + is(notification.throbber.style.visibility, "hidden", "is throbber visible"); + ok(!notification.button.disabled, "Button should not be disabled"); + is(window.gIdentitySelected, null, "Check no identity selected"); + + // Fill in an invalid email address and try again + notification.emailField.value = "foo"; + triggerMainCommand(popup); + is(notification.throbber.style.visibility, "hidden", "is throbber visible"); + ok(!notification.button.disabled, "Button should not be disabled"); + is(window.gIdentitySelected, null, "Check no identity selected"); + + // Fill in an email address and try again + notification.emailField.value = TEST_EMAIL; + triggerMainCommand(popup); + is(window.gIdentitySelected.rpId, outerWinId, "Check identity selected rpId"); + is(window.gIdentitySelected.identity, TEST_EMAIL, "Check identity selected email"); + is(notification.identity.selected, TEST_EMAIL, "Check persisted email"); + is(notification.throbber.style.visibility, "visible", "is throbber visible"); + ok(notification.button.disabled, "Button should be disabled"); + ok(notification.emailField.disabled, "Email field should be disabled"); + ok(notification.identityList.disabled, "Identity list should be disabled"); + + PopupNotifications.getNotification("identity-request").remove(); + }, + + onHidden: function(popup) {}, + }, + { + name: "test_login_state_changed", + run: function () { + this.notifyOptions = { + rpId: outerWinId, + }; + this.notifyObj = new NotificationBase("identity-logged-in"); + this.notifyObj.message = "Signed in as: user@example.com"; + this.notifyObj.mainAction.label = "Sign Out"; + this.notifyObj.mainAction.accessKey = "O"; + Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions }, + "identity-login-state-changed", TEST_EMAIL); + executeSoon(function() { + PopupNotifications.getNotification("identity-logged-in").anchorElement.click(); + }); + }, + + onShown: function(popup) { + checkPopup(popup, this.notifyObj); + + // Fire the notification that the user is no longer logged-in to close the UI. + Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions }, + "identity-login-state-changed", null); + }, + + onHidden: function(popup) {}, + }, + { + name: "test_login_state_changed_logout", + run: function () { + this.notifyOptions = { + rpId: outerWinId, + }; + this.notifyObj = new NotificationBase("identity-logged-in"); + this.notifyObj.message = "Signed in as: user@example.com"; + this.notifyObj.mainAction.label = "Sign Out"; + this.notifyObj.mainAction.accessKey = "O"; + Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions }, + "identity-login-state-changed", TEST_EMAIL); + executeSoon(function() { + PopupNotifications.getNotification("identity-logged-in").anchorElement.click(); + }); + }, + + onShown: function(popup) { + checkPopup(popup, this.notifyObj); + + // This time trigger the Sign Out button and make sure the UI goes away. + triggerMainCommand(popup); + }, + + onHidden: function(popup) {}, + }, +]; + +function test_auth() { + let notifyOptions = { + provId: outerWinId, + origin: TEST_ORIGIN, + }; + + Services.obs.addObserver(function() { + // prepare to send auth-complete and close the window + let winCloseObs = new WindowObserver(function(closedWin) { + info("closed window"); + finish(); + }, "domwindowclosed"); + Services.ww.registerNotification(winCloseObs); + Services.obs.notifyObservers(null, "identity-auth-complete", IdentityService.IDP.authenticationFlowSet.authId); + + }, "test-identity-auth-window", false); + + let winObs = new WindowObserver(function(authWin) { + ok(authWin, "Authentication window opened"); + ok(authWin.contentWindow.location); + }); + + Services.ww.registerNotification(winObs); + + Services.obs.notifyObservers({ wrappedJSObject: notifyOptions }, + "identity-auth", TEST_ORIGIN + "/auth"); +} + +function test() { + waitForExplicitFinish(); + + registerCleanupFunction(cleanUp); + + let sitw = {}; + Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw); + + ok(sitw.SignInToWebsiteUX, "SignInToWebsiteUX object exists"); + + // Replace implementation of ID Service functions for testing + window.selectIdentity = sitw.SignInToWebsiteUX.selectIdentity; + sitw.SignInToWebsiteUX.selectIdentity = function(aRpId, aIdentity) { + info("Identity selected: " + aIdentity); + window.gIdentitySelected = {rpId: aRpId, identity: aIdentity}; + }; + + window.setAuthenticationFlow = IdentityService.IDP.setAuthenticationFlow; + IdentityService.IDP.setAuthenticationFlow = function(aAuthId, aProvId) { + info("setAuthenticationFlow: " + aAuthId + " : " + aProvId); + this.authenticationFlowSet = { authId: aAuthId, provId: aProvId }; + Services.obs.notifyObservers(null, "test-identity-auth-window", aAuthId); + }; + + runNextTest(); +} + +// Cleanup between tests +function resetState() { + delete window.gIdentitySelected; + delete IdentityService.IDP.authenticationFlowSet; + IdentityService.reset(); +} + +// Cleanup after all tests +function cleanUp() { + info("cleanup"); + resetState(); + + for (let topic in gActiveObservers) + Services.obs.removeObserver(gActiveObservers[topic], topic); + for (let eventName in gActiveListeners) + PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false); + delete IdentityService.RP._rpFlows[outerWinId]; + + // Put the JSM functions back to how they were + IdentityService.IDP.setAuthenticationFlow = window.setAuthenticationFlow; + delete window.setAuthenticationFlow; + + let sitw = {}; + Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw); + sitw.SignInToWebsiteUX.selectIdentity = window.selectIdentity; + delete window.selectIdentity; + + Services.prefs.clearUserPref("toolkit.identity.debug"); +} + +let gActiveListeners = {}; +let gActiveObservers = {}; +let gShownState = {}; + +function runNextTest() { + let nextTest = tests[gTestIndex]; + + function goNext() { + resetState(); + if (++gTestIndex == tests.length) + executeSoon(test_auth); + else + executeSoon(runNextTest); + } + + function addObserver(topic) { + function observer() { + Services.obs.removeObserver(observer, "PopupNotifications-" + topic); + delete gActiveObservers["PopupNotifications-" + topic]; + + info("[Test #" + gTestIndex + "] observer for " + topic + " called"); + nextTest[topic](); + goNext(); + } + Services.obs.addObserver(observer, "PopupNotifications-" + topic, false); + gActiveObservers["PopupNotifications-" + topic] = observer; + } + + if (nextTest.backgroundShow) { + addObserver("backgroundShow"); + } else if (nextTest.updateNotShowing) { + addObserver("updateNotShowing"); + } else { + doOnPopupEvent("popupshowing", function () { + info("[Test #" + gTestIndex + "] popup showing"); + }); + doOnPopupEvent("popupshown", function () { + gShownState[gTestIndex] = true; + info("[Test #" + gTestIndex + "] popup shown"); + nextTest.onShown(this); + }); + + // We allow multiple onHidden functions to be defined in an array. They're + // called in the order they appear. + let onHiddenArray = nextTest.onHidden instanceof Array ? + nextTest.onHidden : + [nextTest.onHidden]; + doOnPopupEvent("popuphidden", function () { + if (!gShownState[gTestIndex]) { + // TODO: needed? + info("Popup from test " + gTestIndex + " was hidden before its popupshown fired"); + } + + let onHidden = onHiddenArray.shift(); + info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)"); + executeSoon(function () { + onHidden.call(nextTest, this); + if (!onHiddenArray.length) + goNext(); + }.bind(this)); + }, onHiddenArray.length); + info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen); + } + + info("[Test #" + gTestIndex + "] running test"); + nextTest.run(); +} + +function doOnPopupEvent(eventName, callback, numExpected) { + gActiveListeners[eventName] = function (event) { + if (event.target != PopupNotifications.panel) + return; + if (typeof(numExpected) === "number") + numExpected--; + if (!numExpected) { + PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false); + delete gActiveListeners[eventName]; + } + + callback.call(PopupNotifications.panel); + }; + PopupNotifications.panel.addEventListener(eventName, gActiveListeners[eventName], false); +} + +function checkPopup(popup, notificationObj) { + info("[Test #" + gTestIndex + "] checking popup"); + + let notifications = popup.childNodes; + is(notifications.length, 1, "only one notification displayed"); + let notification = notifications[0]; + let icon = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-icon"); + is(notification.getAttribute("label"), notificationObj.message, "message matches"); + is(notification.id, notificationObj.id + "-notification", "id matches"); + if (notificationObj.id != "identity-request" && notificationObj.mainAction) { + is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches"); + is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches"); + } + let actualSecondaryActions = notification.childNodes; + let secondaryActions = notificationObj.secondaryActions || []; + let actualSecondaryActionsCount = actualSecondaryActions.length; + if (secondaryActions.length) { + let lastChild = actualSecondaryActions.item(actualSecondaryActions.length - 1); + is(lastChild.tagName, "menuseparator", "menuseparator exists"); + actualSecondaryActionsCount--; + } + is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions"); + secondaryActions.forEach(function (a, i) { + is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches"); + is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches"); + }); +} + +function triggerMainCommand(popup) { + info("[Test #" + gTestIndex + "] triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + let notification = notifications[0]; + + // 20, 10 so that the inner button is hit + EventUtils.synthesizeMouse(notification.button, 20, 10, {}); +} + +function triggerSecondaryCommand(popup, index) { + info("[Test #" + gTestIndex + "] triggering secondary command"); + let notifications = popup.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + let notification = notifications[0]; + + notification.button.focus(); + + popup.addEventListener("popupshown", function () { + popup.removeEventListener("popupshown", arguments.callee, false); + + // Press down until the desired command is selected + for (let i = 0; i <= index; i++) + EventUtils.synthesizeKey("VK_DOWN", {}); + + // Activate + EventUtils.synthesizeKey("VK_ENTER", {}); + }, false); + + // One down event to open the popup + EventUtils.synthesizeKey("VK_DOWN", { altKey: (navigator.platform.indexOf("Mac") == -1) }); +} + +function dismissNotification(popup) { + info("[Test #" + gTestIndex + "] dismissing notification"); + executeSoon(function () { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); +} + +function partial(fn) { + let args = Array.prototype.slice.call(arguments, 1); + return function() { + return fn.apply(this, args.concat(Array.prototype.slice.call(arguments))); + }; +} + +// create a mock "doc" object, which the Identity Service +// uses as a pointer back into the doc object +function mock_doc(aIdentity, aOrigin, aDoFunc) { + let mockedDoc = {}; + mockedDoc.id = outerWinId; + mockedDoc.loggedInEmail = aIdentity; + mockedDoc.origin = aOrigin; + mockedDoc['do'] = aDoFunc; + mockedDoc.doReady = partial(aDoFunc, 'ready'); + mockedDoc.doLogin = partial(aDoFunc, 'login'); + mockedDoc.doLogout = partial(aDoFunc, 'logout'); + mockedDoc.doError = partial(aDoFunc, 'error'); + mockedDoc.doCancel = partial(aDoFunc, 'cancel'); + mockedDoc.doCoffee = partial(aDoFunc, 'coffee'); + + return mockedDoc; +} + +// takes a list of functions and returns a function that +// when called the first time, calls the first func, +// then the next time the second, etc. +function call_sequentially() { + let numCalls = 0; + let funcs = arguments; + + return function() { + if (!funcs[numCalls]) { + let argString = Array.prototype.slice.call(arguments).join(","); + ok(false, "Too many calls: " + argString); + return; + } + funcs[numCalls].apply(funcs[numCalls], arguments); + numCalls += 1; + }; +} + +function setupRPFlow(aIdentity) { + IdentityService.RP.watch(mock_doc(aIdentity, TEST_ORIGIN, call_sequentially( + function(action, params) { + is(action, "ready", "1st callback"); + is(params, null); + }, + function(action, params) { + is(action, "logout", "2nd callback"); + is(params, null); + }, + function(action, params) { + is(action, "ready", "3rd callback"); + is(params, null); + } + ))); +} + +function WindowObserver(aCallback, aObserveTopic = "domwindowopened") { + this.observe = function(aSubject, aTopic, aData) { + if (aTopic != aObserveTopic) { + return; + } + info(aObserveTopic); + Services.ww.unregisterNotification(this); + + SimpleTest.executeSoon(function() { + let domWin = aSubject.QueryInterface(Ci.nsIDOMWindow); + aCallback(domWin); + }); + }; +}
--- a/browser/themes/gnomestripe/browser.css +++ b/browser/themes/gnomestripe/browser.css @@ -1235,16 +1235,20 @@ toolbar[iconsize="small"] #feed-button { .notification-anchor-icon:-moz-focusring { outline: 1px dotted -moz-DialogText; } #default-notification-icon { list-style-image: url(chrome://global/skin/icons/information-16.png); } +#identity-notification-icon { + list-style-image: url(chrome://mozapps/skin/profile/profileicon.png); +} + #geo-notification-icon { list-style-image: url(chrome://browser/skin/Geolocation-16.png); } #addons-notification-icon { list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric-16.png); }
--- a/browser/themes/pinstripe/browser.css +++ b/browser/themes/pinstripe/browser.css @@ -2364,22 +2364,29 @@ toolbarbutton.chevron > .toolbarbutton-m box-shadow: 0 0 2px 1px -moz-mac-focusring inset, 0 0 3px 2px -moz-mac-focusring; } #default-notification-icon { list-style-image: url(chrome://global/skin/icons/information-16.png); } +#identity-notification-icon { + list-style-image: url(chrome://mozapps/skin/profile/profileicon.png); +} + #geo-notification-icon { list-style-image: url(chrome://browser/skin/Geolocation-16.png); } +#notification-popup .text-link { + color: #fff; +} + .geolocation-text-link { - color: #fff; -moz-margin-start: 0; /* override default label margin to match description margin */ } .telemetry-text-link { color: #fff; } #addons-notification-icon {
--- a/browser/themes/winstripe/browser.css +++ b/browser/themes/winstripe/browser.css @@ -2370,16 +2370,20 @@ toolbarbutton.bookmark-item[dragover="tr outline: 1px dotted -moz-DialogText; outline-offset: -3px; } #default-notification-icon { list-style-image: url(chrome://global/skin/icons/information-16.png); } +#identity-notification-icon { + list-style-image: url(chrome://mozapps/skin/profile/profileicon.png); +} + #geo-notification-icon { list-style-image: url(chrome://browser/skin/Geolocation-16.png); } #addons-notification-icon { list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric-16.png); }
--- a/toolkit/identity/RelyingParty.jsm +++ b/toolkit/identity/RelyingParty.jsm @@ -205,23 +205,23 @@ IdentityRelyingParty.prototype = { * @param aRPId * (integer) the id of the doc object obtained in .watch() * * @param aOptions * (Object) options including privacyPolicy, termsOfService */ request: function request(aRPId, aOptions) { log("request: rpId:", aRPId); + let rp = this._rpFlows[aRPId]; // Notify UX to display identity picker. // Pass the doc id to UX so it can pass it back to us later. - let options = {rpId: aRPId}; + let options = {rpId: aRPId, origin: rp.origin}; // Append URLs after resolving - let rp = this._rpFlows[aRPId]; let baseURI = Services.io.newURI(rp.origin, null, null); for (let optionName of ["privacyPolicy", "termsOfService"]) { if (aOptions[optionName]) { options[optionName] = baseURI.resolve(aOptions[optionName]); } } Services.obs.notifyObservers({wrappedJSObject: options}, "identity-request", null);
--- a/toolkit/identity/tests/chrome/test_sandbox.xul +++ b/toolkit/identity/tests/chrome/test_sandbox.xul @@ -212,17 +212,17 @@ function WindowObserver(aCallback) { this.observe = function(aSubject, aTopic, aData) { if (aTopic != "domwindowopened") { return; } Services.ww.unregisterNotification(this); let domWin = aSubject.QueryInterface(Ci.nsIDOMWindow); ok(!domWin, "No window should be opened"); - SimpleTest.executesoon(function() { + SimpleTest.executeSoon(function() { info("Closing opened window"); domWin.close(); aCallback(); }); } } // Can the sandbox call window.alert() or popup other UI?
--- a/toolkit/identity/tests/mochitest/head_identity.js +++ b/toolkit/identity/tests/mochitest/head_identity.js @@ -24,16 +24,27 @@ const TEST_IDPPARAMS = { }; const Services = Cu.import("resource://gre/modules/Services.jsm").Services; // Set the debug pref before loading other modules SpecialPowers.setBoolPref("toolkit.identity.debug", true); SpecialPowers.setBoolPref("dom.identity.enabled", true); +// Shutdown the UX if it exists so that it won't interfere with tests by also responding to +// observer notifications. +try { + const SignInToWebsiteUX = Cu.import("resource:///modules/SignInToWebsite.jsm").SignInToWebsiteUX; + if (SignInToWebsiteUX) { + SignInToWebsiteUX.uninit(); + } +} catch (ex) { + // The module doesn't exist +} + const jwcrypto = Cu.import("resource://gre/modules/identity/jwcrypto.jsm").jwcrypto; const IdentityStore = Cu.import("resource://gre/modules/identity/IdentityStore.jsm").IdentityStore; const RelyingParty = Cu.import("resource://gre/modules/identity/RelyingParty.jsm").RelyingParty; const XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm").XPCOMUtils; const IDService = Cu.import("resource://gre/modules/identity/Identity.jsm").IdentityService; const IdentityProvider = Cu.import("resource://gre/modules/identity/IdentityProvider.jsm").IdentityProvider; const identity = navigator.id || navigator.mozId; @@ -170,16 +181,25 @@ function setup_provisioning(identity, af function resetState() { IDService.reset(); } function cleanup() { resetState(); SpecialPowers.clearUserPref("toolkit.identity.debug"); SpecialPowers.clearUserPref("dom.identity.enabled"); + // Re-init the UX that we uninit + try { + const SignInToWebsiteUX = Cu.import("resource:///modules/SignInToWebsite.jsm").SignInToWebsiteUX; + if (SignInToWebsiteUX) { + SignInToWebsiteUX.init(); + } + } catch (ex) { + // The module doesn't exist + } } var TESTS = []; function run_next_test() { if (!identity) { todo(false, "DOM API is not available. Skipping tests."); cleanup();
--- a/toolkit/identity/tests/mochitest/test_relying_party.html +++ b/toolkit/identity/tests/mochitest/test_relying_party.html @@ -24,17 +24,17 @@ Test of Relying Party (RP) using the DOM "use strict"; SimpleTest.waitForExplicitFinish(); const DOMIdentity = Cu.import("resource://gre/modules/DOMIdentity.jsm") .DOMIdentity; let outerWinId = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID; // Reset the DOM state then run the next test function run_next_rp_test() { let rpContext = RelyingParty._rpFlows[outerWinId]; if (rpContext) { makeObserver("identity-DOM-state-reset", function() { SimpleTest.executeSoon(run_next_test); });