summaryrefslogtreecommitdiffstats
path: root/browser/components/firefoxview/tests/browser/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/firefoxview/tests/browser/head.js')
-rw-r--r--browser/components/firefoxview/tests/browser/head.js708
1 files changed, 708 insertions, 0 deletions
diff --git a/browser/components/firefoxview/tests/browser/head.js b/browser/components/firefoxview/tests/browser/head.js
new file mode 100644
index 0000000000..b0b41b759d
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/head.js
@@ -0,0 +1,708 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {
+ getFirefoxViewURL,
+ withFirefoxView,
+ assertFirefoxViewTab,
+ assertFirefoxViewTabSelected,
+ openFirefoxViewTab,
+ closeFirefoxViewTab,
+ isFirefoxViewTabSelectedInWindow,
+ init: FirefoxViewTestUtilsInit,
+} = ChromeUtils.importESModule(
+ "resource://testing-common/FirefoxViewTestUtils.sys.mjs"
+);
+
+/* exported testVisibility */
+
+const { ASRouter } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/ASRouter.sys.mjs"
+);
+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:///modules/asrouter/FeatureCalloutMessages.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+const { SessionStoreTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SessionStoreTestUtils.sys.mjs"
+);
+SessionStoreTestUtils.init(this, window);
+FirefoxViewTestUtilsInit(this, window);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AboutWelcomeParent: "resource:///actors/AboutWelcomeParent.sys.mjs",
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
+ SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+const calloutId = "feature-callout";
+const calloutSelector = `#${calloutId}.featureCallout`;
+const CTASelector = `#${calloutId} :is(.primary, .secondary)`;
+
+/**
+ * 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/",
+ "about:robots",
+ "https://www.mozilla.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
+ client: 1,
+ },
+ {
+ 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
+ client: 1,
+ },
+ ],
+ },
+ {
+ 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
+ client: 2,
+ },
+ {
+ 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
+ client: 2,
+ },
+ ],
+ },
+];
+
+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.isVisible(elem),
+ `Expected ${selector} to be visible`
+ );
+ } else {
+ ok(BrowserTestUtils.isHidden(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.isVisible(elem)
+ : BrowserTestUtils.isHidden(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.isVisible(nextStepElem);
+ }
+ );
+
+ for (let elem of stepElems) {
+ if (elem == nextStepElem) {
+ ok(
+ BrowserTestUtils.isVisible(elem),
+ `Expected ${elem.id || elem.className} to be visible`
+ );
+ } else {
+ ok(
+ BrowserTestUtils.isHidden(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");
+}
+
+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,
+ }),
+ };
+ });
+ // This is converting the device list to a client list.
+ // There are two primary differences:
+ // 1. The client list doesn't return the current device.
+ // 2. It uses clientType instead of type.
+ let tabClients = fxaDevices ? [...fxaDevices] : [];
+ for (let client of tabClients) {
+ client.clientType = client.type;
+ }
+ tabClients = tabClients.filter(device => !device.isCurrentDevice);
+ sandbox.stub(SyncedTabs, "getTabClients").callsFake(() => {
+ return Promise.resolve(tabClients);
+ });
+ return sandbox;
+}
+
+async function tearDown(sandbox) {
+ sandbox?.restore();
+ Services.prefs.clearUserPref("services.sync.lastTabFetch");
+}
+
+const featureTourPref = "browser.firefox-view.feature-tour";
+const launchFeatureTourIn = win => {
+ const { FeatureCallout } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/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
+ * @returns {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 clickCTA = async doc => {
+ doc.querySelector(CTASelector).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} id
+ * The 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
+ * @returns {Promise} Promise that resolves when the session store
+ * has been updated after closing the tab.
+ */
+async function open_then_close(url, win = window) {
+ return SessionStoreTestUtils.openAndCloseTab(win, url);
+}
+
+/**
+ * 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");
+}
+
+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();
+}
+
+async function navigateToCategoryAndWait(document, category) {
+ info(`navigateToCategoryAndWait, for ${category}`);
+ const navigation = document.querySelector("fxview-category-navigation");
+ const win = document.ownerGlobal;
+ SimpleTest.promiseFocus(win);
+ let navButton = Array.from(navigation.categoryButtons).find(
+ categoryButton => {
+ return categoryButton.name === category;
+ }
+ );
+ const namedDeck = document.querySelector("named-deck");
+
+ await BrowserTestUtils.waitForCondition(
+ () => navButton.getBoundingClientRect().height,
+ `Waiting for ${category} button to be clickable`
+ );
+
+ EventUtils.synthesizeMouseAtCenter(navButton, {}, win);
+
+ await BrowserTestUtils.waitForCondition(() => {
+ let selectedView = Array.from(namedDeck.children).find(
+ child => child.slot == "selected"
+ );
+ return (
+ namedDeck.selectedViewName == category &&
+ selectedView?.getBoundingClientRect().height
+ );
+ }, `Waiting for ${category} to be visible`);
+}
+
+/**
+ * Switch to the Firefox View tab.
+ *
+ * @param {Window} [win]
+ * The window to use, if specified. Defaults to the global window instance.
+ * @returns {Promise<MozTabbrowserTab>}
+ * The tab switched to.
+ */
+async function switchToFxViewTab(win = window) {
+ return BrowserTestUtils.switchTab(win.gBrowser, win.FirefoxViewHandler.tab);
+}
+
+function isElInViewport(element) {
+ const boundingRect = element.getBoundingClientRect();
+ return (
+ boundingRect.top >= 0 &&
+ boundingRect.left >= 0 &&
+ boundingRect.bottom <=
+ (window.innerHeight || document.documentElement.clientHeight) &&
+ boundingRect.right <=
+ (window.innerWidth || document.documentElement.clientWidth)
+ );
+}
+
+// TODO once we port over old tests, helpers and cleanup old firefox view
+// we should decide whether to keep this or openFirefoxViewTab.
+async function clickFirefoxViewButton(win) {
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#firefox-view-button",
+ { type: "mousedown" },
+ win.browsingContext
+ );
+}
+
+/**
+ * Wait for and assert telemetry events.
+ *
+ * @param {Array} eventDetails
+ * Nested array of event details
+ */
+async function telemetryEvent(eventDetails) {
+ await TestUtils.waitForCondition(
+ () => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ ).parent;
+ return events && events.length >= 1;
+ },
+ "Waiting for firefoxview_next telemetry event.",
+ 200,
+ 100
+ );
+
+ TelemetryTestUtils.assertEvents(
+ eventDetails,
+ { category: "firefoxview_next" },
+ { clear: true, process: "parent" }
+ );
+}
+
+function setSortOption(component, value) {
+ info(`Sort by ${value}.`);
+ const el = component.optionsContainer.querySelector(
+ `input[value='${value}']`
+ );
+ EventUtils.synthesizeMouseAtCenter(el, {}, el.ownerGlobal);
+}
+
+function getOpenTabsCards(openTabs) {
+ return openTabs.shadowRoot.querySelectorAll("view-opentabs-card");
+}
+
+async function click_recently_closed_tab_item(itemElem, itemProperty = "") {
+ // Make sure the firefoxview tab still has focus
+ is(
+ itemElem.ownerDocument.location.href,
+ "about:firefoxview#recentlyclosed",
+ "about:firefoxview is the selected tab and showing the Recently closed view page"
+ );
+
+ // Scroll to the tab element to ensure dismiss button is visible
+ itemElem.scrollIntoView();
+ is(isElInViewport(itemElem), true, "Tab is visible in viewport");
+ let clickTarget;
+ switch (itemProperty) {
+ case "dismiss":
+ clickTarget = itemElem.buttonEl;
+ break;
+ default:
+ clickTarget = itemElem.mainEl;
+ break;
+ }
+
+ const closedObjectsChangePromise = TestUtils.topicObserved(
+ "sessionstore-closed-objects-changed"
+ );
+ EventUtils.synthesizeMouseAtCenter(clickTarget, {}, itemElem.ownerGlobal);
+ await closedObjectsChangePromise;
+}
+
+async function waitForRecentlyClosedTabsList(doc) {
+ let recentlyClosedComponent = doc.querySelector(
+ "view-recentlyclosed:not([slot=recentlyclosed])"
+ );
+ // Check that the tabs list is rendered
+ await TestUtils.waitForCondition(() => {
+ return recentlyClosedComponent.cardEl;
+ });
+ let cardContainer = recentlyClosedComponent.cardEl;
+ let cardMainSlotNode = Array.from(
+ cardContainer?.mainSlot?.assignedNodes()
+ )[0];
+ await TestUtils.waitForCondition(() => {
+ return cardMainSlotNode.rowEls.length;
+ });
+ return [cardMainSlotNode, cardMainSlotNode.rowEls];
+}