1 /* Any copyright is dedicated to the Public Domain.
2 http://creativecommons.org/publicdomain/zero/1.0/ */
6 requestLongerTimeout(10);
9 * Ensure that the add_new_tab, close_tab, and open_then_close
10 * functions are creating sessionstore entries associated with
11 * the correct window where the tests are run.
14 ChromeUtils.defineESModuleGetters(globalThis, {
15 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
18 const RECENTLY_CLOSED_EVENT = [
19 ["firefoxview", "recently_closed", "tabs", undefined],
22 const CLOSED_TABS_OPEN_EVENT = [
23 ["firefoxview", "closed_tabs_open", "tabs", "false"],
26 const RECENTLY_CLOSED_DISMISS_EVENT = [
27 ["firefoxview", "dismiss_closed_tab", "tabs", undefined],
30 async function add_new_tab(URL) {
31 let tab = BrowserTestUtils.addTab(gBrowser, URL);
32 await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
36 async function close_tab(tab) {
37 const sessionStorePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
38 BrowserTestUtils.removeTab(tab);
39 await sessionStorePromise;
42 async function dismiss_tab(tab, content) {
43 info(`Dismissing tab ${tab.dataset.targeturi}`);
44 const closedObjectsChanged = () =>
45 TestUtils.topicObserved("sessionstore-closed-objects-changed");
46 let dismissButton = tab.querySelector(".closed-tab-li-dismiss");
47 EventUtils.synthesizeMouseAtCenter(dismissButton, {}, content);
48 await closedObjectsChanged();
51 add_setup(async function setup() {
52 // set updateTimeMs to 0 to prevent unexpected/unrelated DOM mutations during testing
53 await SpecialPowers.pushPrefEnv({
54 set: [["browser.tabs.firefox-view.updateTimeMs", 100000]],
58 add_task(async function test_empty_list() {
61 await withFirefoxView({}, async browser => {
62 const { document } = browser.contentWindow;
63 let container = document.querySelector("#collapsible-tabs-container");
65 container.classList.contains("empty-container"),
66 "collapsible container should have correct styling when the list is empty"
70 document.getElementById("recently-closed-tabs-placeholder"),
71 "The empty message is displayed."
75 !document.querySelector("ol.closed-tabs-list"),
76 "The recently closed tabs list is not displayed."
79 const tab1 = await add_new_tab(URLs[0]);
81 await close_tab(tab1);
83 // The UI update happens asynchronously as we learn of the new closed tab.
84 await BrowserTestUtils.waitForMutationCondition(
86 { attributeFilter: ["class"] },
87 () => !container.classList.contains("empty-container")
90 !container.classList.contains("empty-container"),
91 "collapsible container should have correct styling when the list is not empty"
95 !document.getElementById("recently-closed-tabs-placeholder"),
96 "The empty message is not displayed."
100 document.querySelector("ol.closed-tabs-list"),
101 "The recently closed tabs list is displayed."
105 document.querySelector("ol.closed-tabs-list").children.length,
107 "recently-closed-tabs-list should have one list item"
112 add_task(async function test_list_ordering() {
113 Services.obs.notifyObservers(null, "browser:purge-session-history");
115 SessionStore.getClosedTabCount(),
117 "Closed tab count after purging session history"
120 const closedObjectsChanged = () =>
121 TestUtils.topicObserved("sessionstore-closed-objects-changed");
123 const tab1 = await add_new_tab(URLs[0]);
124 const tab2 = await add_new_tab(URLs[1]);
125 const tab3 = await add_new_tab(URLs[2]);
127 gBrowser.selectedTab = tab3;
129 await close_tab(tab3);
130 await closedObjectsChanged();
132 await close_tab(tab2);
133 await closedObjectsChanged();
135 await close_tab(tab1);
136 await closedObjectsChanged();
138 await withFirefoxView({}, async browser => {
139 const { document } = browser.contentWindow;
140 const tabsList = document.querySelector("ol.closed-tabs-list");
141 await BrowserTestUtils.waitForMutationCondition(
144 () => tabsList.children.length > 1
148 document.querySelector("ol.closed-tabs-list").children.length,
150 "recently-closed-tabs-list should have three list items"
153 // check that the ordering is correct when user navigates to another tab, and then closes multiple tabs.
156 .querySelector("ol.closed-tabs-list")
157 .children[0].textContent.includes("mochi.test"),
158 "first list item in recently-closed-tabs-list is in the correct order"
163 .querySelector("ol.closed-tabs-list")
164 .children[2].textContent.includes("example.net"),
165 "last list item in recently-closed-tabs-list is in the correct order"
168 let ele = document.querySelector("ol.closed-tabs-list").firstElementChild;
169 let uri = ele.getAttribute("data-targeturi");
170 await clearAllParentTelemetryEvents();
171 let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, uri);
172 ele.querySelector(".closed-tab-li-main").click();
175 await TestUtils.waitForCondition(
177 let events = Services.telemetry.snapshotEvents(
178 Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
181 return events && events.length >= 1;
183 "Waiting for recently_closed firefoxview telemetry events.",
188 TelemetryTestUtils.assertEvents(
189 RECENTLY_CLOSED_EVENT,
190 { category: "firefoxview" },
191 { clear: true, process: "parent" }
194 gBrowser.removeTab(gBrowser.selectedTab);
196 await clearAllParentTelemetryEvents();
198 await waitForElementVisible(
200 "#recently-closed-tabs-container > summary"
202 document.querySelector("#recently-closed-tabs-container > summary").click();
204 await TestUtils.waitForCondition(
206 let events = Services.telemetry.snapshotEvents(
207 Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
210 return events && events.length >= 1;
212 "Waiting for closed_tabs_open firefoxview telemetry event.",
217 TelemetryTestUtils.assertEvents(
218 CLOSED_TABS_OPEN_EVENT,
219 { category: "firefoxview" },
220 { clear: true, process: "parent" }
225 add_task(async function test_max_list_items() {
226 Services.obs.notifyObservers(null, "browser:purge-session-history");
228 SessionStore.getClosedTabCountForWindow(window),
230 "Closed tab count after purging session history"
233 await open_then_close(URLs[0]);
234 await open_then_close(URLs[1]);
235 await open_then_close(URLs[2]);
237 // Seed the closed tabs count. We've assured that we've opened and
238 // closed at least three tabs because of the calls to open_then_close
240 let mockMaxTabsLength = 3;
242 await withFirefoxView({}, async browser => {
243 const { document } = browser.contentWindow;
245 // override this value for testing purposes
246 document.querySelector("recently-closed-tabs-list").maxTabsLength =
251 .querySelector("#collapsible-tabs-container")
252 .classList.contains("empty-container"),
253 "collapsible container should have correct styling when the list is not empty"
257 !document.getElementById("recently-closed-tabs-placeholder"),
258 "The empty message is not displayed."
262 document.querySelector("ol.closed-tabs-list"),
263 "The recently closed tabs list is displayed."
267 document.querySelector("ol.closed-tabs-list").children.length,
269 `recently-closed-tabs-list should have ${mockMaxTabsLength} list items`
272 const closedObjectsChanged = TestUtils.topicObserved(
273 "sessionstore-closed-objects-changed"
276 const tab = await add_new_tab(URLs[3]);
277 await close_tab(tab);
278 await closedObjectsChanged;
279 let firstListItem = document.querySelector("ol.closed-tabs-list")
281 await BrowserTestUtils.waitForMutationCondition(
283 { characterData: true, childList: true, subtree: true },
284 () => firstListItem.textContent.includes(".org")
287 firstListItem.textContent.includes("example.org"),
288 "first list item in recently-closed-tabs-list should have been updated"
292 document.querySelector("ol.closed-tabs-list").children.length,
294 `recently-closed-tabs-list should still have ${mockMaxTabsLength} list items`
299 add_task(async function test_time_updates_correctly() {
302 SessionStore.getClosedTabCountForWindow(window),
304 "Closed tab count after purging session history"
307 // Set the closed tabs state to include one tab that was closed 2 seconds ago.
308 // This is well below the initial threshold for displaying the 'Just now' timestamp.
309 // It is also much greater than the 5ms threshold we use for the updated pref value,
310 // which results in the timestamp text changing after the pref value is changed.
311 const TAB_CLOSED_AGO_MS = 2000;
312 const TAB_UPDATE_TIME_MS = 5;
313 const TAB_CLOSED_STATE = {
316 tabs: [{ entries: [] }],
319 state: { entries: [{ url: "https://www.example.com/" }] },
321 closedAt: Date.now() - TAB_CLOSED_AGO_MS,
329 await SessionStore.setBrowserState(JSON.stringify(TAB_CLOSED_STATE));
332 SessionStore.getClosedTabCountForWindow(window),
334 "Closed tab count after setting browser state"
337 await withFirefoxView({}, async browser => {
338 const { document } = browser.contentWindow;
340 const tabsList = document.querySelector("ol.closed-tabs-list");
341 const numOfListItems = tabsList.children.length;
342 const lastListItem = tabsList.children[numOfListItems - 1];
343 const timeLabel = lastListItem.querySelector("span.closed-tab-li-time");
344 let initialTimeText = timeLabel.textContent;
345 Assert.stringContains(
348 "recently-closed-tabs list item time is 'Just now'"
351 await SpecialPowers.pushPrefEnv({
352 set: [["browser.tabs.firefox-view.updateTimeMs", TAB_UPDATE_TIME_MS]],
355 await BrowserTestUtils.waitForMutationCondition(
358 () => !timeLabel.textContent.includes("now")
362 timeLabel.textContent,
364 "recently-closed-tabs list item time has updated"
367 await SpecialPowers.popPrefEnv();
369 // Cleanup recently closed tab data.
373 add_task(async function test_list_maintains_focus_when_restoring_tab() {
374 await SpecialPowers.clearUserPref(RECENTLY_CLOSED_STATE_PREF);
375 Services.obs.notifyObservers(null, "browser:purge-session-history");
377 SessionStore.getClosedTabCountForWindow(window),
379 "Closed tab count after purging session history"
382 const sandbox = sinon.createSandbox();
383 let setupCompleteStub = sandbox.stub(
384 TabsSetupFlowManager,
385 "isTabSyncSetupComplete"
387 setupCompleteStub.returns(true);
389 await open_then_close(URLs[0]);
390 await open_then_close(URLs[1]);
391 await open_then_close(URLs[2]);
393 await withFirefoxView({}, async browser => {
394 let gBrowser = browser.getTabBrowser();
395 const { document } = browser.contentWindow;
396 const list = document.querySelectorAll(".closed-tab-li");
397 list[0].querySelector(".closed-tab-li-main").focus();
398 EventUtils.synthesizeKey("KEY_Enter");
399 let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View");
400 await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab);
402 document.activeElement.textContent.includes("mochitest index"),
403 "Focus should be on the first item in the recently closed list"
407 // clean up extra tabs
408 while (gBrowser.tabs.length > 1) {
409 BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
413 await open_then_close(URLs[2]);
414 await withFirefoxView({}, async browser => {
415 let gBrowser = browser.getTabBrowser();
416 const { document } = browser.contentWindow;
417 let expectedFocusedElement = document.getElementById(
418 "recently-closed-tabs-header-section"
420 const list = document.querySelectorAll(".closed-tab-li");
421 list[0].querySelector(".closed-tab-li-main").focus();
422 EventUtils.synthesizeKey("KEY_Enter");
423 let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View");
424 await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab);
426 document.activeElement,
427 expectedFocusedElement,
428 "Focus should be on the section header"
432 // clean up extra tabs
433 while (gBrowser.tabs.length > 1) {
434 BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
438 add_task(async function test_switch_before_closing() {
441 const INITIAL_URL = "https://example.org/iwilldisappear";
442 const FINAL_URL = "https://example.com/ishouldappear";
443 await withFirefoxView({}, async function (browser) {
444 let gBrowser = browser.getTabBrowser();
445 let newTab = await BrowserTestUtils.openNewForegroundTab(
449 // Switch back to FxView:
450 await BrowserTestUtils.switchTab(
452 gBrowser.getTabForBrowser(browser)
454 // Update the tab we opened to a different site:
455 let loadPromise = BrowserTestUtils.browserLoaded(
456 newTab.linkedBrowser,
460 BrowserTestUtils.startLoadingURIString(newTab.linkedBrowser, FINAL_URL);
462 // Close the added tab
463 BrowserTestUtils.removeTab(newTab);
465 const { document } = browser.contentWindow;
466 await BrowserTestUtils.waitForMutationCondition(
467 document.querySelector("recently-closed-tabs-list"),
468 { childList: true, subtree: true },
469 () => document.querySelector("ol.closed-tabs-list")
471 const tabsList = document.querySelector("ol.closed-tabs-list");
472 info("A tab appeared in the list, ensure it has the right URL.");
473 let urlBit = tabsList.children[0].querySelector(".closed-tab-li-url");
474 await BrowserTestUtils.waitForMutationCondition(
476 { characterData: true, attributeFilter: ["title"] },
477 () => urlBit.textContent.includes(".com")
480 urlBit.textContent.includes("example.com"),
481 "Item should end up with the correct URL."
486 add_task(async function test_alt_click_no_launch() {
487 Services.obs.notifyObservers(null, "browser:purge-session-history");
489 SessionStore.getClosedTabCountForWindow(window),
491 "Closed tab count after purging session history"
494 await open_then_close(URLs[0]);
496 await withFirefoxView({}, async browser => {
497 let gBrowser = browser.getTabBrowser();
498 let originalTabsLength = gBrowser.tabs.length;
499 await BrowserTestUtils.synthesizeMouseAtCenter(
500 ".closed-tab-li .closed-tab-li-main",
506 gBrowser.tabs.length,
508 `Opened tabs length should still be ${originalTabsLength}`
514 * Asserts that tabs that have been recently closed can be
515 * restored by clicking on them, using the Enter key,
516 * and using the Space bar.
518 add_task(async function test_restore_recently_closed_tabs() {
521 await open_then_close(URLs[0]);
522 await open_then_close(URLs[1]);
523 await open_then_close(URLs[2]);
525 await EventUtils.synthesizeMouseAtCenter(
526 gBrowser.ownerDocument.getElementById("firefox-view-button"),
527 { type: "mousedown" },
530 // Wait for Firefox View to be loaded before interacting
532 await BrowserTestUtils.browserLoaded(
533 window.FirefoxViewHandler.tab.linkedBrowser
535 let { document } = gBrowser.contentWindow;
536 let tabRestored = BrowserTestUtils.waitForNewTab(gBrowser, URLs[2]);
537 EventUtils.synthesizeMouseAtCenter(
538 document.querySelector(".closed-tab-li"),
540 gBrowser.contentWindow
544 ok(true, "Tab was restored by mouse click");
546 await EventUtils.synthesizeMouseAtCenter(
547 gBrowser.ownerDocument.getElementById("firefox-view-button"),
548 { type: "mousedown" },
552 tabRestored = BrowserTestUtils.waitForNewTab(gBrowser, URLs[1]);
553 document.querySelector(".closed-tab-li .closed-tab-li-main").focus();
554 EventUtils.synthesizeKey("KEY_Enter", {}, gBrowser.contentWindow);
557 ok(true, "Tab was restored by using the Enter key");
559 // clean up extra tabs
560 while (gBrowser.tabs.length > 1) {
561 BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
566 * Asserts that tabs are removed from Recently Closed tabs in
567 * Fx View when tabs are removed from latest closed tab data.
568 * Ex: Selecting "Reopen Closed Tab" from the tabs toolbar
571 add_task(async function test_reopen_recently_closed_tabs() {
574 await open_then_close(URLs[0]);
575 await open_then_close(URLs[1]);
576 await open_then_close(URLs[2]);
578 await EventUtils.synthesizeMouseAtCenter(
579 gBrowser.ownerDocument.getElementById("firefox-view-button"),
580 { type: "mousedown" },
583 // Wait for Firefox View to be loaded before interacting
585 await BrowserTestUtils.browserLoaded(
586 window.FirefoxViewHandler.tab.linkedBrowser
589 let { document } = gBrowser.contentWindow;
591 let tabReopened = BrowserTestUtils.waitForNewTab(gBrowser, URLs[2]);
592 SessionStore.undoCloseTab(window);
595 const tabsList = document.querySelector("ol.closed-tabs-list");
597 await EventUtils.synthesizeMouseAtCenter(
598 gBrowser.ownerDocument.getElementById("firefox-view-button"),
599 { type: "mousedown" },
603 await BrowserTestUtils.waitForMutationCondition(
606 () => tabsList.children.length === 2
610 tabsList.children[0].dataset.targeturi,
612 `First recently closed item should be ${URLs[1]}`
615 await close_tab(gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1]);
617 await BrowserTestUtils.waitForMutationCondition(
620 () => tabsList.children.length === 3
624 tabsList.children[0].dataset.targeturi,
626 `First recently closed item should be ${URLs[2]}`
629 await dismiss_tab(tabsList.children[0], content);
631 await BrowserTestUtils.waitForMutationCondition(
634 () => tabsList.children.length === 2
638 tabsList.children[0].dataset.targeturi,
640 `First recently closed item should be ${URLs[1]}`
643 const contextMenu = gBrowser.ownerDocument.getElementById(
644 "contentAreaContextMenu"
646 const promisePopup = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
647 EventUtils.synthesizeMouseAtCenter(
648 tabsList.querySelector(".closed-tab-li-title"),
653 gBrowser.contentWindow
656 const promiseNewTab = BrowserTestUtils.waitForNewTab(gBrowser, URLs[1]);
657 contextMenu.activateItem(
658 gBrowser.ownerDocument.getElementById("context-openlinkintab")
662 await BrowserTestUtils.waitForMutationCondition(
665 () => tabsList.children.length === 1
669 tabsList.children[0].dataset.targeturi,
671 `First recently closed item should be ${URLs[0]}`
674 // clean up extra tabs
675 while (gBrowser.tabs.length > 1) {
676 BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
681 * Asserts that tabs that have been recently closed can be
682 * dismissed by clicking on their respective dismiss buttons.
684 add_task(async function test_dismiss_tab() {
685 const TAB_UPDATE_TIME_MS = 5;
686 Services.obs.notifyObservers(null, "browser:purge-session-history");
688 SessionStore.getClosedTabCountForWindow(window),
690 "Closed tab count after purging session history"
692 await clearAllParentTelemetryEvents();
694 await withFirefoxView({}, async browser => {
695 const { document } = browser.contentWindow;
697 const closedObjectsChanged = () =>
698 TestUtils.topicObserved("sessionstore-closed-objects-changed");
700 const tab1 = await add_new_tab(URLs[0]);
701 const tab2 = await add_new_tab(URLs[1]);
702 const tab3 = await add_new_tab(URLs[2]);
704 await close_tab(tab3);
705 await closedObjectsChanged();
707 await close_tab(tab2);
708 await closedObjectsChanged();
710 await close_tab(tab1);
711 await closedObjectsChanged();
713 await clearAllParentTelemetryEvents();
715 const tabsList = document.querySelector("ol.closed-tabs-list");
716 const numOfListItems = tabsList.children.length;
717 const lastListItem = tabsList.children[numOfListItems - 1];
718 const timeLabel = lastListItem.querySelector("span.closed-tab-li-time");
719 let initialTimeText = timeLabel.textContent;
720 Assert.stringContains(
723 "recently-closed-tabs list item time is 'Just now'"
726 await SpecialPowers.pushPrefEnv({
727 set: [["browser.tabs.firefox-view.updateTimeMs", TAB_UPDATE_TIME_MS]],
730 await BrowserTestUtils.waitForMutationCondition(
733 () => !timeLabel.textContent.includes("Just now")
737 timeLabel.textContent,
739 "recently-closed-tabs list item time has updated"
742 await dismiss_tab(tabsList.children[0], content);
744 await TestUtils.waitForCondition(
746 let events = Services.telemetry.snapshotEvents(
747 Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
750 return events && events.length >= 1;
752 "Waiting for dismiss_closed_tab firefoxview telemetry event.",
757 TelemetryTestUtils.assertEvents(
758 RECENTLY_CLOSED_DISMISS_EVENT,
759 { category: "firefoxview" },
760 { clear: true, process: "parent" }
764 tabsList.children[0].dataset.targeturi,
766 `First recently closed item should be ${URLs[1]}`
770 tabsList.children.length,
772 "recently-closed-tabs-list should have two list items"
775 await clearAllParentTelemetryEvents();
777 await dismiss_tab(tabsList.children[0], content);
779 await TestUtils.waitForCondition(
781 let events = Services.telemetry.snapshotEvents(
782 Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
785 return events && events.length >= 1;
787 "Waiting for dismiss_closed_tab firefoxview telemetry event.",
792 TelemetryTestUtils.assertEvents(
793 RECENTLY_CLOSED_DISMISS_EVENT,
794 { category: "firefoxview" },
795 { clear: true, process: "parent" }
799 tabsList.children[0].dataset.targeturi,
801 `First recently closed item should be ${URLs[2]}`
805 tabsList.children.length,
807 "recently-closed-tabs-list should have one list item"
810 await clearAllParentTelemetryEvents();
812 await dismiss_tab(tabsList.children[0], content);
814 await TestUtils.waitForCondition(
816 let events = Services.telemetry.snapshotEvents(
817 Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
820 return events && events.length >= 1;
822 "Waiting for dismiss_closed_tab firefoxview telemetry event.",
827 TelemetryTestUtils.assertEvents(
828 RECENTLY_CLOSED_DISMISS_EVENT,
829 { category: "firefoxview" },
830 { clear: true, process: "parent" }
834 document.getElementById("recently-closed-tabs-placeholder"),
835 "The empty message is displayed."
839 !document.querySelector("ol.closed-tabs-list"),
840 "The recently closed tabs list is not displayed."
843 await SpecialPowers.popPrefEnv();
848 * Asserts that the actionable part of each list item is role="button".
849 * Discussion on why we want a button role can be seen here:
850 * https://bugzilla.mozilla.org/show_bug.cgi?id=1789875#c1
852 add_task(async function test_button_role() {
853 Services.obs.notifyObservers(null, "browser:purge-session-history");
855 SessionStore.getClosedTabCountForWindow(window),
857 "Closed tab count after purging session history"
860 await withFirefoxView({}, async browser => {
861 const { document } = browser.contentWindow;
865 await open_then_close(URLs[0]);
866 await open_then_close(URLs[1]);
867 await open_then_close(URLs[2]);
869 await EventUtils.synthesizeMouseAtCenter(
870 gBrowser.ownerDocument.getElementById("firefox-view-button"),
871 { type: "mousedown" },
875 const tabsList = document.querySelector("ol.closed-tabs-list");
877 Array.from(tabsList.children).forEach(tabItem => {
878 let actionableElement = tabItem.querySelector(".closed-tab-li-main");
879 Assert.ok(actionableElement.getAttribute("role"), "button");