--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -8,48 +8,94 @@ let LoopUI;
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms", "resource:///modules/loop/LoopRooms.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
(function() {
LoopUI = {
+ /**
+ * @var {XULWidgetSingleWrapper} toolbarButton Getter for the Loop toolbarbutton
+ * instance for this window.
+ */
get toolbarButton() {
delete this.toolbarButton;
return this.toolbarButton = CustomizableUI.getWidget("loop-button").forWindow(window);
},
+ /**
+ * @var {XULElement} panel Getter for the Loop panel element.
+ */
get panel() {
delete this.panel;
return this.panel = document.getElementById("loop-notification-panel");
},
/**
+ * @var {XULElement|null} browser Getter for the Loop panel browser element.
+ * Will be NULL if the panel hasn't loaded yet.
+ */
+ get browser() {
+ let browser = document.querySelector("#loop-notification-panel > #loop");
+ if (browser) {
+ delete this.browser;
+ this.browser = browser;
+ }
+ return browser;
+ },
+
+ /**
+ * @var {String|null} selectedTab Getter for the name of the currently selected
+ * tab inside the Loop panel. Will be NULL if
+ * the panel hasn't loaded yet.
+ */
+ get selectedTab() {
+ if (!this.browser) {
+ return null;
+ }
+
+ let selectedTab = this.browser.contentDocument.querySelector(".tab-view > .selected");
+ return selectedTab && selectedTab.getAttribute("data-tab-name");
+ },
+
+ /**
* @return {Promise}
*/
promiseDocumentVisible(aDocument) {
if (!aDocument.hidden) {
return Promise.resolve();
}
return new Promise((resolve) => {
aDocument.addEventListener("visibilitychange", function onVisibilityChanged() {
aDocument.removeEventListener("visibilitychange", onVisibilityChanged);
resolve();
});
});
},
+ /**
+ * Toggle between opening or hiding the Loop panel.
+ *
+ * @param {DOMEvent} [event] Optional event that triggered the call to this
+ * function.
+ * @param {String} [tabId] Optional name of the tab to select after the panel
+ * has opened. Does nothing when the panel is hidden.
+ * @return {Promise}
+ */
togglePanel: function(event, tabId = null) {
if (this.panel.state == "open") {
- this.panel.hidePopup();
- } else {
- this.openCallPanel(event, tabId);
+ return new Promise(resolve => {
+ this.panel.hidePopup();
+ resolve();
+ });
}
+
+ return this.openCallPanel(event, tabId);
},
/**
* Opens the panel for Loop and sizes it appropriately.
*
* @param {event} event The event opening the panel, used to anchor
* the panel to the button which triggers it.
* @param {String} [tabId] Identifier of the tab to select when the panel is
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -738,23 +738,25 @@ function injectLoopAPI(targetWindow) {
});
}
},
/**
* Notifies the UITour module that an event occurred that it might be
* interested in.
*
- * @param {String} subject Subject of the notification
+ * @param {String} subject Subject of the notification
+ * @param {mixed} [params] Optional parameters, providing more details to
+ * the notification subject
*/
notifyUITour: {
enumerable: true,
writable: true,
- value: function(subject) {
- UITour.notify(subject);
+ value: function(subject, params) {
+ UITour.notify(subject, params);
}
},
};
function onStatusChanged(aSubject, aTopic, aData) {
let event = new targetWindow.CustomEvent("LoopStatusChanged");
targetWindow.dispatchEvent(event);
};
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -20,25 +20,34 @@ loop.panel = (function(_, mozL10n) {
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
var TabView = React.createClass({displayName: 'TabView',
propTypes: {
buttonsHidden: React.PropTypes.array,
// The selectedTab prop is used by the UI showcase.
- selectedTab: React.PropTypes.string
+ selectedTab: React.PropTypes.string,
+ mozLoop: React.PropTypes.object
},
getDefaultProps: function() {
return {
buttonsHidden: []
};
},
+ shouldComponentUpdate: function(nextProps, nextState) {
+ var tabChange = this.state.selectedTab !== nextState.selectedTab;
+ if (tabChange) {
+ this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab);
+ }
+ return tabChange;
+ },
+
getInitialState: function() {
// XXX Work around props.selectedTab being undefined initially.
// When we don't need to rely on the pref, this can move back to
// getDefaultProps (bug 1100258).
return {
selectedTab: this.props.selectedTab ||
(navigator.mozLoop.getLoopPref("rooms.enabled") ?
"rooms" : "call")
@@ -965,17 +974,17 @@ loop.panel = (function(_, mozL10n) {
hideButtons.push("contacts");
}
return (
React.DOM.div(null,
NotificationListView({notifications: this.props.notifications,
clearOnDocumentHidden: true}),
TabView({ref: "tabView", selectedTab: this.props.selectedTab,
- buttonsHidden: hideButtons},
+ buttonsHidden: hideButtons, mozLoop: this.props.mozLoop},
this._renderRoomsOrCallTab(),
Tab({name: "contacts"},
ContactsList({selectTab: this.selectTab,
startForm: this.startForm})
),
Tab({name: "contacts_add", hidden: true},
ContactDetailsForm({ref: "contacts_add", mode: "add",
selectTab: this.selectTab})
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -20,25 +20,34 @@ loop.panel = (function(_, mozL10n) {
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
var TabView = React.createClass({
propTypes: {
buttonsHidden: React.PropTypes.array,
// The selectedTab prop is used by the UI showcase.
- selectedTab: React.PropTypes.string
+ selectedTab: React.PropTypes.string,
+ mozLoop: React.PropTypes.object
},
getDefaultProps: function() {
return {
buttonsHidden: []
};
},
+ shouldComponentUpdate: function(nextProps, nextState) {
+ var tabChange = this.state.selectedTab !== nextState.selectedTab;
+ if (tabChange) {
+ this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab);
+ }
+ return tabChange;
+ },
+
getInitialState: function() {
// XXX Work around props.selectedTab being undefined initially.
// When we don't need to rely on the pref, this can move back to
// getDefaultProps (bug 1100258).
return {
selectedTab: this.props.selectedTab ||
(navigator.mozLoop.getLoopPref("rooms.enabled") ?
"rooms" : "call")
@@ -965,17 +974,17 @@ loop.panel = (function(_, mozL10n) {
hideButtons.push("contacts");
}
return (
<div>
<NotificationListView notifications={this.props.notifications}
clearOnDocumentHidden={true} />
<TabView ref="tabView" selectedTab={this.props.selectedTab}
- buttonsHidden={hideButtons}>
+ buttonsHidden={hideButtons} mozLoop={this.props.mozLoop}>
{this._renderRoomsOrCallTab()}
<Tab name="contacts">
<ContactsList selectTab={this.selectTab}
startForm={this.startForm} />
</Tab>
<Tab name="contacts_add" hidden={true}>
<ContactDetailsForm ref="contacts_add" mode="add"
selectTab={this.selectTab} />
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -59,17 +59,18 @@ describe("loop.panel", function() {
on: sandbox.stub()
},
rooms: {
getAll: function(version, callback) {
callback(null, []);
},
on: sandbox.stub()
},
- confirm: sandbox.stub()
+ confirm: sandbox.stub(),
+ notifyUITour: sandbox.stub()
};
document.mozL10n.initialize(navigator.mozLoop);
// XXX prevent a race whenever mozL10n hasn't been initialized yet
setTimeout(done, 0);
});
afterEach(function() {
--- a/browser/components/loop/test/mochitest/browser_toolbarbutton.js
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -6,37 +6,84 @@
*/
"use strict";
Components.utils.import("resource://gre/modules/Promise.jsm", this);
const {LoopRoomsInternal} = Components.utils.import("resource:///modules/loop/LoopRooms.jsm", {});
Services.prefs.setBoolPref("loop.gettingStarted.seen", true);
+const fxASampleToken = {
+ token_type: "bearer",
+ access_token: "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",
+ scope: "profile"
+};
+
+const fxASampleProfile = {
+ email: "test@example.com",
+ uid: "abcd1234"
+};
+
registerCleanupFunction(function*() {
MozLoopService.doNotDisturb = false;
MozLoopServiceInternal.fxAOAuthProfile = null;
yield MozLoopServiceInternal.clearError("testing");
Services.prefs.clearUserPref("loop.gettingStarted.seen");
});
+add_task(function* test_LoopUI_getters() {
+ Assert.ok(LoopUI.panel, "LoopUI panel element should be set");
+ Assert.strictEqual(LoopUI.browser, null, "Browser element should not be there yet");
+ Assert.strictEqual(LoopUI.selectedTab, null, "No tab should be selected yet");
+
+ // Load and show the Loop panel for the very first time this session.
+ yield loadLoopPanel();
+ Assert.ok(LoopUI.browser, "Browser element should be there");
+ Assert.strictEqual(LoopUI.selectedTab, "rooms", "Initially the rooms tab should be selected");
+
+ // Hide the panel.
+ yield LoopUI.togglePanel();
+ Assert.strictEqual(LoopUI.selectedTab, "rooms", "Rooms tab should still be selected");
+
+ // Make sure the contacts tab shows up by simulating a login.
+ MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
+ MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
+ yield MozLoopServiceInternal.notifyStatusChanged("login");
+
+ // Programmatically select the contacts tab.
+ yield LoopUI.togglePanel(null, "contacts");
+ Assert.strictEqual(LoopUI.selectedTab, "contacts", "Contacts tab should be selected now");
+
+ // Switch back to the rooms tab.
+ yield LoopUI.openCallPanel(null, "rooms");
+ Assert.strictEqual(LoopUI.selectedTab, "rooms", "Rooms tab should be selected now");
+
+ // Hide the panel.
+ yield LoopUI.togglePanel();
+
+ // Logout to prevent interfering with the tests after this one.
+ MozLoopServiceInternal.fxAOAuthTokenData =
+ MozLoopServiceInternal.fxAOAuthProfile = null;
+ yield MozLoopServiceInternal.notifyStatusChanged();
+});
+
add_task(function* test_doNotDisturb() {
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
yield MozLoopService.doNotDisturb = true;
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
yield MozLoopService.doNotDisturb = false;
Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is not in disabled state");
});
add_task(function* test_doNotDisturb_with_login() {
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
yield MozLoopService.doNotDisturb = true;
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
- MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
- MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
+ MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
+ MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
yield MozLoopServiceInternal.notifyStatusChanged("login");
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
yield loadLoopPanel();
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state after opening panel");
LoopUI.panel.hidePopup();
yield MozLoopService.doNotDisturb = false;
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
MozLoopServiceInternal.fxAOAuthTokenData = null;
@@ -51,30 +98,30 @@ add_task(function* test_error() {
yield MozLoopServiceInternal.clearError("testing");
Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is not in error state");
});
add_task(function* test_error_with_login() {
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
yield MozLoopServiceInternal.setError("testing", {});
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
- MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
+ MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
MozLoopServiceInternal.notifyStatusChanged("login");
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
yield MozLoopServiceInternal.clearError("testing");
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
MozLoopServiceInternal.fxAOAuthProfile = null;
MozLoopServiceInternal.notifyStatusChanged();
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
});
add_task(function* test_active() {
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
- MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
- MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
+ MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
+ MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
yield MozLoopServiceInternal.notifyStatusChanged("login");
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
yield loadLoopPanel();
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state after opening panel");
LoopUI.panel.hidePopup();
MozLoopServiceInternal.fxAOAuthTokenData = null;
MozLoopServiceInternal.notifyStatusChanged();
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -129,33 +129,33 @@ this.UITour = {
["loop", {
allowAdd: true,
query: "#loop-button",
widgetName: "loop-button",
}],
["loop-newRoom", {
infoPanelPosition: "leftcenter topright",
query: (aDocument) => {
- let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop");
- if (!loopBrowser) {
+ let loopUI = aDocument.defaultView.LoopUI;
+ if (loopUI.selectedTab != "rooms") {
return null;
}
// Use the parentElement full-width container of the button so our arrow
// doesn't overlap the panel contents much.
- return loopBrowser.contentDocument.querySelector(".new-room-button").parentElement;
+ return loopUI.browser.contentDocument.querySelector(".new-room-button").parentElement;
},
}],
["loop-roomList", {
infoPanelPosition: "leftcenter topright",
query: (aDocument) => {
- let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop");
- if (!loopBrowser) {
+ let loopUI = aDocument.defaultView.LoopUI;
+ if (loopUI.selectedTab != "rooms") {
return null;
}
- return loopBrowser.contentDocument.querySelector(".room-list");
+ return loopUI.browser.contentDocument.querySelector(".room-list");
},
}],
["loop-selectedRoomButtons", {
infoPanelOffsetY: -20,
infoPanelPosition: "start_after",
query: (aDocument) => {
let chatbox = aDocument.querySelector("chatbox[src^='about\:loopconversation'][selected]");
@@ -168,17 +168,17 @@ this.UITour = {
// But anchor on the <browser> in the chatbox so the panel doesn't jump to undefined
// positions when the copy/email buttons disappear e.g. when the feedback form opens or
// somebody else joins the room.
return chatbox.content;
},
}],
["loop-signInUpLink", {
query: (aDocument) => {
- let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop");
+ let loopBrowser = aDocument.defaultView.LoopUI.browser;
if (!loopBrowser) {
return null;
}
return loopBrowser.contentDocument.querySelector(".signin-link");
},
}],
["privateWindow", {query: "#privatebrowsing-button"}],
["quit", {query: "#PanelUI-quit"}],
--- a/browser/modules/test/browser_UITour_loop.js
+++ b/browser/modules/test/browser_UITour_loop.js
@@ -6,16 +6,17 @@
let gTestTab;
let gContentAPI;
let gContentWindow;
let loopButton;
let loopPanel = document.getElementById("loop-notification-panel");
Components.utils.import("resource:///modules/UITour.jsm");
const { LoopRooms } = Components.utils.import("resource:///modules/loop/LoopRooms.jsm", {});
+const { MozLoopServiceInternal } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
function test() {
UITourTest();
}
let tests = [
taskify(function* test_menu_show_hide() {
ise(loopButton.open, false, "Menu should initially be closed");
@@ -89,16 +90,63 @@ let tests = [
yield Promise.all(hiddenPromises);
isnot(infoPanel.state, "open", "Info panel should have automatically hid");
isnot(highlightPanel.state, "open", "Highlight panel should have automatically hid");
done();
}), "Info panel should be anchored to the new room button");
});
});
},
+ taskify(function* test_panelTabChangeNotifications() {
+ // First make sure the Loop panel looks like we're logged in to have more than
+ // just one tab to switch to.
+ const fxASampleToken = {
+ token_type: "bearer",
+ access_token: "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",
+ scope: "profile"
+ };
+ const fxASampleProfile = {
+ email: "test@example.com",
+ uid: "abcd1234"
+ };
+ MozLoopServiceInternal.fxAOAuthTokenData = fxASampleToken;
+ MozLoopServiceInternal.fxAOAuthProfile = fxASampleProfile;
+ yield MozLoopServiceInternal.notifyStatusChanged("login");
+
+ // Show the Loop menu.
+ yield showMenuPromise("loop");
+
+ // Listen for and test the notifications that will arrive from now on.
+ let tabChangePromise = new Promise(resolve => {
+ gContentAPI.observe((event, params) => {
+ is(event, "Loop:PanelTabChanged", "Check Loop:PanelTabChanged notification");
+ is(params, "contacts", "Check the tab name param");
+
+ gContentAPI.observe((event, params) => {
+ is(event, "Loop:PanelTabChanged", "Check Loop:PanelTabChanged notification");
+ is(params, "rooms", "Check the tab name param");
+
+ gContentAPI.observe((event, params) => {
+ ok(false, "No more notifications should have arrived");
+ });
+ resolve();
+ });
+ });
+ });
+
+ // Switch to the contacts tab.
+ yield window.LoopUI.openCallPanel(null, "contacts");
+
+ // Logout. The panel tab will switch back to 'rooms'.
+ MozLoopServiceInternal.fxAOAuthTokenData =
+ MozLoopServiceInternal.fxAOAuthProfile = null;
+ yield MozLoopServiceInternal.notifyStatusChanged();
+
+ yield tabChangePromise;
+ }),
function test_notifyLoopChatWindowOpenedClosed(done) {
gContentAPI.observe((event, params) => {
is(event, "Loop:ChatWindowOpened", "Check Loop:ChatWindowOpened notification");
gContentAPI.observe((event, params) => {
is(event, "Loop:ChatWindowShown", "Check Loop:ChatWindowShown notification");
gContentAPI.observe((event, params) => {
is(event, "Loop:ChatWindowClosed", "Check Loop:ChatWindowClosed notification");
gContentAPI.observe((event, params) => {