forked from mirrors/gecko-dev
588 lines
16 KiB
JavaScript
588 lines
16 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
const {
|
|
withFirefoxView,
|
|
assertFirefoxViewTab,
|
|
assertFirefoxViewTabSelected,
|
|
openFirefoxViewTab,
|
|
closeFirefoxViewTab,
|
|
isFirefoxViewTabSelectedInWindow,
|
|
} = ChromeUtils.importESModule(
|
|
"resource://testing-common/FirefoxViewTestUtils.sys.mjs"
|
|
);
|
|
|
|
/* exported testVisibility */
|
|
|
|
const { ASRouter } = ChromeUtils.import(
|
|
"resource://activity-stream/lib/ASRouter.jsm"
|
|
);
|
|
const { UIState } = ChromeUtils.importESModule(
|
|
"resource://services-sync/UIState.sys.mjs"
|
|
);
|
|
const { sinon } = ChromeUtils.importESModule(
|
|
"resource://testing-common/Sinon.sys.mjs"
|
|
);
|
|
const { FeatureCalloutMessages } = ChromeUtils.importESModule(
|
|
"resource://activity-stream/lib/FeatureCalloutMessages.sys.mjs"
|
|
);
|
|
const { TelemetryTestUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/TelemetryTestUtils.sys.mjs"
|
|
);
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
|
|
SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
AboutWelcomeParent: "resource:///actors/AboutWelcomeParent.jsm",
|
|
});
|
|
|
|
const MOBILE_PROMO_DISMISSED_PREF =
|
|
"browser.tabs.firefox-view.mobilePromo.dismissed";
|
|
const RECENTLY_CLOSED_STATE_PREF =
|
|
"browser.tabs.firefox-view.ui-state.recently-closed-tabs.open";
|
|
const TAB_PICKUP_STATE_PREF =
|
|
"browser.tabs.firefox-view.ui-state.tab-pickup.open";
|
|
|
|
const calloutId = "multi-stage-message-root";
|
|
const calloutSelector = `#${calloutId}.featureCallout`;
|
|
const primaryButtonSelector = `#${calloutId} .primary`;
|
|
|
|
/**
|
|
* URLs used for browser_recently_closed_tabs_keyboard and
|
|
* browser_firefoxview_accessibility
|
|
*/
|
|
const URLs = [
|
|
"http://mochi.test:8888/browser/",
|
|
"https://www.example.com/",
|
|
"https://example.net/",
|
|
"https://example.org/",
|
|
];
|
|
|
|
const syncedTabsData1 = [
|
|
{
|
|
id: 1,
|
|
type: "client",
|
|
name: "My desktop",
|
|
clientType: "desktop",
|
|
lastModified: 1655730486760,
|
|
tabs: [
|
|
{
|
|
type: "tab",
|
|
title: "Sandboxes - Sinon.JS",
|
|
url: "https://sinonjs.org/releases/latest/sandbox/",
|
|
icon: "https://sinonjs.org/assets/images/favicon.png",
|
|
lastUsed: 1655391592, // Thu Jun 16 2022 14:59:52 GMT+0000
|
|
},
|
|
{
|
|
type: "tab",
|
|
title: "Internet for people, not profits - Mozilla",
|
|
url: "https://www.mozilla.org/",
|
|
icon: "https://www.mozilla.org/media/img/favicons/mozilla/favicon.d25d81d39065.ico",
|
|
lastUsed: 1655730486, // Mon Jun 20 2022 13:08:06 GMT+0000
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
type: "client",
|
|
name: "My iphone",
|
|
clientType: "phone",
|
|
lastModified: 1655727832930,
|
|
tabs: [
|
|
{
|
|
type: "tab",
|
|
title: "The Guardian",
|
|
url: "https://www.theguardian.com/",
|
|
icon: "page-icon:https://www.theguardian.com/",
|
|
lastUsed: 1655291890, // Wed Jun 15 2022 11:18:10 GMT+0000
|
|
},
|
|
{
|
|
type: "tab",
|
|
title: "The Times",
|
|
url: "https://www.thetimes.co.uk/",
|
|
icon: "page-icon:https://www.thetimes.co.uk/",
|
|
lastUsed: 1655727485, // Mon Jun 20 2022 12:18:05 GMT+0000
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
async function clearAllParentTelemetryEvents() {
|
|
// Clear everything.
|
|
await TestUtils.waitForCondition(() => {
|
|
Services.telemetry.clearEvents();
|
|
let events = Services.telemetry.snapshotEvents(
|
|
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
|
|
true
|
|
).parent;
|
|
return !events || !events.length;
|
|
});
|
|
}
|
|
|
|
function testVisibility(browser, expected) {
|
|
const { document } = browser.contentWindow;
|
|
for (let [selector, shouldBeVisible] of Object.entries(
|
|
expected.expectedVisible
|
|
)) {
|
|
const elem = document.querySelector(selector);
|
|
if (shouldBeVisible) {
|
|
ok(
|
|
BrowserTestUtils.is_visible(elem),
|
|
`Expected ${selector} to be visible`
|
|
);
|
|
} else {
|
|
ok(BrowserTestUtils.is_hidden(elem), `Expected ${selector} to be hidden`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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(
|
|
elem,
|
|
{
|
|
attributeFilter: ["hidden"],
|
|
},
|
|
() => {
|
|
return isVisible
|
|
? BrowserTestUtils.is_visible(elem)
|
|
: BrowserTestUtils.is_hidden(elem);
|
|
}
|
|
);
|
|
}
|
|
|
|
async function waitForVisibleSetupStep(browser, expected) {
|
|
const { document } = browser.contentWindow;
|
|
|
|
const deck = document.querySelector(".sync-setup-container");
|
|
const nextStepElem = deck.querySelector(expected.expectedVisible);
|
|
const stepElems = deck.querySelectorAll(".setup-step");
|
|
|
|
await BrowserTestUtils.waitForMutationCondition(
|
|
deck,
|
|
{
|
|
attributeFilter: ["selected-view"],
|
|
},
|
|
() => {
|
|
return BrowserTestUtils.is_visible(nextStepElem);
|
|
}
|
|
);
|
|
|
|
for (let elem of stepElems) {
|
|
if (elem == nextStepElem) {
|
|
ok(
|
|
BrowserTestUtils.is_visible(elem),
|
|
`Expected ${elem.id || elem.className} to be visible`
|
|
);
|
|
} else {
|
|
ok(
|
|
BrowserTestUtils.is_hidden(elem),
|
|
`Expected ${elem.id || elem.className} to be hidden`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
var gMockFxaDevices = null;
|
|
var gUIStateStatus;
|
|
var gSandbox;
|
|
function setupSyncFxAMocks({ fxaDevices = null, state, syncEnabled = true }) {
|
|
gUIStateStatus = state || UIState.STATUS_SIGNED_IN;
|
|
if (gSandbox) {
|
|
gSandbox.restore();
|
|
}
|
|
const sandbox = (gSandbox = sinon.createSandbox());
|
|
gMockFxaDevices = fxaDevices;
|
|
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => fxaDevices);
|
|
sandbox.stub(UIState, "get").callsFake(() => {
|
|
return {
|
|
status: gUIStateStatus,
|
|
syncEnabled,
|
|
email:
|
|
gUIStateStatus === UIState.STATUS_NOT_CONFIGURED
|
|
? undefined
|
|
: "email@example.com",
|
|
};
|
|
});
|
|
|
|
return sandbox;
|
|
}
|
|
|
|
function setupRecentDeviceListMocks() {
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => [
|
|
{
|
|
id: 1,
|
|
name: "My desktop",
|
|
isCurrentDevice: true,
|
|
type: "desktop",
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "My iphone",
|
|
type: "mobile",
|
|
},
|
|
]);
|
|
|
|
sandbox.stub(UIState, "get").returns({
|
|
status: UIState.STATUS_SIGNED_IN,
|
|
syncEnabled: true,
|
|
email: "email@example.com",
|
|
});
|
|
|
|
return sandbox;
|
|
}
|
|
|
|
function getMockTabData(clients) {
|
|
return SyncedTabs._internal._createRecentTabsList(clients, 10);
|
|
}
|
|
|
|
async function setupListState(browser) {
|
|
// Skip the synced tabs sign up flow to get to a loaded list state
|
|
await SpecialPowers.pushPrefEnv({
|
|
set: [["services.sync.engine.tabs", true]],
|
|
});
|
|
|
|
UIState.refresh();
|
|
const recentFetchTime = Math.floor(Date.now() / 1000);
|
|
info("updating lastFetch:" + recentFetchTime);
|
|
Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
|
|
|
|
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
|
|
|
|
await waitForElementVisible(browser, "#tabpickup-steps", false);
|
|
await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
|
|
|
|
const tabsContainer = browser.contentWindow.document.querySelector(
|
|
"#tabpickup-tabs-container"
|
|
);
|
|
await tabsContainer.tabListAdded;
|
|
await BrowserTestUtils.waitForMutationCondition(
|
|
tabsContainer,
|
|
{ attributeFilter: ["class"], attributes: true },
|
|
() => {
|
|
return !tabsContainer.classList.contains("loading");
|
|
}
|
|
);
|
|
info("tabsContainer isn't loading anymore, returning");
|
|
}
|
|
|
|
function checkMobilePromo(browser, expected = {}) {
|
|
const { document } = browser.contentWindow;
|
|
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 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();
|
|
}
|
|
|
|
let gUIStateSyncEnabled;
|
|
function setupMocks({ fxaDevices = null, state, syncEnabled = true }) {
|
|
gUIStateStatus = state || UIState.STATUS_SIGNED_IN;
|
|
gUIStateSyncEnabled = syncEnabled;
|
|
if (gSandbox) {
|
|
gSandbox.restore();
|
|
}
|
|
const sandbox = (gSandbox = sinon.createSandbox());
|
|
gMockFxaDevices = fxaDevices;
|
|
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => fxaDevices);
|
|
sandbox.stub(UIState, "get").callsFake(() => {
|
|
return {
|
|
status: gUIStateStatus,
|
|
// Sometimes syncEnabled is not present on UIState, for example when the user signs
|
|
// out the state is just { status: "not_configured" }
|
|
...(gUIStateSyncEnabled != undefined && {
|
|
syncEnabled: gUIStateSyncEnabled,
|
|
}),
|
|
};
|
|
});
|
|
sandbox.stub(SyncedTabs, "getTabClients").callsFake(() => {
|
|
// The real getTabClients does not return the current device
|
|
return Promise.resolve(
|
|
fxaDevices.filter(device => !device.isCurrentDevice)
|
|
);
|
|
});
|
|
return sandbox;
|
|
}
|
|
|
|
async function tearDown(sandbox) {
|
|
sandbox?.restore();
|
|
Services.prefs.clearUserPref("services.sync.lastTabFetch");
|
|
Services.prefs.clearUserPref(MOBILE_PROMO_DISMISSED_PREF);
|
|
}
|
|
|
|
const featureTourPref = "browser.firefox-view.feature-tour";
|
|
const launchFeatureTourIn = win => {
|
|
const { FeatureCallout } = ChromeUtils.importESModule(
|
|
"resource:///modules/FeatureCallout.sys.mjs"
|
|
);
|
|
let callout = new FeatureCallout({
|
|
win,
|
|
pref: { name: featureTourPref },
|
|
location: "about:firefoxview",
|
|
context: "content",
|
|
theme: { preset: "themed-content" },
|
|
});
|
|
callout.showFeatureCallout();
|
|
return callout;
|
|
};
|
|
|
|
/**
|
|
* Returns a value that can be used to set
|
|
* `browser.firefox-view.feature-tour` to change the feature tour's
|
|
* UI state.
|
|
*
|
|
* @see FeatureCalloutMessages.sys.mjs for valid values of "screen"
|
|
*
|
|
* @param {number} screen The full ID of the feature callout screen
|
|
* @return {string} JSON string used to set
|
|
* `browser.firefox-view.feature-tour`
|
|
*/
|
|
const getPrefValueByScreen = screen => {
|
|
return JSON.stringify({
|
|
screen: `FEATURE_CALLOUT_${screen}`,
|
|
complete: false,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Wait for a feature callout screen of given parameters to be shown
|
|
* @param {Document} doc the document where the callout appears.
|
|
* @param {String} screenPostfix The full ID of the feature callout screen.
|
|
*/
|
|
const waitForCalloutScreen = async (doc, screenPostfix) => {
|
|
await BrowserTestUtils.waitForCondition(() =>
|
|
doc.querySelector(`${calloutSelector}:not(.hidden) .${screenPostfix}`)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Waits for the feature callout screen to be removed.
|
|
*
|
|
* @param {Document} doc The document where the callout appears.
|
|
*/
|
|
const waitForCalloutRemoved = async doc => {
|
|
await BrowserTestUtils.waitForCondition(() => {
|
|
return !doc.body.querySelector(calloutSelector);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* NOTE: Should be replaced with synthesizeMouseAtCenter for
|
|
* simulating user input. See Bug 1798322
|
|
*
|
|
* Clicks the primary button in the feature callout dialog
|
|
*
|
|
* @param {document} doc Firefox View document
|
|
*/
|
|
const clickPrimaryButton = async doc => {
|
|
doc.querySelector(primaryButtonSelector).click();
|
|
};
|
|
|
|
/**
|
|
* Closes a feature callout via a click to the dismiss button.
|
|
*
|
|
* @param {Document} doc The document where the callout appears.
|
|
*/
|
|
const closeCallout = async doc => {
|
|
// close the callout dialog
|
|
const dismissBtn = doc.querySelector(`${calloutSelector} .dismiss-button`);
|
|
if (!dismissBtn) {
|
|
return;
|
|
}
|
|
doc.querySelector(`${calloutSelector} .dismiss-button`).click();
|
|
await BrowserTestUtils.waitForCondition(() => {
|
|
return !document.querySelector(calloutSelector);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get a Feature Callout message by id.
|
|
*
|
|
* @param {string} Message id
|
|
*/
|
|
const getCalloutMessageById = id => {
|
|
return {
|
|
message: FeatureCalloutMessages.getMessages().find(m => m.id === id),
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Create a sinon sandbox with `sendTriggerMessage` stubbed
|
|
* to return a specified test message for featureCalloutCheck.
|
|
*
|
|
* @param {object} testMessage
|
|
* @param {string} [source="about:firefoxview"]
|
|
*/
|
|
const createSandboxWithCalloutTriggerStub = (
|
|
testMessage,
|
|
source = "about:firefoxview"
|
|
) => {
|
|
const firefoxViewMatch = sinon.match({
|
|
id: "featureCalloutCheck",
|
|
context: { source },
|
|
});
|
|
const sandbox = sinon.createSandbox();
|
|
const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage");
|
|
sendTriggerStub.withArgs(firefoxViewMatch).resolves(testMessage);
|
|
sendTriggerStub.callThrough();
|
|
return sandbox;
|
|
};
|
|
|
|
/**
|
|
* A helper to check that correct telemetry was sent by AWSendEventTelemetry.
|
|
* This is a wrapper around sinon's spy functionality.
|
|
*
|
|
* @example
|
|
* let spy = new TelemetrySpy();
|
|
* element.click();
|
|
* spy.assertCalledWith({ event: "CLICK" });
|
|
* spy.restore();
|
|
*/
|
|
class TelemetrySpy {
|
|
/**
|
|
* @param {object} [sandbox] A pre-existing sinon sandbox to build the spy in.
|
|
* If not provided, a new sandbox will be created.
|
|
*/
|
|
constructor(sandbox = sinon.createSandbox()) {
|
|
this.sandbox = sandbox;
|
|
this.spy = this.sandbox
|
|
.spy(AboutWelcomeParent.prototype, "onContentMessage")
|
|
.withArgs("AWPage:TELEMETRY_EVENT");
|
|
registerCleanupFunction(() => this.restore());
|
|
}
|
|
/**
|
|
* Assert that AWSendEventTelemetry sent the expected telemetry object.
|
|
* @param {Object} expectedData
|
|
*/
|
|
assertCalledWith(expectedData) {
|
|
let match = this.spy.calledWith("AWPage:TELEMETRY_EVENT", expectedData);
|
|
if (match) {
|
|
ok(true, "Expected telemetry sent");
|
|
} else if (this.spy.called) {
|
|
ok(
|
|
false,
|
|
"Wrong telemetry sent: " + JSON.stringify(this.spy.lastCall.args)
|
|
);
|
|
} else {
|
|
ok(false, "No telemetry sent");
|
|
}
|
|
}
|
|
reset() {
|
|
this.spy.resetHistory();
|
|
}
|
|
restore() {
|
|
this.sandbox.restore();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to open and close a tab so the recently
|
|
* closed tabs list can have data.
|
|
*
|
|
* @param {string} url
|
|
* @return {Promise} Promise that resolves when the session store
|
|
* has been updated after closing the tab.
|
|
*/
|
|
async function open_then_close(url, win = window) {
|
|
let { updatePromise } = await BrowserTestUtils.withNewTab(
|
|
{ url, gBrowser: win.gBrowser },
|
|
async browser => {
|
|
return {
|
|
updatePromise: BrowserTestUtils.waitForSessionStoreUpdate({
|
|
linkedBrowser: browser,
|
|
}),
|
|
};
|
|
}
|
|
);
|
|
await updatePromise;
|
|
return TestUtils.topicObserved("sessionstore-closed-objects-changed");
|
|
}
|
|
|
|
/**
|
|
* Clears session history. Used to clear out the recently closed tabs list.
|
|
*
|
|
*/
|
|
function clearHistory() {
|
|
Services.obs.notifyObservers(null, "browser:purge-session-history");
|
|
}
|
|
|
|
/**
|
|
* Cleanup function for tab pickup tests.
|
|
*
|
|
*/
|
|
function cleanup_tab_pickup() {
|
|
Services.prefs.clearUserPref("services.sync.engine.tabs");
|
|
Services.prefs.clearUserPref("services.sync.lastTabFetch");
|
|
Services.prefs.clearUserPref(TAB_PICKUP_STATE_PREF);
|
|
}
|
|
|
|
function isFirefoxViewTabSelected(win = window) {
|
|
return isFirefoxViewTabSelectedInWindow(win);
|
|
}
|
|
|
|
function promiseAllButPrimaryWindowClosed() {
|
|
let windows = [];
|
|
for (let win of BrowserWindowTracker.orderedWindows) {
|
|
if (win != window) {
|
|
windows.push(win);
|
|
}
|
|
}
|
|
return Promise.all(windows.map(BrowserTestUtils.closeWindow));
|
|
}
|
|
|
|
registerCleanupFunction(() => {
|
|
// ensure all the stubs are restored, regardless of any exceptions
|
|
// that might have prevented it
|
|
gSandbox?.restore();
|
|
});
|
|
|
|
function navigateToCategory(document, category) {
|
|
const navigation = document.querySelector("fxview-category-navigation");
|
|
let navButton = Array.from(navigation.categoryButtons).filter(
|
|
categoryButton => {
|
|
return categoryButton.name === category;
|
|
}
|
|
)[0];
|
|
navButton.buttonEl.click();
|
|
}
|