browser/modules/test/browser_SignInToWebsite.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* Any copyright is dedicated to the Public Domain.
     2  * http://creativecommons.org/publicdomain/zero/1.0/ */
     4 "use strict";
     6 /**
     7  * TO TEST:
     8  * - test state saved on doorhanger dismissal
     9  * - links to switch steps
    10  * - TOS and PP link clicks
    11  * - identityList is populated correctly
    12  */
    14 Services.prefs.setBoolPref("toolkit.identity.debug", true);
    16 XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
    17                                   "resource://gre/modules/identity/Identity.jsm");
    19 const TEST_ORIGIN = "https://example.com";
    20 const TEST_EMAIL = "user@example.com";
    22 let gTestIndex = 0;
    23 let outerWinId = gBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
    24                          .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
    26 function NotificationBase(aNotId) {
    27   this.id = aNotId;
    28 }
    29 NotificationBase.prototype = {
    30   message: TEST_ORIGIN,
    31   mainAction: {
    32     label: "",
    33     callback: function() {
    34       this.mainActionClicked = true;
    35     }.bind(this),
    36   },
    37   secondaryActions: [],
    38   options: {
    39     "identity": {
    40       origin: TEST_ORIGIN,
    41       rpId: outerWinId,
    42     },
    43   },
    44 };
    46 let tests = [
    47   {
    48     name: "test_request_required_typed",
    50     run: function() {
    51       setupRPFlow();
    52       this.notifyOptions = {
    53         rpId: outerWinId,
    54         origin: TEST_ORIGIN,
    55       };
    56       this.notifyObj = new NotificationBase("identity-request");
    57       Services.obs.notifyObservers({wrappedJSObject: this.notifyOptions},
    58                                    "identity-request", null);
    59     },
    61     onShown: function(popup) {
    62       checkPopup(popup, this.notifyObj);
    63       let notification = popup.childNodes[0];
    65       // Check identity popup state
    66       let state = notification.identity;
    67       ok(!state.typedEmail, "Nothing should be typed yet");
    68       ok(!state.selected, "Identity should not be selected yet");
    69       ok(!state.termsOfService, "No TOS specified");
    70       ok(!state.privacyPolicy, "No PP specified");
    71       is(state.step, 0, "Step should be persisted with default value");
    72       is(state.rpId, outerWinId, "Check rpId");
    73       is(state.origin, TEST_ORIGIN, "Check origin");
    75       is(notification.step, 0, "Should be on the new email step");
    76       is(notification.chooseEmailLink.hidden, true, "Identity list is empty so link to list view should be hidden");
    77       is(notification.addEmailLink.parentElement.hidden, true, "We are already on the email input step so choose email pane should be hidden");
    78       is(notification.emailField.value, "", "Email field should default to empty on a new notification");
    79       let notifDoc = notification.ownerDocument;
    80       ok(notifDoc.getAnonymousElementByAttribute(notification, "anonid", "tos").hidden,
    81          "TOS link should be hidden");
    82       ok(notifDoc.getAnonymousElementByAttribute(notification, "anonid", "privacypolicy").hidden,
    83          "PP link should be hidden");
    85       // Try to continue with a missing email address
    86       triggerMainCommand(popup);
    87       is(notification.throbber.style.visibility, "hidden", "is throbber visible");
    88       ok(!notification.button.disabled, "Button should not be disabled");
    89       is(window.gIdentitySelected, null, "Check no identity selected");
    91       // Fill in an invalid email address and try again
    92       notification.emailField.value = "foo";
    93       triggerMainCommand(popup);
    94       is(notification.throbber.style.visibility, "hidden", "is throbber visible");
    95       ok(!notification.button.disabled, "Button should not be disabled");
    96       is(window.gIdentitySelected, null, "Check no identity selected");
    98       // Fill in an email address and try again
    99       notification.emailField.value = TEST_EMAIL;
   100       triggerMainCommand(popup);
   101       is(window.gIdentitySelected.rpId, outerWinId, "Check identity selected rpId");
   102       is(window.gIdentitySelected.identity, TEST_EMAIL, "Check identity selected email");
   103       is(notification.identity.selected, TEST_EMAIL, "Check persisted email");
   104       is(notification.throbber.style.visibility, "visible", "is throbber visible");
   105       ok(notification.button.disabled, "Button should be disabled");
   106       ok(notification.emailField.disabled, "Email field should be disabled");
   107       ok(notification.identityList.disabled, "Identity list should be disabled");
   109       PopupNotifications.getNotification("identity-request").remove();
   110     },
   112     onHidden: function(popup) { },
   113   },
   114   {
   115     name: "test_request_optional",
   117     run: function() {
   118       this.notifyOptions = {
   119         rpId: outerWinId,
   120         origin: TEST_ORIGIN,
   121         privacyPolicy: TEST_ORIGIN + "/pp.txt",
   122         termsOfService: TEST_ORIGIN + "/tos.tzt",
   123       };
   124       this.notifyObj = new NotificationBase("identity-request");
   125       Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions },
   126                                    "identity-request", null);
   127     },
   129     onShown: function(popup) {
   130       checkPopup(popup, this.notifyObj);
   131       let notification = popup.childNodes[0];
   133       // Check identity popup state
   134       let state = notification.identity;
   135       ok(!state.typedEmail, "Nothing should be typed yet");
   136       ok(!state.selected, "Identity should not be selected yet");
   137       is(state.termsOfService, this.notifyOptions.termsOfService, "Check TOS URL");
   138       is(state.privacyPolicy, this.notifyOptions.privacyPolicy, "Check PP URL");
   139       is(state.step, 0, "Step should be persisted with default value");
   140       is(state.rpId, outerWinId, "Check rpId");
   141       is(state.origin, TEST_ORIGIN, "Check origin");
   143       is(notification.step, 0, "Should be on the new email step");
   144       is(notification.chooseEmailLink.hidden, true, "Identity list is empty so link to list view should be hidden");
   145       is(notification.addEmailLink.parentElement.hidden, true, "We are already on the email input step so choose email pane should be hidden");
   146       is(notification.emailField.value, "", "Email field should default to empty on a new notification");
   147       let notifDoc = notification.ownerDocument;
   148       let tosLink = notifDoc.getAnonymousElementByAttribute(notification, "anonid", "tos");
   149       ok(!tosLink.hidden, "TOS link should be visible");
   150       is(tosLink.href, this.notifyOptions.termsOfService, "Check TOS link URL");
   151       let ppLink = notifDoc.getAnonymousElementByAttribute(notification, "anonid", "privacypolicy");
   152       ok(!ppLink.hidden, "PP link should be visible");
   153       is(ppLink.href, this.notifyOptions.privacyPolicy, "Check PP link URL");
   155       // Try to continue with a missing email address
   156       triggerMainCommand(popup);
   157       is(notification.throbber.style.visibility, "hidden", "is throbber visible");
   158       ok(!notification.button.disabled, "Button should not be disabled");
   159       is(window.gIdentitySelected, null, "Check no identity selected");
   161       // Fill in an invalid email address and try again
   162       notification.emailField.value = "foo";
   163       triggerMainCommand(popup);
   164       is(notification.throbber.style.visibility, "hidden", "is throbber visible");
   165       ok(!notification.button.disabled, "Button should not be disabled");
   166       is(window.gIdentitySelected, null, "Check no identity selected");
   168       // Fill in an email address and try again
   169       notification.emailField.value = TEST_EMAIL;
   170       triggerMainCommand(popup);
   171       is(window.gIdentitySelected.rpId, outerWinId, "Check identity selected rpId");
   172       is(window.gIdentitySelected.identity, TEST_EMAIL, "Check identity selected email");
   173       is(notification.identity.selected, TEST_EMAIL, "Check persisted email");
   174       is(notification.throbber.style.visibility, "visible", "is throbber visible");
   175       ok(notification.button.disabled, "Button should be disabled");
   176       ok(notification.emailField.disabled, "Email field should be disabled");
   177       ok(notification.identityList.disabled, "Identity list should be disabled");
   179       PopupNotifications.getNotification("identity-request").remove();
   180     },
   182     onHidden: function(popup) {},
   183   },
   184   {
   185     name: "test_login_state_changed",
   186     run: function () {
   187       this.notifyOptions = {
   188         rpId: outerWinId,
   189       };
   190       this.notifyObj = new NotificationBase("identity-logged-in");
   191       this.notifyObj.message = "Signed in as: user@example.com";
   192       this.notifyObj.mainAction.label = "Sign Out";
   193       this.notifyObj.mainAction.accessKey = "O";
   194       Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions },
   195                                    "identity-login-state-changed", TEST_EMAIL);
   196       executeSoon(function() {
   197         PopupNotifications.getNotification("identity-logged-in").anchorElement.click();
   198       });
   199     },
   201     onShown: function(popup) {
   202       checkPopup(popup, this.notifyObj);
   204       // Fire the notification that the user is no longer logged-in to close the UI.
   205       Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions },
   206                                    "identity-login-state-changed", null);
   207     },
   209     onHidden: function(popup) {},
   210   },
   211   {
   212     name: "test_login_state_changed_logout",
   213     run: function () {
   214       this.notifyOptions = {
   215         rpId: outerWinId,
   216       };
   217       this.notifyObj = new NotificationBase("identity-logged-in");
   218       this.notifyObj.message = "Signed in as: user@example.com";
   219       this.notifyObj.mainAction.label = "Sign Out";
   220       this.notifyObj.mainAction.accessKey = "O";
   221       Services.obs.notifyObservers({ wrappedJSObject: this.notifyOptions },
   222                                    "identity-login-state-changed", TEST_EMAIL);
   223       executeSoon(function() {
   224         PopupNotifications.getNotification("identity-logged-in").anchorElement.click();
   225       });
   226     },
   228     onShown: function(popup) {
   229       checkPopup(popup, this.notifyObj);
   231       // This time trigger the Sign Out button and make sure the UI goes away.
   232       triggerMainCommand(popup);
   233     },
   235     onHidden: function(popup) {},
   236   },
   237 ];
   239 function test_auth() {
   240   let notifyOptions = {
   241     provId: outerWinId,
   242     origin: TEST_ORIGIN,
   243   };
   245   Services.obs.addObserver(function() {
   246     // prepare to send auth-complete and close the window
   247     let winCloseObs = new WindowObserver(function(closedWin) {
   248       info("closed window");
   249       finish();
   250     }, "domwindowclosed");
   251     Services.ww.registerNotification(winCloseObs);
   252     Services.obs.notifyObservers(null, "identity-auth-complete", IdentityService.IDP.authenticationFlowSet.authId);
   254   }, "test-identity-auth-window", false);
   256   let winObs = new WindowObserver(function(authWin) {
   257     ok(authWin, "Authentication window opened");
   258     ok(authWin.contentWindow.location);
   259   });
   261   Services.ww.registerNotification(winObs);
   263   Services.obs.notifyObservers({ wrappedJSObject: notifyOptions },
   264                                "identity-auth", TEST_ORIGIN + "/auth");
   265 }
   267 function test() {
   268   waitForExplicitFinish();
   270   let sitw = {};
   271   try {
   272     Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw);
   273   } catch (ex) {
   274     ok(true, "Skip the test since SignInToWebsite.jsm isn't packaged outside outside mozilla-central");
   275     finish();
   276     return;
   277   }
   279   PopupNotifications.transitionsEnabled = false;
   281   registerCleanupFunction(cleanUp);
   283   ok(sitw.SignInToWebsiteUX, "SignInToWebsiteUX object exists");
   284   if (!Services.prefs.getBoolPref("dom.identity.enabled")) {
   285     // If the pref isn't enabled then init wasn't called so do that for the test.
   286     sitw.SignInToWebsiteUX.init();
   287   }
   289   // Replace implementation of ID Service functions for testing
   290   window.selectIdentity = sitw.SignInToWebsiteUX.selectIdentity;
   291   sitw.SignInToWebsiteUX.selectIdentity = function(aRpId, aIdentity) {
   292     info("Identity selected: " + aIdentity);
   293     window.gIdentitySelected = {rpId: aRpId, identity: aIdentity};
   294   };
   296   window.setAuthenticationFlow = IdentityService.IDP.setAuthenticationFlow;
   297   IdentityService.IDP.setAuthenticationFlow = function(aAuthId, aProvId) {
   298     info("setAuthenticationFlow: " + aAuthId + " : " + aProvId);
   299     this.authenticationFlowSet = { authId: aAuthId, provId: aProvId };
   300     Services.obs.notifyObservers(null, "test-identity-auth-window", aAuthId);
   301   };
   303   runNextTest();
   304 }
   306 // Cleanup between tests
   307 function resetState() {
   308   delete window.gIdentitySelected;
   309   delete IdentityService.IDP.authenticationFlowSet;
   310   IdentityService.reset();
   311 }
   313 // Cleanup after all tests
   314 function cleanUp() {
   315   info("cleanup");
   316   resetState();
   318   PopupNotifications.transitionsEnabled = true;
   320   for (let topic in gActiveObservers)
   321     Services.obs.removeObserver(gActiveObservers[topic], topic);
   322   for (let eventName in gActiveListeners)
   323     PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
   324   delete IdentityService.RP._rpFlows[outerWinId];
   326   // Put the JSM functions back to how they were
   327   IdentityService.IDP.setAuthenticationFlow = window.setAuthenticationFlow;
   328   delete window.setAuthenticationFlow;
   330   let sitw = {};
   331   Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw);
   332   sitw.SignInToWebsiteUX.selectIdentity = window.selectIdentity;
   333   delete window.selectIdentity;
   334   if (!Services.prefs.getBoolPref("dom.identity.enabled")) {
   335     sitw.SignInToWebsiteUX.uninit();
   336   }
   338   Services.prefs.clearUserPref("toolkit.identity.debug");
   339 }
   341 let gActiveListeners = {};
   342 let gActiveObservers = {};
   343 let gShownState = {};
   345 function runNextTest() {
   346   let nextTest = tests[gTestIndex];
   348   function goNext() {
   349     resetState();
   350     if (++gTestIndex == tests.length)
   351       executeSoon(test_auth);
   352     else
   353       executeSoon(runNextTest);
   354   }
   356   function addObserver(topic) {
   357     function observer() {
   358       Services.obs.removeObserver(observer, "PopupNotifications-" + topic);
   359       delete gActiveObservers["PopupNotifications-" + topic];
   361       info("[Test #" + gTestIndex + "] observer for " + topic + " called");
   362       nextTest[topic]();
   363       goNext();
   364     }
   365     Services.obs.addObserver(observer, "PopupNotifications-" + topic, false);
   366     gActiveObservers["PopupNotifications-" + topic] = observer;
   367   }
   369   if (nextTest.backgroundShow) {
   370     addObserver("backgroundShow");
   371   } else if (nextTest.updateNotShowing) {
   372     addObserver("updateNotShowing");
   373   } else {
   374     doOnPopupEvent("popupshowing", function () {
   375       info("[Test #" + gTestIndex + "] popup showing");
   376     });
   377     doOnPopupEvent("popupshown", function () {
   378       gShownState[gTestIndex] = true;
   379       info("[Test #" + gTestIndex + "] popup shown");
   380       nextTest.onShown(this);
   381     });
   383     // We allow multiple onHidden functions to be defined in an array.  They're
   384     // called in the order they appear.
   385     let onHiddenArray = nextTest.onHidden instanceof Array ?
   386                         nextTest.onHidden :
   387                         [nextTest.onHidden];
   388     doOnPopupEvent("popuphidden", function () {
   389       if (!gShownState[gTestIndex]) {
   390         // TODO: needed?
   391         info("Popup from test " + gTestIndex + " was hidden before its popupshown fired");
   392       }
   394       let onHidden = onHiddenArray.shift();
   395       info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)");
   396       executeSoon(function () {
   397         onHidden.call(nextTest, this);
   398         if (!onHiddenArray.length)
   399           goNext();
   400       }.bind(this));
   401     }, onHiddenArray.length);
   402     info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen);
   403   }
   405   info("[Test #" + gTestIndex + "] running test");
   406   nextTest.run();
   407 }
   409 function doOnPopupEvent(eventName, callback, numExpected) {
   410   gActiveListeners[eventName] = function (event) {
   411     if (event.target != PopupNotifications.panel)
   412       return;
   413     if (typeof(numExpected) === "number")
   414       numExpected--;
   415     if (!numExpected) {
   416       PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false);
   417       delete gActiveListeners[eventName];
   418     }
   420     callback.call(PopupNotifications.panel);
   421   };
   422   PopupNotifications.panel.addEventListener(eventName, gActiveListeners[eventName], false);
   423 }
   425 function checkPopup(popup, notificationObj) {
   426   info("[Test #" + gTestIndex + "] checking popup");
   428   let notifications = popup.childNodes;
   429   is(notifications.length, 1, "only one notification displayed");
   430   let notification = notifications[0];
   431   let icon = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-icon");
   432   is(notification.getAttribute("label"), notificationObj.message, "message matches");
   433   is(notification.id, notificationObj.id + "-notification", "id matches");
   434   if (notificationObj.id != "identity-request" && notificationObj.mainAction) {
   435     is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches");
   436     is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches");
   437   }
   438   let actualSecondaryActions = notification.childNodes;
   439   let secondaryActions = notificationObj.secondaryActions || [];
   440   let actualSecondaryActionsCount = actualSecondaryActions.length;
   441   if (secondaryActions.length) {
   442     let lastChild = actualSecondaryActions.item(actualSecondaryActions.length - 1);
   443     is(lastChild.tagName, "menuseparator", "menuseparator exists");
   444     actualSecondaryActionsCount--;
   445   }
   446   is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions");
   447   secondaryActions.forEach(function (a, i) {
   448     is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches");
   449     is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches");
   450   });
   451 }
   453 function triggerMainCommand(popup) {
   454   info("[Test #" + gTestIndex + "] triggering main command");
   455   let notifications = popup.childNodes;
   456   ok(notifications.length > 0, "at least one notification displayed");
   457   let notification = notifications[0];
   459   // 20, 10 so that the inner button is hit
   460   EventUtils.synthesizeMouse(notification.button, 20, 10, {});
   461 }
   463 function triggerSecondaryCommand(popup, index) {
   464   info("[Test #" + gTestIndex + "] triggering secondary command");
   465   let notifications = popup.childNodes;
   466   ok(notifications.length > 0, "at least one notification displayed");
   467   let notification = notifications[0];
   469   notification.button.focus();
   471   popup.addEventListener("popupshown", function () {
   472     popup.removeEventListener("popupshown", arguments.callee, false);
   474     // Press down until the desired command is selected
   475     for (let i = 0; i <= index; i++)
   476       EventUtils.synthesizeKey("VK_DOWN", {});
   478     // Activate
   479     EventUtils.synthesizeKey("VK_RETURN", {});
   480   }, false);
   482   // One down event to open the popup
   483   EventUtils.synthesizeKey("VK_DOWN", { altKey: (navigator.platform.indexOf("Mac") == -1) });
   484 }
   486 function dismissNotification(popup) {
   487   info("[Test #" + gTestIndex + "] dismissing notification");
   488   executeSoon(function () {
   489     EventUtils.synthesizeKey("VK_ESCAPE", {});
   490   });
   491 }
   493 function partial(fn) {
   494   let args = Array.prototype.slice.call(arguments, 1);
   495   return function() {
   496     return fn.apply(this, args.concat(Array.prototype.slice.call(arguments)));
   497   };
   498 }
   500 // create a mock "doc" object, which the Identity Service
   501 // uses as a pointer back into the doc object
   502 function mock_doc(aIdentity, aOrigin, aDoFunc) {
   503   let mockedDoc = {};
   504   mockedDoc.id = outerWinId;
   505   mockedDoc.loggedInEmail = aIdentity;
   506   mockedDoc.origin = aOrigin;
   507   mockedDoc['do'] = aDoFunc;
   508   mockedDoc.doReady = partial(aDoFunc, 'ready');
   509   mockedDoc.doLogin = partial(aDoFunc, 'login');
   510   mockedDoc.doLogout = partial(aDoFunc, 'logout');
   511   mockedDoc.doError = partial(aDoFunc, 'error');
   512   mockedDoc.doCancel = partial(aDoFunc, 'cancel');
   513   mockedDoc.doCoffee = partial(aDoFunc, 'coffee');
   515   return mockedDoc;
   516 }
   518 // takes a list of functions and returns a function that
   519 // when called the first time, calls the first func,
   520 // then the next time the second, etc.
   521 function call_sequentially() {
   522   let numCalls = 0;
   523   let funcs = arguments;
   525   return function() {
   526     if (!funcs[numCalls]) {
   527       let argString = Array.prototype.slice.call(arguments).join(",");
   528       ok(false, "Too many calls: " + argString);
   529       return;
   530     }
   531     funcs[numCalls].apply(funcs[numCalls], arguments);
   532     numCalls += 1;
   533   };
   534 }
   536 function setupRPFlow(aIdentity) {
   537   IdentityService.RP.watch(mock_doc(aIdentity, TEST_ORIGIN, call_sequentially(
   538     function(action, params) {
   539       is(action, "ready", "1st callback");
   540       is(params, null);
   541     },
   542     function(action, params) {
   543       is(action, "logout", "2nd callback");
   544       is(params, null);
   545     },
   546     function(action, params) {
   547       is(action, "ready", "3rd callback");
   548       is(params, null);
   549     }
   550   )));
   551 }
   553 function WindowObserver(aCallback, aObserveTopic = "domwindowopened") {
   554   this.observe = function(aSubject, aTopic, aData) {
   555     if (aTopic != aObserveTopic) {
   556       return;
   557     }
   558     info(aObserveTopic);
   559     Services.ww.unregisterNotification(this);
   561     SimpleTest.executeSoon(function() {
   562       let domWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
   563       aCallback(domWin);
   564     });
   565   };
   566 }

mercurial