Wed, 31 Dec 2014 06:09:35 +0100
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 }