Bug 1776779 - Add mobile promo and success confirmation banner. r=Gijs,markh

* Add mobile promo element to the Fxa/sync setup flow, and logic to show/hide it
* Add success confirmation for the mobile sync connection with logic to show/hide it
* Watch a new pref 'browser.tabs.firefox-view.mobilePromo.dismissed' for the promo
* Add a new notification in FxAccountsDevice when the devicelist cache is updated
* Use the devicelist updated notification drive the state changes in the setup flow manager
* Add test coverage for the mobile promo

Differential Revision: https://phabricator.services.mozilla.com/D151895
This commit is contained in:
Sam Foster 2022-07-30 01:30:15 +00:00
Родитель bc85adb480
Коммит 886cd16954
11 изменённых файлов: 960 добавлений и 316 удалений

Просмотреть файл

@ -15,6 +15,7 @@ const { XPCOMUtils } = ChromeUtils.importESModule(
const lazy = {};
XPCOMUtils.defineLazyModuleGetters(lazy, {
Log: "resource://gre/modules/Log.jsm",
UIState: "resource://services-sync/UIState.jsm",
SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
});
@ -26,7 +27,11 @@ XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
const SYNC_TABS_PREF = "services.sync.engine.tabs";
const RECENT_TABS_SYNC = "services.sync.lastTabFetch";
const MOBILE_PROMO_DISMISSED_PREF =
"browser.tabs.firefox-view.mobilePromo.dismissed";
const LOGGING_PREF = "browser.tabs.firefox-view.logLevel";
const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
function openTabInWindow(window, url) {
const {
@ -40,31 +45,7 @@ export const TabsSetupFlowManager = new (class {
this.QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);
this.setupState = new Map();
this._currentSetupStateName = "";
Services.obs.addObserver(this, lazy.UIState.ON_UPDATE);
Services.obs.addObserver(this, "fxaccounts:device_connected");
Services.obs.addObserver(this, "fxaccounts:device_disconnected");
// this.syncTabsPrefEnabled will track the value of the tabs pref
XPCOMUtils.defineLazyPreferenceGetter(
this,
"syncTabsPrefEnabled",
SYNC_TABS_PREF,
false,
() => {
this.maybeUpdateUI();
}
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"lastTabFetch",
RECENT_TABS_SYNC,
false,
() => {
this.maybeUpdateUI();
}
);
this.resetInternalState();
this.registerSetupState({
uiStateIndex: 0,
@ -76,7 +57,7 @@ export const TabsSetupFlowManager = new (class {
// TODO: handle offline, sync service not ready or available
this.registerSetupState({
uiStateIndex: 1,
name: "connect-mobile-device",
name: "connect-secondary-device",
exitConditions: () => {
return this.secondaryDeviceConnected;
},
@ -109,26 +90,81 @@ export const TabsSetupFlowManager = new (class {
},
});
// attempting to resolve the fxa user is a proxy for readiness
lazy.fxAccounts.getSignedInUser().then(() => {
this.maybeUpdateUI();
});
Services.obs.addObserver(this, lazy.UIState.ON_UPDATE);
Services.obs.addObserver(this, TOPIC_DEVICELIST_UPDATED);
// this.syncTabsPrefEnabled will track the value of the tabs pref
XPCOMUtils.defineLazyPreferenceGetter(
this,
"syncTabsPrefEnabled",
SYNC_TABS_PREF,
false,
() => {
this._uiUpdateNeeded = true;
this.maybeUpdateUI();
}
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"lastTabFetch",
RECENT_TABS_SYNC,
false,
() => {
this._uiUpdateNeeded = true;
this.maybeUpdateUI();
}
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"mobilePromoDismissedPref",
MOBILE_PROMO_DISMISSED_PREF,
false,
() => {
this._uiUpdateNeeded = true;
this.maybeUpdateUI();
}
);
this._uiUpdateNeeded = true;
if (this.fxaSignedIn) {
this.refreshDevices();
}
this.maybeUpdateUI();
}
resetInternalState() {
// assign initial values for all the managed internal properties
this._currentSetupStateName = "not-signed-in";
this._shouldShowSuccessConfirmation = false;
this._didShowMobilePromo = false;
this._uiUpdateNeeded = true;
// keep track of what is connected so we can respond to changes
this._deviceStateSnapshot = {
mobileDeviceConnected: this.mobileDeviceConnected,
secondaryDeviceConnected: this.secondaryDeviceConnected,
};
}
uninit() {
Services.obs.removeObserver(this, lazy.UIState.ON_UPDATE);
Services.obs.removeObserver(this, "fxaccounts:device_connected");
Services.obs.removeObserver(this, "fxaccounts:device_disconnected");
Services.obs.removeObserver(this, TOPIC_DEVICELIST_UPDATED);
}
get currentSetupState() {
return this.setupState.get(this._currentSetupStateName);
}
get uiStateIndex() {
const state =
this._currentSetupStateName &&
this.setupState.get(this._currentSetupStateName);
return state ? state.uiStateIndex : -1;
return this.currentSetupState.uiStateIndex;
}
get fxaSignedIn() {
return lazy.UIState.get().status === lazy.UIState.STATUS_SIGNED_IN;
let { UIState } = lazy;
return (
UIState.isReady() && UIState.get().status === UIState.STATUS_SIGNED_IN
);
}
get secondaryDeviceConnected() {
if (!this.fxaSignedIn) {
return false;
}
let recentDevices = lazy.fxAccounts.device?.recentDeviceList?.length;
return recentDevices > 1;
}
@ -139,6 +175,41 @@ export const TabsSetupFlowManager = new (class {
lazy.SyncedTabs.TABS_FRESH_ENOUGH_INTERVAL_SECONDS
);
}
get mobileDeviceConnected() {
if (!this.fxaSignedIn) {
return false;
}
let mobileClients = lazy.fxAccounts.device.recentDeviceList?.filter(
device => device.type == "mobile"
);
return mobileClients?.length > 0;
}
get shouldShowMobilePromo() {
return (
this.currentSetupState.uiStateIndex >= 3 &&
!this.mobileDeviceConnected &&
!this.mobilePromoDismissedPref
);
}
get shouldShowMobileConnectedSuccess() {
return (
this.currentSetupState.uiStateIndex >= 3 &&
this._shouldShowSuccessConfirmation &&
this.mobileDeviceConnected
);
}
get logger() {
if (!this._log) {
let setupLog = lazy.Log.repository.getLogger("FirefoxView.TabsSetup");
setupLog.manageLevelFromPref(LOGGING_PREF);
setupLog.addAppender(
new lazy.Log.ConsoleAppender(new lazy.Log.BasicFormatter())
);
this._log = setupLog;
}
return this._log;
}
registerSetupState(state) {
this.setupState.set(state.name, state);
}
@ -146,16 +217,60 @@ export const TabsSetupFlowManager = new (class {
async observe(subject, topic, data) {
switch (topic) {
case lazy.UIState.ON_UPDATE:
this.logger.debug("Handling UIState update");
this.maybeUpdateUI();
break;
case "fxaccounts:device_connected":
case "fxaccounts:device_disconnected":
await lazy.fxAccounts.device.refreshDeviceList();
case TOPIC_DEVICELIST_UPDATED:
this.logger.debug("Handling observer notification:", topic, data);
this.refreshDevices();
this.maybeUpdateUI();
break;
}
}
refreshDevices() {
// compare new values to the previous values
const mobileDeviceConnected = this.mobileDeviceConnected;
const secondaryDeviceConnected = this.secondaryDeviceConnected;
this.logger.debug(
`refreshDevices, mobileDeviceConnected: ${mobileDeviceConnected}, `,
`secondaryDeviceConnected: ${secondaryDeviceConnected}`
);
let didDeviceStateChange =
this._deviceStateSnapshot.mobileDeviceConnected !=
mobileDeviceConnected ||
this._deviceStateSnapshot.secondaryDeviceConnected !=
secondaryDeviceConnected;
if (
mobileDeviceConnected &&
!this._deviceStateSnapshot.mobileDeviceConnected
) {
// a mobile device was added, show success if we previously showed the promo
this._shouldShowSuccessConfirmation = this._didShowMobilePromo;
} else if (
!mobileDeviceConnected &&
this._deviceStateSnapshot.mobileDeviceConnected
) {
// no mobile device connected now, reset
Services.prefs.clearUserPref(MOBILE_PROMO_DISMISSED_PREF);
this._shouldShowSuccessConfirmation = false;
}
this._deviceStateSnapshot = {
mobileDeviceConnected,
secondaryDeviceConnected,
};
if (didDeviceStateChange) {
this.logger.debug(
"refreshDevices: device state did change, call maybeUpdateUI"
);
this._uiUpdateNeeded = true;
} else {
this.logger.debug("refreshDevices: no device state change");
}
}
maybeUpdateUI() {
let nextSetupStateName = this._currentSetupStateName;
@ -167,19 +282,34 @@ export const TabsSetupFlowManager = new (class {
}
}
if (nextSetupStateName !== this._currentSetupStateName) {
const state = this.setupState.get(nextSetupStateName);
let setupState = this.currentSetupState;
if (nextSetupStateName != this._currentSetupStateName) {
setupState = this.setupState.get(nextSetupStateName);
this._currentSetupStateName = nextSetupStateName;
Services.obs.notifyObservers(
null,
TOPIC_SETUPSTATE_CHANGED,
state.uiStateIndex
);
if ("function" == typeof state.enter) {
state.enter();
}
this._uiUpdateNeeded = true;
}
this.logger.debug("maybeUpdateUI, _uiUpdateNeeded:", this._uiUpdateNeeded);
if (this._uiUpdateNeeded) {
this._uiUpdateNeeded = false;
if (this.shouldShowMobilePromo) {
this._didShowMobilePromo = true;
}
Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED);
}
if ("function" == typeof setupState.enter) {
setupState.enter();
}
}
dismissMobilePromo() {
Services.prefs.setBoolPref(MOBILE_PROMO_DISMISSED_PREF, true);
}
dismissMobileConfirmation() {
this._shouldShowSuccessConfirmation = false;
this._didShowMobilePromo = false;
this._uiUpdateNeeded = true;
this.maybeUpdateUI();
}
async openFxASignup(window) {
@ -188,10 +318,12 @@ export const TabsSetupFlowManager = new (class {
);
openTabInWindow(window, url, true);
}
openSyncPreferences(window) {
const url = "about:preferences?action=pair#sync";
openTabInWindow(window, url, true);
}
syncOpenTabs(containerElem) {
// Flip the pref on.
// The observer should trigger re-evaluating state and advance to next step

Просмотреть файл

@ -14,6 +14,10 @@ menu-tools-firefox-view =
firefoxview-page-title = { -firefoxview-brand-name }
firefoxview-close-button =
.title = Close
.aria-label = Close
# Used instead of the localized relative time when a timestamp is within a minute or so of now
firefoxview-just-now-timestamp = Just now
@ -43,6 +47,13 @@ firefoxview-tabpickup-synctabs-primarybutton = Sync open tabs
firefoxview-tabpickup-syncing = Sit tight while your tabs sync. Itll be just a moment.
firefoxview-mobile-promo-header = Grab tabs from your phone or tablet
firefoxview-mobile-promo-description = To view your latest mobile tabs, sign in to { -brand-product-name } on iOS or Android.
firefoxview-mobile-promo-primarybutton = Get { -brand-product-name } for mobile
firefoxview-mobile-confirmation-header = 🎉 Good to go!
firefoxview-mobile-confirmation-description = Now you can grab your { -brand-product-name } tabs from your tablet or phone.
firefoxview-closed-tabs-title = Recently closed
firefoxview-closed-tabs-collapse-button =
.title = Show or hide recently closed tabs list

Просмотреть файл

@ -8,6 +8,11 @@
--sidebar-width: 200px;
--header-spacing: 40px;
--footer-spacing: 80px;
--card-border-zap-gradient: linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%);
--success-background-color: #87FFD1;
--success-border-color: #2AC3A2;
--success-color: #15141A;
--colorways-figure-size: 225px;
--colorways-grid-template-areas:
@ -254,6 +259,17 @@ body > main > aside {
display: none !important;
}
button.primary {
white-space: nowrap;
min-width: fit-content;
}
button.close {
background-image: url(chrome://global/skin/icons/close.svg);
-moz-context-properties: fill;
fill: currentColor;
}
.card,
.synced-tab-li,
.synced-tab-li-placeholder,
@ -306,12 +322,12 @@ body > main > aside {
}
/* Bug 1770534 - Only use the zap-gradient for built-in, color-neutral themes */
.setup-step {
.zap-card {
border: none;
position: relative;
z-index: 0;
}
.setup-step::before {
.zap-card::before {
content: "";
position: absolute;
z-index: -1;
@ -339,11 +355,6 @@ body > main > aside {
flex: 1 1 auto;
}
.setup-step > .step-body > button.primary {
white-space: nowrap;
min-width: fit-content;
}
.setup-step > footer {
display: flex;
flex-direction: column;
@ -354,6 +365,44 @@ body > main > aside {
margin-block: 0 8px;
}
.message-box {
display: flex;
align-content: space-between;
align-items: center;
margin-block: 8px;
gap: 8px;
}
.message-content {
flex: 1 1 auto;
}
.message-content > .message-header {
font-size: 1.12em;
margin-block: 0 0.33em;
}
.message-content > .message-description {
margin-block: 0 0.33em;
}
.confirmation-message-box {
background-color: var(--success-background-color);
color: var(--success-color);
border-color: var(--success-border-color);
}
.confirmation-message-box > .message-content {
text-align: center;
}
.confirmation-message-box > .message-content > .message-header {
font-size: inherit;
display: inline;
}
.confirmation-message-box > .icon-button {
/* ensure we get the local color value as container doesnt change color with theme */
color: inherit;
}
/* 117px is the total height of the collapsible-tabs-container; setting that size
for the second row stabilizes the layout so it doesn't shift when collapsibled */
#recently-closed-tabs-container {

Просмотреть файл

@ -33,7 +33,7 @@
<main>
<template id="sync-setup-template">
<named-deck class="sync-setup-container" data-prefix="id:">
<div name="sync-setup-view0" data-prefix="id:-view0" class="card setup-step" data-prefix="aria-labelledby:-view0-header">
<div name="sync-setup-view0" data-prefix="id:-view0" class="card zap-card setup-step" data-prefix="aria-labelledby:-view0-header">
<h2 data-prefix="id:-view0-header" data-l10n-id="firefoxview-tabpickup-step-signin-header" class="card-header"></h2>
<section class="step-body">
<p class="step-content" data-l10n-id="firefoxview-tabpickup-step-signin-description"></p>
@ -47,7 +47,7 @@
data-l10n-args='{"percentValue":"11"}'></label>
</footer>
</div>
<div name="sync-setup-view1" data-prefix="id:-view1" class="card setup-step" data-prefix="aria-labelledby:-view1-header">
<div name="sync-setup-view1" data-prefix="id:-view1" class="card zap-card setup-step" data-prefix="aria-labelledby:-view1-header">
<h2 data-prefix="id:-view1-header" data-l10n-id="firefoxview-tabpickup-adddevice-header" class="card-header"></h2>
<section class="step-body">
<p class="step-content">
@ -65,7 +65,7 @@
data-l10n-args='{"percentValue":"33"}'></label>
</footer>
</div>
<div name="sync-setup-view2" data-prefix="id:-view2" class="card setup-step" data-prefix="aria-labelledby:-view2-header">
<div name="sync-setup-view2" data-prefix="id:-view2" class="card zap-card setup-step" data-prefix="aria-labelledby:-view2-header">
<h2 data-prefix="id:-view2-header" data-l10n-id="firefoxview-tabpickup-synctabs-header" class="card-header"></h2>
<section class="step-body">
<p class="step-content">
@ -104,6 +104,25 @@
<button id="collapsible-synced-tabs-button" class="ghost-button twisty icon arrow-up" hidden></button>
<p class="section-description" data-l10n-id="firefoxview-tabpickup-description"></p>
</header>
<div class="confirmation-message-box message-box card card-no-hover" hidden>
<div class="message-content">
<h2 data-l10n-id="firefoxview-mobile-confirmation-header" class="message-header"></h2>
<span class="message-description" data-l10n-id="firefoxview-mobile-confirmation-description"></span>
</div>
<button data-action="mobile-confirmation-dismiss" class="close icon-button ghost-button" data-l10n-id="firefoxview-close-button"></button>
</div>
<div id="sync-setup-placeholder" hidden></div>
<div id="synced-tabs-placeholder" hidden></div>
<div class="promo-box message-box zap-card card-no-hover card" hidden>
<div class="message-content">
<h2 data-l10n-id="firefoxview-mobile-promo-header" class="message-header"></h2>
<p class="message-description" data-l10n-id="firefoxview-mobile-promo-description"></p>
</div>
<button class="primary" data-action="mobile-promo-primary-action" data-l10n-id="firefoxview-mobile-promo-primarybutton"></button>
<button data-action="mobile-promo-dismiss" class="close icon-button ghost-button" data-l10n-id="firefoxview-close-button"></button>
</div>
</section>
<aside class="content-container" is="colorways-card">

Просмотреть файл

@ -31,8 +31,9 @@ class TabPickupContainer extends HTMLElement {
connectedCallback() {
this.addEventListener("click", this);
this.addEventListener("visibilitychange", this);
Services.obs.addObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED);
this.updateSetupState(TabsSetupFlowManager.uiStateIndex);
this.update();
}
cleanup() {
@ -62,17 +63,43 @@ class TabPickupContainer extends HTMLElement {
TabsSetupFlowManager.syncOpenTabs(event.target);
break;
}
case "mobile-promo-dismiss": {
TabsSetupFlowManager.dismissMobilePromo(event.target);
break;
}
case "mobile-promo-primary-action": {
TabsSetupFlowManager.openSyncPreferences(event.target.ownerGlobal);
break;
}
case "mobile-confirmation-dismiss": {
TabsSetupFlowManager.dismissMobileConfirmation(event.target);
break;
}
}
}
}
async observe(subject, topic, stateIndex) {
if (topic == TOPIC_SETUPSTATE_CHANGED) {
this.updateSetupState(TabsSetupFlowManager.uiStateIndex);
// Returning to fxview seems like a likely time for a device check
if (
event.type == "visibilitychange" &&
document.visibilityState === "visible"
) {
this.update();
}
}
appendTemplatedElement(templateId, elementId) {
async observe(subject, topic, data) {
if (topic == TOPIC_SETUPSTATE_CHANGED) {
this.update();
}
}
get mobilePromoElem() {
return this.querySelector(".promo-box");
}
get mobileSuccessElem() {
return this.querySelector(".confirmation-message-box");
}
insertTemplatedElement(templateId, elementId, replaceNode) {
const template = document.getElementById(templateId);
const templateContent = template.content;
const cloned = templateContent.cloneNode(true);
@ -91,32 +118,57 @@ class TabPickupContainer extends HTMLElement {
elem.dataset.supportUrl;
}
}
this.appendChild(cloned);
if (replaceNode) {
if (typeof replaceNode == "string") {
replaceNode = document.getElementById(replaceNode);
}
this.replaceChild(cloned, replaceNode);
} else {
this.appendChild(cloned);
}
}
updateSetupState(stateIndex) {
const currStateIndex = this._currentSetupStateIndex;
if (stateIndex === undefined) {
stateIndex = currStateIndex;
update({
stateIndex = TabsSetupFlowManager.uiStateIndex,
showMobilePromo = TabsSetupFlowManager.shouldShowMobilePromo,
showMobilePairSuccess = TabsSetupFlowManager.shouldShowMobileConnectedSuccess,
} = {}) {
let needsRender = false;
if (showMobilePromo !== this._showMobilePromo) {
this._showMobilePromo = showMobilePromo;
needsRender = true;
}
if (stateIndex === this._currentSetupStateIndex) {
return;
if (showMobilePairSuccess !== this._showMobilePairSuccess) {
this._showMobilePairSuccess = showMobilePairSuccess;
needsRender = true;
}
this._currentSetupStateIndex = stateIndex;
this.render();
if (stateIndex !== this._currentSetupStateIndex) {
this._currentSetupStateIndex = stateIndex;
needsRender = true;
}
needsRender && this.render();
}
render() {
if (!this.isConnected) {
return;
}
let setupElem = this.setupContainerElem;
let tabsElem = this.tabsContainerElem;
let mobilePromoElem = this.mobilePromoElem;
let mobileSuccessElem = this.mobileSuccessElem;
const stateIndex = this._currentSetupStateIndex;
const isLoading = stateIndex == 3;
// show/hide either the setup or tab list containers, creating each as necessary
if (stateIndex < 3) {
if (!setupElem) {
this.appendTemplatedElement("sync-setup-template", "tabpickup-steps");
this.insertTemplatedElement(
"sync-setup-template",
"tabpickup-steps",
"sync-setup-placeholder"
);
setupElem = this.setupContainerElem;
}
if (tabsElem) {
@ -128,9 +180,10 @@ class TabPickupContainer extends HTMLElement {
}
if (!tabsElem) {
this.appendTemplatedElement(
this.insertTemplatedElement(
"synced-tabs-template",
"tabpickup-tabs-container"
"tabpickup-tabs-container",
"synced-tabs-placeholder"
);
tabsElem = this.tabsContainerElem;
}
@ -143,6 +196,8 @@ class TabPickupContainer extends HTMLElement {
if (stateIndex == 4) {
this.collapsibleButton.hidden = false;
}
mobilePromoElem.hidden = !this._showMobilePromo;
mobileSuccessElem.hidden = !this._showMobilePairSuccess;
}
}
customElements.define("tab-pickup-container", TabPickupContainer);

Просмотреть файл

@ -17,46 +17,6 @@ add_setup(async function() {
}
});
function assertFirefoxViewTab(w = window) {
ok(w.FirefoxViewHandler.tab, "Firefox View tab exists");
ok(w.FirefoxViewHandler.tab?.hidden, "Firefox View tab is hidden");
is(
w.gBrowser.tabs.indexOf(w.FirefoxViewHandler.tab),
0,
"Firefox View tab is the first tab"
);
is(
w.gBrowser.visibleTabs.indexOf(w.FirefoxViewHandler.tab),
-1,
"Firefox View tab is not in the list of visible tabs"
);
}
async function openFirefoxViewTab(w = window) {
ok(
!w.FirefoxViewHandler.tab,
"Firefox View tab doesn't exist prior to clicking the button"
);
info("Clicking the Firefox View button");
await EventUtils.synthesizeMouseAtCenter(
w.document.getElementById("firefox-view-button"),
{},
w
);
assertFirefoxViewTab(w);
is(w.gBrowser.tabContainer.selectedIndex, 0, "Firefox View tab is selected");
await BrowserTestUtils.browserLoaded(w.FirefoxViewHandler.tab.linkedBrowser);
return w.FirefoxViewHandler.tab;
}
function closeFirefoxViewTab(w = window) {
w.gBrowser.removeTab(w.FirefoxViewHandler.tab);
ok(
!w.FirefoxViewHandler.tab,
"Reference to Firefox View tab got removed when closing the tab"
);
}
add_task(async function load_opens_new_tab() {
await openFirefoxViewTab();
gURLBar.focus();

Просмотреть файл

@ -5,28 +5,41 @@ const { UIState } = ChromeUtils.import("resource://services-sync/UIState.jsm");
const { TabsSetupFlowManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
);
const MOBILE_PROMO_DISMISSED_PREF =
"browser.tabs.firefox-view.mobilePromo.dismissed";
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
var gMockFxaDevices = null;
function promiseSyncReady() {
let service = Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
.wrappedJSObject;
return service.whenLoaded();
}
async function touchLastTabFetch() {
// lastTabFetch stores a timestamp in *seconds*.
const nowSeconds = Math.floor(Date.now() / 1000);
info("updating lastFetch:" + nowSeconds);
Services.prefs.setIntPref("services.sync.lastTabFetch", nowSeconds);
// wait so all pref observers can complete
await TestUtils.waitForTick();
}
function setupMocks({ fxaDevices = null, state = UIState.STATUS_SIGNED_IN }) {
const sandbox = sinon.createSandbox();
gMockFxaDevices = fxaDevices;
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => fxaDevices);
sandbox.stub(UIState, "get").returns({
status: state,
syncEnabled: true,
});
sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
sandbox
.stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
.callsFake(fxaDeviceId => {
let target = fxaDevices.find(c => c.id == fxaDeviceId);
let target = gMockFxaDevices.find(c => c.id == fxaDeviceId);
return target ? target.clientRecord : null;
});
sandbox.stub(Weave.Service.clientsEngine, "getClientType").returns("desktop");
@ -34,6 +47,30 @@ function setupMocks({ fxaDevices = null, state = UIState.STATUS_SIGNED_IN }) {
return sandbox;
}
async function setupWithDesktopDevices() {
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
type: "desktop",
},
{
id: 2,
name: "Other Device",
type: "desktop",
},
],
});
// ensure tab sync is false so we don't skip onto next step
await SpecialPowers.pushPrefEnv({
set: [["services.sync.engine.tabs", true]],
});
return sandbox;
}
async function waitForVisibleStep(browser, expected) {
const { document } = browser.contentWindow;
@ -66,37 +103,68 @@ async function waitForVisibleStep(browser, expected) {
}
}
async function waitForElementVisible(browser, selector, isVisible = true) {
function checkMobilePromo(browser, expected = {}) {
const { document } = browser.contentWindow;
const elem = document.querySelector(selector);
ok(elem, `Got element with selector: ${selector}`);
await BrowserTestUtils.waitForMutationCondition(
elem,
{
attributeFilter: ["hidden"],
},
() => {
return isVisible
? BrowserTestUtils.is_visible(elem)
: BrowserTestUtils.is_hidden(elem);
}
const promoElem = document.querySelector(
"#tab-pickup-container > .promo-box"
);
const successElem = document.querySelector(
"#tab-pickup-container > .confirmation-message-box"
);
info("checkMobilePromo: " + JSON.stringify(expected));
if (expected.mobilePromo) {
ok(BrowserTestUtils.is_visible(promoElem), "Mobile promo is visible");
} else {
ok(
!promoElem || BrowserTestUtils.is_hidden(promoElem),
"Mobile promo is hidden"
);
}
if (expected.mobileConfirmation) {
ok(
BrowserTestUtils.is_visible(successElem),
"Success confirmation is visible"
);
} else {
ok(
!successElem || BrowserTestUtils.is_hidden(successElem),
"Success confirmation is hidden"
);
}
}
async function tearDown(sandbox) {
sandbox?.restore();
Services.prefs.clearUserPref("services.sync.lastTabFetch");
Services.prefs.clearUserPref(MOBILE_PROMO_DISMISSED_PREF);
}
add_setup(async function() {
if (!Services.prefs.getBoolPref("browser.tabs.firefox-view")) {
info(
"firefox-view pref was off, toggling it on and adding the tabstrip widget"
);
await SpecialPowers.pushPrefEnv({
set: [["browser.tabs.firefox-view", true]],
});
CustomizableUI.addWidgetToArea(
"firefox-view-button",
CustomizableUI.AREA_TABSTRIP,
0
);
registerCleanupFunction(() => {
CustomizableUI.removeWidgetFromArea("firefox-view-button");
});
}
await promiseSyncReady();
// gSync.init() is called in a requestIdleCallback. Force its initialization.
gSync.init();
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:firefoxview"
);
registerCleanupFunction(async function() {
BrowserTestUtils.removeTab(tab);
Services.prefs.clearUserPref("services.sync.engine.tabs");
Services.prefs.clearUserPref("services.sync.lastTabFetch");
await tearDown();
});
// set tab sync false so we don't skip setup states
await SpecialPowers.pushPrefEnv({
@ -105,19 +173,21 @@ add_setup(async function() {
});
add_task(async function test_unconfigured_initial_state() {
const browser = gBrowser.selectedBrowser;
const sandbox = setupMocks({ state: UIState.STATUS_NOT_CONFIGURED });
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view0",
await withFirefoxView({}, async browser => {
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view0",
});
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
});
sandbox.restore();
await tearDown(sandbox);
});
add_task(async function test_signed_in() {
const browser = gBrowser.selectedBrowser;
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
@ -129,18 +199,26 @@ add_task(async function test_signed_in() {
},
],
});
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view1",
await withFirefoxView({}, async browser => {
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view1",
});
is(
fxAccounts.device.recentDeviceList?.length,
1,
"Just 1 device connected"
);
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
});
is(fxAccounts.device.recentDeviceList?.length, 1, "Just 1 device connected");
sandbox.restore();
await tearDown(sandbox);
});
add_task(async function test_2nd_desktop_connected() {
const browser = gBrowser.selectedBrowser;
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
@ -157,31 +235,34 @@ add_task(async function test_2nd_desktop_connected() {
},
],
});
await withFirefoxView({}, async browser => {
// ensure tab sync is false so we don't skip onto next step
ok(
!Services.prefs.getBoolPref("services.sync.engine.tabs", false),
"services.sync.engine.tabs is initially false"
);
// ensure tab sync is false so we don't skip onto next step
ok(
!Services.prefs.getBoolPref("services.sync.engine.tabs", false),
"services.sync.engine.tabs is initially false"
);
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view2",
});
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view2",
is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
ok(
fxAccounts.device.recentDeviceList?.every(
device => device.type !== "mobile"
),
"No connected device is type:mobile"
);
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
});
is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
ok(
fxAccounts.device.recentDeviceList?.every(
device => device.type !== "mobile"
),
"No connected device is type:mobile"
);
sandbox.restore();
await tearDown(sandbox);
});
add_task(async function test_mobile_connected() {
const browser = gBrowser.selectedBrowser;
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
@ -198,30 +279,34 @@ add_task(async function test_mobile_connected() {
},
],
});
// ensure tab sync is false so we don't skip onto next step
ok(
!Services.prefs.getBoolPref("services.sync.engine.tabs", false),
"services.sync.engine.tabs is initially false"
);
await withFirefoxView({}, async browser => {
// ensure tab sync is false so we don't skip onto next step
ok(
!Services.prefs.getBoolPref("services.sync.engine.tabs", false),
"services.sync.engine.tabs is initially false"
);
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view2",
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view2",
});
is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
ok(
fxAccounts.device.recentDeviceList?.some(
device => device.type !== "mobile"
),
"A connected device is type:mobile"
);
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
});
is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
ok(
fxAccounts.device.recentDeviceList?.some(
device => device.type !== "mobile"
),
"A connected device is type:mobile"
);
sandbox.restore();
await tearDown(sandbox);
});
add_task(async function test_tab_sync_enabled() {
const browser = gBrowser.selectedBrowser;
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
@ -238,43 +323,58 @@ add_task(async function test_tab_sync_enabled() {
},
],
});
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await withFirefoxView({}, async browser => {
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
// test initial state, with the pref not enabled
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view2",
// test initial state, with the pref not enabled
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view2",
});
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
// test with the pref toggled on
await SpecialPowers.pushPrefEnv({
set: [["services.sync.engine.tabs", true]],
});
await waitForElementVisible(browser, "#tabpickup-steps", false);
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
// reset and test clicking the action button
await SpecialPowers.popPrefEnv();
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view2",
});
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
const actionButton = browser.contentWindow.document.querySelector(
"#tabpickup-steps-view2 button.primary"
);
actionButton.click();
await waitForElementVisible(browser, "#tabpickup-steps", false);
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
ok(
Services.prefs.getBoolPref("services.sync.engine.tabs", false),
"tab sync pref should be enabled after button click"
);
});
// test with the pref toggled on
await SpecialPowers.pushPrefEnv({
set: [["services.sync.engine.tabs", true]],
});
await waitForElementVisible(browser, "#tabpickup-steps", false);
// reset and test clicking the action button
await SpecialPowers.popPrefEnv();
await waitForVisibleStep(browser, {
expectedVisible: "#tabpickup-steps-view2",
});
const actionButton = browser.contentWindow.document.querySelector(
"#tabpickup-steps-view2 button.primary"
);
actionButton.click();
await waitForElementVisible(browser, "#tabpickup-steps", false);
ok(
Services.prefs.getBoolPref("services.sync.engine.tabs", false),
"tab sync pref should be enabled after button click"
);
sandbox.restore();
Services.prefs.clearUserPref("services.sync.engine.tabs");
await tearDown(sandbox);
});
add_task(async function test_tab_sync_loading() {
const browser = gBrowser.selectedBrowser;
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
@ -291,95 +391,287 @@ add_task(async function test_tab_sync_loading() {
},
],
});
const { document } = browser.contentWindow;
await withFirefoxView({}, async browser => {
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await SpecialPowers.pushPrefEnv({
set: [["services.sync.engine.tabs", true]],
await SpecialPowers.pushPrefEnv({
set: [["services.sync.engine.tabs", true]],
});
const { document } = browser.contentWindow;
const tabsContainer = document.querySelector("#tabpickup-tabs-container");
const tabsList = document.querySelector(
"#tabpickup-tabs-container tab-pickup-list"
);
const loadingElem = document.querySelector(
"#tabpickup-tabs-container .loading-content"
);
const setupElem = document.querySelector("#tabpickup-steps");
await waitForElementVisible(browser, "#tabpickup-steps", false);
await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
function checkLoadingState(isLoading = false) {
if (isLoading) {
ok(
tabsContainer.classList.contains("loading"),
"Tabs container has loading class"
);
BrowserTestUtils.is_visible(
loadingElem,
"Loading content is visible when loading"
);
!tabsList ||
BrowserTestUtils.is_hidden(
tabsList,
"Synced tabs list is not visible when loading"
);
!setupElem ||
BrowserTestUtils.is_hidden(
setupElem,
"Setup content is not visible when loading"
);
} else {
ok(
!tabsContainer.classList.contains("loading"),
"Tabs container has no loading class"
);
!loadingElem ||
BrowserTestUtils.is_hidden(
loadingElem,
"Loading content is not visible when tabs are loaded"
);
BrowserTestUtils.is_visible(
tabsList,
"Synced tabs list is visible when loaded"
);
!setupElem ||
BrowserTestUtils.is_hidden(
setupElem,
"Setup content is not visible when tabs are loaded"
);
}
}
checkLoadingState(true);
await touchLastTabFetch();
await BrowserTestUtils.waitForMutationCondition(
tabsContainer,
{ attributeFilter: ["class"], attributes: true },
() => {
return !tabsContainer.classList.contains("loading");
}
);
checkLoadingState(false);
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
// Simulate stale data by setting lastTabFetch to 10mins ago
const TEN_MINUTES_MS = 1000 * 60 * 10;
const staleFetchSeconds = Math.floor((Date.now() - TEN_MINUTES_MS) / 1000);
info("updating lastFetch:" + staleFetchSeconds);
Services.prefs.setIntPref("services.sync.lastTabFetch", staleFetchSeconds);
await BrowserTestUtils.waitForMutationCondition(
tabsContainer,
{ attributeFilter: ["class"], attributes: true },
() => {
return tabsContainer.classList.contains("loading");
}
);
checkLoadingState(true);
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
});
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForElementVisible(browser, "#tabpickup-steps", false);
await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
const tabsContainer = document.querySelector("#tabpickup-tabs-container");
const tabsList = document.querySelector(
"#tabpickup-tabs-container tab-pickup-list"
);
const loadingElem = document.querySelector(
"#tabpickup-tabs-container .loading-content"
);
const setupElem = document.querySelector("#tabpickup-steps");
function checkLoadingState(isLoading = false) {
if (isLoading) {
ok(
tabsContainer.classList.contains("loading"),
"Tabs container has loading class"
);
BrowserTestUtils.is_visible(
loadingElem,
"Loading content is visible when loading"
);
BrowserTestUtils.is_hidden(
tabsList,
"Synced tabs list is not visible when loading"
);
BrowserTestUtils.is_hidden(
setupElem,
"Setup content is not visible when loading"
);
} else {
ok(
!tabsContainer.classList.contains("loading"),
"Tabs container has no loading class"
);
BrowserTestUtils.is_hidden(
loadingElem,
"Loading content is not visible when tabs are loaded"
);
BrowserTestUtils.is_visible(
tabsList,
"Synced tabs list is visible when loaded"
);
BrowserTestUtils.is_hidden(
setupElem,
"Setup content is not visible when tabs are loaded"
);
}
}
checkLoadingState(true);
// lastTabFetch stores a timestamp in *seconds*.
const nowSeconds = Math.floor(Date.now() / 1000);
info("updating lastFetch:" + nowSeconds);
Services.prefs.setIntPref("services.sync.lastTabFetch", nowSeconds);
await BrowserTestUtils.waitForMutationCondition(
tabsContainer,
{ attributeFilter: ["class"], attributes: true },
() => {
return !tabsContainer.classList.contains("loading");
}
);
checkLoadingState(false);
// Simulate stale data by setting lastTabFetch to 10mins ago
const TEN_MINUTES_MS = 1000 * 60 * 10;
const staleFetchSeconds = Math.floor((Date.now() - TEN_MINUTES_MS) / 1000);
info("updating lastFetch:" + staleFetchSeconds);
Services.prefs.setIntPref("services.sync.lastTabFetch", staleFetchSeconds);
await BrowserTestUtils.waitForMutationCondition(
tabsContainer,
{ attributeFilter: ["class"], attributes: true },
() => {
return tabsContainer.classList.contains("loading");
}
);
checkLoadingState(true);
await SpecialPowers.popPrefEnv();
sandbox.restore();
Services.prefs.clearUserPref("services.sync.engine.tabs");
Services.prefs.clearUserPref("services.sync.lastTabFetch");
await tearDown(sandbox);
});
add_task(async function test_mobile_promo() {
const sandbox = await setupWithDesktopDevices();
await withFirefoxView({}, async browser => {
// ensure last tab fetch was just now so we don't get the loading state
await touchLastTabFetch();
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForElementVisible(browser, ".synced-tabs-container");
is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
info("checking mobile promo, should be visible now");
checkMobilePromo(browser, {
mobilePromo: true,
mobileConfirmation: false,
});
gMockFxaDevices.push({
id: 3,
name: "Mobile Device",
type: "mobile",
});
Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated");
// Wait for the async refreshDeviceList(),
// which should result in the promo being hidden
await waitForElementVisible(
browser,
"#tab-pickup-container > .promo-box",
false
);
is(fxAccounts.device.recentDeviceList?.length, 3, "3 devices connected");
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: true,
});
});
await tearDown(sandbox);
});
add_task(async function test_mobile_promo_pref() {
const sandbox = await setupWithDesktopDevices();
await SpecialPowers.pushPrefEnv({
set: [[MOBILE_PROMO_DISMISSED_PREF, true]],
});
await withFirefoxView({}, async browser => {
// ensure tab sync is false so we don't skip onto next step
info("starting test, will notify of UIState update");
// ensure last tab fetch was just now so we don't get the loading state
await touchLastTabFetch();
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForElementVisible(browser, ".synced-tabs-container");
is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
info("checking mobile promo, should be still hidden because of the pref");
checkMobilePromo(browser, {
mobilePromo: false,
mobileConfirmation: false,
});
// reset the dismissed pref, which should case the promo to get shown
await SpecialPowers.popPrefEnv();
await waitForElementVisible(
browser,
"#tab-pickup-container > .promo-box",
true
);
const promoElem = browser.contentWindow.document.querySelector(
"#tab-pickup-container > .promo-box"
);
const promoElemClose = promoElem.querySelector(".close");
ok(promoElemClose.hasAttribute("aria-label"), "Button has an a11y name");
// check that dismissing the promo sets the pref
info("Clicking the promo close button: " + promoElemClose);
EventUtils.sendMouseEvent({ type: "click" }, promoElemClose);
info("Check the promo box got hidden");
BrowserTestUtils.is_hidden(promoElem);
ok(
SpecialPowers.getBoolPref(MOBILE_PROMO_DISMISSED_PREF),
"Promo pref is updated when close is clicked"
);
});
await tearDown(sandbox);
});
add_task(async function test_mobile_promo_windows() {
// make sure interacting with the promo and success confirmation in one window
// also updates the others
const sandbox = await setupWithDesktopDevices();
await withFirefoxView({}, async browser => {
// ensure last tab fetch was just now so we don't get the loading state
await touchLastTabFetch();
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
await waitForElementVisible(browser, ".synced-tabs-container");
is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
info("checking mobile promo is visible");
checkMobilePromo(browser, {
mobilePromo: true,
mobileConfirmation: false,
});
info(
"opening new window, pref is: " +
SpecialPowers.getBoolPref("browser.tabs.firefox-view")
);
let win2 = await BrowserTestUtils.openNewBrowserWindow();
info("Got window, now opening Firefox View in it");
await withFirefoxView(
{ resetFlowManager: false, win: win2 },
async win2Browser => {
info("In withFirefoxView taskFn for win2");
// promo should be visible in the 2nd window too
info("check mobile promo is visible in the new window");
checkMobilePromo(win2Browser, {
mobilePromo: true,
mobileConfirmation: false,
});
// add the mobile device to get the success confirmation in both instances
info("add a mobile device and send device_connected notification");
gMockFxaDevices.push({
id: 3,
name: "Mobile Device",
type: "mobile",
});
Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated");
is(
fxAccounts.device.recentDeviceList?.length,
3,
"3 devices connected"
);
// Wait for the async refreshDevices(),
// which should result in the promo being hidden
info("waiting for the confirmation box to be visible");
await waitForElementVisible(
win2Browser,
"#tab-pickup-container > .promo-box",
false
);
for (let fxviewBrowser of [browser, win2Browser]) {
info(
"checking promo is hidden and confirmation is visible in each window"
);
checkMobilePromo(fxviewBrowser, {
mobilePromo: false,
mobileConfirmation: true,
});
}
// dismiss the confirmation and check its gone from both instances
const confirmBox = win2Browser.contentWindow.document.querySelector(
"#tab-pickup-container > .confirmation-message-box"
);
const closeButton = confirmBox.querySelector(".close");
ok(closeButton.hasAttribute("aria-label"), "Button has an a11y name");
EventUtils.sendMouseEvent({ type: "click" }, closeButton, win2);
BrowserTestUtils.is_hidden(confirmBox);
for (let fxviewBrowser of [browser, win2Browser]) {
checkMobilePromo(fxviewBrowser, {
mobilePromo: false,
mobileConfirmation: false,
});
}
}
);
await BrowserTestUtils.closeWindow(win2);
});
await tearDown(sandbox);
});

Просмотреть файл

@ -22,6 +22,9 @@ function testVisibility(browser, expected) {
async function waitForElementVisible(browser, selector, isVisible = true) {
const { document } = browser.contentWindow;
const elem = document.querySelector(selector);
if (!isVisible && !elem) {
return;
}
ok(elem, `Got element with selector: ${selector}`);
await BrowserTestUtils.waitForMutationCondition(
@ -36,3 +39,73 @@ async function waitForElementVisible(browser, selector, isVisible = true) {
}
);
}
function assertFirefoxViewTab(w = window) {
ok(w.FirefoxViewHandler.tab, "Firefox View tab exists");
ok(w.FirefoxViewHandler.tab?.hidden, "Firefox View tab is hidden");
is(
w.gBrowser.tabs.indexOf(w.FirefoxViewHandler.tab),
0,
"Firefox View tab is the first tab"
);
is(
w.gBrowser.visibleTabs.indexOf(w.FirefoxViewHandler.tab),
-1,
"Firefox View tab is not in the list of visible tabs"
);
}
async function openFirefoxViewTab(w = window) {
ok(
!w.FirefoxViewHandler.tab,
"Firefox View tab doesn't exist prior to clicking the button"
);
info("Clicking the Firefox View button");
await EventUtils.synthesizeMouseAtCenter(
w.document.getElementById("firefox-view-button"),
{},
w
);
assertFirefoxViewTab(w);
is(w.gBrowser.tabContainer.selectedIndex, 0, "Firefox View tab is selected");
await BrowserTestUtils.browserLoaded(w.FirefoxViewHandler.tab.linkedBrowser);
return w.FirefoxViewHandler.tab;
}
function closeFirefoxViewTab(w = window) {
w.gBrowser.removeTab(w.FirefoxViewHandler.tab);
ok(
!w.FirefoxViewHandler.tab,
"Reference to Firefox View tab got removed when closing the tab"
);
}
async function withFirefoxView(
{ resetFlowManager = true, win = window },
taskFn
) {
if (resetFlowManager) {
const { TabsSetupFlowManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
);
// reset internal state so we aren't reacting to whatever state the last invocation left behind
TabsSetupFlowManager.resetInternalState();
}
let tab = await openFirefoxViewTab(win);
let originalWindow = tab.ownerGlobal;
let result = await taskFn(tab.linkedBrowser);
let finalWindow = tab.ownerGlobal;
if (originalWindow == finalWindow && !tab.closing && tab.linkedBrowser) {
// taskFn may resolve within a tick after opening a new tab.
// We shouldn't remove the newly opened tab in the same tick.
// Wait for the next tick here.
await TestUtils.waitForTick();
BrowserTestUtils.removeTab(tab);
} else {
Services.console.logStringMessage(
"withFirefoxView: Tab was already closed before " +
"removeTab would have been called"
);
}
return Promise.resolve(result);
}

Просмотреть файл

@ -88,6 +88,7 @@ exports.FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update";
exports.ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange"; // WebChannel
exports.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION = "fxaccounts:statechange";
exports.ON_NEW_DEVICE_ID = "fxaccounts:new_device_id";
exports.ON_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
// The common prefix for all commands.
exports.COMMAND_PREFIX = "https://identity.mozilla.com/cmd/";

Просмотреть файл

@ -12,6 +12,7 @@ const {
ERRNO_DEVICE_SESSION_CONFLICT,
ERRNO_UNKNOWN_DEVICE,
ON_NEW_DEVICE_ID,
ON_DEVICELIST_UPDATED,
ON_DEVICE_CONNECTED_NOTIFICATION,
ON_DEVICE_DISCONNECTED_NOTIFICATION,
ONVERIFIED_NOTIFICATION,
@ -255,6 +256,7 @@ class FxAccountsDevice {
lastFetch: this._fxai.now(),
devices,
};
Services.obs.notifyObservers(null, ON_DEVICELIST_UPDATED);
return true;
} finally {
this._fetchAndCacheDeviceListPromise = null;

Просмотреть файл

@ -18,6 +18,7 @@ const {
ERRNO_UNKNOWN_DEVICE,
ON_DEVICE_CONNECTED_NOTIFICATION,
ON_DEVICE_DISCONNECTED_NOTIFICATION,
ON_DEVICELIST_UPDATED,
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
var { AccountState } = ChromeUtils.import(
"resource://gre/modules/FxAccounts.jsm"
@ -789,6 +790,14 @@ add_task(async function test_refreshDeviceList() {
let spy = {
getDeviceList: { count: 0 },
};
const deviceListUpdateObserver = {
count: 0,
observe(subject, topic, data) {
this.count++;
},
};
Services.obs.addObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED);
fxAccountsClient.getDeviceList = (function(old) {
return function getDeviceList() {
spy.getDeviceList.count += 1;
@ -826,6 +835,11 @@ add_task(async function test_refreshDeviceList() {
"Should not have device list initially"
);
Assert.ok(await device.refreshDeviceList(), "Should refresh list");
Assert.equal(
deviceListUpdateObserver.count,
1,
`${ON_DEVICELIST_UPDATED} was notified`
);
Assert.deepEqual(
device.recentDeviceList,
[
@ -847,6 +861,11 @@ add_task(async function test_refreshDeviceList() {
!(await device.refreshDeviceList()),
"Should not refresh device list if fresh"
);
Assert.equal(
deviceListUpdateObserver.count,
1,
`${ON_DEVICELIST_UPDATED} was not notified`
);
fxai._now += device.TIME_BETWEEN_FXA_DEVICES_FETCH_MS;
@ -861,6 +880,11 @@ add_task(async function test_refreshDeviceList() {
2,
"Should only make one request if called with pending request"
);
Assert.equal(
deviceListUpdateObserver.count,
2,
`${ON_DEVICELIST_UPDATED} only notified once`
);
device.observe(null, ON_DEVICE_CONNECTED_NOTIFICATION);
await device.refreshDeviceList();
@ -869,6 +893,11 @@ add_task(async function test_refreshDeviceList() {
3,
"Should refresh device list after connecting new device"
);
Assert.equal(
deviceListUpdateObserver.count,
3,
`${ON_DEVICELIST_UPDATED} notified when new device connects`
);
device.observe(
null,
ON_DEVICE_DISCONNECTED_NOTIFICATION,
@ -880,6 +909,11 @@ add_task(async function test_refreshDeviceList() {
4,
"Should refresh device list after disconnecting device"
);
Assert.equal(
deviceListUpdateObserver.count,
4,
`${ON_DEVICELIST_UPDATED} notified when device disconnects`
);
device.observe(
null,
ON_DEVICE_DISCONNECTED_NOTIFICATION,
@ -891,11 +925,21 @@ add_task(async function test_refreshDeviceList() {
4,
"Should not refresh device list after disconnecting this device"
);
Assert.equal(
deviceListUpdateObserver.count,
4,
`${ON_DEVICELIST_UPDATED} not notified again`
);
let refreshBeforeResetPromise = device.refreshDeviceList({
ignoreCached: true,
});
fxai._generation++;
Assert.equal(
deviceListUpdateObserver.count,
4,
`${ON_DEVICELIST_UPDATED} not notified`
);
await Assert.rejects(refreshBeforeResetPromise, /Another user has signed in/);
device.reset();
@ -908,6 +952,12 @@ add_task(async function test_refreshDeviceList() {
await device.refreshDeviceList(),
"Should fetch new list after resetting"
);
Assert.equal(
deviceListUpdateObserver.count,
5,
`${ON_DEVICELIST_UPDATED} notified after reset`
);
Services.obs.removeObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED);
});
add_task(async function test_checking_remote_availableCommands_mismatch() {