--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -280,17 +280,17 @@
accesskey="&savePageCmd.accesskey2;"
oncommand="gContextMenu.savePageAs();"/>
<menuseparator id="context-sep-sendpagetodevice" hidden="true"/>
<menu id="context-sendpagetodevice"
label="&sendPageToDevice.label;"
accesskey="&sendPageToDevice.accesskey;"
hidden="true">
<menupopup id="context-sendpagetodevice-popup"
- onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
+ onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gSync.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
</menu>
<menuseparator id="context-sep-viewbgimage"/>
<menuitem id="context-viewbgimage"
label="&viewBGImageCmd.label;"
accesskey="&viewBGImageCmd.accesskey;"
oncommand="gContextMenu.viewBGImage(event);"
onclick="checkForMiddleClick(this, event);"/>
<menuitem id="context-undo"
@@ -327,17 +327,17 @@
<menuitem id="context-searchselect"
oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
<menuseparator id="context-sep-sendlinktodevice" hidden="true"/>
<menu id="context-sendlinktodevice"
label="&sendLinkToDevice.label;"
accesskey="&sendLinkToDevice.accesskey;"
hidden="true">
<menupopup id="context-sendlinktodevice-popup"
- onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
+ onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
</menu>
<menuitem id="context-shareselect"
label="&shareSelect.label;"
accesskey="&shareSelect.accesskey;"
oncommand="gContextMenu.shareSelect();"/>
<menuseparator id="frame-sep"/>
<menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
<menupopup>
deleted file mode 100644
--- a/browser/base/content/browser-fxaccounts.js
+++ /dev/null
@@ -1,398 +0,0 @@
-/* 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/. */
-
-var gFxAccounts = {
-
- _initialized: false,
- _cachedProfile: null,
-
- get weave() {
- delete this.weave;
- return this.weave = Cc["@mozilla.org/weave/service;1"]
- .getService(Ci.nsISupports)
- .wrappedJSObject;
- },
-
- get topics() {
- // Do all this dance to lazy-load FxAccountsCommon.
- delete this.topics;
- return this.topics = [
- "weave:service:ready",
- "weave:service:login:change",
- "weave:service:setup-complete",
- "weave:service:sync:error",
- "weave:ui:login:error",
- this.FxAccountsCommon.ONLOGIN_NOTIFICATION,
- this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
- this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
- ];
- },
-
- get panelUIFooter() {
- delete this.panelUIFooter;
- return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
- },
-
- get panelUIStatus() {
- delete this.panelUIStatus;
- return this.panelUIStatus = document.getElementById("PanelUI-fxa-status");
- },
-
- get panelUIAvatar() {
- delete this.panelUIAvatar;
- return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar");
- },
-
- get panelUILabel() {
- delete this.panelUILabel;
- return this.panelUILabel = document.getElementById("PanelUI-fxa-label");
- },
-
- get panelUIIcon() {
- delete this.panelUIIcon;
- return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon");
- },
-
- get strings() {
- delete this.strings;
- return this.strings = Services.strings.createBundle(
- "chrome://browser/locale/accounts.properties"
- );
- },
-
- get loginFailed() {
- // Referencing Weave.Service will implicitly initialize sync, and we don't
- // want to force that - so first check if it is ready.
- if (!this.weaveService.ready) {
- return false;
- }
- // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
- // All other login failures are assumed to be transient and should go
- // away by themselves, so aren't reflected here.
- return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
- },
-
- get sendTabToDeviceEnabled() {
- return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
- },
-
- isSendableURI(aURISpec) {
- if (!aURISpec) {
- return false;
- }
- // Disallow sending tabs with more than 65535 characters.
- if (aURISpec.length > 65535) {
- return false;
- }
- try {
- // Filter out un-sendable URIs -- things like local files, object urls, etc.
- const unsendableRegexp = new RegExp(
- Services.prefs.getCharPref("services.sync.engine.tabs.filteredUrls"), "i");
- return !unsendableRegexp.test(aURISpec);
- } catch (e) {
- // The preference has been removed, or is an invalid regexp, so we log an
- // error and treat it as a valid URI -- and the more problematic case is
- // the length, which we've already addressed.
- Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
- return true;
- }
- },
-
- get remoteClients() {
- return Weave.Service.clientsEngine.remoteClients
- .sort((a, b) => a.name.localeCompare(b.name));
- },
-
- init() {
- // Bail out if we're already initialized and for pop-up windows.
- if (this._initialized || !window.toolbar.visible) {
- return;
- }
-
- for (let topic of this.topics) {
- Services.obs.addObserver(this, topic);
- }
-
- EnsureFxAccountsWebChannel();
- this._initialized = true;
-
- this.updateUI();
- },
-
- uninit() {
- if (!this._initialized) {
- return;
- }
-
- for (let topic of this.topics) {
- Services.obs.removeObserver(this, topic);
- }
-
- this._initialized = false;
- },
-
- observe(subject, topic, data) {
- switch (topic) {
- case this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION:
- this._cachedProfile = null;
- // Fallthrough intended
- default:
- this.updateUI();
- break;
- }
- },
-
- // Note that updateUI() returns a Promise that's only used by tests.
- updateUI() {
- this.panelUIFooter.hidden = false;
-
- let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
- let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
- let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
- // The localization string is for the signed in text, but it's the default text as well
- let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
-
- let updateWithUserData = (userData) => {
- // Window might have been closed while fetching data.
- if (window.closed) {
- return;
- }
-
- // Reset the button to its original state.
- this.panelUILabel.setAttribute("label", defaultLabel);
- this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
- this.panelUIFooter.removeAttribute("fxastatus");
- this.panelUIAvatar.style.removeProperty("list-style-image");
- let showErrorBadge = false;
- if (userData) {
- // At this point we consider the user as logged-in (but still can be in an error state)
- if (this.loginFailed) {
- let tooltipDescription = this.strings.formatStringFromName("reconnectDescription", [userData.email], 1);
- this.panelUIFooter.setAttribute("fxastatus", "error");
- this.panelUILabel.setAttribute("label", errorLabel);
- this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
- showErrorBadge = true;
- } else if (!userData.verified) {
- let tooltipDescription = this.strings.formatStringFromName("verifyDescription", [userData.email], 1);
- this.panelUIFooter.setAttribute("fxastatus", "error");
- this.panelUIFooter.setAttribute("unverified", "true");
- this.panelUILabel.setAttribute("label", unverifiedLabel);
- this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
- showErrorBadge = true;
- } else {
- this.panelUIFooter.setAttribute("fxastatus", "signedin");
- this.panelUILabel.setAttribute("label", userData.email);
- }
- }
- if (showErrorBadge) {
- PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
- } else {
- PanelUI.removeNotification("fxa-needs-authentication");
- }
- }
-
- let updateWithProfile = (profile) => {
- if (profile.displayName) {
- this.panelUILabel.setAttribute("label", profile.displayName);
- }
- if (profile.avatar) {
- let bgImage = "url(\"" + profile.avatar + "\")";
- this.panelUIAvatar.style.listStyleImage = bgImage;
-
- let img = new Image();
- img.onerror = () => {
- // Clear the image if it has trouble loading. Since this callback is asynchronous
- // we check to make sure the image is still the same before we clear it.
- if (this.panelUIAvatar.style.listStyleImage === bgImage) {
- this.panelUIAvatar.style.removeProperty("list-style-image");
- }
- };
- img.src = profile.avatar;
- }
- }
-
- return fxAccounts.getSignedInUser().then(userData => {
- // userData may be null here when the user is not signed-in, but that's expected
- updateWithUserData(userData);
- // unverified users cause us to spew log errors fetching an OAuth token
- // to fetch the profile, so don't even try in that case.
- if (!userData || !userData.verified) {
- return null; // don't even try to grab the profile.
- }
- if (this._cachedProfile) {
- return this._cachedProfile;
- }
- return fxAccounts.getSignedInUserProfile().catch(err => {
- // Not fetching the profile is sad but the FxA logs will already have noise.
- return null;
- });
- }).then(profile => {
- if (!profile) {
- return;
- }
- updateWithProfile(profile);
- this._cachedProfile = profile; // Try to avoid fetching the profile on every UI update
- }).catch(error => {
- // This is most likely in tests, were we quickly log users in and out.
- // The most likely scenario is a user logged out, so reflect that.
- // Bug 995134 calls for better errors so we could retry if we were
- // sure this was the failure reason.
- this.FxAccountsCommon.log.error("Error updating FxA account info", error);
- updateWithUserData(null);
- });
- },
-
- onMenuPanelCommand() {
-
- switch (this.panelUIFooter.getAttribute("fxastatus")) {
- case "signedin":
- this.openPreferences();
- break;
- case "error":
- if (this.panelUIFooter.getAttribute("unverified")) {
- this.openPreferences();
- } else {
- this.openSignInAgainPage("menupanel");
- }
- break;
- default:
- this.openPreferences();
- break;
- }
-
- PanelUI.hide();
- },
-
- openPreferences() {
- openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } });
- },
-
- openAccountsPage(action, urlParams = {}) {
- let params = new URLSearchParams();
- if (action) {
- params.set("action", action);
- }
- for (let name in urlParams) {
- if (urlParams[name] !== undefined) {
- params.set(name, urlParams[name]);
- }
- }
- let url = "about:accounts?" + params;
- switchToTabHavingURI(url, true, {
- replaceQueryString: true
- });
- },
-
- openSignInAgainPage(entryPoint) {
- this.openAccountsPage("reauth", { entrypoint: entryPoint });
- },
-
- async openDevicesManagementPage(entryPoint) {
- let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
- switchToTabHavingURI(url, true, {
- replaceQueryString: true
- });
- },
-
- sendTabToDevice(url, clientId, title) {
- Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
- },
-
- populateSendTabToDevicesMenu(devicesPopup, url, title) {
- // remove existing menu items
- while (devicesPopup.hasChildNodes()) {
- devicesPopup.firstChild.remove();
- }
-
- const fragment = document.createDocumentFragment();
-
- const onTargetDeviceCommand = (event) => {
- let clients = event.target.getAttribute("clientId") ?
- [event.target.getAttribute("clientId")] :
- this.remoteClients.map(client => client.id);
-
- clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
- }
-
- function addTargetDevice(clientId, name) {
- const targetDevice = document.createElement("menuitem");
- targetDevice.addEventListener("command", onTargetDeviceCommand, true);
- targetDevice.setAttribute("class", "sendtab-target");
- targetDevice.setAttribute("clientId", clientId);
- targetDevice.setAttribute("label", name);
- fragment.appendChild(targetDevice);
- }
-
- const clients = this.remoteClients;
- for (let client of clients) {
- addTargetDevice(client.id, client.name);
- }
-
- // "All devices" menu item
- if (clients.length > 1) {
- const separator = document.createElement("menuseparator");
- fragment.appendChild(separator);
- const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
- addTargetDevice("", allDevicesLabel);
- }
-
- devicesPopup.appendChild(fragment);
- },
-
- updateTabContextMenu(aPopupMenu, aTargetTab) {
- if (!this.sendTabToDeviceEnabled ||
- !this.weaveService.ready) {
- return;
- }
-
- const targetURI = aTargetTab.linkedBrowser.currentURI.spec;
- const showSendTab = this.remoteClients.length > 0 && this.isSendableURI(targetURI);
-
- ["context_sendTabToDevice", "context_sendTabToDevice_separator"]
- .forEach(id => { document.getElementById(id).hidden = !showSendTab });
- },
-
- initPageContextMenu(contextMenu) {
- if (!this.sendTabToDeviceEnabled ||
- !this.weaveService.ready) {
- return;
- }
-
- const remoteClientPresent = this.remoteClients.length > 0;
- // showSendLink and showSendPage are mutually exclusive
- let showSendLink = remoteClientPresent
- && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
- const showSendPage = !showSendLink && remoteClientPresent
- && !(contextMenu.isContentSelected ||
- contextMenu.onImage || contextMenu.onCanvas ||
- contextMenu.onVideo || contextMenu.onAudio ||
- contextMenu.onLink || contextMenu.onTextInput)
- && this.isSendableURI(contextMenu.browser.currentURI.spec);
-
- if (showSendLink) {
- // This isn't part of the condition above since we don't want to try and
- // send the page if a link is clicked on or selected but is not sendable.
- showSendLink = this.isSendableURI(contextMenu.linkURL);
- }
-
- ["context-sendpagetodevice", "context-sep-sendpagetodevice"]
- .forEach(id => contextMenu.showItem(id, showSendPage));
- ["context-sendlinktodevice", "context-sep-sendlinktodevice"]
- .forEach(id => contextMenu.showItem(id, showSendLink));
- }
-};
-
-XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function() {
- return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
-});
-
-XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
- "resource://gre/modules/FxAccountsWebChannel.jsm");
-
-
-XPCOMUtils.defineLazyGetter(gFxAccounts, "weaveService", function() {
- return Components.classes["@mozilla.org/weave/service;1"]
- .getService(Components.interfaces.nsISupports)
- .wrappedJSObject;
-});
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -470,49 +470,44 @@
<!-- Bookmarks menu items -->
</menupopup>
</menu>
<menu id="tools-menu"
label="&toolsMenu.label;"
accesskey="&toolsMenu.accesskey;"
onpopupshowing="mirrorShow(this)">
- <menupopup id="menu_ToolsPopup"
-# We have to use setTimeout() here to avoid a flickering menu bar when opening
-# the Tools menu, see bug 970769. This can be removed once we got rid of the
-# event loop spinning in Weave.Status._authManager.
- onpopupshowing="setTimeout(() => gSyncUI.updateUI());"
- >
+ <menupopup id="menu_ToolsPopup">
<menuitem id="menu_openDownloads"
label="&downloads.label;"
accesskey="&downloads.accesskey;"
key="key_openDownloads"
command="Tools:Downloads"/>
<menuitem id="menu_openAddons"
label="&addons.label;"
accesskey="&addons.accesskey;"
key="key_openAddons"
command="Tools:Addons"/>
<!-- only one of sync-setup, sync-syncnowitem or sync-reauthitem will be showing at once -->
<menuitem id="sync-setup"
label="&syncSignIn.label;"
accesskey="&syncSignIn.accesskey;"
observes="sync-setup-state"
- oncommand="gSyncUI.openPrefs('menubar')"/>
+ oncommand="gSync.openPrefs('menubar')"/>
<menuitem id="sync-syncnowitem"
label="&syncSyncNowItem.label;"
accesskey="&syncSyncNowItem.accesskey;"
observes="sync-syncnow-state"
- oncommand="gSyncUI.doSync(event);"/>
+ oncommand="gSync.doSync(event);"/>
<menuitem id="sync-reauthitem"
label="&syncReAuthItem.label;"
accesskey="&syncReAuthItem.accesskey;"
observes="sync-reauth-state"
- oncommand="gSyncUI.openSignInAgainPage('menubar');"/>
+ oncommand="gSync.openSignInAgainPage('menubar');"/>
<menuseparator id="devToolsSeparator"/>
<menu id="webDeveloperMenu"
label="&webDeveloperMenu.label;"
accesskey="&webDeveloperMenu.accesskey;">
<menupopup id="menuWebDeveloperPopup">
<menuitem id="menu_pageSource"
observes="devtoolsMenuBroadcaster_PageSource"
accesskey="&pageSourceCmd.accesskey;"/>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-sync.js
@@ -0,0 +1,574 @@
+/* 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/. */
+
+Cu.import("resource://services-sync/UIState.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
+ "resource://gre/modules/FxAccountsWebChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+ "resource://services-sync/main.js");
+if (AppConstants.MOZ_SERVICES_CLOUDSYNC) {
+ XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
+ "resource://gre/modules/CloudSync.jsm");
+}
+
+const MIN_STATUS_ANIMATION_DURATION = 1600;
+
+var gSync = {
+ _initialized: false,
+ // The last sync start time. Used to calculate the leftover animation time
+ // once syncing completes (bug 1239042).
+ _syncStartTime: 0,
+ _syncAnimationTimer: 0,
+
+ _obs: [
+ "weave:engine:sync:finish",
+ "quit-application",
+ UIState.ON_UPDATE
+ ],
+
+ get panelUIFooter() {
+ delete this.panelUIFooter;
+ return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
+ },
+
+ get panelUIStatus() {
+ delete this.panelUIStatus;
+ return this.panelUIStatus = document.getElementById("PanelUI-fxa-status");
+ },
+
+ get panelUIAvatar() {
+ delete this.panelUIAvatar;
+ return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar");
+ },
+
+ get panelUILabel() {
+ delete this.panelUILabel;
+ return this.panelUILabel = document.getElementById("PanelUI-fxa-label");
+ },
+
+ get panelUIIcon() {
+ delete this.panelUIIcon;
+ return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon");
+ },
+
+ get fxaStrings() {
+ delete this.fxaStrings;
+ return this.fxaStrings = Services.strings.createBundle(
+ "chrome://browser/locale/accounts.properties"
+ );
+ },
+
+ get syncStrings() {
+ delete this.syncStrings;
+ // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
+ // but for now just make it work
+ return this.syncStrings = Services.strings.createBundle(
+ "chrome://weave/locale/sync.properties"
+ );
+ },
+
+ get sendTabToDeviceEnabled() {
+ return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
+ },
+
+ get remoteClients() {
+ return Weave.Service.clientsEngine.remoteClients
+ .sort((a, b) => a.name.localeCompare(b.name));
+ },
+
+ init() {
+ // Bail out if we're already initialized or for pop-up windows.
+ if (this._initialized || !window.toolbar.visible) {
+ return;
+ }
+
+ for (let topic of this._obs) {
+ Services.obs.addObserver(this, topic, true);
+ }
+
+ // initial label for the sync buttons.
+ let broadcaster = document.getElementById("sync-status");
+ broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
+
+ // Update the UI
+ if (UIState.isReady()) {
+ const state = UIState.get();
+ // If we are not configured, the UI is already in the right state when
+ // we open the window. We can avoid a repaint.
+ if (state.status != UIState.STATUS_NOT_CONFIGURED) {
+ this.updateAllUI(state);
+ }
+ }
+
+ this.maybeMoveSyncedTabsButton();
+
+ EnsureFxAccountsWebChannel();
+
+ this._initialized = true;
+ },
+
+ uninit() {
+ if (!this._initialized) {
+ return;
+ }
+
+ for (let topic of this._obs) {
+ Services.obs.removeObserver(this, topic);
+ }
+
+ this._initialized = false;
+ },
+
+ observe(subject, topic, data) {
+ if (!this._initialized) {
+ Cu.reportError("browser-sync observer called after unload: " + topic);
+ return;
+ }
+ switch (topic) {
+ case UIState.ON_UPDATE:
+ const state = UIState.get();
+ this.updateAllUI(state);
+ break;
+ case "quit-application":
+ // Stop the animation timer on shutdown, since we can't update the UI
+ // after this.
+ clearTimeout(this._syncAnimationTimer);
+ break;
+ case "weave:engine:sync:finish":
+ if (data != "clients") {
+ return;
+ }
+ this.onClientsSynced();
+ break;
+ }
+ },
+
+ updateAllUI(state) {
+ this.updatePanelBadge(state);
+ this.updatePanelPopup(state);
+ this.updateStateBroadcasters(state);
+ this.updateSyncButtonsTooltip(state);
+ this.updateSyncStatus(state);
+ },
+
+ updatePanelPopup(state) {
+ let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
+ // The localization string is for the signed in text, but it's the default text as well
+ let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
+
+ const status = state.status;
+ // Reset the status bar to its original state.
+ this.panelUILabel.setAttribute("label", defaultLabel);
+ this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
+ this.panelUIFooter.removeAttribute("fxastatus");
+ this.panelUIAvatar.style.removeProperty("list-style-image");
+
+ if (status == UIState.STATUS_NOT_CONFIGURED) {
+ return;
+ }
+
+ // At this point we consider sync to be configured (but still can be in an error state).
+ if (status == UIState.STATUS_LOGIN_FAILED) {
+ let tooltipDescription = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
+ let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
+ this.panelUIFooter.setAttribute("fxastatus", "login-failed");
+ this.panelUILabel.setAttribute("label", errorLabel);
+ this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
+ return;
+ } else if (status == UIState.STATUS_NOT_VERIFIED) {
+ let tooltipDescription = this.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
+ let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
+ this.panelUIFooter.setAttribute("fxastatus", "unverified");
+ this.panelUILabel.setAttribute("label", unverifiedLabel);
+ this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
+ return;
+ }
+
+ // At this point we consider sync to be logged-in.
+ this.panelUIFooter.setAttribute("fxastatus", "signedin");
+ this.panelUILabel.setAttribute("label", state.displayName || state.email);
+
+ if (state.avatarURL) {
+ let bgImage = "url(\"" + state.avatarURL + "\")";
+ this.panelUIAvatar.style.listStyleImage = bgImage;
+
+ let img = new Image();
+ img.onerror = () => {
+ // Clear the image if it has trouble loading. Since this callback is asynchronous
+ // we check to make sure the image is still the same before we clear it.
+ if (this.panelUIAvatar.style.listStyleImage === bgImage) {
+ this.panelUIAvatar.style.removeProperty("list-style-image");
+ }
+ };
+ img.src = state.avatarURL;
+ }
+ },
+
+ updatePanelBadge(state) {
+ if (state.status == UIState.STATUS_LOGIN_FAILED ||
+ state.status == UIState.STATUS_NOT_VERIFIED) {
+ PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+ } else {
+ PanelUI.removeNotification("fxa-needs-authentication");
+ }
+ },
+
+ updateStateBroadcasters(state) {
+ const status = state.status;
+
+ // Start off with a clean slate
+ document.getElementById("sync-reauth-state").hidden = true;
+ document.getElementById("sync-setup-state").hidden = true;
+ document.getElementById("sync-syncnow-state").hidden = true;
+
+ if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
+ document.getElementById("sync-syncnow-state").hidden = false;
+ } else if (status == UIState.STATUS_LOGIN_FAILED) {
+ // unhiding this element makes the menubar show the login failure state.
+ document.getElementById("sync-reauth-state").hidden = false;
+ } else if (status == UIState.STATUS_NOT_CONFIGURED ||
+ status == UIState.STATUS_NOT_VERIFIED) {
+ document.getElementById("sync-setup-state").hidden = false;
+ } else {
+ document.getElementById("sync-syncnow-state").hidden = false;
+ }
+ },
+
+ updateSyncStatus(state) {
+ const broadcaster = document.getElementById("sync-status");
+ const syncingUI = broadcaster.getAttribute("syncstatus") == "active";
+ if (state.syncing != syncingUI) { // Do we need to update the UI?
+ state.syncing ? this.onActivityStart() : this.onActivityStop();
+ }
+ },
+
+ onMenuPanelCommand() {
+ switch (this.panelUIFooter.getAttribute("fxastatus")) {
+ case "signedin":
+ this.openPrefs("menupanel");
+ break;
+ case "error":
+ if (this.panelUIFooter.getAttribute("fxastatus") == "unverified") {
+ this.openPrefs("menupanel");
+ } else {
+ this.openSignInAgainPage("menupanel");
+ }
+ break;
+ default:
+ this.openPrefs("menupanel");
+ break;
+ }
+
+ PanelUI.hide();
+ },
+
+ openAccountsPage(action, urlParams = {}) {
+ let params = new URLSearchParams();
+ if (action) {
+ params.set("action", action);
+ }
+ for (let name in urlParams) {
+ if (urlParams[name] !== undefined) {
+ params.set(name, urlParams[name]);
+ }
+ }
+ let url = "about:accounts?" + params;
+ switchToTabHavingURI(url, true, {
+ replaceQueryString: true
+ });
+ },
+
+ openSignInAgainPage(entryPoint) {
+ this.openAccountsPage("reauth", { entrypoint: entryPoint });
+ },
+
+ async openDevicesManagementPage(entryPoint) {
+ let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
+ switchToTabHavingURI(url, true, {
+ replaceQueryString: true
+ });
+ },
+
+ sendTabToDevice(url, clientId, title) {
+ Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
+ },
+
+ populateSendTabToDevicesMenu(devicesPopup, url, title) {
+ // remove existing menu items
+ while (devicesPopup.hasChildNodes()) {
+ devicesPopup.firstChild.remove();
+ }
+
+ const fragment = document.createDocumentFragment();
+
+ const onTargetDeviceCommand = (event) => {
+ let clients = event.target.getAttribute("clientId") ?
+ [event.target.getAttribute("clientId")] :
+ this.remoteClients.map(client => client.id);
+
+ clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
+ }
+
+ function addTargetDevice(clientId, name) {
+ const targetDevice = document.createElement("menuitem");
+ targetDevice.addEventListener("command", onTargetDeviceCommand, true);
+ targetDevice.setAttribute("class", "sendtab-target");
+ targetDevice.setAttribute("clientId", clientId);
+ targetDevice.setAttribute("label", name);
+ fragment.appendChild(targetDevice);
+ }
+
+ const clients = this.remoteClients;
+ for (let client of clients) {
+ addTargetDevice(client.id, client.name);
+ }
+
+ // "All devices" menu item
+ if (clients.length > 1) {
+ const separator = document.createElement("menuseparator");
+ fragment.appendChild(separator);
+ const allDevicesLabel = this.fxaStrings.GetStringFromName("sendTabToAllDevices.menuitem");
+ addTargetDevice("", allDevicesLabel);
+ }
+
+ devicesPopup.appendChild(fragment);
+ },
+
+ isSendableURI(aURISpec) {
+ if (!aURISpec) {
+ return false;
+ }
+ // Disallow sending tabs with more than 65535 characters.
+ if (aURISpec.length > 65535) {
+ return false;
+ }
+ try {
+ // Filter out un-sendable URIs -- things like local files, object urls, etc.
+ const unsendableRegexp = new RegExp(
+ Services.prefs.getCharPref("services.sync.engine.tabs.filteredUrls"), "i");
+ return !unsendableRegexp.test(aURISpec);
+ } catch (e) {
+ // The preference has been removed, or is an invalid regexp, so we log an
+ // error and treat it as a valid URI -- and the more problematic case is
+ // the length, which we've already addressed.
+ Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
+ return true;
+ }
+ },
+
+ updateTabContextMenu(aPopupMenu, aTargetTab) {
+ if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
+ return;
+ }
+
+ const targetURI = aTargetTab.linkedBrowser.currentURI.spec;
+ const showSendTab = this.remoteClients.length > 0 && this.isSendableURI(targetURI);
+
+ ["context_sendTabToDevice", "context_sendTabToDevice_separator"]
+ .forEach(id => { document.getElementById(id).hidden = !showSendTab });
+ },
+
+ initPageContextMenu(contextMenu) {
+ if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
+ return;
+ }
+
+ const remoteClientPresent = this.remoteClients.length > 0;
+ // showSendLink and showSendPage are mutually exclusive
+ let showSendLink = remoteClientPresent
+ && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
+ const showSendPage = !showSendLink && remoteClientPresent
+ && !(contextMenu.isContentSelected ||
+ contextMenu.onImage || contextMenu.onCanvas ||
+ contextMenu.onVideo || contextMenu.onAudio ||
+ contextMenu.onLink || contextMenu.onTextInput)
+ && this.isSendableURI(contextMenu.browser.currentURI.spec);
+
+ if (showSendLink) {
+ // This isn't part of the condition above since we don't want to try and
+ // send the page if a link is clicked on or selected but is not sendable.
+ showSendLink = this.isSendableURI(contextMenu.linkURL);
+ }
+
+ ["context-sendpagetodevice", "context-sep-sendpagetodevice"]
+ .forEach(id => contextMenu.showItem(id, showSendPage));
+ ["context-sendlinktodevice", "context-sep-sendlinktodevice"]
+ .forEach(id => contextMenu.showItem(id, showSendLink));
+ },
+
+ // Functions called by observers
+ onActivityStart() {
+ clearTimeout(this._syncAnimationTimer);
+ this._syncStartTime = Date.now();
+
+ let broadcaster = document.getElementById("sync-status");
+ broadcaster.setAttribute("syncstatus", "active");
+ broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncing2.label"));
+ broadcaster.setAttribute("disabled", "true");
+ },
+
+ _onActivityStop() {
+ if (!gBrowser)
+ return;
+ let broadcaster = document.getElementById("sync-status");
+ broadcaster.removeAttribute("syncstatus");
+ broadcaster.removeAttribute("disabled");
+ broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
+ Services.obs.notifyObservers(null, "test:browser-sync:activity-stop");
+ },
+
+ onActivityStop() {
+ let now = Date.now();
+ let syncDuration = now - this._syncStartTime;
+
+ if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
+ let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
+ clearTimeout(this._syncAnimationTimer);
+ this._syncAnimationTimer = setTimeout(() => this._onActivityStop(), animationTime);
+ } else {
+ this._onActivityStop();
+ }
+ },
+
+ // doSync forces a sync - it *does not* return a promise as it is called
+ // via the various UI components.
+ doSync() {
+ if (!UIState.isReady()) {
+ return;
+ }
+ const state = UIState.get();
+ if (state.status == UIState.STATUS_SIGNED_IN) {
+ setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
+ }
+ Services.obs.notifyObservers(null, "cloudsync:user-sync");
+ },
+
+ openPrefs(entryPoint = "syncbutton") {
+ window.openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } });
+ },
+
+ openSyncedTabsPanel() {
+ let placement = CustomizableUI.getPlacementOfWidget("sync-button");
+ let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR;
+ let anchor = document.getElementById("sync-button") ||
+ document.getElementById("PanelUI-menu-button");
+ if (area == CustomizableUI.AREA_PANEL) {
+ // The button is in the panel, so we need to show the panel UI, then our
+ // subview.
+ PanelUI.show().then(() => {
+ PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
+ }).catch(Cu.reportError);
+ } else {
+ // It is placed somewhere else - just try and show it.
+ PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
+ }
+ },
+
+ /* After we are initialized we perform a once-only check for the sync
+ button being in "customize purgatory" and if so, move it to the panel.
+ This is done primarily for profiles created before SyncedTabs landed,
+ where the button defaulted to being in that purgatory.
+ We use a preference to ensure we only do it once, so people can still
+ customize it away and have it stick.
+ */
+ maybeMoveSyncedTabsButton() {
+ const prefName = "browser.migrated-sync-button";
+ let migrated = Services.prefs.getBoolPref(prefName, false);
+ if (migrated) {
+ return;
+ }
+ if (!CustomizableUI.getPlacementOfWidget("sync-button")) {
+ CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+ }
+ Services.prefs.setBoolPref(prefName, true);
+ },
+
+ /* Update the tooltip for the sync-status broadcaster (which will update the
+ Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
+ If Sync is configured, the tooltip is when the last sync occurred,
+ otherwise the tooltip reflects the fact that Sync needs to be
+ (re-)configured.
+ */
+ updateSyncButtonsTooltip(state) {
+ const status = state.status;
+
+ // This is a little messy as the Sync buttons are 1/2 Sync related and
+ // 1/2 FxA related - so for some strings we use Sync strings, but for
+ // others we reach into gSync for strings.
+ let tooltiptext;
+ if (status == UIState.STATUS_NOT_VERIFIED) {
+ // "needs verification"
+ tooltiptext = this.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
+ } else if (status == UIState.STATUS_NOT_CONFIGURED) {
+ // "needs setup".
+ tooltiptext = this.syncStrings.GetStringFromName("signInToSync.description");
+ } else if (status == UIState.STATUS_LOGIN_FAILED) {
+ // "need to reconnect/re-enter your password"
+ tooltiptext = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
+ } else {
+ // Sync appears configured - format the "last synced at" time.
+ tooltiptext = this.formatLastSyncDate(state.lastSync);
+ }
+
+ let broadcaster = document.getElementById("sync-status");
+ if (broadcaster) {
+ if (tooltiptext) {
+ broadcaster.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ broadcaster.removeAttribute("tooltiptext");
+ }
+ }
+ },
+
+ get withinLastWeekFormat() {
+ delete this.withinLastWeekFormat;
+ return this.withinLastWeekFormat = new Intl.DateTimeFormat(undefined,
+ {weekday: "long", hour: "numeric", minute: "numeric"});
+ },
+
+ get oneWeekOrOlderFormat() {
+ delete this.oneWeekOrOlderFormat;
+ return this.oneWeekOrOlderFormat = new Intl.DateTimeFormat(undefined,
+ {month: "long", day: "numeric"});
+ },
+
+ formatLastSyncDate(date) {
+ let sixDaysAgo = (() => {
+ let tempDate = new Date();
+ tempDate.setDate(tempDate.getDate() - 6);
+ tempDate.setHours(0, 0, 0, 0);
+ return tempDate;
+ })();
+
+ // It may be confusing for the user to see "Last Sync: Monday" when the last
+ // sync was indeed a Monday, but 3 weeks ago.
+ let dateFormat = date < sixDaysAgo ? this.oneWeekOrOlderFormat : this.withinLastWeekFormat;
+
+ let lastSyncDateString = dateFormat.format(date);
+ return this.syncStrings.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
+ },
+
+ onClientsSynced() {
+ let broadcaster = document.getElementById("sync-syncnow-state");
+ if (broadcaster) {
+ if (Weave.Service.clientsEngine.stats.numClients > 1) {
+ broadcaster.setAttribute("devices-status", "multi");
+ } else {
+ broadcaster.setAttribute("devices-status", "single");
+ }
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ])
+};
+
+XPCOMUtils.defineLazyGetter(gSync, "weaveService", function() {
+ return Components.classes["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+});
deleted file mode 100644
--- a/browser/base/content/browser-syncui.js
+++ /dev/null
@@ -1,498 +0,0 @@
-/* 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/. */
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-if (AppConstants.MOZ_SERVICES_CLOUDSYNC) {
- XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
- "resource://gre/modules/CloudSync.jsm");
-}
-
-XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
- "resource://gre/modules/FxAccounts.jsm");
-
-const MIN_STATUS_ANIMATION_DURATION = 1600;
-
-// gSyncUI handles updating the tools menu and displaying notifications.
-var gSyncUI = {
- _obs: ["weave:service:sync:start",
- "weave:service:sync:finish",
- "weave:service:sync:error",
- "weave:service:setup-complete",
- "weave:service:login:start",
- "weave:service:login:finish",
- "weave:service:login:error",
- "weave:service:logout:finish",
- "weave:service:start-over",
- "weave:service:start-over:finish",
- "weave:ui:login:error",
- "weave:ui:sync:error",
- "weave:ui:sync:finish",
- "weave:ui:clear-error",
- "weave:engine:sync:finish"
- ],
-
- _unloaded: false,
- // The last sync start time. Used to calculate the leftover animation time
- // once syncing completes (bug 1239042).
- _syncStartTime: 0,
- _syncAnimationTimer: 0,
- _withinLastWeekFormat: null,
- _oneWeekOrOlderFormat: null,
-
- init() {
- // Proceed to set up the UI if Sync has already started up.
- // Otherwise we'll do it when Sync is firing up.
- if (this.weaveService.ready) {
- this.initUI();
- return;
- }
-
- // Sync isn't ready yet, but we can still update the UI with an initial
- // state - we haven't called initUI() yet, but that's OK - that's more
- // about observers for state changes, and will be called once Sync is
- // ready to start sending notifications.
- this.updateUI();
-
- Services.obs.addObserver(this, "weave:service:ready", true);
- Services.obs.addObserver(this, "quit-application", true);
-
- // Remove the observer if the window is closed before the observer
- // was triggered.
- window.addEventListener("unload", () => {
- this._unloaded = true;
- Services.obs.removeObserver(this, "weave:service:ready");
- Services.obs.removeObserver(this, "quit-application");
-
- if (this.weaveService.ready) {
- this._obs.forEach(topic => {
- Services.obs.removeObserver(this, topic);
- });
- }
- }, { once: true });
- },
-
- initUI: function SUI_initUI() {
- // If this is a browser window?
- if (gBrowser) {
- this._obs.push("weave:notification:added");
- }
-
- this._obs.forEach(function(topic) {
- Services.obs.addObserver(this, topic, true);
- }, this);
-
- // initial label for the sync buttons.
- let broadcaster = document.getElementById("sync-status");
- broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
-
- this.maybeMoveSyncedTabsButton();
-
- this.updateUI();
- },
-
-
- // Returns a promise that resolves with true if Sync needs to be configured,
- // false otherwise.
- _needsSetup() {
- return fxAccounts.getSignedInUser().then(user => {
- // We want to treat "account needs verification" as "needs setup".
- return !(user && user.verified);
- });
- },
-
- // Returns a promise that resolves with true if the user currently signed in
- // to Sync needs to be verified, false otherwise.
- _needsVerification() {
- return fxAccounts.getSignedInUser().then(user => {
- // If there is no user, they can't be in a "needs verification" state.
- if (!user) {
- return false;
- }
- return !user.verified;
- });
- },
-
- // Note that we don't show login errors in a notification bar here, but do
- // still need to track a login-failed state so the "Tools" menu updates
- // with the correct state.
- loginFailed() {
- // If Sync isn't already ready, we don't want to force it to initialize
- // by referencing Weave.Status - and it isn't going to be accurate before
- // Sync is ready anyway.
- if (!this.weaveService.ready) {
- this.log.debug("loginFailed has sync not ready, so returning false");
- return false;
- }
- this.log.debug("loginFailed has sync state=${sync}",
- { sync: Weave.Status.login});
- return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
- },
-
- // Kick off an update of the UI - does *not* return a promise.
- updateUI() {
- this._promiseUpdateUI().catch(err => {
- this.log.error("updateUI failed", err);
- })
- },
-
- // Updates the UI - returns a promise.
- _promiseUpdateUI() {
- return this._needsSetup().then(needsSetup => {
- if (!gBrowser)
- return Promise.resolve();
-
- let loginFailed = this.loginFailed();
-
- // Start off with a clean slate
- document.getElementById("sync-reauth-state").hidden = true;
- document.getElementById("sync-setup-state").hidden = true;
- document.getElementById("sync-syncnow-state").hidden = true;
-
- if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
- document.getElementById("sync-syncnow-state").hidden = false;
- } else if (loginFailed) {
- // unhiding this element makes the menubar show the login failure state.
- document.getElementById("sync-reauth-state").hidden = false;
- } else if (needsSetup) {
- document.getElementById("sync-setup-state").hidden = false;
- } else {
- document.getElementById("sync-syncnow-state").hidden = false;
- }
-
- return this._updateSyncButtonsTooltip();
- });
- },
-
- // Functions called by observers
- onActivityStart() {
- if (!gBrowser)
- return;
-
- this.log.debug("onActivityStart");
-
- clearTimeout(this._syncAnimationTimer);
- this._syncStartTime = Date.now();
-
- let broadcaster = document.getElementById("sync-status");
- broadcaster.setAttribute("syncstatus", "active");
- broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncing2.label"));
- broadcaster.setAttribute("disabled", "true");
-
- this.updateUI();
- },
-
- _updateSyncStatus() {
- if (!gBrowser)
- return;
- let broadcaster = document.getElementById("sync-status");
- broadcaster.removeAttribute("syncstatus");
- broadcaster.removeAttribute("disabled");
- broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
- this.updateUI();
- },
-
- onActivityStop() {
- if (!gBrowser)
- return;
- this.log.debug("onActivityStop");
-
- let now = Date.now();
- let syncDuration = now - this._syncStartTime;
-
- if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
- let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
- clearTimeout(this._syncAnimationTimer);
- this._syncAnimationTimer = setTimeout(() => this._updateSyncStatus(), animationTime);
- } else {
- this._updateSyncStatus();
- }
- },
-
- onLoginError: function SUI_onLoginError() {
- this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status);
-
- // We don't show any login errors here; browser-fxaccounts shows them in
- // the hamburger menu.
- this.updateUI();
- },
-
- onLogout: function SUI_onLogout() {
- this.updateUI();
- },
-
- _getAppName() {
- let brand = Services.strings.createBundle(
- "chrome://branding/locale/brand.properties");
- return brand.GetStringFromName("brandShortName");
- },
-
- // Commands
- // doSync forces a sync - it *does not* return a promise as it is called
- // via the various UI components.
- doSync() {
- this._needsSetup().then(needsSetup => {
- if (!needsSetup) {
- setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
- }
- Services.obs.notifyObservers(null, "cloudsync:user-sync");
- }).catch(err => {
- this.log.error("Failed to force a sync", err);
- });
- },
-
- // Handle clicking the toolbar button - which either opens the Sync setup
- // pages or forces a sync now. Does *not* return a promise as it is called
- // via the UI.
- handleToolbarButton() {
- this._needsSetup().then(needsSetup => {
- if (needsSetup || this.loginFailed()) {
- this.openPrefs();
- } else {
- this.doSync();
- }
- }).catch(err => {
- this.log.error("Failed to handle toolbar button command", err);
- });
- },
-
- /**
- * Open the Sync preferences.
- *
- * @param entryPoint
- * Indicates the entrypoint from where this method was called.
- */
- openPrefs(entryPoint = "syncbutton") {
- openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } });
- },
-
- openSignInAgainPage(entryPoint = "syncbutton") {
- gFxAccounts.openSignInAgainPage(entryPoint);
- },
-
- openSyncedTabsPanel() {
- let placement = CustomizableUI.getPlacementOfWidget("sync-button");
- let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR;
- let anchor = document.getElementById("sync-button") ||
- document.getElementById("PanelUI-menu-button");
- if (area == CustomizableUI.AREA_PANEL) {
- // The button is in the panel, so we need to show the panel UI, then our
- // subview.
- PanelUI.show().then(() => {
- PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
- }).catch(Cu.reportError);
- } else {
- // It is placed somewhere else - just try and show it.
- PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
- }
- },
-
- /* After Sync is initialized we perform a once-only check for the sync
- button being in "customize purgatory" and if so, move it to the panel.
- This is done primarily for profiles created before SyncedTabs landed,
- where the button defaulted to being in that purgatory.
- We use a preference to ensure we only do it once, so people can still
- customize it away and have it stick.
- */
- maybeMoveSyncedTabsButton() {
- const prefName = "browser.migrated-sync-button";
- let migrated = Services.prefs.getBoolPref(prefName, false);
- if (migrated) {
- return;
- }
- if (!CustomizableUI.getPlacementOfWidget("sync-button")) {
- CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
- }
- Services.prefs.setBoolPref(prefName, true);
- },
-
- /* Update the tooltip for the sync-status broadcaster (which will update the
- Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
- If Sync is configured, the tooltip is when the last sync occurred,
- otherwise the tooltip reflects the fact that Sync needs to be
- (re-)configured.
- */
- _updateSyncButtonsTooltip: Task.async(function* () {
- if (!gBrowser)
- return;
-
- let email;
- let user = yield fxAccounts.getSignedInUser();
- if (user) {
- email = user.email;
- }
-
- let needsSetup = yield this._needsSetup();
- let needsVerification = yield this._needsVerification();
- let loginFailed = this.loginFailed();
- // This is a little messy as the Sync buttons are 1/2 Sync related and
- // 1/2 FxA related - so for some strings we use Sync strings, but for
- // others we reach into gFxAccounts for strings.
- let tooltiptext;
- if (needsVerification) {
- // "needs verification"
- tooltiptext = gFxAccounts.strings.formatStringFromName("verifyDescription", [email], 1);
- } else if (needsSetup) {
- // "needs setup".
- tooltiptext = this._stringBundle.GetStringFromName("signInToSync.description");
- } else if (loginFailed) {
- // "need to reconnect/re-enter your password"
- tooltiptext = gFxAccounts.strings.formatStringFromName("reconnectDescription", [email], 1);
- } else {
- // Sync appears configured - format the "last synced at" time.
- try {
- let lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync"));
- tooltiptext = this.formatLastSyncDate(lastSync);
- } catch (e) {
- // pref doesn't exist (which will be the case until we've seen the
- // first successful sync) or is invalid (which should be impossible!)
- // Just leave tooltiptext as the empty string in these cases, which
- // will cause the tooltip to be removed below.
- }
- }
-
- // We've done all our promise-y work and ready to update the UI - make
- // sure it hasn't been torn down since we started.
- if (!gBrowser)
- return;
-
- let broadcaster = document.getElementById("sync-status");
- if (broadcaster) {
- if (tooltiptext) {
- broadcaster.setAttribute("tooltiptext", tooltiptext);
- } else {
- broadcaster.removeAttribute("tooltiptext");
- }
- }
- }),
-
- getWithinLastWeekFormat() {
- return this._withinLastWeekFormat ||
- (this._withinLastWeekFormat =
- new Intl.DateTimeFormat(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}));
- },
-
- getOneWeekOrOlderFormat() {
- return this._oneWeekOrOlderFormat ||
- (this._oneWeekOrOlderFormat =
- new Intl.DateTimeFormat(undefined, {month: "long", day: "numeric"}));
- },
-
- formatLastSyncDate(date) {
- let sixDaysAgo = (() => {
- let tempDate = new Date();
- tempDate.setDate(tempDate.getDate() - 6);
- tempDate.setHours(0, 0, 0, 0);
- return tempDate;
- })();
-
- // It may be confusing for the user to see "Last Sync: Monday" when the last
- // sync was indeed a Monday, but 3 weeks ago.
- let dateFormat = date < sixDaysAgo ? this.getOneWeekOrOlderFormat() : this.getWithinLastWeekFormat();
-
- let lastSyncDateString = dateFormat.format(date);
- return this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
- },
-
- onClientsSynced() {
- let broadcaster = document.getElementById("sync-syncnow-state");
- if (broadcaster) {
- if (Weave.Service.clientsEngine.stats.numClients > 1) {
- broadcaster.setAttribute("devices-status", "multi");
- } else {
- broadcaster.setAttribute("devices-status", "single");
- }
- }
- },
-
- observe: function SUI_observe(subject, topic, data) {
- this.log.debug("observed", topic);
- if (this._unloaded) {
- Cu.reportError("SyncUI observer called after unload: " + topic);
- return;
- }
-
- // Unwrap, just like Svc.Obs, but without pulling in that dependency.
- if (subject && typeof subject == "object" &&
- ("wrappedJSObject" in subject) &&
- ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) {
- subject = subject.wrappedJSObject.object;
- }
-
- // First handle "activity" only.
- switch (topic) {
- case "weave:service:sync:start":
- this.onActivityStart();
- break;
- case "weave:service:sync:finish":
- case "weave:service:sync:error":
- this.onActivityStop();
- break;
- }
- // Now non-activity state (eg, enabled, errors, etc)
- // Note that sync uses the ":ui:" notifications for errors because sync.
- switch (topic) {
- case "weave:ui:sync:finish":
- // Do nothing.
- break;
- case "weave:ui:sync:error":
- case "weave:service:setup-complete":
- case "weave:service:login:finish":
- case "weave:service:login:start":
- case "weave:service:start-over":
- this.updateUI();
- break;
- case "weave:ui:login:error":
- case "weave:service:login:error":
- this.onLoginError();
- break;
- case "weave:service:logout:finish":
- this.onLogout();
- break;
- case "weave:service:start-over:finish":
- this.updateUI();
- break;
- case "weave:service:ready":
- this.initUI();
- break;
- case "weave:notification:added":
- this.initNotifications();
- break;
- case "weave:engine:sync:finish":
- if (data != "clients") {
- return;
- }
- this.onClientsSynced();
- break;
- case "quit-application":
- // Stop the animation timer on shutdown, since we can't update the UI
- // after this.
- clearTimeout(this._syncAnimationTimer);
- break;
- }
- },
-
- QueryInterface: XPCOMUtils.generateQI([
- Ci.nsIObserver,
- Ci.nsISupportsWeakReference
- ])
-};
-
-XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() {
- // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
- // but for now just make it work
- return Services.strings.createBundle(
- "chrome://weave/locale/sync.properties");
-});
-
-XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() {
- return Log.repository.getLogger("browserwindow.syncui");
-});
-
-XPCOMUtils.defineLazyGetter(gSyncUI, "weaveService", function() {
- return Components.classes["@mozilla.org/weave/service;1"]
- .getService(Components.interfaces.nsISupports)
- .wrappedJSObject;
-});
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1498,18 +1498,17 @@ var gBrowserInit = {
if (Win7Features)
Win7Features.onOpenWindow();
FullScreen.init();
PointerLock.init();
// initialize the sync UI
- gSyncUI.init();
- gFxAccounts.init();
+ gSync.init();
if (AppConstants.MOZ_DATA_REPORTING)
gDataNotificationInfoBar.init();
gBrowserThumbnails.init();
gMenuButtonUpdateBadge.init();
@@ -1646,17 +1645,17 @@ var gBrowserInit = {
CombinedStopReload.uninit();
gGestureSupport.init(false);
gHistorySwipeAnimation.uninit();
FullScreen.uninit();
- gFxAccounts.uninit();
+ gSync.uninit();
gExtensionsNotifications.uninit();
Services.obs.removeObserver(gPluginHandler.NPAPIPluginCrashed, "plugin-crashed");
try {
gBrowser.removeProgressListener(window.XULBrowserWindow);
gBrowser.removeTabsProgressListener(window.TabsProgressListener);
@@ -1809,17 +1808,17 @@ if (AppConstants.platform == "macosx") {
// initialise the offline listener
BrowserOffline.init();
// initialize the private browsing UI
gPrivateBrowsingUI.init();
// initialize the sync UI
- gSyncUI.init();
+ gSync.init();
if (AppConstants.E10S_TESTING_ONLY) {
gRemoteTabsUI.init();
}
};
gBrowserInit.nonBrowserWindowShutdown = function() {
let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"]
@@ -6794,17 +6793,17 @@ function checkEmptyPageOrigin(browser =
}
// ... so for those that don't have them, enforce that the page has the
// system principal (this matches e.g. on about:newtab).
let ssm = Services.scriptSecurityManager;
return ssm.isSystemPrincipal(contentPrincipal);
}
function BrowserOpenSyncTabs() {
- gSyncUI.openSyncedTabsPanel();
+ gSync.openSyncedTabsPanel();
}
function ReportFalseDeceptiveSite() {
let docURI = gBrowser.selectedBrowser.documentURI;
let isPhishingPage =
docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked");
if (isPhishingPage) {
@@ -8168,17 +8167,17 @@ var TabContextMenu = {
}
this.contextTab.toggleMuteMenuItem = toggleMute;
this._updateToggleMuteMenuItem(this.contextTab);
this.contextTab.addEventListener("TabAttrModified", this);
aPopupMenu.addEventListener("popuphiding", this);
- gFxAccounts.updateTabContextMenu(aPopupMenu, this.contextTab);
+ gSync.updateTabContextMenu(aPopupMenu, this.contextTab);
},
handleEvent(aEvent) {
switch (aEvent.type) {
case "popuphiding":
gBrowser.removeEventListener("TabAttrModified", this);
aEvent.target.removeEventListener("popuphiding", this);
break;
case "TabAttrModified":
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -101,17 +101,17 @@
tbattr="tabbrowser-remote"
hidden="true"
oncommand="gBrowser.openNonRemoteWindow(TabContextMenu.contextTab);"/>
#endif
<menuseparator id="context_sendTabToDevice_separator" hidden="true"/>
<menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
accesskey="&sendTabToDevice.accesskey;" hidden="true">
<menupopup id="context_sendTabToDevicePopupMenu"
- onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
+ onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
</menu>
<menuseparator/>
<menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
tbattr="tabbrowser-multiple-visible"
oncommand="gBrowser.reloadAllTabs();"/>
<menuitem id="context_bookmarkAllTabs"
label="&bookmarkAllTabs.label;"
accesskey="&bookmarkAllTabs.accesskey;"
@@ -487,17 +487,17 @@
id="syncedTabsCopySelected"/>
<menuseparator/>
<menuitem label="&syncedTabs.context.openAllInTabs.label;"
accesskey="&syncedTabs.context.openAllInTabs.accesskey;"
id="syncedTabsOpenAllInTabs"/>
<menuitem label="&syncedTabs.context.managedevices.label;"
accesskey="&syncedTabs.context.managedevices.accesskey;"
id="syncedTabsManageDevices"
- oncommand="gFxAccounts.openDevicesManagementPage('syncedtabs-sidebar');"/>
+ oncommand="gSync.openDevicesManagementPage('syncedtabs-sidebar');"/>
<menuitem label="&syncSyncNowItem.label;"
accesskey="&syncSyncNowItem.accesskey;"
id="syncedTabsRefresh"/>
</menupopup>
<menupopup id="SyncedTabsSidebarTabsFilterContext"
class="textbox-contextmenu">
<menuitem label="&undoCmd.label;"
accesskey="&undoCmd.accesskey;"
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -25,18 +25,16 @@
<script type="application/javascript" src="chrome://browser/content/browser-gestureSupport.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-media.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-plugins.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-refreshblocker.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-safebrowsing.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-sidebar.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
-<script type="application/javascript" src="chrome://browser/content/browser-syncui.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-tabsintitlebar.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-thumbnails.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-trackingprotection.js"/>
#ifdef MOZ_DATA_REPORTING
<script type="application/javascript" src="chrome://browser/content/browser-data-submission-info-bar.js"/>
#endif
-
-<script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
--- a/browser/base/content/moz.build
+++ b/browser/base/content/moz.build
@@ -116,19 +116,16 @@ with Files("browser-customization.js"):
BUG_COMPONENT = ("Firefox", "Toolbars and Customization")
with Files("browser-feeds.js"):
BUG_COMPONENT = ("Firefox", "Toolbars and Customization")
with Files("browser-fullZoom.js"):
BUG_COMPONENT = ("Firefox", "Tabbed Browsing")
-with Files("browser-fxaccounts.js"):
- BUG_COMPONENT = ("Core", "FxAccounts")
-
with Files("browser-gestureSupport.js"):
BUG_COMPONENT = ("Core", "Widget: Cocoa")
with Files("browser-media.js"):
BUG_COMPONENT = ("Core", "Audio/Video: Playback")
with Files("browser-places.js"):
BUG_COMPONENT = ("Firefox", "Bookmarks & History")
@@ -140,17 +137,17 @@ with Files("browser-refreshblocker.js"):
BUG_COMPONENT = ("Firefox", "Disability Access")
with Files("browser-safebrowsing.js"):
BUG_COMPONENT = ("Toolkit", "Safe Browsing")
with Files("*social*"):
BUG_COMPONENT = ("Firefox", "SocialAPI")
-with Files("browser-syncui.js"):
+with Files("browser-sync.js"):
BUG_COMPONENT = ("Firefox", "Sync")
with Files("browser-tabPreviews.xml"):
BUG_COMPONENT = ("Firefox", "Tabbed Browser")
with Files("contentSearch*"):
BUG_COMPONENT = ("Firefox", "Search")
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -596,17 +596,17 @@ nsContextMenu.prototype = {
return;
}
let popup = document.getElementById("fill-login-popup");
let insertBeforeElement = document.getElementById("fill-login-no-logins");
popup.insertBefore(fragment, insertBeforeElement);
},
initSyncItems() {
- gFxAccounts.initPageContextMenu(this);
+ gSync.initPageContextMenu(this);
},
openPasswordManager() {
LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
},
inspectNode() {
let gBrowser = this.browser.ownerGlobal.gBrowser;
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -3,27 +3,25 @@
# TRY ONE OF THE MORE TOPICAL SIBLING DIRECTORIES. #
# THIS DIRECTORY HAS 200+ TESTS AND TAKES AGES TO RUN ON A DEBUG BUILD. #
# PLEASE, FOR THE LOVE OF WHATEVER YOU HOLD DEAR, DO NOT ADD MORE TESTS HERE. #
###############################################################################
[DEFAULT]
support-files =
POSTSearchEngine.xml
- accounts_testRemoteCommands.html
alltabslistener.html
app_bug575561.html
app_subframe_bug575561.html
aboutHome_content_script.js
audio.ogg
browser_bug479408_sample.html
browser_bug678392-1.html
browser_bug678392-2.html
browser_bug970746.xhtml
- browser_fxa_web_channel.html
browser_registerProtocolHandler_notification.html
browser_star_hsts.sjs
browser_tab_dragdrop2_frame1.xul
browser_web_channel.html
browser_web_channel_iframe.html
bug592338.html
bug792517-2.html
bug792517.html
@@ -88,21 +86,16 @@ support-files =
!/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
!/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs
!/toolkit/mozapps/extensions/test/xpinstall/restartless-unsigned.xpi
!/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
!/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
!/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_aboutAccounts.js]
-skip-if = os == "linux" # Bug 958026
-support-files =
- content_aboutAccounts.js
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_aboutCertError.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_aboutNetError.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_aboutSupport.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_aboutSupport_newtab_security_state.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
@@ -370,21 +363,16 @@ skip-if = true # browser_drag.js is disa
[browser_findbarClose.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_focusonkeydown.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_fullscreen-window-open.js]
tags = fullscreen
skip-if = os == "linux" # Linux: Intermittent failures - bug 941575.
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_fxaccounts.js]
-support-files = fxa_profile_handler.sjs
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_fxa_web_channel.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_gestureSupport.js]
skip-if = e10s # Bug 863514 - no gesture support.
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_getshortcutoruri.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_hide_removing.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_homeDrop.js]
@@ -491,19 +479,16 @@ support-files =
[browser_ssl_error_reports.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_star_hsts.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_storagePressure_notification.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_subframe_favicons_not_used.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_syncui.js]
-skip-if = os == 'linux' # Bug 1304272
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_tab_close_dependent_window.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_tabDrop.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_tabReorder.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_tab_detach_restore.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
deleted file mode 100644
--- a/browser/base/content/test/general/browser_aboutAccounts.js
+++ /dev/null
@@ -1,487 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-//
-// Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed.
-//
-thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: window.location is null");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
- "resource://gre/modules/Promise.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
- "resource://gre/modules/Task.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
- "resource://gre/modules/FxAccounts.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
- "resource://gre/modules/FileUtils.jsm");
-
-const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
-// Preference helpers.
-var changedPrefs = new Set();
-
-function setPref(name, value) {
- changedPrefs.add(name);
- Services.prefs.setCharPref(name, value);
-}
-
-registerCleanupFunction(function() {
- // Ensure we don't pollute prefs for next tests.
- for (let name of changedPrefs) {
- Services.prefs.clearUserPref(name);
- }
-});
-
-var gTests = [
-{
- desc: "Test the remote commands",
- *teardown() {
- gBrowser.removeCurrentTab();
- yield signOut();
- },
- *run() {
- setPref("identity.fxaccounts.remote.signup.uri",
- "https://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
- let tab = yield promiseNewTabLoadEvent("about:accounts");
- let mm = tab.linkedBrowser.messageManager;
-
- let deferred = Promise.defer();
-
- // We'll get a message when openPrefs() is called, which this test should
- // arrange.
- let promisePrefsOpened = promiseOneMessage(tab, "test:openPrefsCalled");
- let results = 0;
- try {
- mm.addMessageListener("test:response", function responseHandler(msg) {
- let data = msg.data.data;
- if (data.type == "testResult") {
- ok(data.pass, data.info);
- results++;
- } else if (data.type == "testsComplete") {
- is(results, data.count, "Checking number of results received matches the number of tests that should have run");
- mm.removeMessageListener("test:response", responseHandler);
- deferred.resolve();
- }
- });
- } catch (e) {
- ok(false, "Failed to get all commands");
- deferred.reject();
- }
- yield deferred.promise;
- yield promisePrefsOpened;
- }
-},
-{
- desc: "Test action=signin - no user logged in",
- teardown: () => gBrowser.removeCurrentTab(),
- *run() {
- // When this loads with no user logged-in, we expect the "normal" URL
- const expected_url = "https://example.com/?is_sign_in";
- setPref("identity.fxaccounts.remote.signin.uri", expected_url);
- let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
- is(url, expected_url, "action=signin got the expected URL");
- // we expect the remote iframe to be shown.
- yield checkVisibilities(tab, {
- stage: false, // parent of 'manage' and 'intro'
- manage: false,
- intro: false, // this is "get started"
- remote: true,
- networkError: false
- });
- }
-},
-{
- desc: "Test action=signin - user logged in",
- *teardown() {
- gBrowser.removeCurrentTab();
- yield signOut();
- },
- *run() {
- // When this loads with a user logged-in, we expect the normal URL to
- // have been ignored and the "manage" page to be shown.
- const expected_url = "https://example.com/?is_sign_in";
- setPref("identity.fxaccounts.remote.signin.uri", expected_url);
- yield setSignedInUser();
- let tab = yield promiseNewTabLoadEvent("about:accounts?action=signin");
- // about:accounts initializes after fetching the current user from Fxa -
- // so we also request it - by the time we get it we know it should have
- // done its thing.
- yield fxAccounts.getSignedInUser();
- // we expect "manage" to be shown.
- yield checkVisibilities(tab, {
- stage: true, // parent of 'manage' and 'intro'
- manage: true,
- intro: false, // this is "get started"
- remote: false,
- networkError: false
- });
- }
-},
-{
- desc: "Test action=signin - captive portal",
- teardown: () => gBrowser.removeCurrentTab(),
- *run() {
- const signinUrl = "https://redirproxy.example.com/test";
- setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
- let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
- yield checkVisibilities(tab, {
- stage: true, // parent of 'manage' and 'intro'
- manage: false,
- intro: false, // this is "get started"
- remote: false,
- networkError: true
- });
- }
-},
-{
- desc: "Test action=signin - offline",
- teardown: () => {
- gBrowser.removeCurrentTab();
- BrowserOffline.toggleOfflineStatus();
- },
- *run() {
- BrowserOffline.toggleOfflineStatus();
- Services.cache2.clear();
-
- const signinUrl = "https://unknowndomain.cow";
- setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
- let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
- yield checkVisibilities(tab, {
- stage: true, // parent of 'manage' and 'intro'
- manage: false,
- intro: false, // this is "get started"
- remote: false,
- networkError: true
- });
- }
-},
-{
- desc: "Test action=signup - no user logged in",
- teardown: () => gBrowser.removeCurrentTab(),
- *run() {
- const expected_url = "https://example.com/?is_sign_up";
- setPref("identity.fxaccounts.remote.signup.uri", expected_url);
- let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signup");
- is(url, expected_url, "action=signup got the expected URL");
- // we expect the remote iframe to be shown.
- yield checkVisibilities(tab, {
- stage: false, // parent of 'manage' and 'intro'
- manage: false,
- intro: false, // this is "get started"
- remote: true,
- networkError: false
- });
- },
-},
-{
- desc: "Test action=signup - user logged in",
- teardown: () => gBrowser.removeCurrentTab(),
- *run() {
- const expected_url = "https://example.com/?is_sign_up";
- setPref("identity.fxaccounts.remote.signup.uri", expected_url);
- yield setSignedInUser();
- let tab = yield promiseNewTabLoadEvent("about:accounts?action=signup");
- yield fxAccounts.getSignedInUser();
- // we expect "manage" to be shown.
- yield checkVisibilities(tab, {
- stage: true, // parent of 'manage' and 'intro'
- manage: true,
- intro: false, // this is "get started"
- remote: false,
- networkError: false
- });
- },
-},
-{
- desc: "Test action=reauth",
- *teardown() {
- gBrowser.removeCurrentTab();
- yield signOut();
- },
- *run() {
- const expected_url = "https://example.com/force_auth";
- setPref("identity.fxaccounts.remote.force_auth.uri", expected_url);
-
- yield setSignedInUser();
- let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=reauth");
- // The current user will be appended to the url
- let expected = expected_url + "?uid=1234%40lcip.org&email=foo%40example.com";
- is(url, expected, "action=reauth got the expected URL");
- },
-},
-{
- desc: "Test with migrateToDevEdition enabled (success)",
- *teardown() {
- gBrowser.removeCurrentTab();
- yield signOut();
- },
- *run() {
- let fxAccountsCommon = {};
- Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
- const pref = "identity.fxaccounts.migrateToDevEdition";
- changedPrefs.add(pref);
- Services.prefs.setBoolPref(pref, true);
-
- // Create the signedInUser.json file that will be used as the source of
- // migrated user data.
- let signedInUser = {
- version: 1,
- accountData: {
- email: "foo@example.com",
- uid: "1234@lcip.org",
- sessionToken: "dead",
- verified: true
- }
- };
- // We use a sub-dir of the real profile dir as the "pretend" profile dir
- // for this test.
- let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
- let mockDir = profD.clone();
- mockDir.append("about-accounts-mock-profd");
- mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
- let fxAccountsStorage = OS.Path.join(mockDir.path, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
- yield OS.File.writeAtomic(fxAccountsStorage, JSON.stringify(signedInUser));
- info("Wrote file " + fxAccountsStorage);
-
- // this is a little subtle - we load about:robots so we get a non-remote
- // tab, then we send a message which does both (a) load the URL we want and
- // (b) mocks the default profile path used by about:accounts.
- let tab = yield promiseNewTabLoadEvent("about:robots");
- let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
-
- let mm = tab.linkedBrowser.messageManager;
- mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
- url: "about:accounts",
- profilePath: mockDir.path,
- });
-
- let response = yield readyPromise;
- // We are expecting the iframe to be on the "force reauth" URL
- let expected = yield fxAccounts.promiseAccountsForceSigninURI();
- is(response.data.url, expected);
-
- let userData = yield fxAccounts.getSignedInUser();
- SimpleTest.isDeeply(userData, signedInUser.accountData, "All account data were migrated");
- // The migration pref will have been switched off by now.
- is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
-
- yield OS.File.remove(fxAccountsStorage);
- yield OS.File.removeEmptyDir(mockDir.path);
- },
-},
-{
- desc: "Test with migrateToDevEdition enabled (no user to migrate)",
- *teardown() {
- gBrowser.removeCurrentTab();
- yield signOut();
- },
- *run() {
- const pref = "identity.fxaccounts.migrateToDevEdition";
- changedPrefs.add(pref);
- Services.prefs.setBoolPref(pref, true);
-
- let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
- let mockDir = profD.clone();
- mockDir.append("about-accounts-mock-profd");
- mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
- // but leave it empty, so we don't think a user is logged in.
-
- let tab = yield promiseNewTabLoadEvent("about:robots");
- let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
-
- let mm = tab.linkedBrowser.messageManager;
- mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
- url: "about:accounts",
- profilePath: mockDir.path,
- });
-
- let response = yield readyPromise;
- // We are expecting the iframe to be on the "signup" URL
- let expected = yield fxAccounts.promiseAccountsSignUpURI();
- is(response.data.url, expected);
-
- // and expect no signed in user.
- let userData = yield fxAccounts.getSignedInUser();
- is(userData, null);
- // The migration pref should have still been switched off.
- is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
- yield OS.File.removeEmptyDir(mockDir.path);
- },
-},
-{
- desc: "Test observers about:accounts",
- teardown() {
- gBrowser.removeCurrentTab();
- },
- *run() {
- setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
- yield setSignedInUser();
- let tab = yield promiseNewTabLoadEvent("about:accounts");
- // sign the user out - the tab should have action=signin
- let loadPromise = promiseOneMessage(tab, "test:document:load");
- yield signOut();
- // wait for the new load.
- yield loadPromise;
- is(tab.linkedBrowser.contentDocument.location.href, "about:accounts?action=signin");
- }
-},
-{
- desc: "Test entrypoint query string, no action, no user logged in",
- teardown: () => gBrowser.removeCurrentTab(),
- *run() {
- // When this loads with no user logged-in, we expect the "normal" URL
- setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
- let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome");
- is(url, "https://example.com/?entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
- },
-},
-{
- desc: "Test entrypoint query string for signin",
- teardown: () => gBrowser.removeCurrentTab(),
- *run() {
- // When this loads with no user logged-in, we expect the "normal" URL
- const expected_url = "https://example.com/?is_sign_in";
- setPref("identity.fxaccounts.remote.signin.uri", expected_url);
- let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin&entrypoint=abouthome");
- is(url, expected_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
- },
-},
-{
- desc: "Test entrypoint query string for signup",
- teardown: () => gBrowser.removeCurrentTab(),
- *run() {
- // When this loads with no user logged-in, we expect the "normal" URL
- const sign_up_url = "https://example.com/?is_sign_up";
- setPref("identity.fxaccounts.remote.signup.uri", sign_up_url);
- let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome&action=signup");
- is(url, sign_up_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
- },
-},
-{
- desc: "about:accounts URL params should be copied to remote URL params " +
- "when remote URL has no URL params, except for 'action'",
- teardown() {
- gBrowser.removeCurrentTab();
- },
- *run() {
- let signupURL = "https://example.com/";
- setPref("identity.fxaccounts.remote.signup.uri", signupURL);
- let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
- let [, url] =
- yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
- "&action=action");
- is(url, signupURL + "?" + queryStr, "URL params are copied to signup URL");
- },
-},
-{
- desc: "about:accounts URL params should be copied to remote URL params " +
- "when remote URL already has some URL params, except for 'action'",
- teardown() {
- gBrowser.removeCurrentTab();
- },
- *run() {
- let signupURL = "https://example.com/?param";
- setPref("identity.fxaccounts.remote.signup.uri", signupURL);
- let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
- let [, url] =
- yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
- "&action=action");
- is(url, signupURL + "&" + queryStr, "URL params are copied to signup URL");
- },
-},
-]; // gTests
-
-function test() {
- waitForExplicitFinish();
-
- Task.spawn(function* () {
- for (let testCase of gTests) {
- info(testCase.desc);
- try {
- yield testCase.run();
- } finally {
- yield testCase.teardown();
- }
- }
-
- finish();
- });
-}
-
-function promiseOneMessage(tab, messageName) {
- let mm = tab.linkedBrowser.messageManager;
- let deferred = Promise.defer();
- mm.addMessageListener(messageName, function onmessage(message) {
- mm.removeMessageListener(messageName, onmessage);
- deferred.resolve(message);
- });
- return deferred.promise;
-}
-
-function promiseNewTabLoadEvent(aUrl) {
- let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
- let browser = tab.linkedBrowser;
- let mm = browser.messageManager;
-
- // give it an e10s-friendly content script to help with our tests,
- // and wait for it to tell us about the load.
- let promise = promiseOneMessage(tab, "test:document:load");
- mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
- return promise.then(() => tab);
-}
-
-// Returns a promise which is resolved with the iframe's URL after a new
-// tab is created and the iframe in that tab loads.
-function promiseNewTabWithIframeLoadEvent(aUrl) {
- let deferred = Promise.defer();
- let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
- let browser = tab.linkedBrowser;
- let mm = browser.messageManager;
-
- // give it an e10s-friendly content script to help with our tests,
- // and wait for it to tell us about the iframe load.
- mm.addMessageListener("test:iframe:load", function onFrameLoad(message) {
- mm.removeMessageListener("test:iframe:load", onFrameLoad);
- deferred.resolve([tab, message.data.url]);
- });
- mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
- return deferred.promise;
-}
-
-function checkVisibilities(tab, data) {
- let ids = Object.keys(data);
- let mm = tab.linkedBrowser.messageManager;
- let deferred = Promise.defer();
- mm.addMessageListener("test:check-visibilities-response", function onResponse(message) {
- mm.removeMessageListener("test:check-visibilities-response", onResponse);
- for (let id of ids) {
- is(message.data[id], data[id], "Element '" + id + "' has correct visibility");
- }
- deferred.resolve();
- });
- mm.sendAsyncMessage("test:check-visibilities", {ids});
- return deferred.promise;
-}
-
-// watch out - these will fire observers which if you aren't careful, may
-// interfere with the tests.
-function setSignedInUser(data) {
- if (!data) {
- data = {
- email: "foo@example.com",
- uid: "1234@lcip.org",
- assertion: "foobar",
- sessionToken: "dead",
- kA: "beef",
- kB: "cafe",
- verified: true
- }
- }
- return fxAccounts.setSignedInUser(data);
-}
-
-function signOut() {
- // we always want a "localOnly" signout here...
- return fxAccounts.signOut(true);
-}
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -898,17 +898,17 @@ add_task(function* test_input_spell_fals
]
);
*/
});
const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
add_task(function* test_plaintext_sendpagetodevice() {
- if (!gFxAccounts.sendTabToDeviceEnabled) {
+ if (!gSync.sendTabToDeviceEnabled) {
return;
}
yield ensureSyncReady();
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
let plainTextItemsWithSendPage =
["context-navigation", null,
["context-back", false,
@@ -937,17 +937,17 @@ add_task(function* test_plaintext_sendpa
yield openMenuItemSubmenu("context-sendpagetodevice");
}
});
restoreRemoteClients(oldGetter);
});
add_task(function* test_link_sendlinktodevice() {
- if (!gFxAccounts.sendTabToDeviceEnabled) {
+ if (!gSync.sendTabToDeviceEnabled) {
return;
}
yield ensureSyncReady();
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
yield test_contextmenu("#test-link",
["context-openlinkintab", true,
...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
deleted file mode 100644
--- a/browser/base/content/test/general/browser_fxa_web_channel.js
+++ /dev/null
@@ -1,210 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
- return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
-});
-
-XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
- "resource://gre/modules/WebChannel.jsm");
-
-// FxAccountsWebChannel isn't explicitly exported by FxAccountsWebChannel.jsm
-// but we can get it here via a backstage pass.
-var {FxAccountsWebChannel} = Components.utils.import("resource://gre/modules/FxAccountsWebChannel.jsm", {});
-
-const TEST_HTTP_PATH = "http://example.com";
-const TEST_BASE_URL = TEST_HTTP_PATH + "/browser/browser/base/content/test/general/browser_fxa_web_channel.html";
-const TEST_CHANNEL_ID = "account_updates_test";
-
-var gTests = [
- {
- desc: "FxA Web Channel - should receive message about profile changes",
- *run() {
- let client = new FxAccountsWebChannel({
- content_uri: TEST_HTTP_PATH,
- channel_id: TEST_CHANNEL_ID,
- });
- let promiseObserver = new Promise((resolve, reject) => {
- makeObserver(FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, function(subject, topic, data) {
- Assert.equal(data, "abc123");
- client.tearDown();
- resolve();
- });
- });
-
- yield BrowserTestUtils.withNewTab({
- gBrowser,
- url: TEST_BASE_URL + "?profile_change"
- }, function* () {
- yield promiseObserver;
- });
- }
- },
- {
- desc: "fxa web channel - login messages should notify the fxAccounts object",
- *run() {
-
- let promiseLogin = new Promise((resolve, reject) => {
- let login = (accountData) => {
- Assert.equal(typeof accountData.authAt, "number");
- Assert.equal(accountData.email, "testuser@testuser.com");
- Assert.equal(accountData.keyFetchToken, "key_fetch_token");
- Assert.equal(accountData.sessionToken, "session_token");
- Assert.equal(accountData.uid, "uid");
- Assert.equal(accountData.unwrapBKey, "unwrap_b_key");
- Assert.equal(accountData.verified, true);
-
- client.tearDown();
- resolve();
- };
-
- let client = new FxAccountsWebChannel({
- content_uri: TEST_HTTP_PATH,
- channel_id: TEST_CHANNEL_ID,
- helpers: {
- login
- }
- });
- });
-
- yield BrowserTestUtils.withNewTab({
- gBrowser,
- url: TEST_BASE_URL + "?login"
- }, function* () {
- yield promiseLogin;
- });
- }
- },
- {
- desc: "fxa web channel - can_link_account messages should respond",
- *run() {
- let properUrl = TEST_BASE_URL + "?can_link_account";
-
- let promiseEcho = new Promise((resolve, reject) => {
-
- let webChannelOrigin = Services.io.newURI(properUrl);
- // responses sent to content are echoed back over the
- // `fxaccounts_webchannel_response_echo` channel. Ensure the
- // fxaccounts:can_link_account message is responded to.
- let echoWebChannel = new WebChannel("fxaccounts_webchannel_response_echo", webChannelOrigin);
- echoWebChannel.listen((webChannelId, message, target) => {
- Assert.equal(message.command, "fxaccounts:can_link_account");
- Assert.equal(message.messageId, 2);
- Assert.equal(message.data.ok, true);
-
- client.tearDown();
- echoWebChannel.stopListening();
-
- resolve();
- });
-
- let client = new FxAccountsWebChannel({
- content_uri: TEST_HTTP_PATH,
- channel_id: TEST_CHANNEL_ID,
- helpers: {
- shouldAllowRelink(acctName) {
- return acctName === "testuser@testuser.com";
- }
- }
- });
- });
-
- yield BrowserTestUtils.withNewTab({
- gBrowser,
- url: properUrl
- }, function* () {
- yield promiseEcho;
- });
- }
- },
- {
- desc: "fxa web channel - logout messages should notify the fxAccounts object",
- *run() {
- let promiseLogout = new Promise((resolve, reject) => {
- let logout = (uid) => {
- Assert.equal(uid, "uid");
-
- client.tearDown();
- resolve();
- };
-
- let client = new FxAccountsWebChannel({
- content_uri: TEST_HTTP_PATH,
- channel_id: TEST_CHANNEL_ID,
- helpers: {
- logout
- }
- });
- });
-
- yield BrowserTestUtils.withNewTab({
- gBrowser,
- url: TEST_BASE_URL + "?logout"
- }, function* () {
- yield promiseLogout;
- });
- }
- },
- {
- desc: "fxa web channel - delete messages should notify the fxAccounts object",
- *run() {
- let promiseDelete = new Promise((resolve, reject) => {
- let logout = (uid) => {
- Assert.equal(uid, "uid");
-
- client.tearDown();
- resolve();
- };
-
- let client = new FxAccountsWebChannel({
- content_uri: TEST_HTTP_PATH,
- channel_id: TEST_CHANNEL_ID,
- helpers: {
- logout
- }
- });
- });
-
- yield BrowserTestUtils.withNewTab({
- gBrowser,
- url: TEST_BASE_URL + "?delete"
- }, function* () {
- yield promiseDelete;
- });
- }
- }
-]; // gTests
-
-function makeObserver(aObserveTopic, aObserveFunc) {
- let callback = function(aSubject, aTopic, aData) {
- if (aTopic == aObserveTopic) {
- removeMe();
- aObserveFunc(aSubject, aTopic, aData);
- }
- };
-
- function removeMe() {
- Services.obs.removeObserver(callback, aObserveTopic);
- }
-
- Services.obs.addObserver(callback, aObserveTopic);
- return removeMe;
-}
-
-function test() {
- waitForExplicitFinish();
-
- Task.spawn(function* () {
- for (let testCase of gTests) {
- info("Running: " + testCase.desc);
- yield testCase.run();
- }
- }).then(finish, ex => {
- Assert.ok(false, "Unexpected Exception: " + ex);
- finish();
- });
-}
deleted file mode 100644
--- a/browser/base/content/test/general/browser_fxaccounts.js
+++ /dev/null
@@ -1,258 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-var {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
-var {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
-var {fxAccounts} = Cu.import("resource://gre/modules/FxAccounts.jsm", {});
-var FxAccountsCommon = {};
-Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
-
-const TEST_ROOT = "http://example.com/browser/browser/base/content/test/general/";
-
-// instrument gFxAccounts to send observer notifications when it's done
-// what it does.
-(function() {
- let unstubs = {}; // The original functions we stub out.
-
- // The stub functions.
- let stubs = {
- updateUI() {
- return unstubs["updateUI"].call(gFxAccounts).then(() => {
- Services.obs.notifyObservers(null, "test:browser_fxaccounts:updateUI");
- });
- },
- // Opening preferences is trickier than it should be as leaks are reported
- // due to the promises it fires off at load time and there's no clear way to
- // know when they are done.
- // So just ensure openPreferences is called rather than whether it opens.
- openPreferences() {
- Services.obs.notifyObservers(null, "test:browser_fxaccounts:openPreferences");
- }
- };
-
- for (let name in stubs) {
- unstubs[name] = gFxAccounts[name];
- gFxAccounts[name] = stubs[name];
- }
- // and undo our damage at the end.
- registerCleanupFunction(() => {
- for (let name in unstubs) {
- gFxAccounts[name] = unstubs[name];
- }
- stubs = unstubs = null;
- });
-})();
-
-// Other setup/cleanup
-var newTab;
-
-Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri",
- TEST_ROOT + "accounts_testRemoteCommands.html");
-
-registerCleanupFunction(() => {
- Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
- Services.prefs.clearUserPref("identity.fxaccounts.remote.profile.uri");
- gBrowser.removeTab(newTab);
-});
-
-add_task(function* initialize() {
- // Set a new tab with something other than about:blank, so it doesn't get reused.
- // We must wait for it to load or the promiseTabOpen() call in the next test
- // gets confused.
- newTab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
- yield promiseTabLoaded(newTab);
-});
-
-// The elements we care about.
-var panelUILabel = document.getElementById("PanelUI-fxa-label");
-var panelUIStatus = document.getElementById("PanelUI-fxa-status");
-var panelUIFooter = document.getElementById("PanelUI-footer-fxa");
-
-// The tests
-add_task(function* test_nouser() {
- let user = yield fxAccounts.getSignedInUser();
- Assert.strictEqual(user, null, "start with no user signed in");
- let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI");
- Services.obs.notifyObservers(null, this.FxAccountsCommon.ONLOGOUT_NOTIFICATION);
- yield promiseUpdateDone;
-
- // Check the world - the FxA footer area is visible as it is offering a signin.
- Assert.ok(isFooterVisible())
-
- Assert.equal(panelUILabel.getAttribute("label"), panelUIStatus.getAttribute("defaultlabel"));
- Assert.equal(panelUIStatus.getAttribute("tooltiptext"), panelUIStatus.getAttribute("signedinTooltiptext"));
- Assert.ok(!panelUIFooter.hasAttribute("fxastatus"), "no fxsstatus when signed out");
- Assert.ok(!panelUIFooter.hasAttribute("fxaprofileimage"), "no fxaprofileimage when signed out");
-
- let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
- panelUIStatus.click();
- yield promisePreferencesOpened;
-});
-
-/*
-XXX - Bug 1191162 - need a better hawk mock story or this will leak in debug builds.
-
-add_task(function* test_unverifiedUser() {
- let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI");
- yield setSignedInUser(false); // this will fire the observer that does the update.
- yield promiseUpdateDone;
-
- // Check the world.
- Assert.ok(isFooterVisible())
-
- Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
- Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
- panelUIStatus.getAttribute("signedinTooltiptext"));
- Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
- let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
- panelUIStatus.click();
- yield promisePreferencesOpened
- yield signOut();
-});
-*/
-
-add_task(function* test_verifiedUserEmptyProfile() {
- // We see 2 updateUI() calls - one for the signedInUser and one after
- // we first fetch the profile. We want them both to fire or we aren't testing
- // the state we think we are testing.
- let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 2);
- gFxAccounts._cachedProfile = null;
- configureProfileURL({}); // successful but empty profile.
- yield setSignedInUser(true); // this will fire the observer that does the update.
- yield promiseUpdateDone;
-
- // Check the world.
- Assert.ok(isFooterVisible())
- Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
- Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
- panelUIStatus.getAttribute("signedinTooltiptext"));
- Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
-
- let promisePreferencesOpened = promiseObserver("test:browser_fxaccounts:openPreferences");
- panelUIStatus.click();
- yield promisePreferencesOpened;
- yield signOut();
-});
-
-add_task(function* test_verifiedUserDisplayName() {
- let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 2);
- gFxAccounts._cachedProfile = null;
- configureProfileURL({ displayName: "Test User Display Name" });
- yield setSignedInUser(true); // this will fire the observer that does the update.
- yield promiseUpdateDone;
-
- Assert.ok(isFooterVisible())
- Assert.equal(panelUILabel.getAttribute("label"), "Test User Display Name");
- Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
- panelUIStatus.getAttribute("signedinTooltiptext"));
- Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
- yield signOut();
-});
-
-add_task(function* test_profileNotificationsClearsCache() {
- let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 1);
- gFxAccounts._cachedProfile = { foo: "bar" };
- Services.obs.notifyObservers(null, this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION);
- Assert.ok(!gFxAccounts._cachedProfile);
- yield promiseUpdateDone;
-});
-
-add_task(function* test_verifiedUserProfileFailure() {
- // profile failure means only one observer fires.
- let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI", 1);
- gFxAccounts._cachedProfile = null;
- configureProfileURL(null, 500);
- yield setSignedInUser(true); // this will fire the observer that does the update.
- yield promiseUpdateDone;
-
- Assert.ok(isFooterVisible());
- Assert.equal(panelUILabel.getAttribute("label"), "foo@example.com");
- Assert.equal(panelUIStatus.getAttribute("tooltiptext"),
- panelUIStatus.getAttribute("signedinTooltiptext"));
- Assert.equal(panelUIFooter.getAttribute("fxastatus"), "signedin");
- yield signOut();
-});
-
-// Helpers.
-function isFooterVisible() {
- let style = window.getComputedStyle(panelUIFooter);
- return style.getPropertyValue("display") == "flex";
-}
-
-function configureProfileURL(profile, responseStatus = 200) {
- let responseBody = profile ? JSON.stringify(profile) : "";
- let url = TEST_ROOT + "fxa_profile_handler.sjs?" +
- "responseStatus=" + responseStatus +
- "responseBody=" + responseBody +
- // This is a bit cheeky - the FxA code will just append "/profile"
- // to the preference value. We arrange for this to be seen by our
- // .sjs as part of the query string.
- "&path=";
-
- Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", url);
-}
-
-function promiseObserver(topic, count = 1) {
- return new Promise(resolve => {
- let obs = (aSubject, aTopic, aData) => {
- if (--count == 0) {
- Services.obs.removeObserver(obs, aTopic);
- resolve(aSubject);
- }
- }
- Services.obs.addObserver(obs, topic);
- });
-}
-
-var promiseTabOpen = Task.async(function*(urlBase) {
- info("Waiting for tab to open...");
- let event = yield promiseWaitForEvent(gBrowser.tabContainer, "TabOpen", true);
- let tab = event.target;
- yield promiseTabLoadEvent(tab);
- ok(tab.linkedBrowser.currentURI.spec.startsWith(urlBase),
- "Got " + tab.linkedBrowser.currentURI.spec + ", expecting " + urlBase);
- let whenUnloaded = promiseTabUnloaded(tab);
- gBrowser.removeTab(tab);
- yield whenUnloaded;
-});
-
-function promiseTabUnloaded(tab) {
- return new Promise(resolve => {
- info("Wait for tab to unload");
- function handle(event) {
- tab.linkedBrowser.removeEventListener("unload", handle, true);
- info("Got unload event");
- resolve(event);
- }
- tab.linkedBrowser.addEventListener("unload", handle, true, true);
- });
-}
-
-// FxAccounts helpers.
-function setSignedInUser(verified) {
- let data = {
- email: "foo@example.com",
- uid: "1234@lcip.org",
- assertion: "foobar",
- sessionToken: "dead",
- kA: "beef",
- kB: "cafe",
- verified,
-
- oauthTokens: {
- // a token for the profile server.
- profile: "key value",
- }
- }
- return fxAccounts.setSignedInUser(data);
-}
-
-var signOut = Task.async(function* () {
- // This test needs to make sure that any updates for the logout have
- // completed before starting the next test, or we see the observer
- // notifications get out of sync.
- let promiseUpdateDone = promiseObserver("test:browser_fxaccounts:updateUI");
- // we always want a "localOnly" signout here...
- yield fxAccounts.signOut(true);
- yield promiseUpdateDone;
-});
deleted file mode 100644
--- a/browser/base/content/test/general/browser_syncui.js
+++ /dev/null
@@ -1,205 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-var {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
-var {Weave} = Cu.import("resource://services-sync/main.js", {});
-
-var stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]
- .getService(Ci.nsIStringBundleService)
- .createBundle("chrome://weave/locale/sync.properties");
-
-// ensure test output sees log messages.
-Log.repository.getLogger("browserwindow.syncui").addAppender(new Log.DumpAppender());
-
-// Send the specified sync-related notification and return a promise that
-// resolves once gSyncUI._promiseUpateUI is complete and the UI is ready to check.
-function notifyAndPromiseUIUpdated(topic) {
- return new Promise(resolve => {
- // Instrument gSyncUI so we know when the update is complete.
- let oldPromiseUpdateUI = gSyncUI._promiseUpdateUI.bind(gSyncUI);
- gSyncUI._promiseUpdateUI = function() {
- return oldPromiseUpdateUI().then(() => {
- // Restore our override.
- gSyncUI._promiseUpdateUI = oldPromiseUpdateUI;
- // Resolve the promise so the caller knows the update is done.
- resolve();
- });
- };
- // Now send the notification.
- Services.obs.notifyObservers(null, topic);
- });
-}
-
-// Sync manages 3 broadcasters so the menus correctly reflect the Sync state.
-// Only one of these 3 should ever be visible - pass the ID of the broadcaster
-// you expect to be visible and it will check it's the only one that is.
-function checkBroadcasterVisible(broadcasterId) {
- let all = ["sync-reauth-state", "sync-setup-state", "sync-syncnow-state"];
- Assert.ok(all.indexOf(broadcasterId) >= 0, "valid id");
- for (let check of all) {
- let eltHidden = document.getElementById(check).hidden;
- Assert.equal(eltHidden, check == broadcasterId ? false : true, check);
- }
-}
-
-function promiseObserver(topic) {
- return new Promise(resolve => {
- let obs = (aSubject, aTopic, aData) => {
- Services.obs.removeObserver(obs, aTopic);
- resolve(aSubject);
- }
- Services.obs.addObserver(obs, topic);
- });
-}
-
-function checkButtonTooltips(stringPrefix) {
- for (let butId of ["PanelUI-remotetabs-syncnow", "PanelUI-fxa-icon"]) {
- let text = document.getElementById(butId).getAttribute("tooltiptext");
- let desc = `Text is "${text}", expecting it to start with "${stringPrefix}"`
- Assert.ok(text.startsWith(stringPrefix), desc);
- }
-}
-
-add_task(function* prepare() {
- // add the Sync button to the toolbar so we can get it!
- CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_NAVBAR);
- registerCleanupFunction(() => {
- CustomizableUI.removeWidgetFromArea("sync-button");
- });
-
- let xps = Components.classes["@mozilla.org/weave/service;1"]
- .getService(Components.interfaces.nsISupports)
- .wrappedJSObject;
- yield xps.whenLoaded();
- // Put Sync and the UI into a known state.
- Weave.Status.login = Weave.LOGIN_FAILED_NO_USERNAME;
- yield notifyAndPromiseUIUpdated("weave:service:login:error");
-
- checkBroadcasterVisible("sync-setup-state");
- checkButtonTooltips("Sign In To Sync");
- // mock out the "_needsSetup()" function so we don't short-circuit.
- let oldNeedsSetup = window.gSyncUI._needsSetup;
- window.gSyncUI._needsSetup = () => Promise.resolve(false);
- registerCleanupFunction(() => {
- window.gSyncUI._needsSetup = oldNeedsSetup;
- // and an observer to set the state back to what it should be now we've
- // restored the stub.
- Services.obs.notifyObservers(null, "weave:service:login:finish");
- });
- // and a notification to have the state change away from "needs setup"
- yield notifyAndPromiseUIUpdated("weave:service:login:finish");
- checkBroadcasterVisible("sync-syncnow-state");
- // open the sync-button panel so we can check elements in that.
- document.getElementById("sync-button").click();
-});
-
-add_task(function* testSyncNeedsVerification() {
- // mock out the "_needsVerification()" function
- let oldNeedsVerification = window.gSyncUI._needsVerification;
- window.gSyncUI._needsVerification = () => true;
- try {
- // a notification for the state change
- yield notifyAndPromiseUIUpdated("weave:service:login:finish");
- checkButtonTooltips("Verify");
- } finally {
- window.gSyncUI._needsVerification = oldNeedsVerification;
- }
-});
-
-
-add_task(function* testSyncLoginError() {
- checkBroadcasterVisible("sync-syncnow-state");
-
- // Pretend we are in a "login failed" error state
- Weave.Status.sync = Weave.LOGIN_FAILED;
- Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED;
- yield notifyAndPromiseUIUpdated("weave:ui:sync:error");
-
- // But the menu *should* reflect the login error.
- checkBroadcasterVisible("sync-reauth-state");
- // The tooltips for the buttons should also reflect it.
- checkButtonTooltips("Reconnect");
-
- // Now pretend we just had a successful login - the error notification should go away.
- Weave.Status.sync = Weave.STATUS_OK;
- Weave.Status.login = Weave.LOGIN_SUCCEEDED;
- yield notifyAndPromiseUIUpdated("weave:service:login:start");
- yield notifyAndPromiseUIUpdated("weave:service:login:finish");
- // The menus should be back to "all good"
- checkBroadcasterVisible("sync-syncnow-state");
-});
-
-function checkButtonsStatus(shouldBeActive) {
- for (let eid of [
- "sync-status", // the broadcaster itself.
- "sync-button", // the main sync button which observes the broadcaster
- "PanelUI-fxa-icon", // the sync icon in the fxa footer that observes it.
- ]) {
- let elt = document.getElementById(eid);
- if (shouldBeActive) {
- Assert.equal(elt.getAttribute("syncstatus"), "active", `${eid} should be active`);
- } else {
- Assert.ok(!elt.hasAttribute("syncstatus"), `${eid} should have no status attr`);
- }
- }
-}
-
-function* testButtonActions(startNotification, endNotification, expectActive = true) {
- checkButtonsStatus(false);
- // pretend a sync is starting.
- yield notifyAndPromiseUIUpdated(startNotification);
- checkButtonsStatus(expectActive);
- // and has stopped
- yield notifyAndPromiseUIUpdated(endNotification);
- checkButtonsStatus(false);
-}
-
-function *doTestButtonActivities() {
- // logins do not "activate" the spinner/button as they may block and make
- // the UI look like Sync is never completing.
- yield testButtonActions("weave:service:login:start", "weave:service:login:finish", false);
- yield testButtonActions("weave:service:login:start", "weave:service:login:error", false);
-
- // But notifications for Sync itself should activate it.
- yield testButtonActions("weave:service:sync:start", "weave:service:sync:finish");
- yield testButtonActions("weave:service:sync:start", "weave:service:sync:error");
-
- // and ensure the counters correctly handle multiple in-flight syncs
- yield notifyAndPromiseUIUpdated("weave:service:sync:start");
- checkButtonsStatus(true);
- // sync stops.
- yield notifyAndPromiseUIUpdated("weave:service:sync:finish");
- // Button should not be active.
- checkButtonsStatus(false);
-}
-
-add_task(function* testButtonActivitiesInNavBar() {
- // check the button's functionality while the button is in the NavBar - which
- // it already is.
- yield doTestButtonActivities();
-});
-
-add_task(function* testFormatLastSyncDateNow() {
- let now = new Date();
- let nowString = gSyncUI.formatLastSyncDate(now);
- Assert.equal(nowString, "Last sync: " + now.toLocaleDateString(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}));
-});
-
-add_task(function* testFormatLastSyncDateMonthAgo() {
- let monthAgo = new Date();
- monthAgo.setMonth(monthAgo.getMonth() - 1);
- let monthAgoString = gSyncUI.formatLastSyncDate(monthAgo);
- Assert.equal(monthAgoString, "Last sync: " + monthAgo.toLocaleDateString(undefined, {month: "long", day: "numeric"}));
-});
-
-add_task(function* testButtonActivitiesInPanel() {
- // check the button's functionality while the button is in the panel - it's
- // currently in the NavBar - move it to the panel and open it.
- CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
- yield PanelUI.show();
- try {
- yield doTestButtonActivities();
- } finally {
- PanelUI.hide();
- }
-});
--- a/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
+++ b/browser/base/content/test/general/browser_visibleTabs_contextMenu.js
@@ -12,35 +12,35 @@ add_task(function* test() {
is(gBrowser.visibleTabs.length, 2, "there are now two visible tabs");
// Check the context menu with two tabs
updateTabContextMenu(origTab);
is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled");
is(document.getElementById("context_reloadAllTabs").disabled, false, "Reload All Tabs is enabled");
- if (gFxAccounts.sendTabToDeviceEnabled) {
- const origIsSendableURI = gFxAccounts.isSendableURI;
- gFxAccounts.isSendableURI = () => true;
+ if (gSync.sendTabToDeviceEnabled) {
+ const origIsSendableURI = gSync.isSendableURI;
+ gSync.isSendableURI = () => true;
// Check the send tab to device menu item
yield ensureSyncReady();
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
yield updateTabContextMenu(origTab, function* () {
yield openMenuItemSubmenu("context_sendTabToDevice");
});
is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
let targets = document.getElementById("context_sendTabToDevicePopupMenu").childNodes;
is(targets[0].getAttribute("label"), "Foo", "Foo target is present");
is(targets[1].getAttribute("label"), "Bar", "Bar target is present");
is(targets[3].getAttribute("label"), "All Devices", "All Devices target is present");
- gFxAccounts.isSendableURI = () => false;
+ gSync.isSendableURI = () => false;
updateTabContextMenu(origTab);
is(document.getElementById("context_sendTabToDevice").hidden, true, "Send tab to device is hidden");
restoreRemoteClients(oldGetter);
- gFxAccounts.isSendableURI = origIsSendableURI;
+ gSync.isSendableURI = origIsSendableURI;
}
// Hide the original tab.
gBrowser.selectedTab = testTab;
gBrowser.showOnlyTheseTabs([testTab]);
is(gBrowser.visibleTabs.length, 1, "now there is only one visible tab");
// Check the context menu with one tab.
deleted file mode 100644
--- a/browser/base/content/test/general/fxa_profile_handler.sjs
+++ /dev/null
@@ -1,34 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// This is basically an echo server!
-// We just grab responseStatus and responseBody query params!
-
-function reallyHandleRequest(request, response) {
- var query = "?" + request.queryString;
-
- var responseStatus = 200;
- var match = /responseStatus=([^&]*)/.exec(query);
- if (match) {
- responseStatus = parseInt(match[1]);
- }
-
- var responseBody = "";
- match = /responseBody=([^&]*)/.exec(query);
- if (match) {
- responseBody = decodeURIComponent(match[1]);
- }
-
- response.setStatusLine("1.0", responseStatus, "OK");
- response.write(responseBody);
-}
-
-function handleRequest(request, response)
-{
- try {
- reallyHandleRequest(request, response);
- } catch (e) {
- response.setStatusLine("1.0", 500, "NotOK");
- response.write("Error handling request: " + e);
- }
-}
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -831,26 +831,26 @@ function getCertExceptionDialog(aLocatio
}
}
}
return undefined;
}
function setupRemoteClientsFixture(fixture) {
let oldRemoteClientsGetter =
- Object.getOwnPropertyDescriptor(gFxAccounts, "remoteClients").get;
+ Object.getOwnPropertyDescriptor(gSync, "remoteClients").get;
- Object.defineProperty(gFxAccounts, "remoteClients", {
+ Object.defineProperty(gSync, "remoteClients", {
get() { return fixture; }
});
return oldRemoteClientsGetter;
}
function restoreRemoteClients(getter) {
- Object.defineProperty(gFxAccounts, "remoteClients", {
+ Object.defineProperty(gSync, "remoteClients", {
get: getter
});
}
function* openMenuItemSubmenu(id) {
let menuPopup = document.getElementById(id).menupopup;
let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
menuPopup.showPopup();
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "plugin:mozilla/browser-test"
+ ]
+};
rename from browser/base/content/test/general/accounts_testRemoteCommands.html
rename to browser/base/content/test/sync/accounts_testRemoteCommands.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser.ini
@@ -0,0 +1,9 @@
+[browser_sync.js]
+[browser_fxa_web_channel.js]
+support-files=
+ browser_fxa_web_channel.html
+[browser_aboutAccounts.js]
+skip-if = os == "linux" # Bug 958026
+support-files =
+ content_aboutAccounts.js
+ accounts_testRemoteCommands.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_aboutAccounts.js
@@ -0,0 +1,487 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: window.location is null");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/sync/";
+// Preference helpers.
+var changedPrefs = new Set();
+
+function setPref(name, value) {
+ changedPrefs.add(name);
+ Services.prefs.setCharPref(name, value);
+}
+
+registerCleanupFunction(function() {
+ // Ensure we don't pollute prefs for next tests.
+ for (let name of changedPrefs) {
+ Services.prefs.clearUserPref(name);
+ }
+});
+
+var gTests = [
+{
+ desc: "Test the remote commands",
+ *teardown() {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ *run() {
+ setPref("identity.fxaccounts.remote.signup.uri",
+ "https://example.com/browser/browser/base/content/test/sync/accounts_testRemoteCommands.html");
+ let tab = yield promiseNewTabLoadEvent("about:accounts");
+ let mm = tab.linkedBrowser.messageManager;
+
+ let deferred = Promise.defer();
+
+ // We'll get a message when openPrefs() is called, which this test should
+ // arrange.
+ let promisePrefsOpened = promiseOneMessage(tab, "test:openPrefsCalled");
+ let results = 0;
+ try {
+ mm.addMessageListener("test:response", function responseHandler(msg) {
+ let data = msg.data.data;
+ if (data.type == "testResult") {
+ ok(data.pass, data.info);
+ results++;
+ } else if (data.type == "testsComplete") {
+ is(results, data.count, "Checking number of results received matches the number of tests that should have run");
+ mm.removeMessageListener("test:response", responseHandler);
+ deferred.resolve();
+ }
+ });
+ } catch (e) {
+ ok(false, "Failed to get all commands");
+ deferred.reject();
+ }
+ yield deferred.promise;
+ yield promisePrefsOpened;
+ }
+},
+{
+ desc: "Test action=signin - no user logged in",
+ teardown: () => gBrowser.removeCurrentTab(),
+ *run() {
+ // When this loads with no user logged-in, we expect the "normal" URL
+ const expected_url = "https://example.com/?is_sign_in";
+ setPref("identity.fxaccounts.remote.signin.uri", expected_url);
+ let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
+ is(url, expected_url, "action=signin got the expected URL");
+ // we expect the remote iframe to be shown.
+ yield checkVisibilities(tab, {
+ stage: false, // parent of 'manage' and 'intro'
+ manage: false,
+ intro: false, // this is "get started"
+ remote: true,
+ networkError: false
+ });
+ }
+},
+{
+ desc: "Test action=signin - user logged in",
+ *teardown() {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ *run() {
+ // When this loads with a user logged-in, we expect the normal URL to
+ // have been ignored and the "manage" page to be shown.
+ const expected_url = "https://example.com/?is_sign_in";
+ setPref("identity.fxaccounts.remote.signin.uri", expected_url);
+ yield setSignedInUser();
+ let tab = yield promiseNewTabLoadEvent("about:accounts?action=signin");
+ // about:accounts initializes after fetching the current user from Fxa -
+ // so we also request it - by the time we get it we know it should have
+ // done its thing.
+ yield fxAccounts.getSignedInUser();
+ // we expect "manage" to be shown.
+ yield checkVisibilities(tab, {
+ stage: true, // parent of 'manage' and 'intro'
+ manage: true,
+ intro: false, // this is "get started"
+ remote: false,
+ networkError: false
+ });
+ }
+},
+{
+ desc: "Test action=signin - captive portal",
+ teardown: () => gBrowser.removeCurrentTab(),
+ *run() {
+ const signinUrl = "https://redirproxy.example.com/test";
+ setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
+ let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
+ yield checkVisibilities(tab, {
+ stage: true, // parent of 'manage' and 'intro'
+ manage: false,
+ intro: false, // this is "get started"
+ remote: false,
+ networkError: true
+ });
+ }
+},
+{
+ desc: "Test action=signin - offline",
+ teardown: () => {
+ gBrowser.removeCurrentTab();
+ BrowserOffline.toggleOfflineStatus();
+ },
+ *run() {
+ BrowserOffline.toggleOfflineStatus();
+ Services.cache2.clear();
+
+ const signinUrl = "https://unknowndomain.cow";
+ setPref("identity.fxaccounts.remote.signin.uri", signinUrl);
+ let [tab, ] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin");
+ yield checkVisibilities(tab, {
+ stage: true, // parent of 'manage' and 'intro'
+ manage: false,
+ intro: false, // this is "get started"
+ remote: false,
+ networkError: true
+ });
+ }
+},
+{
+ desc: "Test action=signup - no user logged in",
+ teardown: () => gBrowser.removeCurrentTab(),
+ *run() {
+ const expected_url = "https://example.com/?is_sign_up";
+ setPref("identity.fxaccounts.remote.signup.uri", expected_url);
+ let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signup");
+ is(url, expected_url, "action=signup got the expected URL");
+ // we expect the remote iframe to be shown.
+ yield checkVisibilities(tab, {
+ stage: false, // parent of 'manage' and 'intro'
+ manage: false,
+ intro: false, // this is "get started"
+ remote: true,
+ networkError: false
+ });
+ },
+},
+{
+ desc: "Test action=signup - user logged in",
+ teardown: () => gBrowser.removeCurrentTab(),
+ *run() {
+ const expected_url = "https://example.com/?is_sign_up";
+ setPref("identity.fxaccounts.remote.signup.uri", expected_url);
+ yield setSignedInUser();
+ let tab = yield promiseNewTabLoadEvent("about:accounts?action=signup");
+ yield fxAccounts.getSignedInUser();
+ // we expect "manage" to be shown.
+ yield checkVisibilities(tab, {
+ stage: true, // parent of 'manage' and 'intro'
+ manage: true,
+ intro: false, // this is "get started"
+ remote: false,
+ networkError: false
+ });
+ },
+},
+{
+ desc: "Test action=reauth",
+ *teardown() {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ *run() {
+ const expected_url = "https://example.com/force_auth";
+ setPref("identity.fxaccounts.remote.force_auth.uri", expected_url);
+
+ yield setSignedInUser();
+ let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=reauth");
+ // The current user will be appended to the url
+ let expected = expected_url + "?uid=1234%40lcip.org&email=foo%40example.com";
+ is(url, expected, "action=reauth got the expected URL");
+ },
+},
+{
+ desc: "Test with migrateToDevEdition enabled (success)",
+ *teardown() {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ *run() {
+ let fxAccountsCommon = {};
+ Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+ const pref = "identity.fxaccounts.migrateToDevEdition";
+ changedPrefs.add(pref);
+ Services.prefs.setBoolPref(pref, true);
+
+ // Create the signedInUser.json file that will be used as the source of
+ // migrated user data.
+ let signedInUser = {
+ version: 1,
+ accountData: {
+ email: "foo@example.com",
+ uid: "1234@lcip.org",
+ sessionToken: "dead",
+ verified: true
+ }
+ };
+ // We use a sub-dir of the real profile dir as the "pretend" profile dir
+ // for this test.
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let mockDir = profD.clone();
+ mockDir.append("about-accounts-mock-profd");
+ mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ let fxAccountsStorage = OS.Path.join(mockDir.path, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
+ yield OS.File.writeAtomic(fxAccountsStorage, JSON.stringify(signedInUser));
+ info("Wrote file " + fxAccountsStorage);
+
+ // this is a little subtle - we load about:robots so we get a non-remote
+ // tab, then we send a message which does both (a) load the URL we want and
+ // (b) mocks the default profile path used by about:accounts.
+ let tab = yield promiseNewTabLoadEvent("about:robots");
+ let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
+
+ let mm = tab.linkedBrowser.messageManager;
+ mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
+ url: "about:accounts",
+ profilePath: mockDir.path,
+ });
+
+ let response = yield readyPromise;
+ // We are expecting the iframe to be on the "force reauth" URL
+ let expected = yield fxAccounts.promiseAccountsForceSigninURI();
+ is(response.data.url, expected);
+
+ let userData = yield fxAccounts.getSignedInUser();
+ SimpleTest.isDeeply(userData, signedInUser.accountData, "All account data were migrated");
+ // The migration pref will have been switched off by now.
+ is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
+
+ yield OS.File.remove(fxAccountsStorage);
+ yield OS.File.removeEmptyDir(mockDir.path);
+ },
+},
+{
+ desc: "Test with migrateToDevEdition enabled (no user to migrate)",
+ *teardown() {
+ gBrowser.removeCurrentTab();
+ yield signOut();
+ },
+ *run() {
+ const pref = "identity.fxaccounts.migrateToDevEdition";
+ changedPrefs.add(pref);
+ Services.prefs.setBoolPref(pref, true);
+
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let mockDir = profD.clone();
+ mockDir.append("about-accounts-mock-profd");
+ mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ // but leave it empty, so we don't think a user is logged in.
+
+ let tab = yield promiseNewTabLoadEvent("about:robots");
+ let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
+
+ let mm = tab.linkedBrowser.messageManager;
+ mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
+ url: "about:accounts",
+ profilePath: mockDir.path,
+ });
+
+ let response = yield readyPromise;
+ // We are expecting the iframe to be on the "signup" URL
+ let expected = yield fxAccounts.promiseAccountsSignUpURI();
+ is(response.data.url, expected);
+
+ // and expect no signed in user.
+ let userData = yield fxAccounts.getSignedInUser();
+ is(userData, null);
+ // The migration pref should have still been switched off.
+ is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
+ yield OS.File.removeEmptyDir(mockDir.path);
+ },
+},
+{
+ desc: "Test observers about:accounts",
+ teardown() {
+ gBrowser.removeCurrentTab();
+ },
+ *run() {
+ setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
+ yield setSignedInUser();
+ let tab = yield promiseNewTabLoadEvent("about:accounts");
+ // sign the user out - the tab should have action=signin
+ let loadPromise = promiseOneMessage(tab, "test:document:load");
+ yield signOut();
+ // wait for the new load.
+ yield loadPromise;
+ is(tab.linkedBrowser.contentDocument.location.href, "about:accounts?action=signin");
+ }
+},
+{
+ desc: "Test entrypoint query string, no action, no user logged in",
+ teardown: () => gBrowser.removeCurrentTab(),
+ *run() {
+ // When this loads with no user logged-in, we expect the "normal" URL
+ setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
+ let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome");
+ is(url, "https://example.com/?entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
+ },
+},
+{
+ desc: "Test entrypoint query string for signin",
+ teardown: () => gBrowser.removeCurrentTab(),
+ *run() {
+ // When this loads with no user logged-in, we expect the "normal" URL
+ const expected_url = "https://example.com/?is_sign_in";
+ setPref("identity.fxaccounts.remote.signin.uri", expected_url);
+ let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=signin&entrypoint=abouthome");
+ is(url, expected_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
+ },
+},
+{
+ desc: "Test entrypoint query string for signup",
+ teardown: () => gBrowser.removeCurrentTab(),
+ *run() {
+ // When this loads with no user logged-in, we expect the "normal" URL
+ const sign_up_url = "https://example.com/?is_sign_up";
+ setPref("identity.fxaccounts.remote.signup.uri", sign_up_url);
+ let [, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?entrypoint=abouthome&action=signup");
+ is(url, sign_up_url + "&entrypoint=abouthome", "entrypoint=abouthome got the expected URL");
+ },
+},
+{
+ desc: "about:accounts URL params should be copied to remote URL params " +
+ "when remote URL has no URL params, except for 'action'",
+ teardown() {
+ gBrowser.removeCurrentTab();
+ },
+ *run() {
+ let signupURL = "https://example.com/";
+ setPref("identity.fxaccounts.remote.signup.uri", signupURL);
+ let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
+ let [, url] =
+ yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
+ "&action=action");
+ is(url, signupURL + "?" + queryStr, "URL params are copied to signup URL");
+ },
+},
+{
+ desc: "about:accounts URL params should be copied to remote URL params " +
+ "when remote URL already has some URL params, except for 'action'",
+ teardown() {
+ gBrowser.removeCurrentTab();
+ },
+ *run() {
+ let signupURL = "https://example.com/?param";
+ setPref("identity.fxaccounts.remote.signup.uri", signupURL);
+ let queryStr = "email=foo%40example.com&foo=bar&baz=quux";
+ let [, url] =
+ yield promiseNewTabWithIframeLoadEvent("about:accounts?" + queryStr +
+ "&action=action");
+ is(url, signupURL + "&" + queryStr, "URL params are copied to signup URL");
+ },
+},
+]; // gTests
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ for (let testCase of gTests) {
+ info(testCase.desc);
+ try {
+ yield testCase.run();
+ } finally {
+ yield testCase.teardown();
+ }
+ }
+
+ finish();
+ });
+}
+
+function promiseOneMessage(tab, messageName) {
+ let mm = tab.linkedBrowser.messageManager;
+ let deferred = Promise.defer();
+ mm.addMessageListener(messageName, function onmessage(message) {
+ mm.removeMessageListener(messageName, onmessage);
+ deferred.resolve(message);
+ });
+ return deferred.promise;
+}
+
+function promiseNewTabLoadEvent(aUrl) {
+ let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+ let browser = tab.linkedBrowser;
+ let mm = browser.messageManager;
+
+ // give it an e10s-friendly content script to help with our tests,
+ // and wait for it to tell us about the load.
+ let promise = promiseOneMessage(tab, "test:document:load");
+ mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
+ return promise.then(() => tab);
+}
+
+// Returns a promise which is resolved with the iframe's URL after a new
+// tab is created and the iframe in that tab loads.
+function promiseNewTabWithIframeLoadEvent(aUrl) {
+ let deferred = Promise.defer();
+ let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+ let browser = tab.linkedBrowser;
+ let mm = browser.messageManager;
+
+ // give it an e10s-friendly content script to help with our tests,
+ // and wait for it to tell us about the iframe load.
+ mm.addMessageListener("test:iframe:load", function onFrameLoad(message) {
+ mm.removeMessageListener("test:iframe:load", onFrameLoad);
+ deferred.resolve([tab, message.data.url]);
+ });
+ mm.loadFrameScript(CHROME_BASE + "content_aboutAccounts.js", true);
+ return deferred.promise;
+}
+
+function checkVisibilities(tab, data) {
+ let ids = Object.keys(data);
+ let mm = tab.linkedBrowser.messageManager;
+ let deferred = Promise.defer();
+ mm.addMessageListener("test:check-visibilities-response", function onResponse(message) {
+ mm.removeMessageListener("test:check-visibilities-response", onResponse);
+ for (let id of ids) {
+ is(message.data[id], data[id], "Element '" + id + "' has correct visibility");
+ }
+ deferred.resolve();
+ });
+ mm.sendAsyncMessage("test:check-visibilities", {ids});
+ return deferred.promise;
+}
+
+// watch out - these will fire observers which if you aren't careful, may
+// interfere with the tests.
+function setSignedInUser(data) {
+ if (!data) {
+ data = {
+ email: "foo@example.com",
+ uid: "1234@lcip.org",
+ assertion: "foobar",
+ sessionToken: "dead",
+ kA: "beef",
+ kB: "cafe",
+ verified: true
+ }
+ }
+ return fxAccounts.setSignedInUser(data);
+}
+
+function signOut() {
+ // we always want a "localOnly" signout here...
+ return fxAccounts.signOut(true);
+}
rename from browser/base/content/test/general/browser_fxa_web_channel.html
rename to browser/base/content/test/sync/browser_fxa_web_channel.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_web_channel.js
@@ -0,0 +1,210 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
+ return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
+ "resource://gre/modules/WebChannel.jsm");
+
+// FxAccountsWebChannel isn't explicitly exported by FxAccountsWebChannel.jsm
+// but we can get it here via a backstage pass.
+var {FxAccountsWebChannel} = Components.utils.import("resource://gre/modules/FxAccountsWebChannel.jsm", {});
+
+const TEST_HTTP_PATH = "http://example.com";
+const TEST_BASE_URL = TEST_HTTP_PATH + "/browser/browser/base/content/test/sync/browser_fxa_web_channel.html";
+const TEST_CHANNEL_ID = "account_updates_test";
+
+var gTests = [
+ {
+ desc: "FxA Web Channel - should receive message about profile changes",
+ *run() {
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ });
+ let promiseObserver = new Promise((resolve, reject) => {
+ makeObserver(FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, function(subject, topic, data) {
+ Assert.equal(data, "abc123");
+ client.tearDown();
+ resolve();
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_BASE_URL + "?profile_change"
+ }, function* () {
+ yield promiseObserver;
+ });
+ }
+ },
+ {
+ desc: "fxa web channel - login messages should notify the fxAccounts object",
+ *run() {
+
+ let promiseLogin = new Promise((resolve, reject) => {
+ let login = (accountData) => {
+ Assert.equal(typeof accountData.authAt, "number");
+ Assert.equal(accountData.email, "testuser@testuser.com");
+ Assert.equal(accountData.keyFetchToken, "key_fetch_token");
+ Assert.equal(accountData.sessionToken, "session_token");
+ Assert.equal(accountData.uid, "uid");
+ Assert.equal(accountData.unwrapBKey, "unwrap_b_key");
+ Assert.equal(accountData.verified, true);
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ login
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_BASE_URL + "?login"
+ }, function* () {
+ yield promiseLogin;
+ });
+ }
+ },
+ {
+ desc: "fxa web channel - can_link_account messages should respond",
+ *run() {
+ let properUrl = TEST_BASE_URL + "?can_link_account";
+
+ let promiseEcho = new Promise((resolve, reject) => {
+
+ let webChannelOrigin = Services.io.newURI(properUrl);
+ // responses sent to content are echoed back over the
+ // `fxaccounts_webchannel_response_echo` channel. Ensure the
+ // fxaccounts:can_link_account message is responded to.
+ let echoWebChannel = new WebChannel("fxaccounts_webchannel_response_echo", webChannelOrigin);
+ echoWebChannel.listen((webChannelId, message, target) => {
+ Assert.equal(message.command, "fxaccounts:can_link_account");
+ Assert.equal(message.messageId, 2);
+ Assert.equal(message.data.ok, true);
+
+ client.tearDown();
+ echoWebChannel.stopListening();
+
+ resolve();
+ });
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ shouldAllowRelink(acctName) {
+ return acctName === "testuser@testuser.com";
+ }
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: properUrl
+ }, function* () {
+ yield promiseEcho;
+ });
+ }
+ },
+ {
+ desc: "fxa web channel - logout messages should notify the fxAccounts object",
+ *run() {
+ let promiseLogout = new Promise((resolve, reject) => {
+ let logout = (uid) => {
+ Assert.equal(uid, "uid");
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ logout
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_BASE_URL + "?logout"
+ }, function* () {
+ yield promiseLogout;
+ });
+ }
+ },
+ {
+ desc: "fxa web channel - delete messages should notify the fxAccounts object",
+ *run() {
+ let promiseDelete = new Promise((resolve, reject) => {
+ let logout = (uid) => {
+ Assert.equal(uid, "uid");
+
+ client.tearDown();
+ resolve();
+ };
+
+ let client = new FxAccountsWebChannel({
+ content_uri: TEST_HTTP_PATH,
+ channel_id: TEST_CHANNEL_ID,
+ helpers: {
+ logout
+ }
+ });
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_BASE_URL + "?delete"
+ }, function* () {
+ yield promiseDelete;
+ });
+ }
+ }
+]; // gTests
+
+function makeObserver(aObserveTopic, aObserveFunc) {
+ let callback = function(aSubject, aTopic, aData) {
+ if (aTopic == aObserveTopic) {
+ removeMe();
+ aObserveFunc(aSubject, aTopic, aData);
+ }
+ };
+
+ function removeMe() {
+ Services.obs.removeObserver(callback, aObserveTopic);
+ }
+
+ Services.obs.addObserver(callback, aObserveTopic);
+ return removeMe;
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Task.spawn(function* () {
+ for (let testCase of gTests) {
+ info("Running: " + testCase.desc);
+ yield testCase.run();
+ }
+ }).then(finish, ex => {
+ Assert.ok(false, "Unexpected Exception: " + ex);
+ finish();
+ });
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -0,0 +1,241 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_ui_state_notification_calls_updateAllUI() {
+ let called = false;
+ let updateAllUI = gSync.updateAllUI;
+ gSync.updateAllUI = () => { called = true; };
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ ok(called);
+
+ gSync.updateAllUI = updateAllUI;
+});
+
+add_task(async function test_ui_state_signedin() {
+ let state = {
+ status: UIState.STATUS_SIGNED_IN,
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ avatarURL: "https://foo.bar",
+ lastSync: new Date(),
+ syncing: false
+ };
+
+ gSync.updateAllUI(state);
+
+ checkFxABadge(false);
+ let statusBarTooltip = gSync.panelUIStatus.getAttribute("signedinTooltiptext");
+ let lastSyncTooltip = gSync.formatLastSyncDate(new Date(state.lastSync));
+ checkPanelUIStatusBar({
+ label: "Foo Bar",
+ tooltip: statusBarTooltip,
+ fxastatus: "signedin",
+ avatarURL: "https://foo.bar",
+ syncing: false,
+ syncNowTooltip: lastSyncTooltip
+ });
+ checkRemoteTabsPanel("PanelUI-remotetabs-main", false);
+ checkMenuBarItem("sync-syncnowitem");
+});
+
+add_task(async function test_ui_state_syncing() {
+ let state = {
+ status: UIState.STATUS_SIGNED_IN,
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ avatarURL: "https://foo.bar",
+ lastSync: new Date(),
+ syncing: true
+ };
+
+ gSync.updateAllUI(state);
+
+ checkSyncNowButton("PanelUI-fxa-icon", true);
+ checkSyncNowButton("PanelUI-remotetabs-syncnow", true);
+
+ // Be good citizens and remove the "syncing" state.
+ gSync.updateAllUI({
+ status: UIState.STATUS_SIGNED_IN,
+ email: "foo@bar.com",
+ lastSync: new Date(),
+ syncing: false
+ });
+ // Because we switch from syncing to non-syncing, and there's a timeout involved.
+ await promiseObserver("test:browser-sync:activity-stop");
+});
+
+add_task(async function test_ui_state_unconfigured() {
+ let state = {
+ status: UIState.STATUS_NOT_CONFIGURED
+ };
+
+ gSync.updateAllUI(state);
+
+ checkFxABadge(false);
+ let signedOffLabel = gSync.panelUIStatus.getAttribute("defaultlabel");
+ let statusBarTooltip = gSync.panelUIStatus.getAttribute("signedinTooltiptext");
+ checkPanelUIStatusBar({
+ label: signedOffLabel,
+ tooltip: statusBarTooltip
+ });
+ checkRemoteTabsPanel("PanelUI-remotetabs-setupsync");
+ checkMenuBarItem("sync-setup");
+});
+
+add_task(async function test_ui_state_unverified() {
+ let state = {
+ status: UIState.STATUS_NOT_VERIFIED,
+ email: "foo@bar.com",
+ lastSync: new Date(),
+ syncing: false
+ };
+
+ gSync.updateAllUI(state);
+
+ checkFxABadge(true);
+ let expectedLabel = gSync.panelUIStatus.getAttribute("unverifiedlabel");
+ let tooltipText = gSync.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
+ checkPanelUIStatusBar({
+ label: expectedLabel,
+ tooltip: tooltipText,
+ fxastatus: "unverified",
+ avatarURL: null,
+ syncing: false,
+ syncNowTooltip: tooltipText
+ });
+ checkRemoteTabsPanel("PanelUI-remotetabs-setupsync", false);
+ checkMenuBarItem("sync-setup");
+});
+
+add_task(async function test_ui_state_loginFailed() {
+ let state = {
+ status: UIState.STATUS_LOGIN_FAILED,
+ email: "foo@bar.com"
+ };
+
+ gSync.updateAllUI(state);
+
+ checkFxABadge(true);
+ let expectedLabel = gSync.panelUIStatus.getAttribute("errorlabel");
+ let tooltipText = gSync.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
+ checkPanelUIStatusBar({
+ label: expectedLabel,
+ tooltip: tooltipText,
+ fxastatus: "login-failed",
+ avatarURL: null,
+ syncing: false,
+ syncNowTooltip: tooltipText
+ });
+ checkRemoteTabsPanel("PanelUI-remotetabs-reauthsync", false);
+ checkMenuBarItem("sync-reauthitem");
+});
+
+add_task(async function test_FormatLastSyncDateNow() {
+ let now = new Date();
+ let nowString = gSync.formatLastSyncDate(now);
+ is(nowString, "Last sync: " + now.toLocaleDateString(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}),
+ "The date is correctly formatted");
+});
+
+add_task(async function test_FormatLastSyncDateMonthAgo() {
+ let monthAgo = new Date();
+ monthAgo.setMonth(monthAgo.getMonth() - 1);
+ let monthAgoString = gSync.formatLastSyncDate(monthAgo);
+ is(monthAgoString, "Last sync: " + monthAgo.toLocaleDateString(undefined, {month: "long", day: "numeric"}),
+ "The date is correctly formatted");
+});
+
+function checkFxABadge(shouldBeShown) {
+ let isShown = false;
+ for (let notification of PanelUI.notifications) {
+ if (notification.id == "fxa-needs-authentication") {
+ isShown = true;
+ break;
+ }
+ }
+ is(isShown, shouldBeShown, "the fxa badge has the right visibility");
+}
+
+function checkPanelUIStatusBar({label, tooltip, fxastatus, avatarURL, syncing, syncNowTooltip}) {
+ let labelNode = document.getElementById("PanelUI-fxa-label");
+ let tooltipNode = document.getElementById("PanelUI-fxa-status");
+ let statusNode = document.getElementById("PanelUI-footer-fxa");
+ let avatar = document.getElementById("PanelUI-fxa-avatar");
+
+ is(labelNode.getAttribute("label"), label, "panelUI-fxa label has the right value");
+ is(tooltipNode.getAttribute("tooltiptext"), tooltip, "panelUI-fxa tooltip has the right value");
+ if (fxastatus) {
+ is(statusNode.getAttribute("fxastatus"), fxastatus, "panelUI-fxa fxastatus has the right value");
+ } else {
+ ok(!statusNode.hasAttribute("fxastatus"), "panelUI-fxa fxastatus is unset")
+ }
+ if (avatarURL) {
+ is(avatar.style.listStyleImage, `url("${avatarURL}")`, "panelUI-fxa avatar URL is set");
+ } else {
+ ok(!statusNode.style.listStyleImage, "panelUI-fxa avatar URL is unset");
+ }
+
+ if (syncing != undefined && syncNowTooltip != undefined) {
+ checkSyncNowButton("PanelUI-fxa-icon", syncing, syncNowTooltip);
+ }
+}
+
+function checkRemoteTabsPanel(expectedShownItemId, syncing, syncNowTooltip) {
+ checkItemsVisiblities(["PanelUI-remotetabs-main",
+ "PanelUI-remotetabs-setupsync",
+ "PanelUI-remotetabs-reauthsync"],
+ expectedShownItemId);
+
+ if (syncing != undefined && syncNowTooltip != undefined) {
+ checkSyncNowButton("PanelUI-remotetabs-syncnow", syncing, syncNowTooltip);
+ }
+}
+
+function checkMenuBarItem(expectedShownItemId) {
+ checkItemsVisiblities(["sync-setup", "sync-syncnowitem", "sync-reauthitem"],
+ expectedShownItemId);
+}
+
+function checkSyncNowButton(buttonId, syncing, tooltip = null) {
+ const remoteTabsButton = document.getElementById(buttonId);
+
+ is(remoteTabsButton.getAttribute("syncstatus"), syncing ? "active" : "", "button active has the right value");
+ if (tooltip) {
+ is(remoteTabsButton.getAttribute("tooltiptext"), tooltip, "button tooltiptext is set to the right value");
+ }
+
+ if (buttonId == "PanelUI-fxa-icon") {
+ return;
+ }
+
+ is(remoteTabsButton.hasAttribute("disabled"), syncing, "disabled has the right value");
+ if (syncing) {
+ is(remoteTabsButton.getAttribute("label"), gSync.syncStrings.GetStringFromName("syncing2.label"), "label is set to the right value");
+ } else {
+ is(remoteTabsButton.getAttribute("label"), gSync.syncStrings.GetStringFromName("syncnow.label"), "label is set to the right value");
+ }
+}
+
+// Only one item visible at a time.
+function checkItemsVisiblities(itemsIds, expectedShownItemId) {
+ for (let id of itemsIds) {
+ if (id == expectedShownItemId) {
+ ok(!document.getElementById(id).hidden, "menuitem " + id + " should be visible");
+ } else {
+ ok(document.getElementById(id).hidden, "menuitem " + id + " should be hidden");
+ }
+ }
+}
+
+function promiseObserver(topic) {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ Services.obs.removeObserver(obs, aTopic);
+ resolve(aSubject);
+ }
+ Services.obs.addObserver(obs, topic);
+ });
+}
rename from browser/base/content/test/general/content_aboutAccounts.js
rename to browser/base/content/test/sync/content_aboutAccounts.js
--- a/browser/base/content/web-panels.xul
+++ b/browser/base/content/web-panels.xul
@@ -19,17 +19,17 @@
<page id="webpanels-window"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
onload="load()" onunload="unload()">
<script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
<script type="application/javascript" src="chrome://browser/content/browser.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
- <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
<script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
<script type="application/javascript" src="chrome://browser/content/web-panels.js"/>
<stringbundleset id="stringbundleset">
<stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
</stringbundleset>
<broadcasterset id="mainBroadcasterSet">
--- a/browser/base/content/webext-panels.xul
+++ b/browser/base/content/webext-panels.xul
@@ -19,17 +19,17 @@
<page id="webextpanels-window"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
onload="load()">
<script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
<script type="application/javascript" src="chrome://browser/content/browser.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-social.js"/>
- <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/>
+ <script type="application/javascript" src="chrome://browser/content/browser-sync.js"/>
<script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
<script type="application/javascript" src="chrome://browser/content/webext-panels.js"/>
<stringbundleset id="stringbundleset">
<stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
</stringbundleset>
<broadcasterset id="mainBroadcasterSet">
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -68,26 +68,25 @@ browser.jar:
content/browser/browser-captivePortal.js (content/browser-captivePortal.js)
content/browser/browser-ctrlTab.js (content/browser-ctrlTab.js)
content/browser/browser-customization.js (content/browser-customization.js)
content/browser/browser-data-submission-info-bar.js (content/browser-data-submission-info-bar.js)
content/browser/browser-compacttheme.js (content/browser-compacttheme.js)
content/browser/browser-feeds.js (content/browser-feeds.js)
content/browser/browser-fullScreenAndPointerLock.js (content/browser-fullScreenAndPointerLock.js)
content/browser/browser-fullZoom.js (content/browser-fullZoom.js)
- content/browser/browser-fxaccounts.js (content/browser-fxaccounts.js)
content/browser/browser-gestureSupport.js (content/browser-gestureSupport.js)
content/browser/browser-media.js (content/browser-media.js)
content/browser/browser-places.js (content/browser-places.js)
content/browser/browser-plugins.js (content/browser-plugins.js)
content/browser/browser-refreshblocker.js (content/browser-refreshblocker.js)
content/browser/browser-safebrowsing.js (content/browser-safebrowsing.js)
content/browser/browser-sidebar.js (content/browser-sidebar.js)
content/browser/browser-social.js (content/browser-social.js)
- content/browser/browser-syncui.js (content/browser-syncui.js)
+ content/browser/browser-sync.js (content/browser-sync.js)
* content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml)
#ifdef CAN_DRAW_IN_TITLEBAR
content/browser/browser-tabsintitlebar.js (content/browser-tabsintitlebar.js)
#else
content/browser/browser-tabsintitlebar.js (content/browser-tabsintitlebar-stub.js)
#endif
content/browser/browser-thumbnails.js (content/browser-thumbnails.js)
content/browser/browser-trackingprotection.js (content/browser-trackingprotection.js)
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -24,16 +24,17 @@ BROWSER_CHROME_MANIFESTS += [
'content/test/permissions/browser.ini',
'content/test/plugins/browser.ini',
'content/test/popupNotifications/browser.ini',
'content/test/popups/browser.ini',
'content/test/referrer/browser.ini',
'content/test/siteIdentity/browser.ini',
'content/test/social/browser.ini',
'content/test/static/browser.ini',
+ 'content/test/sync/browser.ini',
'content/test/tabcrashed/browser.ini',
'content/test/tabPrompts/browser.ini',
'content/test/tabs/browser.ini',
'content/test/urlbar/browser.ini',
'content/test/webextensions/browser.ini',
'content/test/webrtc/browser.ini',
'content/test/windows/browser.ini',
]
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -467,17 +467,17 @@ const CustomizableWidgets = [
},
_appendClient(client, attachFragment, maxTabs = this.TABS_PER_PAGE) {
let doc = attachFragment.ownerDocument;
// Create the element for the remote client.
let clientItem = doc.createElementNS(kNSXUL, "label");
clientItem.setAttribute("itemtype", "client");
let window = doc.defaultView;
clientItem.setAttribute("tooltiptext",
- window.gSyncUI.formatLastSyncDate(new Date(client.lastModified)));
+ window.gSync.formatLastSyncDate(new Date(client.lastModified)));
clientItem.textContent = client.name;
attachFragment.appendChild(clientItem);
if (client.tabs.length == 0) {
let label = this._appendMessageLabel("notabsforclientlabel", attachFragment);
label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
} else {
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -26,29 +26,31 @@
label="&updateManual.panelUI.label;"
hidden="true"/>
<toolbarbutton id="PanelUI-update-restart-menu-item"
wrap="true"
label="&updateRestart.panelUI.label;"
hidden="true"/>
<hbox id="PanelUI-footer-fxa">
<hbox id="PanelUI-fxa-status"
+ label="&fxaSignedIn.tooltip;"
defaultlabel="&fxaSignIn.label;"
signedinTooltiptext="&fxaSignedIn.tooltip;"
tooltiptext="&fxaSignedIn.tooltip;"
errorlabel="&fxaSignInError.label;"
unverifiedlabel="&fxaUnverified.label;"
- onclick="if (event.which == 1) gFxAccounts.onMenuPanelCommand();">
+ onclick="if (event.which == 1) gSync.onMenuPanelCommand();">
<image id="PanelUI-fxa-avatar"/>
<toolbarbutton id="PanelUI-fxa-label"
+ label="&fxaSignIn.label;"
fxabrandname="&syncBrand.fxAccount.label;"/>
</hbox>
<toolbarseparator/>
<toolbarbutton id="PanelUI-fxa-icon"
- oncommand="gSyncUI.doSync();"
+ oncommand="gSync.doSync();"
closemenu="none">
<observes element="sync-status" attribute="syncstatus"/>
<observes element="sync-status" attribute="tooltiptext"/>
</toolbarbutton>
</hbox>
<hbox id="PanelUI-footer-inner">
<toolbarbutton id="PanelUI-customize" label="&appMenuCustomize.label;"
@@ -120,21 +122,21 @@
<vbox id="PanelUI-remotetabs-buttons">
<toolbarbutton id="PanelUI-remotetabs-view-sidebar"
class="subviewbutton"
observes="viewTabsSidebar"
label="&appMenuRemoteTabs.sidebar.label;"/>
<toolbarbutton id="PanelUI-remotetabs-view-managedevices"
class="subviewbutton"
label="&appMenuRemoteTabs.managedevices.label;"
- oncommand="gFxAccounts.openDevicesManagementPage('syncedtabs-menupanel');"/>
+ oncommand="gSync.openDevicesManagementPage('syncedtabs-menupanel');"/>
<toolbarbutton id="PanelUI-remotetabs-syncnow"
observes="sync-status"
class="subviewbutton"
- oncommand="gSyncUI.doSync();"
+ oncommand="gSync.doSync();"
closemenu="none"/>
<menuseparator id="PanelUI-remotetabs-separator"/>
</vbox>
<deck id="PanelUI-remotetabs-deck">
<!-- Sync is ready to Sync and the "tabs" engine is enabled -->
<vbox id="PanelUI-remotetabs-tabspane">
<vbox id="PanelUI-remotetabs-tabslist"
showAllLabel="&appMenuRemoteTabs.showAll.label;"
@@ -149,17 +151,17 @@
<vbox class="PanelUI-remotetabs-instruction-box" align="center">
<hbox pack="center">
<html:img class="fxaSyncIllustration" src="chrome://browser/skin/fxa/sync-illustration.svg"/>
</hbox>
<label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.tabsnotsyncing.label;</label>
<hbox pack="center">
<toolbarbutton class="PanelUI-remotetabs-prefs-button"
label="&appMenuRemoteTabs.openprefs.label;"
- oncommand="gSyncUI.openPrefs('synced-tabs');"/>
+ oncommand="gSync.openPrefs('synced-tabs');"/>
</hbox>
</vbox>
</hbox>
<!-- Sync is ready to Sync but we are still fetching the tabs to show -->
<vbox id="PanelUI-remotetabs-fetching">
<!-- Show intentionally blank panel, see bug 1239845 -->
</vbox>
<!-- Sync has only 1 (ie, this) device connected -->
@@ -183,31 +185,31 @@
flex="1"
align="center"
class="PanelUI-remotetabs-instruction-box"
observes="sync-setup-state">
<html:img class="fxaSyncIllustration" src="chrome://browser/skin/fxa/sync-illustration.svg"/>
<label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
<toolbarbutton class="PanelUI-remotetabs-prefs-button"
label="&appMenuRemoteTabs.signin.label;"
- oncommand="gSyncUI.openPrefs('synced-tabs');"/>
+ oncommand="gSync.openPrefs('synced-tabs');"/>
</vbox>
<!-- When Sync needs re-authentication. This uses the exact same messaging
as "Sync is not configured" but remains a separate box so we get
the goodness of observing broadcasters to manage the hidden states -->
<vbox id="PanelUI-remotetabs-reauthsync"
flex="1"
align="center"
class="PanelUI-remotetabs-instruction-box"
observes="sync-reauth-state">
<html:img class="fxaSyncIllustration" src="chrome://browser/skin/fxa/sync-illustration.svg"/>
<label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
<toolbarbutton class="PanelUI-remotetabs-prefs-button"
label="&appMenuRemoteTabs.signin.label;"
- oncommand="gSyncUI.openPrefs('synced-tabs');"/>
+ oncommand="gSync.openPrefs('synced-tabs');"/>
</vbox>
</hbox>
</vbox>
</panelview>
<panelview id="PanelUI-bookmarks" flex="1" class="PanelUI-subView">
<label value="&bookmarksMenu.label;" class="panel-subview-header"/>
<vbox class="panel-subview-body">
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -117,17 +117,16 @@ skip-if = os == "linux"
[browser_981305_separator_insertion.js]
[browser_981418-widget-onbeforecreated-handler.js]
[browser_982656_restore_defaults_builtin_widgets.js]
[browser_984455_bookmarks_items_reparenting.js]
skip-if = os == "linux"
[browser_985815_propagate_setToolbarVisibility.js]
[browser_987177_destroyWidget_xul.js]
[browser_987177_xul_wrapper_updating.js]
-[browser_987185_syncButton.js]
[browser_987492_window_api.js]
[browser_987640_charEncoding.js]
[browser_988072_sidebar_events.js]
[browser_989338_saved_placements_not_resaved.js]
[browser_989751_subviewbutton_class.js]
[browser_992747_toggle_noncustomizable_toolbar.js]
[browser_993322_widget_notoolbar.js]
[browser_995164_registerArea_during_customize_mode.js]
@@ -152,8 +151,9 @@ skip-if = os == "mac"
[browser_overflow_use_subviews.js]
[browser_panel_toggle.js]
[browser_panelUINotifications.js]
[browser_switch_to_customize_mode.js]
[browser_synced_tabs_menu.js]
[browser_check_tooltips_in_navbar.js]
[browser_editcontrols_update.js]
subsuite = clipboard
+[browser_remote_tabs_button.js]
deleted file mode 100755
--- a/browser/components/customizableui/test/browser_987185_syncButton.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/* 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";
-
-var syncService = {};
-Components.utils.import("resource://services-sync/service.js", syncService);
-
-var needsSetup;
-var originalSync;
-var service = syncService.Service;
-var syncWasCalled = false;
-
-add_task(function* testSyncButtonFunctionality() {
- info("Check Sync button functionality");
- storeInitialValues();
- mockFunctions();
-
- // add the Sync button to the panel
- CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
-
- // check the button's functionality
- yield PanelUI.show();
- info("The panel menu was opened");
-
- let syncButton = document.getElementById("sync-button");
- ok(syncButton, "The Sync button was added to the Panel Menu");
- // click the button - the panel should open.
- syncButton.click();
- let syncPanel = document.getElementById("PanelUI-remotetabs");
- ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
-
- // Find and click the "setup" button.
- let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
- syncNowButton.click();
-
- info("The sync button was clicked");
-
- yield waitForCondition(() => syncWasCalled);
-});
-
-add_task(function* asyncCleanup() {
- // reset the panel UI to the default state
- yield resetCustomization();
- ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
-
- if (isPanelUIOpen()) {
- let panelHidePromise = promisePanelHidden(window);
- PanelUI.hide();
- yield panelHidePromise;
- }
-
- restoreValues();
-});
-
-function mockFunctions() {
- // mock needsSetup
- gSyncUI._needsSetup = () => Promise.resolve(false);
-
- // mock service.errorHandler.syncAndReportErrors()
- service.errorHandler.syncAndReportErrors = mocked_syncAndReportErrors;
-}
-
-function mocked_syncAndReportErrors() {
- syncWasCalled = true;
-}
-
-function restoreValues() {
- gSyncUI._needsSetup = needsSetup;
- service.sync = originalSync;
-}
-
-function storeInitialValues() {
- needsSetup = gSyncUI._needsSetup;
- originalSync = service.sync;
-}
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_remote_tabs_button.js
@@ -0,0 +1,84 @@
+/* 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";
+
+let syncService = {};
+Components.utils.import("resource://services-sync/service.js", syncService);
+const service = syncService.Service;
+Components.utils.import("resource://services-sync/UIState.jsm");
+
+let getState;
+let originalSync;
+let syncWasCalled = false;
+
+// TODO: This test should probably be re-written, we don't really test much here.
+add_task(async function testSyncRemoteTabsButtonFunctionality() {
+ info("Test the Sync Remote Tabs button in the PanelUI");
+ storeInitialValues();
+ mockFunctions();
+
+ // Force UI update.
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ // add the sync remote tabs button to the panel
+ CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+
+ // check the button's functionality
+ await PanelUI.show();
+ info("The panel menu was opened");
+
+ let syncRemoteTabsBtn = document.getElementById("sync-button");
+ ok(syncRemoteTabsBtn, "The sync remote tabs button was added to the Panel Menu");
+ // click the button - the panel should open.
+ syncRemoteTabsBtn.click();
+ let remoteTabsPanel = document.getElementById("PanelUI-remotetabs");
+ ok(remoteTabsPanel.getAttribute("current"), "Sync Panel is in view");
+
+ // Find and click the "setup" button.
+ let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
+ syncNowButton.click();
+ info("The sync now button was clicked");
+
+ await waitForCondition(() => syncWasCalled);
+});
+
+add_task(async function asyncCleanup() {
+ // reset the panel UI to the default state
+ await resetCustomization();
+ ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
+
+ if (isPanelUIOpen()) {
+ let panelHidePromise = promisePanelHidden(window);
+ PanelUI.hide();
+ await panelHidePromise;
+ }
+
+ restoreValues();
+});
+
+function mockFunctions() {
+ // mock UIState.get()
+ UIState.get = () => ({
+ status: UIState.STATUS_SIGNED_IN,
+ email: "user@mozilla.com"
+ });
+
+ // mock service.errorHandler.syncAndReportErrors()
+ service.errorHandler.syncAndReportErrors = mocked_syncAndReportErrors;
+}
+
+function mocked_syncAndReportErrors() {
+ syncWasCalled = true;
+}
+
+function restoreValues() {
+ UIState.get = getState;
+ service.syncAndReportErrors = originalSync;
+}
+
+function storeInitialValues() {
+ getState = UIState.get;
+ originalSync = service.syncAndReportErrors;
+}
--- a/browser/components/customizableui/test/browser_synced_tabs_menu.js
+++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js
@@ -224,21 +224,21 @@ add_task(function* () {
// The widget is still fetching tabs, as we've neutered everything that
// provides them
is(deck.selectedIndex, DECKINDEX_FETCHING, "first deck entry is visible");
let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
let didSync = false;
- let oldDoSync = gSyncUI.doSync;
- gSyncUI.doSync = function() {
+ let oldDoSync = gSync.doSync;
+ gSync.doSync = function() {
didSync = true;
mockedInternal.hasSyncedThisSession = true;
- gSyncUI.doSync = oldDoSync;
+ gSync.doSync = oldDoSync;
}
syncNowButton.click();
ok(didSync, "clicking the button called the correct function");
// Tell the widget there are tabs available, but with zero clients.
mockedInternal.getTabClients = () => {
return Promise.resolve([]);
}
--- a/browser/components/syncedtabs/SyncedTabsDeckComponent.js
+++ b/browser/components/syncedtabs/SyncedTabsDeckComponent.js
@@ -117,17 +117,17 @@ SyncedTabsDeckComponent.prototype = {
// There's no good way to mock fxAccounts in browser tests where it's already
// been instantiated, so we have this method for stubbing.
_accountStatus() {
return this._fxAccounts.accountStatus();
},
getPanelStatus() {
return this._accountStatus().then(exists => {
- if (!exists || this._getChromeWindow(this._window).gSyncUI.loginFailed()) {
+ if (!exists || this._SyncedTabs.loginFailed) {
return this.PANELS.NOT_AUTHED_INFO;
}
if (!this._SyncedTabs.isConfiguredToSyncTabs) {
return this.PANELS.TABS_DISABLED;
}
if (!this._SyncedTabs.hasSyncedThisSession) {
return this.PANELS.TABS_FETCHING;
}
@@ -161,12 +161,12 @@ SyncedTabsDeckComponent.prototype = {
this._openUrl(href, event);
},
_openUrl(url, event) {
this._window.openUILink(url, event);
},
openSyncPrefs() {
- this._getChromeWindow(this._window).gSyncUI.openPrefs("tabs-sidebar");
+ this._getChromeWindow(this._window).gSync.openPrefs("tabs-sidebar");
}
};
--- a/browser/components/syncedtabs/TabListView.js
+++ b/browser/components/syncedtabs/TabListView.js
@@ -208,17 +208,17 @@ TabListView.prototype = {
* Update the element representing an item, ensuring it's in sync with the
* underlying data.
* @param {client} item - Item to use as a source.
* @param {Element} itemNode - Element to update.
*/
_updateClient(item, itemNode) {
itemNode.setAttribute("id", "item-" + item.id);
let lastSync = new Date(item.lastModified);
- let lastSyncTitle = getChromeWindow(this._window).gSyncUI.formatLastSyncDate(lastSync);
+ let lastSyncTitle = getChromeWindow(this._window).gSync.formatLastSyncDate(lastSync);
itemNode.setAttribute("title", lastSyncTitle);
if (item.closed) {
itemNode.classList.add("closed");
} else {
itemNode.classList.remove("closed");
}
if (item.selected) {
itemNode.classList.add("selected");
--- a/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js
+++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js
@@ -135,49 +135,38 @@ add_task(function* testPanelStatus() {
let listStore = new SyncedTabsListStore();
let listComponent = {};
let fxAccounts = {
accountStatus() {}
};
let SyncedTabsMock = {
getTabClients() {}
};
- let loginFailed = false;
- let chromeWindowMock = {
- gSyncUI: {
- loginFailed() {
- return loginFailed;
- }
- }
- };
- let getChromeWindowMock = sinon.stub();
- getChromeWindowMock.returns(chromeWindowMock);
sinon.stub(listStore, "getData");
let component = new SyncedTabsDeckComponent({
fxAccounts,
deckStore,
listComponent,
- SyncedTabs: SyncedTabsMock,
- getChromeWindowMock
+ SyncedTabs: SyncedTabsMock
});
let isAuthed = false;
sinon.stub(fxAccounts, "accountStatus", () => Promise.resolve(isAuthed));
let result = yield component.getPanelStatus();
Assert.equal(result, component.PANELS.NOT_AUTHED_INFO);
isAuthed = true;
- loginFailed = true;
+ SyncedTabsMock.loginFailed = true;
result = yield component.getPanelStatus();
Assert.equal(result, component.PANELS.NOT_AUTHED_INFO);
- loginFailed = false;
+ SyncedTabsMock.loginFailed = false;
SyncedTabsMock.isConfiguredToSyncTabs = false;
result = yield component.getPanelStatus();
Assert.equal(result, component.PANELS.TABS_DISABLED);
SyncedTabsMock.isConfiguredToSyncTabs = true;
SyncedTabsMock.hasSyncedThisSession = false;
@@ -206,22 +195,22 @@ add_task(function* testPanelStatus() {
Assert.ok(deckStore.selectPanel.calledWith("mock-panelId"));
});
add_task(function* testActions() {
let windowMock = {
openUILink() {},
};
let chromeWindowMock = {
- gSyncUI: {
+ gSync: {
openPrefs() {}
}
};
sinon.spy(windowMock, "openUILink");
- sinon.spy(chromeWindowMock.gSyncUI, "openPrefs");
+ sinon.spy(chromeWindowMock.gSync, "openPrefs");
let getChromeWindowMock = sinon.stub();
getChromeWindowMock.returns(chromeWindowMock);
let component = new SyncedTabsDeckComponent({
window: windowMock,
getChromeWindowMock
});
@@ -231,10 +220,10 @@ add_task(function* testActions() {
Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
href = Services.prefs.getCharPref("identity.mobilepromo.ios") + "synced-tabs-sidebar";
component.openiOSLink("mock-event");
Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
component.openSyncPrefs();
Assert.ok(getChromeWindowMock.calledWith(windowMock));
- Assert.ok(chromeWindowMock.gSyncUI.openPrefs.called);
+ Assert.ok(chromeWindowMock.gSync.openPrefs.called);
});
--- a/browser/components/uitour/test/browser_fxa.js
+++ b/browser/components/uitour/test/browser_fxa.js
@@ -13,34 +13,34 @@ var gContentAPI;
var gContentWindow;
function test() {
UITourTest();
}
registerCleanupFunction(function*() {
yield signOut();
- gFxAccounts.updateUI();
+ gSync.updateAllUI(UIState.get());
});
var tests = [
taskify(function* test_highlight_accountStatus_loggedOut() {
let userData = yield fxAccounts.getSignedInUser();
is(userData, null, "Not logged in initially");
yield showMenuPromise("appMenu");
yield showHighlightPromise("accountStatus");
let highlight = document.getElementById("UITourHighlightContainer");
is(highlight.getAttribute("targetName"), "accountStatus", "Correct highlight target");
}),
taskify(function* test_highlight_accountStatus_loggedIn() {
yield setSignedInUser();
let userData = yield fxAccounts.getSignedInUser();
isnot(userData, null, "Logged in now");
- gFxAccounts.updateUI(); // Causes a leak (see bug 1332985)
+ gSync.updateAllUI(UIState.get());
yield showMenuPromise("appMenu");
yield showHighlightPromise("accountStatus");
let highlight = document.getElementById("UITourHighlightContainer");
is(highlight.popupBoxObject.anchorNode.id, "PanelUI-fxa-avatar", "Anchored on avatar");
is(highlight.getAttribute("targetName"), "accountStatus", "Correct highlight target");
}),
];
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -570,17 +570,18 @@ toolbarpaletteitem[place="palette"] > to
display: none;
}
#PanelUI-footer-fxa:not([fxastatus="signedin"]) > toolbarseparator,
#PanelUI-footer-fxa:not([fxastatus="signedin"]) > #PanelUI-fxa-icon {
display: none;
}
-#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status::after {
+#PanelUI-footer-fxa[fxastatus="login-failed"] > #PanelUI-fxa-status::after,
+#PanelUI-footer-fxa[fxastatus="unverified"] > #PanelUI-fxa-status::after {
content: url(chrome://browser/skin/warning.svg);
filter: drop-shadow(0 1px 0 hsla(206,50%,10%,.15));
width: 47px;
padding-top: 1px;
display: block;
text-align: center;
position: relative;
top: 25%;
@@ -949,26 +950,29 @@ toolbarpaletteitem[place="palette"] > to
#PanelUI-fxa-status:hover,
#PanelUI-fxa-status:hover:active,
#PanelUI-fxa-icon:hover,
#PanelUI-fxa-icon:hover:active {
outline: none;
}
-#PanelUI-footer-fxa[fxastatus="error"] {
+#PanelUI-footer-fxa[fxastatus="login-failed"],
+#PanelUI-footer-fxa[fxastatus="unverified"] {
background-color: hsl(42,94%,88%);
border-top: 1px solid hsl(42,94%,70%);
}
-#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover {
+#PanelUI-footer-fxa[fxastatus="login-failed"] > #PanelUI-fxa-status:hover,
+#PanelUI-footer-fxa[fxastatus="unverified"] > #PanelUI-fxa-status:hover {
background-color: hsl(42,94%,85%);
}
-#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover:active {
+#PanelUI-footer-fxa[fxastatus="login-failed"] > #PanelUI-fxa-status:hover:active,
+#PanelUI-footer-fxa[fxastatus="unverified"] > #PanelUI-fxa-status:hover:active {
background-color: hsl(42,94%,82%);
box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
}
.PanelUI-notification-menu-item {
color: black;
background-color: hsla(96,65%,75%,.5);
}
--- a/services/fxaccounts/FxAccountsWebChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -80,17 +80,19 @@ this.FxAccountsWebChannel = function(opt
this._contentUri = options.content_uri;
if (!options["channel_id"]) {
throw new Error("Missing 'channel_id' option");
}
this._webChannelId = options.channel_id;
// options.helpers is only specified by tests.
- this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
+ XPCOMUtils.defineLazyGetter(this, "_helpers", () => {
+ return options.helpers || new FxAccountsWebChannelHelpers(options);
+ });
this._setupChannel();
};
this.FxAccountsWebChannel.prototype = {
/**
* WebChannel that is used to communicate with content page
*/
--- a/services/sync/modules/SyncedTabs.jsm
+++ b/services/sync/modules/SyncedTabs.jsm
@@ -208,16 +208,24 @@ let SyncedTabsInternal = {
case "nsPref:changed":
Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED);
break;
default:
break;
}
},
+ get loginFailed() {
+ if (!weaveXPCService.ready) {
+ log.debug("Sync isn't yet ready; assuming the login didn't fail");
+ return false;
+ }
+ return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+ },
+
// Returns true if Sync is configured to Sync tabs, false otherwise
get isConfiguredToSyncTabs() {
if (!weaveXPCService.ready) {
log.debug("Sync isn't yet ready; assuming tab engine is enabled");
return true;
}
let engine = Weave.Service.engineManager.get("tabs");
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/UIState.jsm
@@ -0,0 +1,263 @@
+/* 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";
+
+ /**
+ * @typedef {Object} UIState
+ * @property {string} status The Sync/FxA status, see STATUS_* constants.
+ * @property {string} [email] The FxA email configured to log-in with Sync.
+ * @property {string} [displayName] The user's FxA display name.
+ * @property {string} [avatarURL] The user's FxA avatar URL.
+ * @property {Date} [lastSync] The last sync time.
+ * @property {boolean} [syncing] Whether or not we are currently syncing.
+ */
+
+this.EXPORTED_SYMBOLS = ["UIState"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+ "resource://services-sync/main.js");
+
+const TOPICS = [
+ "weave:service:login:change",
+ "weave:service:login:error",
+ "weave:service:ready",
+ "weave:service:sync:start",
+ "weave:service:sync:finish",
+ "weave:service:sync:error",
+ "fxaccounts:onlogin", // Defined in FxAccountsCommon, pulling it is expensive.
+ "fxaccounts:onlogout",
+ "fxaccounts:profilechange",
+];
+
+const ON_UPDATE = "sync-ui-state:update"
+
+const STATUS_NOT_CONFIGURED = "not_configured";
+const STATUS_LOGIN_FAILED = "login_failed";
+const STATUS_NOT_VERIFIED = "not_verified";
+const STATUS_SIGNED_IN = "signed_in";
+
+const DEFAULT_STATE = {
+ status: STATUS_NOT_CONFIGURED
+};
+
+const UIStateInternal = {
+ _initialized: false,
+ _state: null,
+
+ // We keep _syncing out of the state object because we can only track it
+ // using sync events and we can't determine it at any point in time.
+ _syncing: false,
+
+ get state() {
+ if (!this._state) {
+ return DEFAULT_STATE;
+ }
+ return Object.assign({}, this._state, { syncing: this._syncing });
+ },
+
+ isReady() {
+ if (!this._initialized) {
+ this.init();
+ return false;
+ }
+ return true;
+ },
+
+ init() {
+ this._initialized = true;
+ // Refresh the state in the background.
+ this.refreshState().catch(e => {
+ Cu.reportError(e);
+ });
+ },
+
+ // Used for testing.
+ reset() {
+ this._state = null;
+ this._syncing = false;
+ this._initialized = false;
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "weave:service:sync:start":
+ this.toggleSyncActivity(true);
+ break;
+ case "weave:service:sync:finish":
+ case "weave:service:sync:error":
+ this.toggleSyncActivity(false);
+ break;
+ default:
+ this.refreshState().catch(e => {
+ Cu.reportError(e);
+ });
+ break;
+ }
+ },
+
+ // Builds a new state from scratch.
+ async refreshState() {
+ this._state = {};
+ await this._refreshFxAState();
+ this._setLastSyncTime(this._state); // We want this in case we change accounts.
+
+ this.notifyStateUpdated();
+ return this.state;
+ },
+
+ // Update the current state with the last sync time/currently syncing status.
+ toggleSyncActivity(syncing) {
+ this._syncing = syncing;
+ this._setLastSyncTime(this._state);
+
+ this.notifyStateUpdated();
+ },
+
+ notifyStateUpdated() {
+ Services.obs.notifyObservers(null, ON_UPDATE);
+ },
+
+ async _refreshFxAState() {
+ let userData = await this._getUserData();
+ this._populateWithUserData(this._state, userData);
+ if (this.state.status != STATUS_SIGNED_IN) {
+ return;
+ }
+ let profile = await this._getProfile();
+ if (!profile) {
+ return;
+ }
+ this._populateWithProfile(this._state, profile);
+ },
+
+ _populateWithUserData(state, userData) {
+ let status;
+ if (!userData) {
+ status = STATUS_NOT_CONFIGURED;
+ } else {
+ if (this._loginFailed()) {
+ status = STATUS_LOGIN_FAILED;
+ } else if (!userData.verified) {
+ status = STATUS_NOT_VERIFIED;
+ } else {
+ status = STATUS_SIGNED_IN;
+ }
+ state.email = userData.email;
+ }
+ state.status = status;
+ },
+
+ _populateWithProfile(state, profile) {
+ state.displayName = profile.displayName;
+ state.avatarURL = profile.avatar;
+ },
+
+ async _getUserData() {
+ try {
+ return await this.fxAccounts.getSignedInUser();
+ } catch (e) {
+ // This is most likely in tests, where we quickly log users in and out.
+ // The most likely scenario is a user logged out, so reflect that.
+ // Bug 995134 calls for better errors so we could retry if we were
+ // sure this was the failure reason.
+ Cu.reportError("Error updating FxA account info: " + e);
+ return null;
+ }
+ },
+
+ async _getProfile() {
+ try {
+ return await this.fxAccounts.getSignedInUserProfile();
+ } catch (e) {
+ // Not fetching the profile is sad but the FxA logs will already have noise.
+ return null;
+ }
+ },
+
+ _setLastSyncTime(state) {
+ if (state.status == UIState.STATUS_SIGNED_IN) {
+ try {
+ state.lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync", null));
+ } catch (_) {
+ state.lastSync = null;
+ }
+ }
+ },
+
+ _loginFailed() {
+ // Referencing Weave.Service will implicitly initialize sync, and we don't
+ // want to force that - so first check if it is ready.
+ let service = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+ if (!service.ready) {
+ return false;
+ }
+ // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
+ // All other login failures are assumed to be transient and should go
+ // away by themselves, so aren't reflected here.
+ return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+ },
+
+ set fxAccounts(mockFxAccounts) {
+ delete this.fxAccounts;
+ this.fxAccounts = mockFxAccounts;
+ }
+};
+
+XPCOMUtils.defineLazyModuleGetter(UIStateInternal, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+
+for (let topic of TOPICS) {
+ Services.obs.addObserver(UIStateInternal, topic);
+}
+
+this.UIState = {
+ _internal: UIStateInternal,
+
+ ON_UPDATE,
+
+ STATUS_NOT_CONFIGURED,
+ STATUS_LOGIN_FAILED,
+ STATUS_NOT_VERIFIED,
+ STATUS_SIGNED_IN,
+
+ /**
+ * Returns true if the module has been initialized and the state set.
+ * If not, return false and trigger an init in the background.
+ */
+ isReady() {
+ return this._internal.isReady();
+ },
+
+ /**
+ * @returns {UIState} The current Sync/FxA UI State.
+ */
+ get() {
+ return this._internal.state;
+ },
+
+ /**
+ * Refresh the state. Used for testing, don't call this directly since
+ * UIState already listens to Sync/FxA notifications to determine if the state
+ * needs to be refreshed. ON_UPDATE will be fired once the state is refreshed.
+ *
+ * @returns {Promise<UIState>} Resolved once the state is refreshed.
+ */
+ refresh() {
+ return this._internal.refreshState();
+ },
+
+ /**
+ * Reset the state of the whole module. Used for testing.
+ */
+ reset() {
+ this._internal.reset();
+ }
+};
--- a/services/sync/moz.build
+++ b/services/sync/moz.build
@@ -31,16 +31,17 @@ EXTRA_JS_MODULES['services-sync'] += [
'modules/policies.js',
'modules/record.js',
'modules/resource.js',
'modules/rest.js',
'modules/service.js',
'modules/status.js',
'modules/SyncedTabs.jsm',
'modules/telemetry.js',
+ 'modules/UIState.jsm',
'modules/util.js',
]
EXTRA_PP_JS_MODULES['services-sync'] += [
'modules/constants.js',
]
# Definitions used by constants.js
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_uistate.js
@@ -0,0 +1,260 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// ================================================
+// Load mocking/stubbing library, sinon
+// docs: http://sinonjs.org/docs/
+/* global sinon */
+Cu.import("resource://gre/modules/Timer.jsm");
+let window = {
+ document: {},
+ location: {},
+ setTimeout,
+ setInterval,
+ clearTimeout,
+ clearInterval,
+};
+let self = window;
+let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
+loader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
+// ================================================
+
+Cu.import("resource://services-sync/UIState.jsm");
+
+const UIStateInternal = UIState._internal;
+
+add_task(async function test_isReady() {
+ UIState.reset();
+
+ let refreshState = sinon.spy(UIStateInternal, "refreshState");
+
+ // On the first call, returns false and triggers a refresh of the state
+ ok(!UIState.isReady());
+ ok(refreshState.calledOnce);
+ refreshState.reset();
+
+ // On subsequent calls, only return true
+ ok(UIState.isReady());
+ ok(!refreshState.called);
+
+ refreshState.restore();
+});
+
+add_task(async function test_refreshState_signedin() {
+ UIState.reset();
+ const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+ const now = new Date().toString();
+ Services.prefs.setCharPref("services.sync.lastSync", now);
+ UIStateInternal.syncing = false;
+
+ UIStateInternal.fxAccounts = {
+ getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
+ getSignedInUserProfile: () => Promise.resolve({ displayName: "Foo Bar", avatar: "https://foo/bar" })
+ }
+
+ let state = await UIState.refresh();
+
+ equal(state.status, UIState.STATUS_SIGNED_IN);
+ equal(state.email, "foo@bar.com");
+ equal(state.displayName, "Foo Bar");
+ equal(state.avatarURL, "https://foo/bar");
+ equal(state.lastSync, now);
+ equal(state.syncing, false);
+
+ UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_refreshState_signedin_profile_unavailable() {
+ UIState.reset();
+ const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+ const now = new Date().toString();
+ Services.prefs.setCharPref("services.sync.lastSync", now);
+ UIStateInternal.syncing = false;
+
+ UIStateInternal.fxAccounts = {
+ getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
+ getSignedInUserProfile: () => Promise.reject(new Error("Profile unavailable"))
+ }
+
+ let state = await UIState.refresh();
+
+ equal(state.status, UIState.STATUS_SIGNED_IN);
+ equal(state.email, "foo@bar.com");
+ equal(state.displayName, undefined);
+ equal(state.avatarURL, undefined);
+ equal(state.lastSync, now);
+ equal(state.syncing, false);
+
+ UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_refreshState_unconfigured() {
+ UIState.reset();
+ const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+ let getSignedInUserProfile = sinon.spy();
+ UIStateInternal.fxAccounts = {
+ getSignedInUser: () => Promise.resolve(null),
+ getSignedInUserProfile
+ }
+
+ let state = await UIState.refresh();
+
+ equal(state.status, UIState.STATUS_NOT_CONFIGURED);
+ equal(state.email, undefined);
+ equal(state.displayName, undefined);
+ equal(state.avatarURL, undefined);
+ equal(state.lastSync, undefined);
+
+ ok(!getSignedInUserProfile.called);
+
+ UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_refreshState_unverified() {
+ UIState.reset();
+ const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+ let getSignedInUserProfile = sinon.spy();
+ UIStateInternal.fxAccounts = {
+ getSignedInUser: () => Promise.resolve({ verified: false, email: "foo@bar.com" }),
+ getSignedInUserProfile
+ }
+
+ let state = await UIState.refresh();
+
+ equal(state.status, UIState.STATUS_NOT_VERIFIED);
+ equal(state.email, "foo@bar.com");
+ equal(state.displayName, undefined);
+ equal(state.avatarURL, undefined);
+ equal(state.lastSync, undefined);
+
+ ok(!getSignedInUserProfile.called);
+
+ UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_refreshState_loginFailed() {
+ UIState.reset();
+ const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+ let loginFailed = sinon.stub(UIStateInternal, "_loginFailed");
+ loginFailed.returns(true);
+
+ let getSignedInUserProfile = sinon.spy();
+ UIStateInternal.fxAccounts = {
+ getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
+ getSignedInUserProfile
+ }
+
+ let state = await UIState.refresh();
+
+ equal(state.status, UIState.STATUS_LOGIN_FAILED);
+ equal(state.email, "foo@bar.com");
+ equal(state.displayName, undefined);
+ equal(state.avatarURL, undefined);
+ equal(state.lastSync, undefined);
+
+ ok(!getSignedInUserProfile.called);
+
+ loginFailed.restore();
+ UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
+add_task(async function test_observer_refreshState() {
+ let refreshState = sinon.spy(UIStateInternal, "refreshState");
+
+ let shouldRefresh = ["weave:service:login:change", "weave:service:login:error",
+ "weave:service:ready", "fxaccounts:onlogin",
+ "fxaccounts:onlogout", "fxaccounts:profilechange"];
+
+ for (let topic of shouldRefresh) {
+ let uiUpdateObserved = observeUIUpdate();
+ Services.obs.notifyObservers(null, topic);
+ await uiUpdateObserved;
+ ok(refreshState.calledOnce);
+ refreshState.reset();
+ }
+
+ refreshState.restore();
+});
+
+// Drive the UIState in a configured state.
+async function configureUIState(syncing, lastSync = new Date()) {
+ UIState.reset();
+ const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+ UIStateInternal._syncing = syncing;
+ Services.prefs.setCharPref("services.sync.lastSync", lastSync.toString());
+
+ UIStateInternal.fxAccounts = {
+ getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
+ getSignedInUserProfile: () => Promise.resolve({ displayName: "Foo Bar", avatar: "https://foo/bar" })
+ }
+ await UIState.refresh();
+ UIStateInternal.fxAccounts = fxAccountsOrig;
+}
+
+add_task(async function test_syncStarted() {
+ await configureUIState(false);
+
+ const oldState = Object.assign({}, UIState.get());
+ ok(!oldState.syncing);
+
+ let uiUpdateObserved = observeUIUpdate();
+ Services.obs.notifyObservers(null, "weave:service:sync:start");
+ await uiUpdateObserved;
+
+ const newState = Object.assign({}, UIState.get());
+ ok(newState.syncing);
+});
+
+add_task(async function test_syncFinished() {
+ let yesterday = new Date();
+ yesterday.setDate(yesterday.getDate() - 1);
+ await configureUIState(true, yesterday);
+
+ const oldState = Object.assign({}, UIState.get());
+ ok(oldState.syncing);
+
+ let uiUpdateObserved = observeUIUpdate();
+ Services.prefs.setCharPref("services.sync.lastSync", new Date().toString());
+ Services.obs.notifyObservers(null, "weave:service:sync:finish");
+ await uiUpdateObserved;
+
+ const newState = Object.assign({}, UIState.get());
+ ok(!newState.syncing);
+ ok(new Date(newState.lastSync) > new Date(oldState.lastSync));
+});
+
+add_task(async function test_syncError() {
+ let yesterday = new Date();
+ yesterday.setDate(yesterday.getDate() - 1);
+ await configureUIState(true, yesterday);
+
+ const oldState = Object.assign({}, UIState.get());
+ ok(oldState.syncing);
+
+ let uiUpdateObserved = observeUIUpdate();
+ Services.obs.notifyObservers(null, "weave:service:sync:error");
+ await uiUpdateObserved;
+
+ const newState = Object.assign({}, UIState.get());
+ ok(!newState.syncing);
+ deepEqual(newState.lastSync, oldState.lastSync);
+});
+
+function observeUIUpdate() {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ Services.obs.removeObserver(obs, aTopic);
+ const state = UIState.get();
+ resolve(state);
+ }
+ Services.obs.addObserver(obs, UIState.ON_UPDATE);
+ });
+}
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -179,8 +179,10 @@ support-files = prefs_test_prefs_store.j
[test_warn_on_truncated_response.js]
[test_postqueue.js]
# Synced tabs.
[test_syncedtabs.js]
[test_telemetry.js]
requesttimeoutfactor = 4
+
+[test_uistate.js]