From 0b7687ec6d44cd29acad285c6222353fdb662ba5 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:41:10 +0200 Subject: [PATCH] gh-14990: Prevent policyContainer from bloating the session file (gh-15098) --- .gitattributes | 2 +- src/-prettierignore.patch | 6 +- .../urlbar/UrlbarPrefs-sys-mjs.patch | 16 +- .../urlbar/content/UrlbarInput-mjs.patch | 27 +- .../urlbar/content/UrlbarView-mjs.patch | 23 +- ..._urlbar_closeonwindowblur_preference.patch | 76 -- .../firefox/bug_2011236.patch | 166 ++++ src/external-patches/manifest.json | 19 +- src/zen/compact-mode/ZenCompactMode.mjs | 5 +- src/zen/spaces/ZenSpaceManager.mjs | 17 +- src/zen/tests/manifest.toml | 99 ++ src/zen/tests/mochitests/alerts/browser.toml | 32 + .../alerts/browser_notification_close.js | 78 ++ .../browser_notification_do_not_disturb.js | 159 +++ .../browser_notification_open_settings.js | 87 ++ .../browser_notification_remove_permission.js | 85 ++ .../browser_notification_tab_switching.js | 116 +++ .../alerts/file_dom_notifications.html | 39 + src/zen/tests/mochitests/alerts/head.js | 73 ++ .../tests/mochitests/backforward/browser.toml | 7 + .../backforward/browser_history_menu.js | 179 ++++ src/zen/tests/mochitests/caps/browser.toml | 10 + .../browser_principalSerialization_csp.js | 106 ++ .../browser_principalSerialization_json.js | 161 ++++ ...browser_principalSerialization_version1.js | 159 +++ src/zen/tests/mochitests/gesture/browser.toml | 11 + .../gesture/browser_gesture_navigation.js | 231 +++++ .../gesture/browser_gesture_scroll.js | 43 + .../mochitests/linkHandling/browser.toml | 12 + ...er_contentAreaClick_subframe_javascript.js | 49 + .../linkHandling/browser_javascript_links.js | 88 ++ ..._contentAreaClick_subframe_javascript.html | 2 + .../file_javascript_links_subframe.html | 2 + src/zen/tests/mochitests/moz.build | 7 + .../SessionStoreTestUtils.sys.mjs | 197 ++++ .../mochitests/sessionstore/browser.toml | 483 ++++++++++ .../sessionstore/browser_1234021.js | 22 + .../sessionstore/browser_1234021_page.html | 6 + .../browser_1284886_suspend_tab.html | 12 + .../browser_1284886_suspend_tab.js | 95 ++ .../browser_1284886_suspend_tab_2.html | 11 + .../browser_1446343-windowsize.js | 39 + .../browser_1933485_tab_groups_history.js | 43 + ..._1953801_tab_groups_history_close_other.js | 55 ++ .../browser_248970_b_perwindowpb.js | 199 ++++ .../sessionstore/browser_248970_b_sample.html | 37 + .../mochitests/sessionstore/browser_339445.js | 39 + .../sessionstore/browser_339445_sample.html | 18 + .../mochitests/sessionstore/browser_345898.js | 69 ++ .../mochitests/sessionstore/browser_350525.js | 136 +++ .../browser_354894_perwindowpb.js | 490 ++++++++++ .../mochitests/sessionstore/browser_367052.js | 52 + .../mochitests/sessionstore/browser_393716.js | 103 ++ .../sessionstore/browser_394759_basic.js | 123 +++ .../sessionstore/browser_394759_behavior.js | 93 ++ .../browser_394759_perwindowpb.js | 61 ++ .../sessionstore/browser_394759_purge.js | 250 +++++ .../mochitests/sessionstore/browser_423132.js | 52 + .../sessionstore/browser_423132_sample.html | 14 + .../mochitests/sessionstore/browser_447951.js | 86 ++ .../sessionstore/browser_447951_sample.html | 5 + .../mochitests/sessionstore/browser_454908.js | 65 ++ .../sessionstore/browser_454908_sample.html | 8 + .../mochitests/sessionstore/browser_456342.js | 90 ++ .../sessionstore/browser_456342_sample.xhtml | 46 + .../mochitests/sessionstore/browser_459906.js | 78 ++ .../sessionstore/browser_459906_empty.html | 3 + .../sessionstore/browser_459906_sample.html | 41 + .../mochitests/sessionstore/browser_461634.js | 133 +++ .../mochitests/sessionstore/browser_461743.js | 53 + .../sessionstore/browser_461743_sample.html | 56 ++ .../mochitests/sessionstore/browser_463205.js | 40 + .../sessionstore/browser_463205_sample.html | 7 + .../mochitests/sessionstore/browser_463206.js | 120 +++ .../sessionstore/browser_463206_sample.html | 11 + .../mochitests/sessionstore/browser_464199.js | 176 ++++ .../sessionstore/browser_464620_a.html | 54 ++ .../sessionstore/browser_464620_a.js | 64 ++ .../sessionstore/browser_464620_b.html | 57 ++ .../sessionstore/browser_464620_b.js | 64 ++ .../sessionstore/browser_464620_xd.html | 5 + .../mochitests/sessionstore/browser_465215.js | 38 + .../mochitests/sessionstore/browser_465223.js | 51 + .../mochitests/sessionstore/browser_466937.js | 51 + .../sessionstore/browser_466937_sample.html | 20 + .../browser_467409-backslashplosion.js | 88 ++ .../mochitests/sessionstore/browser_477657.js | 80 ++ .../mochitests/sessionstore/browser_480893.js | 45 + .../mochitests/sessionstore/browser_485482.js | 76 ++ .../sessionstore/browser_485482_sample.html | 12 + .../mochitests/sessionstore/browser_485563.js | 35 + .../mochitests/sessionstore/browser_490040.js | 107 +++ .../mochitests/sessionstore/browser_491168.js | 113 +++ .../mochitests/sessionstore/browser_491577.js | 212 ++++ .../mochitests/sessionstore/browser_495495.js | 47 + .../mochitests/sessionstore/browser_500328.js | 132 +++ .../mochitests/sessionstore/browser_506482.js | 78 ++ .../mochitests/sessionstore/browser_514751.js | 41 + .../mochitests/sessionstore/browser_522375.js | 25 + .../mochitests/sessionstore/browser_522545.js | 443 +++++++++ .../mochitests/sessionstore/browser_524745.js | 55 ++ .../mochitests/sessionstore/browser_526613.js | 86 ++ .../mochitests/sessionstore/browser_528776.js | 27 + .../mochitests/sessionstore/browser_579868.js | 31 + .../mochitests/sessionstore/browser_579879.js | 31 + .../mochitests/sessionstore/browser_580512.js | 117 +++ .../mochitests/sessionstore/browser_581937.js | 24 + .../sessionstore/browser_586068-apptabs.js | 104 ++ .../browser_586068-apptabs_ondemand.js | 102 ++ ...rowser_586068-browser_state_interrupted.js | 207 ++++ .../sessionstore/browser_586068-cascade.js | 104 ++ .../browser_586068-multi_window.js | 110 +++ .../sessionstore/browser_586068-reload.js | 118 +++ .../sessionstore/browser_586068-select.js | 125 +++ .../browser_586068-window_state.js | 115 +++ .../browser_586068-window_state_override.js | 113 +++ .../mochitests/sessionstore/browser_586147.js | 52 + .../mochitests/sessionstore/browser_588426.js | 62 ++ .../mochitests/sessionstore/browser_589246.js | 288 ++++++ .../mochitests/sessionstore/browser_590268.js | 159 +++ .../mochitests/sessionstore/browser_590563.js | 120 +++ .../browser_595601-restore_hidden.js | 165 ++++ .../mochitests/sessionstore/browser_597071.js | 36 + .../mochitests/sessionstore/browser_600545.js | 123 +++ .../mochitests/sessionstore/browser_601955.js | 54 ++ .../mochitests/sessionstore/browser_607016.js | 155 +++ ...15394-SSWindowState_events_duplicateTab.js | 69 ++ ...94-SSWindowState_events_setBrowserState.js | 152 +++ ...615394-SSWindowState_events_setTabState.js | 61 ++ ...394-SSWindowState_events_setWindowState.js | 65 ++ ...15394-SSWindowState_events_undoCloseTab.js | 65 ++ ...94-SSWindowState_events_undoCloseWindow.js | 146 +++ .../mochitests/sessionstore/browser_618151.js | 67 ++ .../mochitests/sessionstore/browser_623779.js | 13 + .../mochitests/sessionstore/browser_624727.js | 33 + .../mochitests/sessionstore/browser_625016.js | 103 ++ .../mochitests/sessionstore/browser_628270.js | 43 + .../mochitests/sessionstore/browser_635418.js | 58 ++ .../mochitests/sessionstore/browser_636279.js | 144 +++ .../mochitests/sessionstore/browser_637020.js | 77 ++ .../sessionstore/browser_637020_slow.sjs | 22 + .../mochitests/sessionstore/browser_645428.js | 22 + .../mochitests/sessionstore/browser_659591.js | 39 + .../mochitests/sessionstore/browser_662743.js | 138 +++ .../sessionstore/browser_662743_sample.html | 15 + .../mochitests/sessionstore/browser_662812.js | 41 + .../browser_665702-state_session.js | 26 + .../mochitests/sessionstore/browser_682507.js | 22 + .../mochitests/sessionstore/browser_687710.js | 58 ++ .../sessionstore/browser_687710_2.js | 78 ++ .../mochitests/sessionstore/browser_694378.js | 41 + .../mochitests/sessionstore/browser_701377.js | 56 ++ .../mochitests/sessionstore/browser_705597.js | 70 ++ .../mochitests/sessionstore/browser_707862.js | 70 ++ .../mochitests/sessionstore/browser_739531.js | 55 ++ .../sessionstore/browser_739531_frame.html | 1 + .../sessionstore/browser_739531_sample.html | 23 + .../mochitests/sessionstore/browser_739805.js | 51 + .../browser_819510_perwindowpb.js | 152 +++ .../sessionstore/browser_906076_lazy_tabs.js | 163 ++++ .../browser_911547_sample.html^headers^ | 1 + .../browser_aboutPrivateBrowsing.js | 24 + .../browser_aboutRestartrequired_noRestore.js | 165 ++++ .../browser_aboutSessionRestore.js | 67 ++ .../browser_async_duplicate_tab.js | 87 ++ .../sessionstore/browser_async_flushes.js | 79 ++ .../sessionstore/browser_async_remove_tab.js | 196 ++++ .../browser_async_window_flushing.js | 201 ++++ .../sessionstore/browser_attributes.js | 106 ++ .../browser_background_tab_crash.js | 262 +++++ .../sessionstore/browser_backup_recovery.js | 308 ++++++ .../sessionstore/browser_bfcache_telemetry.js | 44 + .../sessionstore/browser_broadcast.js | 162 ++++ .../sessionstore/browser_capabilities.js | 90 ++ .../sessionstore/browser_cleaner.js | 252 +++++ .../sessionstore/browser_closedId.js | 109 +++ ...osed_objects_changed_notifications_tabs.js | 138 +++ ...d_objects_changed_notifications_windows.js | 131 +++ .../browser_closed_tabs_closed_windows.js | 310 ++++++ .../browser_closed_tabs_windows.js | 337 +++++++ .../sessionstore/browser_cookies.js | 81 ++ .../sessionstore/browser_cookies_legacy.js | 77 ++ .../browser_cookies_partitioned.js | 73 ++ .../sessionstore/browser_cookies_privacy.js | 126 +++ .../sessionstore/browser_cookies_sameSite.js | 91 ++ .../sessionstore/browser_crashedTabs.js | 503 ++++++++++ .../browser_csp_policy_container_migration.js | 68 ++ .../browser_docshell_uuid_consistency.js | 56 ++ .../sessionstore/browser_duplicate_history.js | 30 + .../browser_duplicate_tab_in_new_window.js | 37 + .../sessionstore/browser_dying_cache.js | 84 ++ .../sessionstore/browser_dynamic_frames.js | 107 +++ .../browser_firefoxView_restore.js | 45 + .../browser_firefoxView_selected_restore.js | 94 ++ .../browser_focus_after_restore.js | 34 + .../browser_forget_async_closings.js | 163 ++++ .../browser_forget_closed_tab_window_byId.js | 179 ++++ .../sessionstore/browser_formdata.js | 227 +++++ .../sessionstore/browser_formdata_cc.js | 107 +++ .../sessionstore/browser_formdata_face.js | 168 ++++ .../sessionstore/browser_formdata_format.js | 172 ++++ .../browser_formdata_format_sample.html | 7 + .../sessionstore/browser_formdata_max_size.js | 131 +++ .../sessionstore/browser_formdata_password.js | 69 ++ .../sessionstore/browser_formdata_sample.html | 20 + .../sessionstore/browser_formdata_xpath.js | 242 +++++ .../browser_formdata_xpath_sample.html | 37 + .../sessionstore/browser_frame_history.js | 247 +++++ .../sessionstore/browser_frame_history_a.html | 5 + .../sessionstore/browser_frame_history_b.html | 10 + .../sessionstore/browser_frame_history_c.html | 5 + .../browser_frame_history_c1.html | 5 + .../browser_frame_history_c2.html | 5 + .../browser_frame_history_index.html | 9 + .../browser_frame_history_index2.html | 3 + .../browser_frame_history_index_blank.html | 4 + .../sessionstore/browser_frametree.js | 134 +++ .../browser_frametree_sample.html | 8 + .../browser_frametree_sample_frameset.html | 11 + .../browser_frametree_sample_iframes.html | 9 + .../sessionstore/browser_global_store.js | 53 + .../sessionstore/browser_history_persist.js | 140 +++ .../browser_ignore_updates_crashed_tabs.js | 108 +++ .../sessionstore/browser_label_and_icon.js | 59 ++ .../browser_movePendingTabToNewWindow.js | 124 +++ .../browser_multiple_navigateAndRestore.js | 45 + .../browser_multiple_select_after_load.js | 54 ++ .../browser_navigation_api_restore.js | 156 +++ .../browser_newtab_userTypedValue.js | 97 ++ .../browser_not_collect_when_idle.js | 118 +++ .../sessionstore/browser_oldformat.toml | 301 ++++++ .../sessionstore/browser_page_title.js | 54 ++ .../browser_parentProcessRestoreHash.js | 115 +++ .../sessionstore/browser_pending_tabs.js | 38 + .../sessionstore/browser_pinned_tabs.js | 324 +++++++ .../browser_policy_container_not_stored.js | 51 + .../browser_policy_container_sample.html | 12 + ...browser_policy_container_sample_frame.html | 10 + .../sessionstore/browser_privatetabs.js | 60 ++ .../sessionstore/browser_purge_domaindata.js | 196 ++++ .../sessionstore/browser_purge_shistory.js | 65 ++ .../browser_remoteness_flip_on_restore.js | 311 ++++++ .../browser_reopen_all_windows.js | 154 +++ .../sessionstore/browser_replace_load.js | 56 ++ .../browser_restoreLastActionCorrectOrder.js | 101 ++ ...r_restoreLastClosedTabOrWindowOrSession.js | 352 +++++++ .../browser_restoreTabContainer.js | 81 ++ .../browser_restore_container_tabs_oa.js | 272 ++++++ ...wser_restore_cookies_noOriginAttributes.js | 194 ++++ .../browser_restore_pageProxyState.js | 77 ++ .../browser_restore_private_tab_os.js | 59 ++ .../sessionstore/browser_restore_redirect.js | 84 ++ .../browser_restore_reversed_z_order.js | 128 +++ .../sessionstore/browser_restore_srcdoc.js | 44 + .../browser_restore_tabless_window.js | 56 ++ .../browser_restore_verticalPinnedTabs.js | 105 ++ .../browser_restored_window_features.js | 164 ++++ .../browser_revive_crashed_bg_tabs.js | 59 ++ .../sessionstore/browser_scrollPositions.js | 263 +++++ .../browser_scrollPositionsReaderMode.js | 76 ++ ...ser_scrollPositions_readerModeArticle.html | 26 + .../browser_scrollPositions_sample.html | 8 + .../browser_scrollPositions_sample2.html | 8 + ...owser_scrollPositions_sample_frameset.html | 11 + .../browser_searchModeSwitcher_restore.js | 105 ++ .../sessionstore/browser_sessionHistory.js | 294 ++++++ ...onHistory_partitionedPrincipalToInherit.js | 151 +++ .../browser_sessionHistory_slow.sjs | 22 + .../sessionstore/browser_sessionStorage.html | 35 + .../sessionstore/browser_sessionStorage.js | 308 ++++++ .../browser_sessionStorage_size.js | 34 + .../browser_sessionStoreContainer.js | 166 ++++ .../browser_should_restore_tab.js | 139 +++ .../browser_sizemodeBeforeMinimized.js | 44 + .../browser_speculative_connect.html | 8 + .../browser_speculative_connect.js | 145 +++ .../browser_splitview_integer_ids.js | 178 ++++ ...wser_splitview_restore_in_closed_window.js | 203 ++++ .../browser_splitview_string_migration.js | 328 +++++++ .../sessionstore/browser_swapDocShells.js | 40 + .../sessionstore/browser_switch_remoteness.js | 57 ++ .../sessionstore/browser_tab_groups_closed.js | 197 ++++ ..._groups_closed_groups_in_closed_windows.js | 92 ++ .../sessionstore/browser_tab_groups_empty.js | 31 + ..._groups_restore_closed_in_closed_window.js | 135 +++ ...ab_groups_restore_closed_in_open_window.js | 105 ++ ...ser_tab_groups_restore_closed_many_tabs.js | 202 ++++ .../browser_tab_groups_restore_multiple.js | 135 +++ .../browser_tab_groups_restore_saved.js | 183 ++++ .../browser_tab_groups_restore_simple.js | 68 ++ .../browser_tab_groups_restore_to_group.js | 62 ++ ...ser_tab_groups_save_on_removeAllTabsBut.js | 127 +++ ...r_tab_groups_save_on_removeTabsToTheEnd.js | 112 +++ ...tab_groups_save_on_removeTabsToTheStart.js | 112 +++ ...browser_tab_groups_save_on_window_close.js | 254 +++++ .../sessionstore/browser_tab_groups_saved.js | 94 ++ .../sessionstore/browser_tab_groups_state.js | 101 ++ .../sessionstore/browser_tab_groups_undo.js | 117 +++ .../browser_tab_label_during_restore.js | 182 ++++ .../browser_tab_notes_canonicalurl.js | 98 ++ .../browser_tabicon_after_bg_tab_crash.js | 63 ++ .../sessionstore/browser_tabs_in_urlbar.js | 151 +++ .../sessionstore/browser_undoCloseById.js | 185 ++++ .../browser_undoCloseById_targetWindow.js | 93 ++ .../browser_unrestored_crashedTabs.js | 73 ++ .../sessionstore/browser_upgrade_backup.js | 161 ++++ .../sessionstore/browser_urlbarSearchMode.js | 60 ++ ...rowser_userTyped_restored_after_discard.js | 47 + .../browser_windowRestore_perwindowpb.js | 32 + .../browser_windowStateContainer.js | 168 ++++ .../sessionstore/browser_wireframe_basic.js | 48 + .../sessionstore/coopHeaderCommon.sjs | 32 + .../mochitests/sessionstore/coop_coep.html | 6 + .../sessionstore/coop_coep.html^headers^ | 2 + .../tests/mochitests/sessionstore/empty.html | 6 + .../mochitests/sessionstore/empty_frame.html | 7 + .../tests/mochitests/sessionstore/entry1.html | 6 + .../tests/mochitests/sessionstore/entry2.html | 6 + .../tests/mochitests/sessionstore/entry3.html | 6 + .../file_async_duplicate_tab.html | 1 + .../sessionstore/file_async_flushes.html | 1 + .../sessionstore/file_formdata_password.html | 17 + .../file_sessionHistory_hashchange.html | 1 + src/zen/tests/mochitests/sessionstore/head.js | 680 +++++++++++++ .../sessionstore/marionette/manifest.toml | 65 ++ .../marionette/session_store_test_case.py | 473 +++++++++ .../sessionstore/marionette/test_log_files.py | 176 ++++ .../marionette/test_new_tab_on_restore.py | 370 +++++++ ...st_persist_closed_tabs_restore_manually.py | 221 +++++ .../marionette/test_restore_aiwindow.py | 387 ++++++++ .../marionette/test_restore_loading_tab.py | 69 ++ .../marionette/test_restore_manually.py | 140 +++ .../test_restore_manually_with_pinned_tabs.py | 106 ++ .../test_restore_manually_with_tab_groups.py | 325 +++++++ .../marionette/test_restore_sidebar.py | 304 ++++++ .../test_restore_sidebar_automatic.py | 106 ++ ...t_restore_windows_after_close_last_tabs.py | 57 ++ ..._restore_windows_after_restart_and_quit.py | 82 ++ ..._restore_windows_after_windows_shutdown.py | 62 ++ .../marionette/test_splitview_restore.py | 183 ++++ .../marionette/test_tabgroups_restore.py | 185 ++++ .../test_tabnotes_canonicalurl_restore.py | 195 ++++ .../marionette/test_taskbartab_restore.py | 132 +++ .../test_taskbartab_sessionstate.py | 87 ++ .../sessionstore/restore_redirect_http.html | 0 .../restore_redirect_http.html^headers^ | 2 + .../sessionstore/restore_redirect_js.html | 10 + .../sessionstore/restore_redirect_target.html | 8 + .../unit/data/sessionCheckpoints_all.json | 11 + .../unit/data/sessionstore_complete.json | 907 ++++++++++++++++++ .../unit/data/sessionstore_invalid.js | 3 + .../unit/data/sessionstore_valid.js | 3 + .../mochitests/sessionstore/unit/head.js | 37 + .../sessionstore/unit/test_backup_once.js | 137 +++ .../unit/test_final_write_cleanup.js | 116 +++ .../unit/test_histogram_corrupt_files.js | 117 +++ .../unit/test_migration_lz4compression.js | 151 +++ .../sessionstore/unit/test_session_schema.js | 29 + .../unit/test_startup_invalid_session.js | 27 + .../unit/test_startup_nosession_async.js | 19 + .../unit/test_startup_session_async.js | 32 + .../unit/test_write_json_length_hint.js | 65 ++ .../sessionstore/unit/xpcshell.toml | 48 + src/zen/tests/mochitests/zoom/browser.toml | 63 ++ .../browser_background_link_zoom_reset.js | 45 + .../zoom/browser_background_zoom.js | 115 +++ .../mochitests/zoom/browser_default_zoom.js | 149 +++ .../zoom/browser_default_zoom_fission.js | 114 +++ .../zoom/browser_default_zoom_multitab.js | 190 ++++ .../zoom/browser_default_zoom_sitespecific.js | 108 +++ .../zoom/browser_image_zoom_tabswitch.js | 41 + ...er_keyboard_mousewheel_zoom_consistency.js | 123 +++ .../zoom/browser_mousewheel_zoom.js | 72 ++ .../browser_sitespecific_background_pref.js | 35 + .../zoom/browser_sitespecific_image_zoom.js | 52 + .../zoom/browser_sitespecific_video_zoom.js | 128 +++ .../zoom/browser_subframe_textzoom.js | 52 + .../zoom/browser_tabswitch_zoom_flicker.js | 45 + .../mochitests/zoom/browser_tooltip_zoom.js | 41 + .../mochitests/zoom/browser_zoom_commands.js | 202 ++++ src/zen/tests/mochitests/zoom/head.js | 190 ++++ src/zen/tests/mochitests/zoom/zoom_test.html | 14 + 382 files changed, 37824 insertions(+), 108 deletions(-) delete mode 100644 src/external-patches/firefox/add_urlbar_closeonwindowblur_preference.patch create mode 100644 src/external-patches/firefox/bug_2011236.patch create mode 100644 src/zen/tests/mochitests/alerts/browser.toml create mode 100644 src/zen/tests/mochitests/alerts/browser_notification_close.js create mode 100644 src/zen/tests/mochitests/alerts/browser_notification_do_not_disturb.js create mode 100644 src/zen/tests/mochitests/alerts/browser_notification_open_settings.js create mode 100644 src/zen/tests/mochitests/alerts/browser_notification_remove_permission.js create mode 100644 src/zen/tests/mochitests/alerts/browser_notification_tab_switching.js create mode 100644 src/zen/tests/mochitests/alerts/file_dom_notifications.html create mode 100644 src/zen/tests/mochitests/alerts/head.js create mode 100644 src/zen/tests/mochitests/backforward/browser.toml create mode 100644 src/zen/tests/mochitests/backforward/browser_history_menu.js create mode 100644 src/zen/tests/mochitests/caps/browser.toml create mode 100644 src/zen/tests/mochitests/caps/browser_principalSerialization_csp.js create mode 100644 src/zen/tests/mochitests/caps/browser_principalSerialization_json.js create mode 100644 src/zen/tests/mochitests/caps/browser_principalSerialization_version1.js create mode 100644 src/zen/tests/mochitests/gesture/browser.toml create mode 100644 src/zen/tests/mochitests/gesture/browser_gesture_navigation.js create mode 100644 src/zen/tests/mochitests/gesture/browser_gesture_scroll.js create mode 100644 src/zen/tests/mochitests/linkHandling/browser.toml create mode 100644 src/zen/tests/mochitests/linkHandling/browser_contentAreaClick_subframe_javascript.js create mode 100644 src/zen/tests/mochitests/linkHandling/browser_javascript_links.js create mode 100644 src/zen/tests/mochitests/linkHandling/file_contentAreaClick_subframe_javascript.html create mode 100644 src/zen/tests/mochitests/linkHandling/file_javascript_links_subframe.html create mode 100644 src/zen/tests/mochitests/sessionstore/SessionStoreTestUtils.sys.mjs create mode 100644 src/zen/tests/mochitests/sessionstore/browser.toml create mode 100644 src/zen/tests/mochitests/sessionstore/browser_1234021.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_1234021_page.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab_2.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_1446343-windowsize.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_1933485_tab_groups_history.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_1953801_tab_groups_history_close_other.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_248970_b_perwindowpb.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_248970_b_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_339445.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_339445_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_345898.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_350525.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_354894_perwindowpb.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_367052.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_393716.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_394759_basic.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_394759_behavior.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_394759_perwindowpb.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_394759_purge.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_423132.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_423132_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_447951.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_447951_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_454908.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_454908_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_456342.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_456342_sample.xhtml create mode 100644 src/zen/tests/mochitests/sessionstore/browser_459906.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_459906_empty.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_459906_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_461634.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_461743.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_461743_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_463205.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_463205_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_463206.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_463206_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_464199.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_464620_a.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_464620_a.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_464620_b.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_464620_b.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_464620_xd.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_465215.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_465223.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_466937.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_466937_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_467409-backslashplosion.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_477657.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_480893.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_485482.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_485482_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_485563.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_490040.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_491168.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_491577.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_495495.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_500328.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_506482.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_514751.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_522375.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_522545.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_524745.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_526613.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_528776.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_579868.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_579879.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_580512.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_581937.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586068-apptabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586068-apptabs_ondemand.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586068-browser_state_interrupted.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586068-cascade.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586068-multi_window.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586068-reload.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586068-select.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586068-window_state.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586068-window_state_override.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_586147.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_588426.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_589246.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_590268.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_590563.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_595601-restore_hidden.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_597071.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_600545.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_601955.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_607016.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_615394-SSWindowState_events_duplicateTab.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_615394-SSWindowState_events_setBrowserState.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_615394-SSWindowState_events_setTabState.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_615394-SSWindowState_events_setWindowState.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_615394-SSWindowState_events_undoCloseTab.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_615394-SSWindowState_events_undoCloseWindow.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_618151.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_623779.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_624727.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_625016.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_628270.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_635418.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_636279.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_637020.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_637020_slow.sjs create mode 100644 src/zen/tests/mochitests/sessionstore/browser_645428.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_659591.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_662743.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_662743_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_662812.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_665702-state_session.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_682507.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_687710.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_687710_2.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_694378.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_701377.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_705597.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_707862.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_739531.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_739531_frame.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_739531_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_739805.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_819510_perwindowpb.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_906076_lazy_tabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_911547_sample.html^headers^ create mode 100644 src/zen/tests/mochitests/sessionstore/browser_aboutPrivateBrowsing.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_aboutRestartrequired_noRestore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_aboutSessionRestore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_async_duplicate_tab.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_async_flushes.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_async_remove_tab.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_async_window_flushing.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_attributes.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_background_tab_crash.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_backup_recovery.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_bfcache_telemetry.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_broadcast.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_capabilities.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_cleaner.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_closedId.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_closed_objects_changed_notifications_tabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_closed_objects_changed_notifications_windows.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_closed_tabs_closed_windows.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_closed_tabs_windows.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_cookies.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_cookies_legacy.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_cookies_partitioned.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_cookies_privacy.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_cookies_sameSite.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_crashedTabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_csp_policy_container_migration.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_docshell_uuid_consistency.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_duplicate_history.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_duplicate_tab_in_new_window.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_dying_cache.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_dynamic_frames.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_firefoxView_restore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_firefoxView_selected_restore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_focus_after_restore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_forget_async_closings.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_forget_closed_tab_window_byId.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata_cc.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata_face.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata_format.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata_format_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata_max_size.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata_password.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata_xpath.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_formdata_xpath_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frame_history.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frame_history_a.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frame_history_b.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frame_history_c.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frame_history_c1.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frame_history_c2.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frame_history_index.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frame_history_index2.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frame_history_index_blank.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frametree.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frametree_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frametree_sample_frameset.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_frametree_sample_iframes.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_global_store.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_history_persist.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_ignore_updates_crashed_tabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_label_and_icon.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_movePendingTabToNewWindow.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_multiple_navigateAndRestore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_multiple_select_after_load.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_navigation_api_restore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_newtab_userTypedValue.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_not_collect_when_idle.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_oldformat.toml create mode 100644 src/zen/tests/mochitests/sessionstore/browser_page_title.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_parentProcessRestoreHash.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_pending_tabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_pinned_tabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_policy_container_not_stored.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_policy_container_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_policy_container_sample_frame.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_privatetabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_purge_domaindata.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_purge_shistory.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_remoteness_flip_on_restore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_reopen_all_windows.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_replace_load.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restoreLastActionCorrectOrder.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restoreLastClosedTabOrWindowOrSession.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restoreTabContainer.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restore_container_tabs_oa.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restore_cookies_noOriginAttributes.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restore_pageProxyState.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restore_private_tab_os.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restore_redirect.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restore_reversed_z_order.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restore_srcdoc.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restore_tabless_window.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restore_verticalPinnedTabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_restored_window_features.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_revive_crashed_bg_tabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_scrollPositions.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_scrollPositionsReaderMode.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_scrollPositions_readerModeArticle.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_scrollPositions_sample.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_scrollPositions_sample2.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_scrollPositions_sample_frameset.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_searchModeSwitcher_restore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_sessionHistory.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_sessionHistory_partitionedPrincipalToInherit.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_sessionHistory_slow.sjs create mode 100644 src/zen/tests/mochitests/sessionstore/browser_sessionStorage.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_sessionStorage.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_sessionStorage_size.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_sessionStoreContainer.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_should_restore_tab.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_sizemodeBeforeMinimized.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_speculative_connect.html create mode 100644 src/zen/tests/mochitests/sessionstore/browser_speculative_connect.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_splitview_integer_ids.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_splitview_restore_in_closed_window.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_splitview_string_migration.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_swapDocShells.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_switch_remoteness.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_closed.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_closed_groups_in_closed_windows.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_empty.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_in_closed_window.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_in_open_window.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_many_tabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_multiple.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_saved.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_simple.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_to_group.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeAllTabsBut.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeTabsToTheEnd.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeTabsToTheStart.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_window_close.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_saved.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_state.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_groups_undo.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_label_during_restore.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tab_notes_canonicalurl.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tabicon_after_bg_tab_crash.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_tabs_in_urlbar.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_undoCloseById.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_undoCloseById_targetWindow.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_unrestored_crashedTabs.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_upgrade_backup.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_urlbarSearchMode.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_userTyped_restored_after_discard.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_windowRestore_perwindowpb.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_windowStateContainer.js create mode 100644 src/zen/tests/mochitests/sessionstore/browser_wireframe_basic.js create mode 100644 src/zen/tests/mochitests/sessionstore/coopHeaderCommon.sjs create mode 100644 src/zen/tests/mochitests/sessionstore/coop_coep.html create mode 100644 src/zen/tests/mochitests/sessionstore/coop_coep.html^headers^ create mode 100644 src/zen/tests/mochitests/sessionstore/empty.html create mode 100644 src/zen/tests/mochitests/sessionstore/empty_frame.html create mode 100644 src/zen/tests/mochitests/sessionstore/entry1.html create mode 100644 src/zen/tests/mochitests/sessionstore/entry2.html create mode 100644 src/zen/tests/mochitests/sessionstore/entry3.html create mode 100644 src/zen/tests/mochitests/sessionstore/file_async_duplicate_tab.html create mode 100644 src/zen/tests/mochitests/sessionstore/file_async_flushes.html create mode 100644 src/zen/tests/mochitests/sessionstore/file_formdata_password.html create mode 100644 src/zen/tests/mochitests/sessionstore/file_sessionHistory_hashchange.html create mode 100644 src/zen/tests/mochitests/sessionstore/head.js create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/manifest.toml create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/session_store_test_case.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_log_files.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_new_tab_on_restore.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_persist_closed_tabs_restore_manually.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_aiwindow.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_loading_tab.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually_with_pinned_tabs.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually_with_tab_groups.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_sidebar.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_sidebar_automatic.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_close_last_tabs.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_restart_and_quit.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_windows_shutdown.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_splitview_restore.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_tabgroups_restore.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_tabnotes_canonicalurl_restore.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_taskbartab_restore.py create mode 100644 src/zen/tests/mochitests/sessionstore/marionette/test_taskbartab_sessionstate.py create mode 100644 src/zen/tests/mochitests/sessionstore/restore_redirect_http.html create mode 100644 src/zen/tests/mochitests/sessionstore/restore_redirect_http.html^headers^ create mode 100644 src/zen/tests/mochitests/sessionstore/restore_redirect_js.html create mode 100644 src/zen/tests/mochitests/sessionstore/restore_redirect_target.html create mode 100644 src/zen/tests/mochitests/sessionstore/unit/data/sessionCheckpoints_all.json create mode 100644 src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_complete.json create mode 100644 src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_invalid.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_valid.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/head.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/test_backup_once.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/test_final_write_cleanup.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/test_histogram_corrupt_files.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/test_migration_lz4compression.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/test_session_schema.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/test_startup_invalid_session.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/test_startup_nosession_async.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/test_startup_session_async.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/test_write_json_length_hint.js create mode 100644 src/zen/tests/mochitests/sessionstore/unit/xpcshell.toml create mode 100644 src/zen/tests/mochitests/zoom/browser.toml create mode 100644 src/zen/tests/mochitests/zoom/browser_background_link_zoom_reset.js create mode 100644 src/zen/tests/mochitests/zoom/browser_background_zoom.js create mode 100644 src/zen/tests/mochitests/zoom/browser_default_zoom.js create mode 100644 src/zen/tests/mochitests/zoom/browser_default_zoom_fission.js create mode 100644 src/zen/tests/mochitests/zoom/browser_default_zoom_multitab.js create mode 100644 src/zen/tests/mochitests/zoom/browser_default_zoom_sitespecific.js create mode 100644 src/zen/tests/mochitests/zoom/browser_image_zoom_tabswitch.js create mode 100644 src/zen/tests/mochitests/zoom/browser_keyboard_mousewheel_zoom_consistency.js create mode 100644 src/zen/tests/mochitests/zoom/browser_mousewheel_zoom.js create mode 100644 src/zen/tests/mochitests/zoom/browser_sitespecific_background_pref.js create mode 100644 src/zen/tests/mochitests/zoom/browser_sitespecific_image_zoom.js create mode 100644 src/zen/tests/mochitests/zoom/browser_sitespecific_video_zoom.js create mode 100644 src/zen/tests/mochitests/zoom/browser_subframe_textzoom.js create mode 100644 src/zen/tests/mochitests/zoom/browser_tabswitch_zoom_flicker.js create mode 100644 src/zen/tests/mochitests/zoom/browser_tooltip_zoom.js create mode 100644 src/zen/tests/mochitests/zoom/browser_zoom_commands.js create mode 100644 src/zen/tests/mochitests/zoom/head.js create mode 100644 src/zen/tests/mochitests/zoom/zoom_test.html diff --git a/.gitattributes b/.gitattributes index d18643924..683555503 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,4 +5,4 @@ *.patch linguist-language=C++ *.d.ts linguist-language=TypeScript -src/zen/tests/* linguist-language=C++ +src/zen/tests/** linguist-language=C++ diff --git a/src/-prettierignore.patch b/src/-prettierignore.patch index ed444531d..3142c427a 100644 --- a/src/-prettierignore.patch +++ b/src/-prettierignore.patch @@ -1,8 +1,8 @@ diff --git a/.prettierignore b/.prettierignore -index 949896ff064ae0b54b6a657ea074bc88e12820f7..5249f420972667bece4d85fe8d35073afaebeb8a 100644 +index bd8ab46ff7175f5039b69b04c1afe53ab8efe746..7ae12a1ff3724e99aeec2e93518640a2fa9611e6 100644 --- a/.prettierignore +++ b/.prettierignore -@@ -1803,3 +1803,12 @@ tools/ts/test/baselines/ +@@ -1807,3 +1807,12 @@ tools/ts/test/baselines/ try_task_config.json xpcom/idl-parser/xpidl/fixtures/xpctest.d.json **/package-lock.json @@ -12,6 +12,6 @@ index 949896ff064ae0b54b6a657ea074bc88e12820f7..5249f420972667bece4d85fe8d35073a +*.min.js +*.min.mjs +*.inc -+*/mochitests/* ++**/mochitests/** +*.svg + diff --git a/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch b/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch index 9b41ab880..9644cca42 100644 --- a/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch @@ -1,8 +1,18 @@ diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs -index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489fb04e8b7a 100644 +index 89f0d44d462de3d7d0f581d6371606f475443822..0d45ce5f6455e9c7276c54f504906bade5b3d286 100644 --- a/browser/components/urlbar/UrlbarPrefs.sys.mjs +++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs -@@ -462,6 +462,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([ +@@ -163,6 +163,9 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([ + // Feature gate pref for flight status suggestions in the urlbar. + ["flightStatus.featureGate", false], + ++ // Whether to close the urlbar when blurring the window while the urlbar is focused. ++ ["closeOnWindowBlur", true], ++ + // The minimum prefix length of a flight status keyword the user must type to + // trigger the suggestion. 0 means the min length should be taken from Nimbus + // or remote settings. +@@ -482,6 +485,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([ ["shortcuts.tabs", true], ["shortcuts.history", true], ["shortcuts.actions", true], @@ -10,7 +20,7 @@ index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489f // Boolean to determine if the providers defined in `exposureResults` // should be displayed in search results. This can be set by a -@@ -799,6 +800,8 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) { +@@ -840,6 +844,8 @@ function makeDefaultResultGroups({ */ let rootGroup = { children: [ diff --git a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch index 354834a4a..b6b5daebe 100644 --- a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch +++ b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs -index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a6182579cbaf4d 100644 +index e7e9495e56076cd112beafb8297dece4ae4cea58..95e4333866a5f0d41fbdbe23910c396406a247e9 100644 --- a/browser/components/urlbar/content/UrlbarInput.mjs +++ b/browser/components/urlbar/content/UrlbarInput.mjs @@ -101,6 +101,13 @@ const lazy = XPCOMUtils.declareLazy({ @@ -280,7 +280,20 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825 l10nId, l10nId == "urlbar-placeholder-with-name" ? { name: engineName } -@@ -5424,6 +5540,11 @@ ${ +@@ -5347,6 +5463,12 @@ ${ + } + + lazy.logger.debug("Blur Event"); ++ if ( ++ this.document.commandDispatcher.focusedElement == this.inputField && ++ !lazy.UrlbarPrefs.get("closeOnWindowBlur") ++ ) { ++ return; ++ } + // We cannot count every blur events after a missed engagement as abandoment + // because the user may have clicked on some view element that executes + // a command causing a focus change. For example opening preferences from +@@ -5424,6 +5546,11 @@ ${ } _on_click(event) { @@ -292,7 +305,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825 switch (event.target) { case this.inputField: case this._inputContainer: -@@ -5513,10 +5634,11 @@ ${ +@@ -5513,10 +5640,11 @@ ${ } if (untrim) { this.setValue(this._untrimmedValue); @@ -305,7 +318,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825 this.view.autoOpen({ event }); } else { if (this._untrimOnFocusAfterKeydown) { -@@ -5556,9 +5678,16 @@ ${ +@@ -5556,9 +5684,16 @@ ${ } _on_mousedown(event) { @@ -323,7 +336,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825 if ( event.composedTarget != this.inputField && event.composedTarget != this._inputContainer -@@ -5568,6 +5697,10 @@ ${ +@@ -5568,6 +5703,10 @@ ${ this.focusedViaMousedown = !this.focused; this.#preventClickSelectsAll = this.focused; @@ -334,7 +347,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825 // Keep the focus status, since the attribute may be changed // upon calling this.focus(). -@@ -5605,7 +5738,7 @@ ${ +@@ -5605,7 +5744,7 @@ ${ // view open on tab switch, and the TabSelect event arrived earlier. // Also ignore mousedown on the urlbarView context menu: opening/closing the // view is already handled by the result opening flow. @@ -343,7 +356,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825 break; } -@@ -5894,7 +6027,7 @@ ${ +@@ -5894,7 +6033,7 @@ ${ // When we are in actions search mode we can show more results so // increase the limit. let maxResults = diff --git a/src/browser/components/urlbar/content/UrlbarView-mjs.patch b/src/browser/components/urlbar/content/UrlbarView-mjs.patch index 21d69f2b0..7225a807e 100644 --- a/src/browser/components/urlbar/content/UrlbarView-mjs.patch +++ b/src/browser/components/urlbar/content/UrlbarView-mjs.patch @@ -1,8 +1,8 @@ diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs -index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac823fedcb 100644 +index e465806c4ae4fc372d9aa2b71aa7f0e74c65d026..c0b3781901298bedee2736819a93edbf545507a7 100644 --- a/browser/components/urlbar/content/UrlbarView.mjs +++ b/browser/components/urlbar/content/UrlbarView.mjs -@@ -674,7 +674,7 @@ export class UrlbarView { +@@ -803,7 +803,7 @@ export class UrlbarView { !this.input.value || this.input.getAttribute("pageproxystate") == "valid" ) { @@ -11,7 +11,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac // Try to reuse the cached top-sites context. If it's not cached, then // there will be a gap of time between when the input is focused and // when the view opens that can be perceived as flicker. -@@ -812,10 +812,6 @@ export class UrlbarView { +@@ -941,10 +941,6 @@ export class UrlbarView { } // If search mode isn't active, close the view. @@ -22,7 +22,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac // Search mode is active. If the one-offs should be shown, make sure they // are enabled and show the view. -@@ -3031,6 +3027,8 @@ export class UrlbarView { +@@ -3203,6 +3199,8 @@ export class UrlbarView { if (row?.hasAttribute("row-selectable")) { row?.toggleAttribute("selected", true); } @@ -31,7 +31,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac if (element != row) { row?.toggleAttribute("descendant-selected", true); } -@@ -3544,7 +3542,7 @@ export class UrlbarView { +@@ -3715,7 +3713,7 @@ export class UrlbarView { } #enableOrDisableRowWrap() { @@ -40,3 +40,16 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac this.#rows.toggleAttribute("wrap", wrap); this.oneOffSearchButtons?.container.toggleAttribute("wrap", wrap); } +@@ -4144,6 +4142,12 @@ export class UrlbarView { + } + + on_blur() { ++ if ( ++ this.document.commandDispatcher.focusedElement == this.input.inputField && ++ !lazy.UrlbarPrefs.get("closeOnWindowBlur") ++ ) { ++ return; ++ } + // If the view is open without the input being focused, it will not close + // automatically when the window loses focus. We might be in this state + // after a Search Tip is shown on an engine homepage. diff --git a/src/external-patches/firefox/add_urlbar_closeonwindowblur_preference.patch b/src/external-patches/firefox/add_urlbar_closeonwindowblur_preference.patch deleted file mode 100644 index 0f29eec74..000000000 --- a/src/external-patches/firefox/add_urlbar_closeonwindowblur_preference.patch +++ /dev/null @@ -1,76 +0,0 @@ -diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js ---- a/browser/app/profile/firefox.js -+++ b/browser/app/profile/firefox.js -@@ -411,10 +411,13 @@ - pref("browser.urlbar.maxRichResults", 10); - - // The maximum number of historical search results to show. - pref("browser.urlbar.maxHistoricalSearchSuggestions", 2); - -+// Whether to close the urlbar when blurring the window while the urlbar is focused. -+pref("browser.urlbar.closeOnWindowBlur", true); -+ - // The default behavior for the urlbar can be configured to use any combination - // of the match filters with each additional filter adding more results (union). - pref("browser.urlbar.suggest.bookmark", true); - pref("browser.urlbar.suggest.clipboard", true); - pref("browser.urlbar.suggest.history", true); -diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs ---- a/browser/components/urlbar/UrlbarPrefs.sys.mjs -+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs -@@ -152,10 +152,13 @@ - ["filter.javascript", true], - - // Feature gate pref for flight status suggestions in the urlbar. - ["flightStatus.featureGate", false], - -+ // Whether to close the urlbar when blurring the window while the urlbar is focused. -+ ["closeOnWindowBlur", true], -+ - // The minimum prefix length of a flight status keyword the user must type to - // trigger the suggestion. 0 means the min length should be taken from Nimbus - // or remote settings. - ["flightStatus.minKeywordLength", 0], - -diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs ---- a/browser/components/urlbar/content/UrlbarView.mjs -+++ b/browser/components/urlbar/content/UrlbarView.mjs -@@ -3909,10 +3909,16 @@ - } - } - } - - on_blur() { -+ if ( -+ this.document.commandDispatcher.focusedElement == this.input.inputField && -+ !lazy.UrlbarPrefs.get("closeOnWindowBlur") -+ ) { -+ return; -+ } - // If the view is open without the input being focused, it will not close - // automatically when the window loses focus. We might be in this state - // after a Search Tip is shown on an engine homepage. - if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { - this.close(); -diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs ---- a/browser/components/urlbar/content/UrlbarInput.mjs -+++ b/browser/components/urlbar/content/UrlbarInput.mjs -@@ -5460,11 +5460,17 @@ - _on_blur(event) { - if (this.view.resultMenu.hasAttribute("open")) { - return; - } - - lazy.logger.debug("Blur Event"); -+ if ( -+ this.document.commandDispatcher.focusedElement == this.inputField && -+ !lazy.UrlbarPrefs.get("closeOnWindowBlur") -+ ) { -+ return; -+ } - // We cannot count every blur events after a missed engagement as abandoment - // because the user may have clicked on some view element that executes - // a command causing a focus change. For example opening preferences from - // the oneoff settings button. - // For now we detect that case by discarding the event on command, but we - // may want to figure out a more robust way to detect abandonment. \ No newline at end of file diff --git a/src/external-patches/firefox/bug_2011236.patch b/src/external-patches/firefox/bug_2011236.patch new file mode 100644 index 000000000..73b1221f3 --- /dev/null +++ b/src/external-patches/firefox/bug_2011236.patch @@ -0,0 +1,166 @@ +diff --git a/browser/components/sessionstore/test/browser.toml b/browser/components/sessionstore/test/browser.toml +--- a/browser/components/sessionstore/test/browser.toml ++++ b/browser/components/sessionstore/test/browser.toml +@@ -14,10 +14,12 @@ + "browser_frame_history_c.html", + "browser_frame_history_c1.html", + "browser_frame_history_c2.html", + "browser_formdata_format_sample.html", + "browser_sessionHistory_slow.sjs", ++ "browser_policy_container_sample.html", ++ "browser_policy_container_sample_frame.html", + "browser_scrollPositions_sample.html", + "browser_scrollPositions_sample2.html", + "browser_scrollPositions_sample_frameset.html", + "browser_scrollPositions_readerModeArticle.html", + "browser_sessionStorage.html", +@@ -226,10 +228,12 @@ + ["browser_pinned_tabs.js"] + skip-if = [ + "ccov", # Bug 1625525 + ] + ++["browser_policy_container_not_stored.js"] ++ + ["browser_privatetabs.js"] + + ["browser_purge_domaindata.js"] + + ["browser_purge_shistory.js"] +diff --git a/browser/components/sessionstore/test/browser_policy_container_not_stored.js b/browser/components/sessionstore/test/browser_policy_container_not_stored.js +new file mode 100644 +--- /dev/null ++++ b/browser/components/sessionstore/test/browser_policy_container_not_stored.js +@@ -0,0 +1,51 @@ ++/* This Source Code Form is subject to the terms of the Mozilla Public ++ * License, v. 2.0. If a copy of the MPL was not distributed with this ++ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ ++ ++"use strict"; ++ ++/** ++ * A session history entry only needs to retain the policy container for loads ++ * that inherit their policies (about:blank, about:srcdoc, blob:, data:, ...). ++ * Anything fetched over the network gets its policies from the response again, ++ * so storing them would only bloat the session store (bug 2011236). ++ */ ++ ++const TEST_ROOT = getRootDirectory(gTestPath).replace( ++ "chrome://mochitests/content", ++ "https://example.com" ++); ++const PARENT_URL = TEST_ROOT + "browser_policy_container_sample.html"; ++const FRAME_URL = TEST_ROOT + "browser_policy_container_sample_frame.html"; ++ ++add_task(async function test_policy_container_only_for_inheriting_loads() { ++ const tab = BrowserTestUtils.addTab(gBrowser, PARENT_URL); ++ gBrowser.selectedTab = tab; ++ await promiseBrowserLoaded(tab.linkedBrowser, true, PARENT_URL); ++ await TabStateFlusher.flush(tab.linkedBrowser); ++ ++ const state = JSON.parse(ss.getTabState(tab)); ++ const topEntry = state.entries.at(-1); ++ is(topEntry.url, PARENT_URL, "collected the parent entry"); ++ ++ const children = topEntry.children ?? []; ++ const networkFrame = children.find(child => child.url === FRAME_URL); ++ const blankFrame = children.find(child => child.url === "about:blank"); ++ ++ ok(networkFrame, "collected the network-scheme subframe entry"); ++ ok(blankFrame, "collected the about:blank subframe entry"); ++ ++ // The parent has a CSP, so both subframes inherit a policy container onto ++ // their load state. Only the one that cannot recover it from a response ++ // should keep it in session history. ++ ok( ++ !("policyContainer" in networkFrame), ++ "https subframe entry does not store a policyContainer" ++ ); ++ ok( ++ "policyContainer" in blankFrame, ++ "about:blank subframe entry still stores its inherited policyContainer" ++ ); ++ ++ BrowserTestUtils.removeTab(tab); ++}); +diff --git a/browser/components/sessionstore/test/browser_policy_container_sample.html b/browser/components/sessionstore/test/browser_policy_container_sample.html +new file mode 100644 +--- /dev/null ++++ b/browser/components/sessionstore/test/browser_policy_container_sample.html +@@ -0,0 +1,12 @@ ++ ++ ++ ++ ++ ++ policyContainer session store sample ++ ++ ++ ++ ++ ++ +diff --git a/browser/components/sessionstore/test/browser_policy_container_sample_frame.html b/browser/components/sessionstore/test/browser_policy_container_sample_frame.html +new file mode 100644 +--- /dev/null ++++ b/browser/components/sessionstore/test/browser_policy_container_sample_frame.html +@@ -0,0 +1,10 @@ ++ ++ ++ ++ ++ policyContainer session store sample frame ++ ++ ++ frame ++ ++ +diff --git a/docshell/shistory/SessionHistoryEntry.cpp b/docshell/shistory/SessionHistoryEntry.cpp +--- a/docshell/shistory/SessionHistoryEntry.cpp ++++ b/docshell/shistory/SessionHistoryEntry.cpp +@@ -41,10 +41,18 @@ + extern mozilla::LazyLogModule gSHLog; + + namespace mozilla { + namespace dom { + ++// Only store policy container for loads that can't carry it themselves ++// (about:blank, about:srcdoc, blob:, data:, ...) ++// bug 1867137, bug 2011236 ++static nsIPolicyContainer* PolicyContainerToStore( ++ nsIURI* aURI, nsIPolicyContainer* aPolicyContainer) { ++ return CSP_ShouldURIInheritCSP(aURI) ? aPolicyContainer : nullptr; ++} ++ + SessionHistoryInfo::SessionHistoryInfo(nsDocShellLoadState* aLoadState, + nsIChannel* aChannel) + : mURI(aLoadState->URI()), + mOriginalURI(aLoadState->OriginalURI()), + mResultPrincipalURI(aLoadState->ResultPrincipalURI()), +@@ -59,9 +67,10 @@ + mLoadReplace(aLoadState->LoadReplace()), + mHasUserActivation(aLoadState->HasValidUserGestureActivation()), + mSharedState(SharedState::Create( + aLoadState->TriggeringPrincipal(), aLoadState->PrincipalToInherit(), + aLoadState->PartitionedPrincipalToInherit(), +- aLoadState->PolicyContainer(), ++ PolicyContainerToStore(aLoadState->URI(), ++ aLoadState->PolicyContainer()), + /* FIXME Is this correct? */ + aLoadState->TypeHint())) { + // Pull the upload stream off of the channel instead of the load state, as +@@ -124,11 +133,12 @@ + loadInfo->GetPrincipalToInherit( + getter_AddRefs(mSharedState.Get()->mPrincipalToInherit)); + + mSharedState.Get()->mPartitionedPrincipalToInherit = + aPartitionedPrincipalToInherit; +- mSharedState.Get()->mPolicyContainer = aPolicyContainer; ++ mSharedState.Get()->mPolicyContainer = ++ PolicyContainerToStore(mURI, aPolicyContainer); + aChannel->GetContentType(mSharedState.Get()->mContentType); + aChannel->GetOriginalURI(getter_AddRefs(mOriginalURI)); + + uint32_t loadFlags; + aChannel->GetLoadFlags(&loadFlags); + diff --git a/src/external-patches/manifest.json b/src/external-patches/manifest.json index bede642e2..8c71d8623 100644 --- a/src/external-patches/manifest.json +++ b/src/external-patches/manifest.json @@ -7,14 +7,6 @@ "id": "D299584", "name": "Native MacOS popovers fix" }, - { - "type": "phabricator", - "id": "D284404", - "name": "Add urlbar closeOnWindowBlur preference", - "replaces": { - "\n\n": "\n // may want to figure out a more robust way to detect abandonment." - } - }, { "type": "local", // TODO: Convert into https://phabricator.services.mozilla.com/D298079 @@ -48,5 +40,16 @@ "this._lastJsonLength = uncompressedBytes;": "this._lastJsonLength = uncompressedBytes.jsonLength;", " jsonLengthHint,": " lengthHint: jsonLengthHint," } + }, + { + "type": "phabricator", + "id": "D320897", + "name": "Bug 2011236", + "replaces": { + // The upstream patch context includes a MOZ_DIAGNOSTIC_ASSERT that + // doesn't exist yet in our Firefox version. + "@@ -59,11 +67,12 @@": "@@ -59,9 +67,10 @@", + "aLoadState->TypeHint())) {\n MOZ_DIAGNOSTIC_ASSERT(!mURI->SchemeIs(\"javascript\"));\n \n // Pull": "aLoadState->TypeHint())) {\n // Pull" + } } ] diff --git a/src/zen/compact-mode/ZenCompactMode.mjs b/src/zen/compact-mode/ZenCompactMode.mjs index 9f85c4125..03ac69479 100644 --- a/src/zen/compact-mode/ZenCompactMode.mjs +++ b/src/zen/compact-mode/ZenCompactMode.mjs @@ -832,8 +832,9 @@ window.gZenCompactModeManager = { const onLeave = event => { if (AppConstants.platform == "macosx") { - const buttonRect = - gZenVerticalTabsManager.actualWindowButtons.getBoundingClientRect(); + const buttonRect = window.windowUtils.getBoundsWithoutFlushing( + gZenVerticalTabsManager.actualWindowButtons + ); const MAC_WINDOW_BUTTONS_X_BORDER = buttonRect.width + buttonRect.x; const MAC_WINDOW_BUTTONS_Y_BORDER = buttonRect.height + buttonRect.y; if ( diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index df6fe45b6..6da584818 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -1706,6 +1706,15 @@ class nsZenWorkspaces { } makeSureEmptyTabIsFirst() { + if ( + gZenUIManager.testingEnabled && + this._emptyTab && + (this._emptyTab.closing || !this._emptyTab.isConnected) + ) { + this._emptyTab = gBrowser.tabs.find( + tab => tab.hasAttribute("zen-empty-tab") && !tab.closing + ); + } const emptyTab = this._emptyTab; if (emptyTab) { emptyTab.setAttribute("zen-workspace-id", this.activeWorkspace); @@ -3236,8 +3245,10 @@ class nsZenWorkspaces { return width; }, 0); - // Check if the total width exceeds the parent's width - if (totalWidth > parent.clientWidth) { + // Check if the total width exceeds the parent's width. + const parentWidth = + window.windowUtils.getBoundsWithoutFlushing(parent).width; + if (totalWidth > parentWidth) { parent.setAttribute("icons-overflow", "true"); } else { parent.removeAttribute("icons-overflow"); @@ -3245,7 +3256,7 @@ class nsZenWorkspaces { // Set the width of each icon to the maximum size they can fit on const widthPerButton = Math.max( - (parent.clientWidth - separation * (parent.children.length - 1)) / + (parentWidth - separation * (parent.children.length - 1)) / parent.children.length, minButtonSize ); diff --git a/src/zen/tests/manifest.toml b/src/zen/tests/manifest.toml index d242dd5ba..c01ed3e6c 100644 --- a/src/zen/tests/manifest.toml +++ b/src/zen/tests/manifest.toml @@ -3,6 +3,36 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +[alerts] +source = "browser/base/content/test/alerts" +is_direct_path = true + +[backforward] +source = "browser/base/content/test/backforward" +is_direct_path = true + +[caps] +source = "browser/base/content/test/caps" +is_direct_path = true + +[gesture] +source = "browser/base/content/test/gesture" +is_direct_path = true + +[gesture.replace-manifest] +"[DEFAULT]" = '''[DEFAULT] +prefs = [ + "zen.window-sync.enabled=false", +]''' + +[linkHandling] +source = "browser/base/content/test/linkHandling" +is_direct_path = true +disable = [ + # Glance opens with alt+click, firefox doesnt know that. + "browser_javascript_links.js", +] + [readermode] source = "toolkit/components/reader/tests/browser" is_direct_path = true @@ -33,6 +63,68 @@ xpcshell = true [services-crypto.replace-manifest] "../../../" = "../../../../services/" +[sessionstore] +source = "browser/components/sessionstore/test" +is_direct_path = true +disable = [ + "browser_1933485_tab_groups_history.js", + "browser_1953801_tab_groups_history_close_other.js", + "browser_aboutSessionRestore.js", + "browser_bfcache_telemetry.js", + "browser_closedId.js", + "browser_closed_objects_changed_notifications_tabs.js", + "browser_closed_objects_changed_notifications_windows.js", + "browser_closed_tabs_closed_windows.js", + "browser_closed_tabs_windows.js", + "browser_cookies_partitioned.js", + "browser_crashedTabs.js", + "browser_dying_cache.js", + "browser_firefoxView_restore.js", + "browser_firefoxView_selected_restore.js", + "browser_forget_closed_tab_window_byId.js", + "browser_movePendingTabToNewWindow.js", + "browser_newtab_userTypedValue.js", + "browser_pinned_tabs.js", + "browser_purge_domaindata.js", + "browser_reopen_all_windows.js", + "browser_restoreLastActionCorrectOrder.js", + "browser_restoreLastClosedTabOrWindowOrSession.js", + "browser_restore_container_tabs_oa.js", + "browser_restore_tabless_window.js", + "browser_restore_verticalPinnedTabs.js", + "browser_restored_window_features.js", + "browser_scrollPositions.js", + "browser_scrollPositionsReaderMode.js", + "browser_searchModeSwitcher_restore.js", + "browser_speculative_connect.js", + "browser_splitview_integer_ids.js", + "browser_splitview_restore_in_closed_window.js", + "browser_splitview_string_migration.js", + "browser_swapDocShells.js", + "browser_tab_groups_restore_closed_in_closed_window.js", + "browser_tab_groups_restore_closed_in_open_window.js", + "browser_tab_groups_restore_closed_many_tabs.js", + "browser_tab_groups_closed.js", + "browser_tab_groups_restore_multiple.js", + "browser_tab_groups_restore_saved.js", + "browser_tab_groups_restore_simple.js", + "browser_tab_groups_save_on_removeAllTabsBut.js", + "browser_tab_groups_save_on_removeTabsToTheEnd.js", + "browser_tab_groups_save_on_removeTabsToTheStart.js", + "browser_tab_groups_save_on_window_close.js", + "browser_tab_groups_state.js", + "browser_tab_groups_undo.js", + "browser_tab_label_during_restore.js", + "browser_undoCloseById.js", + "browser_urlbarSearchMode.js", + "browser_windowStateContainer.js", + "browser_wireframe_basic.js", +] + +[sessionstore.replace-manifest] +'prefs = [' = '''prefs = [ + "zen.window-sync.enabled=false",''' + [shell] source = "browser/components/shell/test" is_direct_path = true @@ -59,3 +151,10 @@ source = "toolkit/components/tooltiptext" disable = [ "browser_input_file_tooltips.js", ] + +[zoom] +source = "browser/base/content/test/zoom" +is_direct_path = true + +[zoom.replace-manifest] +"../general/" = "../../../../browser/base/content/test/general/" diff --git a/src/zen/tests/mochitests/alerts/browser.toml b/src/zen/tests/mochitests/alerts/browser.toml new file mode 100644 index 000000000..6ac3bc4f1 --- /dev/null +++ b/src/zen/tests/mochitests/alerts/browser.toml @@ -0,0 +1,32 @@ +[DEFAULT] +support-files = [ + "head.js", + "file_dom_notifications.html", +] + +["browser_notification_close.js"] +https_first_disabled = true +skip-if = [ + "os == 'win'", # Bug 1227785 +] + +["browser_notification_do_not_disturb.js"] +https_first_disabled = true + +["browser_notification_open_settings.js"] +https_first_disabled = true +skip-if = [ + "os == 'win'", # Bug 1411118 +] + +["browser_notification_remove_permission.js"] +https_first_disabled = true +skip-if = [ + "os == 'win'", # Bug 1411118 +] + +["browser_notification_tab_switching.js"] +https_first_disabled = true +skip-if = [ + "os == 'win'", # Bug 1243263 +] diff --git a/src/zen/tests/mochitests/alerts/browser_notification_close.js b/src/zen/tests/mochitests/alerts/browser_notification_close.js new file mode 100644 index 000000000..7e9d43072 --- /dev/null +++ b/src/zen/tests/mochitests/alerts/browser_notification_close.js @@ -0,0 +1,78 @@ +"use strict"; + +const { PlacesTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PlacesTestUtils.sys.mjs" +); + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +let notificationURL = + "https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html"; + +add_task(async function test_notificationClose() { + await addNotificationPermission(notificationURL); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: notificationURL, + }, + async function dummyTabTask(aBrowser) { + await openNotification(aBrowser, "showNotification2"); + + info("Notification alert showing"); + + let alertWindow = Services.wm.getMostRecentWindow("alert:alert"); + if (!alertWindow) { + ok(true, "Notifications don't use XUL windows on all platforms."); + await closeNotification(aBrowser); + return; + } + + let alertTitleLabel = + alertWindow.document.getElementById("alertTitleLabel"); + is( + alertTitleLabel.value, + "Test title", + "Title text of notification should be present" + ); + let alertTextLabel = + alertWindow.document.getElementById("alertTextLabel"); + is( + alertTextLabel.textContent, + "Test body 2", + "Body text of notification should be present" + ); + + let alertCloseButton = alertWindow.document.querySelector(".close-icon"); + is(alertCloseButton.localName, "toolbarbutton", "close button found"); + let promiseBeforeUnloadEvent = BrowserTestUtils.waitForEvent( + alertWindow, + "beforeunload" + ); + let closedTime = alertWindow.Date.now(); + alertCloseButton.click(); + info("Clicked on close button"); + await promiseBeforeUnloadEvent; + + ok(true, "Alert should close when the close button is clicked"); + let currentTime = alertWindow.Date.now(); + // The notification will self-close at 12 seconds, so this checks + // that the notification closed before the timeout. + Assert.less( + currentTime - closedTime, + 5000, + "Close requested at " + + closedTime + + ", actually closed at " + + currentTime + ); + } + ); +}); + +add_task(async function cleanup() { + PermissionTestUtils.remove(notificationURL, "desktop-notification"); +}); diff --git a/src/zen/tests/mochitests/alerts/browser_notification_do_not_disturb.js b/src/zen/tests/mochitests/alerts/browser_notification_do_not_disturb.js new file mode 100644 index 000000000..852e0d1e2 --- /dev/null +++ b/src/zen/tests/mochitests/alerts/browser_notification_do_not_disturb.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that notifications can be silenced using nsIAlertsDoNotDisturb + * on systems where that interface and its methods are implemented for + * the nsIAlertService. + */ + +const ALERT_SERVICE = Cc["@mozilla.org/alerts-service;1"] + .getService(Ci.nsIAlertsService) + .QueryInterface(Ci.nsIAlertsDoNotDisturb); + +const PAGE = + "https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html"; + +// The amount of time in seconds that we will wait for a notification +// to show up before we decide that it's not coming. +const NOTIFICATION_TIMEOUT_SECS = 2000; + +add_setup(async function () { + await addNotificationPermission(PAGE); +}); + +/** + * Test that the manualDoNotDisturb attribute can prevent + * notifications from appearing. + */ +add_task(async function test_manualDoNotDisturb() { + try { + // Only run the test if the do-not-disturb + // interface has been implemented. + ALERT_SERVICE.manualDoNotDisturb; + ok(true, "Alert service implements do-not-disturb interface"); + } catch (e) { + ok( + true, + "Alert service doesn't implement do-not-disturb interface, exiting test" + ); + return; + } + + // In the event that something goes wrong during this test, make sure + // we put the attribute back to the default setting when this test file + // exits. + registerCleanupFunction(() => { + ALERT_SERVICE.manualDoNotDisturb = false; + }); + + // Make sure that do-not-disturb is not enabled before we start. + ok( + !ALERT_SERVICE.manualDoNotDisturb, + "Alert service should not be disabled when test starts" + ); + + await BrowserTestUtils.withNewTab(PAGE, async browser => { + await openNotification(browser, "showNotification2"); + + info("Notification alert showing"); + + let alertWindow = Services.wm.getMostRecentWindow("alert:alert"); + + // For now, only the XUL alert backend implements the manualDoNotDisturb + // method for nsIAlertsDoNotDisturb, so we expect there to be a XUL alert + // window. If the method gets implemented by native backends in the future, + // we'll probably want to branch here and set the manualDoNotDisturb + // attribute manually. + ok(alertWindow, "Expected a XUL alert window."); + + // We're using the XUL notification backend. This means that there's + // a menuitem for enabling manualDoNotDisturb. We exercise that + // menuitem here. + let doNotDisturbMenuItem = alertWindow.document.getElementById( + "doNotDisturbMenuItem" + ); + is(doNotDisturbMenuItem.localName, "menuitem", "menuitem found"); + + let unloadPromise = BrowserTestUtils.waitForEvent( + alertWindow, + "beforeunload" + ); + + doNotDisturbMenuItem.click(); + info("Clicked on do-not-disturb menuitem"); + await unloadPromise; + + // At this point, we should be configured to not display notifications + // to the user. + ok( + ALERT_SERVICE.manualDoNotDisturb, + "Alert service should be disabled after clicking menuitem" + ); + + // The notification should not appear, but there is no way from the + // client-side to know that it was blocked, except for waiting some time + // and realizing that the "onshow" event never fired. + await Assert.rejects( + openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS), + /timed out/, + "The notification should never display." + ); + + ALERT_SERVICE.manualDoNotDisturb = false; + }); +}); + +/** + * Test that the suppressForScreenSharing attribute can prevent + * notifications from appearing. + */ +add_task(async function test_suppressForScreenSharing() { + try { + // Only run the test if the do-not-disturb + // interface has been implemented. + ALERT_SERVICE.suppressForScreenSharing; + ok(true, "Alert service implements do-not-disturb interface"); + } catch (e) { + ok( + true, + "Alert service doesn't implement do-not-disturb interface, exiting test" + ); + return; + } + + // In the event that something goes wrong during this test, make sure + // we put the attribute back to the default setting when this test file + // exits. + registerCleanupFunction(() => { + ALERT_SERVICE.suppressForScreenSharing = false; + }); + + // Make sure that do-not-disturb is not enabled before we start. + ok( + !ALERT_SERVICE.suppressForScreenSharing, + "Alert service should not be suppressing for screen sharing when test " + + "starts" + ); + + await BrowserTestUtils.withNewTab(PAGE, async browser => { + await openNotification(browser, "showNotification2"); + + info("Notification alert showing"); + await closeNotification(browser); + ALERT_SERVICE.suppressForScreenSharing = true; + + // The notification should not appear, but there is no way from the + // client-side to know that it was blocked, except for waiting some time + // and realizing that the "onshow" event never fired. + await Assert.rejects( + openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS), + /timed out/, + "The notification should never display." + ); + }); + + ALERT_SERVICE.suppressForScreenSharing = false; +}); diff --git a/src/zen/tests/mochitests/alerts/browser_notification_open_settings.js b/src/zen/tests/mochitests/alerts/browser_notification_open_settings.js new file mode 100644 index 000000000..4d25a357f --- /dev/null +++ b/src/zen/tests/mochitests/alerts/browser_notification_open_settings.js @@ -0,0 +1,87 @@ +"use strict"; + +var notificationURL = + "https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html"; +// BrowserGlue passes "privacy-permissions" to openPreferences when the +// notifications-open-settings observer fires; the Settings Redesign +// LegacyPaneMappings shim routes that to the permissionsData pane. +var expectedURL = Services.prefs.getBoolPref( + "browser.settings-redesign.enabled", + false +) + ? "about:preferences#permissionsData" + : "about:preferences#privacy"; + +add_task(async function test_settingsOpen_observer() { + info( + "Opening a dummy tab so openPreferences=>switchToTabHavingURI doesn't use the blank tab." + ); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:robots", + }, + async function dummyTabTask() { + // Ensure preferences is loaded before removing the tab. + let syncPaneLoadedPromise = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL); + info("simulate a notifications-open-settings notification"); + let uri = NetUtil.newURI("https://example.com"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + Services.obs.notifyObservers(principal, "notifications-open-settings"); + let tab = await tabPromise; + ok(tab, "The notification settings tab opened"); + await syncPaneLoadedPromise; + BrowserTestUtils.removeTab(tab); + } + ); +}); + +add_task(async function test_settingsOpen_button() { + info("Adding notification permission"); + await addNotificationPermission(notificationURL); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: notificationURL, + }, + async function tabTask(aBrowser) { + info("Waiting for notification"); + await openNotification(aBrowser, "showNotification2"); + + let alertWindow = Services.wm.getMostRecentWindow("alert:alert"); + if (!alertWindow) { + ok(true, "Notifications don't use XUL windows on all platforms."); + await closeNotification(aBrowser); + return; + } + + // Ensure preferences is loaded before removing the tab. + let syncPaneLoadedPromise = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + let closePromise = promiseWindowClosed(alertWindow); + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL); + let openSettingsMenuItem = alertWindow.document.getElementById( + "openSettingsMenuItem" + ); + openSettingsMenuItem.click(); + + info("Waiting for notification settings tab"); + let tab = await tabPromise; + ok(tab, "The notification settings tab opened"); + + await syncPaneLoadedPromise; + await closePromise; + BrowserTestUtils.removeTab(tab); + } + ); +}); diff --git a/src/zen/tests/mochitests/alerts/browser_notification_remove_permission.js b/src/zen/tests/mochitests/alerts/browser_notification_remove_permission.js new file mode 100644 index 000000000..ec6916e1b --- /dev/null +++ b/src/zen/tests/mochitests/alerts/browser_notification_remove_permission.js @@ -0,0 +1,85 @@ +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +var tab; +var notificationURL = + "https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html"; +var alertWindowClosed = false; +var permRemoved = false; + +function test() { + waitForExplicitFinish(); + + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + window.restore(); + }); + + addNotificationPermission(notificationURL).then(function openTab() { + tab = BrowserTestUtils.addTab(gBrowser, notificationURL); + gBrowser.selectedTab = tab; + BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => onLoad()); + }); +} + +function onLoad() { + openNotification(tab.linkedBrowser, "showNotification2").then(onAlertShowing); +} + +function onAlertShowing() { + info("Notification alert showing"); + + let alertWindow = Services.wm.getMostRecentWindow("alert:alert"); + if (!alertWindow) { + ok(true, "Notifications don't use XUL windows on all platforms."); + closeNotification(tab.linkedBrowser).then(finish); + return; + } + ok( + PermissionTestUtils.testExactPermission( + notificationURL, + "desktop-notification" + ), + "Permission should exist prior to removal" + ); + let disableForOriginMenuItem = alertWindow.document.getElementById( + "disableForOriginMenuItem" + ); + is(disableForOriginMenuItem.localName, "menuitem", "menuitem found"); + Services.obs.addObserver(permObserver, "perm-changed"); + alertWindow.addEventListener("beforeunload", onAlertClosing); + disableForOriginMenuItem.click(); + info("Clicked on disable-for-origin menuitem"); +} + +function permObserver(subject, topic, data) { + if (topic != "perm-changed") { + return; + } + + let permission = subject.QueryInterface(Ci.nsIPermission); + is( + permission.type, + "desktop-notification", + "desktop-notification permission changed" + ); + is(data, "deleted", "desktop-notification permission deleted"); + + Services.obs.removeObserver(permObserver, "perm-changed"); + permRemoved = true; + if (alertWindowClosed) { + finish(); + } +} + +function onAlertClosing(event) { + event.target.removeEventListener("beforeunload", onAlertClosing); + + alertWindowClosed = true; + if (permRemoved) { + finish(); + } +} diff --git a/src/zen/tests/mochitests/alerts/browser_notification_tab_switching.js b/src/zen/tests/mochitests/alerts/browser_notification_tab_switching.js new file mode 100644 index 000000000..4e5a23f18 --- /dev/null +++ b/src/zen/tests/mochitests/alerts/browser_notification_tab_switching.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +var tab; +var notification; +var notificationURL = + "https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html"; +var newWindowOpenedFromTab; + +add_task(async function test_notificationPreventDefaultAndSwitchTabs() { + await addNotificationPermission(notificationURL); + + let originalTab = gBrowser.selectedTab; + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: notificationURL, + }, + async function dummyTabTask(aBrowser) { + // Put new tab in background so it is obvious when it is re-focused. + await BrowserTestUtils.switchTab(gBrowser, originalTab); + isnot( + gBrowser.selectedBrowser, + aBrowser, + "Notification page loaded as a background tab" + ); + + // First, show a notification that will be have the tab-switching prevented. + function promiseNotificationEvent(evt) { + return SpecialPowers.spawn( + aBrowser, + [evt], + async function (contentEvt) { + return new Promise(resolve => { + let contentNotification = content.wrappedJSObject._notification; + contentNotification.addEventListener( + contentEvt, + function (event) { + resolve({ defaultPrevented: event.defaultPrevented }); + }, + { once: true } + ); + }); + } + ); + } + await openNotification(aBrowser, "showNotification1"); + info("Notification alert showing"); + let alertWindow = Services.wm.getMostRecentWindow("alert:alert"); + if (!alertWindow) { + ok(true, "Notifications don't use XUL windows on all platforms."); + await closeNotification(aBrowser); + return; + } + info("Clicking on notification"); + let promiseClickEvent = promiseNotificationEvent("click"); + + // NB: This executeSoon is needed to allow the non-e10s runs of this test + // a chance to set the event listener on the page. Otherwise, we + // synchronously fire the click event before we listen for the event. + executeSoon(() => { + EventUtils.synthesizeMouseAtCenter( + alertWindow.document.getElementById("alertTitleLabel"), + {}, + alertWindow + ); + }); + let clickEvent = await promiseClickEvent; + ok( + clickEvent.defaultPrevented, + "The event handler for the first notification cancels the event" + ); + isnot( + gBrowser.selectedBrowser, + aBrowser, + "Notification page still a background tab" + ); + let notificationClosed = promiseNotificationEvent("close"); + await closeNotification(aBrowser); + await notificationClosed; + + // Second, show a notification that will cause the tab to get switched. + await openNotification(aBrowser, "showNotification2"); + alertWindow = Services.wm.getMostRecentWindow("alert:alert"); + let promiseTabSelect = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabSelect" + ); + EventUtils.synthesizeMouseAtCenter( + alertWindow.document.getElementById("alertTitleLabel"), + {}, + alertWindow + ); + await promiseTabSelect; + is( + gBrowser.selectedBrowser.currentURI.spec, + notificationURL, + "Clicking on the second notification should select its originating tab" + ); + notificationClosed = promiseNotificationEvent("close"); + await closeNotification(aBrowser); + await notificationClosed; + } + ); +}); + +add_task(async function cleanup() { + PermissionTestUtils.remove(notificationURL, "desktop-notification"); +}); diff --git a/src/zen/tests/mochitests/alerts/file_dom_notifications.html b/src/zen/tests/mochitests/alerts/file_dom_notifications.html new file mode 100644 index 000000000..6deede8fc --- /dev/null +++ b/src/zen/tests/mochitests/alerts/file_dom_notifications.html @@ -0,0 +1,39 @@ + + + + + + +
+ +
+ + diff --git a/src/zen/tests/mochitests/alerts/head.js b/src/zen/tests/mochitests/alerts/head.js new file mode 100644 index 000000000..57b758483 --- /dev/null +++ b/src/zen/tests/mochitests/alerts/head.js @@ -0,0 +1,73 @@ +// Platforms may default to reducing motion. We override this to ensure the +// alert slide animation is enabled in tests. +SpecialPowers.pushPrefEnv({ + set: [["ui.prefersReducedMotion", 0]], +}); + +async function addNotificationPermission(originString) { + return SpecialPowers.pushPermissions([ + { + type: "desktop-notification", + allow: true, + context: originString, + }, + ]); +} + +/** + * Similar to `BrowserTestUtils.closeWindow`, but + * doesn't call `window.close()`. + */ +function promiseWindowClosed(window) { + return new Promise(function (resolve) { + Services.ww.registerNotification(function observer(subject, topic) { + if (topic == "domwindowclosed" && subject == window) { + Services.ww.unregisterNotification(observer); + resolve(); + } + }); + }); +} + +/** + * These two functions work with file_dom_notifications.html to open the + * notification and close it. + * + * |fn| can be showNotification1 or showNotification2. + * if |timeout| is passed, then the promise returned from this function is + * rejected after the requested number of miliseconds. + */ +function openNotification(aBrowser, fn, timeout) { + info(`openNotification: ${fn}`); + return SpecialPowers.spawn( + aBrowser, + [fn, timeout], + async function (contentFn, contentTimeout) { + await new Promise((resolve, reject) => { + let win = content.wrappedJSObject; + let notification = win[contentFn](); + win._notification = notification; + + function listener() { + notification.removeEventListener("show", listener); + resolve(); + } + + notification.addEventListener("show", listener); + + if (contentTimeout) { + content.setTimeout(() => { + notification.removeEventListener("show", listener); + reject("timed out"); + }, contentTimeout); + } + }); + } + ); +} + +function closeNotification(aBrowser) { + return SpecialPowers.spawn(aBrowser, [], function () { + content.wrappedJSObject._notification.close(); + }); +} diff --git a/src/zen/tests/mochitests/backforward/browser.toml b/src/zen/tests/mochitests/backforward/browser.toml new file mode 100644 index 000000000..fd36247df --- /dev/null +++ b/src/zen/tests/mochitests/backforward/browser.toml @@ -0,0 +1,7 @@ +[DEFAULT] + +["browser_history_menu.js"] +fail-if = [ + "a11y_checks", # Bug 1854233 navigator-toolbox may not be focusable +] +https_first_disabled = true diff --git a/src/zen/tests/mochitests/backforward/browser_history_menu.js b/src/zen/tests/mochitests/backforward/browser_history_menu.js new file mode 100644 index 000000000..8623d5405 --- /dev/null +++ b/src/zen/tests/mochitests/backforward/browser_history_menu.js @@ -0,0 +1,179 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test verifies that the back forward button long-press menu and context menu +// shows the correct history items. + +add_task(async function mousedown_back() { + await testBackForwardMenu(false); +}); + +add_task(async function contextmenu_back() { + await testBackForwardMenu(true); +}); + +async function openHistoryMenu(useContextMenu) { + let backButton = document.getElementById("back-button"); + let rect = backButton.getBoundingClientRect(); + + info("waiting for the history menu to open"); + + let popupShownPromise = BrowserTestUtils.waitForEvent( + useContextMenu ? document.getElementById("backForwardMenu") : backButton, + "popupshown" + ); + if (useContextMenu) { + EventUtils.synthesizeMouseAtCenter(backButton, { + type: "contextmenu", + button: 2, + }); + } else { + EventUtils.synthesizeMouseAtCenter(backButton, { type: "mousedown" }); + } + + EventUtils.synthesizeMouse(backButton, rect.width / 2, rect.height, { + type: "mouseup", + }); + let popupEvent = await popupShownPromise; + + ok(true, "history menu opened"); + + return popupEvent; +} + +async function testBackForwardMenu(useContextMenu) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + ); + + for (let iter = 2; iter <= 4; iter++) { + // Iterate three times. For the first two times through the loop, add a new history item. + // But for the last iteration, go back in the history instead. + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [iter], + async function (iterChild) { + if (iterChild == 4) { + let popStatePromise = new Promise(function (resolve) { + content.onpopstate = resolve; + }); + content.history.back(); + await popStatePromise; + } else { + // With the back-button intervention enabled, we need to make sure to + // trigger a user activation on each history entry, or they won't + // show in the menu. + content.document.notifyUserGestureActivation(); + content.history.pushState({}, "" + iterChild, iterChild + ".html"); + } + } + ); + + // Wait for the session data to be flushed before continuing the test + await new Promise(resolve => + SessionStore.getSessionHistory(gBrowser.selectedTab, resolve) + ); + + let popupEvent = await openHistoryMenu(useContextMenu); + + // Wait for the session data to be flushed before continuing the test + await new Promise(resolve => + SessionStore.getSessionHistory(gBrowser.selectedTab, resolve) + ); + + is( + popupEvent.target.children.length, + iter > 3 ? 3 : iter, + "Correct number of history items" + ); + + let node = popupEvent.target.lastElementChild; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + is(node.getAttribute("uri"), "http://example.com/", "'1' item uri"); + is(node.getAttribute("index"), "0", "'1' item index"); + is( + node.getAttribute("historyindex"), + iter == 3 ? "-2" : "-1", + "'1' item historyindex" + ); + + node = node.previousElementSibling; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + is(node.getAttribute("uri"), "http://example.com/2.html", "'2' item uri"); + is(node.getAttribute("index"), "1", "'2' item index"); + is( + node.getAttribute("historyindex"), + iter == 3 ? "-1" : "0", + "'2' item historyindex" + ); + + if (iter >= 3) { + node = node.previousElementSibling; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + is(node.getAttribute("uri"), "http://example.com/3.html", "'3' item uri"); + is(node.getAttribute("index"), "2", "'3' item index"); + is( + node.getAttribute("historyindex"), + iter == 4 ? "1" : "0", + "'3' item historyindex" + ); + } + + // Close the popup, but on the last iteration, click on one of the history items + // to ensure it opens in a new tab. + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + popupEvent.target, + "popuphidden" + ); + + if (iter < 4) { + popupEvent.target.hidePopup(); + } else { + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + url => url == "http://example.com/" + ); + + popupEvent.target.activateItem(popupEvent.target.children[2], { + button: 1, + }); + + let newtab = await newTabPromise; + gBrowser.removeTab(newtab); + } + + await popupHiddenPromise; + } + + gBrowser.removeTab(tab); +} + +// Make sure that the history popup appears after navigating around in a preferences page. +add_task(async function test_preferences_page() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences" + ); + + openPreferences("search"); + let popupEvent = await openHistoryMenu(true); + + // Wait for the session data to be flushed before continuing the test + await new Promise(resolve => + SessionStore.getSessionHistory(gBrowser.selectedTab, resolve) + ); + + is(popupEvent.target.children.length, 2, "Correct number of history items"); + + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + popupEvent.target, + "popuphidden" + ); + popupEvent.target.hidePopup(); + await popupHiddenPromise; + + gBrowser.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/caps/browser.toml b/src/zen/tests/mochitests/caps/browser.toml new file mode 100644 index 000000000..2afb307d4 --- /dev/null +++ b/src/zen/tests/mochitests/caps/browser.toml @@ -0,0 +1,10 @@ +[DEFAULT] + +["browser_principalSerialization_csp.js"] + +["browser_principalSerialization_json.js"] +skip-if = [ + "debug", # deliberately bypass assertions when deserializing. Bug 965637 removed the CSP from Principals, but the remaining bits in such Principals should deserialize correctly. +] + +["browser_principalSerialization_version1.js"] diff --git a/src/zen/tests/mochitests/caps/browser_principalSerialization_csp.js b/src/zen/tests/mochitests/caps/browser_principalSerialization_csp.js new file mode 100644 index 000000000..909e72879 --- /dev/null +++ b/src/zen/tests/mochitests/caps/browser_principalSerialization_csp.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Within Bug 965637 we move the CSP away from the Principal. Serialized Principals however + * might still have CSPs serialized within them. This tests ensures that we do not + * encounter a memory corruption when deserializing. It's fine that the deserialized + * CSP is null, but the Principal itself should deserialize correctly. + */ + +add_task(async function test_deserialize_principal_with_csp() { + /* + This test should be resilient to changes in principal serialization, if these are failing then it's likely the code will break session storage. + To recreate this for another version, copy the function into the browser console, browse some pages and printHistory. + + Generated with: + function printHistory() { + let tests = []; + let entries = SessionStore.getSessionHistory(gBrowser.selectedTab).entries.map((entry) => { return entry.triggeringPrincipal_base64 }); + entries.push(E10SUtils.serializePrincipal(gBrowser.selectedTab.linkedBrowser._contentPrincipal)); + for (let entry of entries) { + console.log(entry); + let testData = {}; + testData.input = entry; + let principal = E10SUtils.deserializePrincipal(testData.input); + testData.output = {}; + if (principal.URI === null) { + testData.output.URI = false; + } else { + testData.output.URISpec = principal.URI.spec; + } + testData.output.originAttributes = principal.originAttributes; + testData.output.cspJSON = principal.cspJSON; + + tests.push(testData); + } + return tests; + } + printHistory(); // Copy this into: serializedPrincipalsFromFirefox + */ + + let serializedPrincipalsFromFirefox = [ + { + input: + "ZT4OTT7kRfqycpfCC8AeuAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAHmh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTLwAAAAAAAAAFAAAACAAAAA8AAAAA/////wAAAAD/////AAAACAAAAA8AAAAXAAAABwAAABcAAAAHAAAAFwAAAAcAAAAeAAAAAAAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AQAAAAAAAAAAAAAAAQnZ7Rrl1EAEv+Anzrkj2ayzxMCuvV5MrYfgjSENuz+fAd6UctCANBHTk5kAEEug/UCSBzpUbXhPMJE6uHGBMgjGAAAAAv////8AAAG7AQAAAB5odHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy8AAAAAAAAABQAAAAgAAAAPAAAAAP////8AAAAA/////wAAAAgAAAAPAAAAFwAAAAcAAAAXAAAABwAAABcAAAAHAAAAHgAAAAAAAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wEAAAAAAAAAAAABAAAFtgBzAGMAcgBpAHAAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwAgACcAdQBuAHMAYQBmAGUALQBlAHYAYQBsACcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdABhAGcAbQBhAG4AYQBnAGUAcgAuAGcAbwBvAGcAbABlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AcwAuAHkAdABpAG0AZwAuAGMAbwBtADsAIABpAG0AZwAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIABkAGEAdABhADoAIABoAHQAdABwAHMAOgAvAC8AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAZABzAGUAcgB2AGkAYwBlAC4AZwBvAG8AZwBsAGUALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AZABlACAAaAB0AHQAcABzADoALwAvAGEAZABzAGUAcgB2AGkAYwBlAC4AZwBvAG8AZwBsAGUALgBkAGsAIABoAHQAdABwAHMAOgAvAC8AYwByAGUAYQB0AGkAdgBlAGMAbwBtAG0AbwBuAHMALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwBhAGQALgBkAG8AdQBiAGwAZQBjAGwAaQBjAGsALgBuAGUAdAA7ACAAZABlAGYAYQB1AGwAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AOwAgAGYAcgBhAG0AZQAtAHMAcgBjACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAtAG4AbwBjAG8AbwBrAGkAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHQAcgBhAGMAawBlAHIAdABlAHMAdAAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AcwB1AHIAdgBlAHkAZwBpAHoAbQBvAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAuAGMAbgAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQA7ACAAcwB0AHkAbABlAC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgACcAdQBuAHMAYQBmAGUALQBpAG4AbABpAG4AZQAnADsAIABjAG8AbgBuAGUAYwB0AC0AcwByAGMAIAAnAHMAZQBsAGYAJwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG4AZQB0ACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALwAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALgBjAG4ALwA7ACAAYwBoAGkAbABkAC0AcwByAGMAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC0AbgBvAGMAbwBvAGsAaQBlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdAByAGEAYwBrAGUAcgB0AGUAcwB0AC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBzAHUAcgB2AGUAeQBnAGkAegBtAG8ALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAuAGMAbwBtAAA=", + output: { + // Within Bug 965637 we removed CSP from Principals. Already serialized Principals however should still deserialize correctly (just without the CSP). + // "cspJSON": "{\"csp-policies\":[{\"child-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"connect-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://accounts.firefox.com/\",\"https://accounts.firefox.com.cn/\"],\"default-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\"],\"frame-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"img-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"data:\",\"https://mozilla.org\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://adservice.google.com\",\"https://adservice.google.de\",\"https://adservice.google.dk\",\"https://creativecommons.org\",\"https://ad.doubleclick.net\"],\"report-only\":false,\"script-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\",\"'unsafe-eval'\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://tagmanager.google.com\",\"https://www.youtube.com\",\"https://s.ytimg.com\"],\"style-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\"]}]}", + URISpec: "https://www.mozilla.org/en-US/", + originAttributes: { + firstPartyDomain: "", + inIsolatedMozBrowser: false, + privateBrowsingId: 0, + userContextId: 0, + geckoViewSessionContextId: "", + partitionKey: "", + }, + }, + }, + { + input: + "ZT4OTT7kRfqycpfCC8AeuAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAL2h0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTL2ZpcmVmb3gvYWNjb3VudHMvAAAAAAAAAAUAAAAIAAAADwAAAAj/////AAAACP////8AAAAIAAAADwAAABcAAAAYAAAAFwAAABgAAAAXAAAAGAAAAC8AAAAAAAAAL/////8AAAAA/////wAAABf/////AAAAF/////8BAAAAAAAAAAAAAAABCdntGuXUQAS/4CfOuSPZrLPEwK69Xkyth+CNIQ27P58B3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAL2h0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2VuLVVTL2ZpcmVmb3gvYWNjb3VudHMvAAAAAAAAAAUAAAAIAAAADwAAAAj/////AAAACP////8AAAAIAAAADwAAABcAAAAYAAAAFwAAABgAAAAXAAAAGAAAAC8AAAAAAAAAL/////8AAAAA/////wAAABf/////AAAAF/////8BAAAAAAAAAAAAAQAABbYAcwBjAHIAaQBwAHQALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtACAAJwB1AG4AcwBhAGYAZQAtAGkAbgBsAGkAbgBlACcAIAAnAHUAbgBzAGEAZgBlAC0AZQB2AGEAbAAnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBnAG8AbwBnAGwAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHMALgB5AHQAaQBtAGcALgBjAG8AbQA7ACAAaQBtAGcALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtACAAZABhAHQAYQA6ACAAaAB0AHQAcABzADoALwAvAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBkAHMAZQByAHYAaQBjAGUALgBnAG8AbwBnAGwAZQAuAGQAZQAgAGgAdAB0AHAAcwA6AC8ALwBhAGQAcwBlAHIAdgBpAGMAZQAuAGcAbwBvAGcAbABlAC4AZABrACAAaAB0AHQAcABzADoALwAvAGMAcgBlAGEAdABpAHYAZQBjAG8AbQBtAG8AbgBzAC4AbwByAGcAIABoAHQAdABwAHMAOgAvAC8AYQBkAC4AZABvAHUAYgBsAGUAYwBsAGkAYwBrAC4AbgBlAHQAOwAgAGQAZQBmAGEAdQBsAHQALQBzAHIAYwAgACcAcwBlAGwAZgAnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AbgBlAHQAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAGMAbwBtADsAIABmAHIAYQBtAGUALQBzAHIAYwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAHQAYQBnAG0AYQBuAGEAZwBlAHIALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC0AYQBuAGEAbAB5AHQAaQBjAHMALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALQBuAG8AYwBvAG8AawBpAGUALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwB0AHIAYQBjAGsAZQByAHQAZQBzAHQALgBvAHIAZwAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHMAdQByAHYAZQB5AGcAaQB6AG0AbwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAgAGgAdAB0AHAAcwA6AC8ALwBhAGMAYwBvAHUAbgB0AHMALgBmAGkAcgBlAGYAbwB4AC4AYwBvAG0ALgBjAG4AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgB5AG8AdQB0AHUAYgBlAC4AYwBvAG0AOwAgAHMAdAB5AGwAZQAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwA7ACAAYwBvAG4AbgBlAGMAdAAtAHMAcgBjACAAJwBzAGUAbABmACcAIABoAHQAdABwAHMAOgAvAC8AKgAuAG0AbwB6AGkAbABsAGEALgBuAGUAdAAgAGgAdAB0AHAAcwA6AC8ALwAqAC4AbQBvAHoAaQBsAGwAYQAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvACoALgBtAG8AegBpAGwAbABhAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQB0AGEAZwBtAGEAbgBhAGcAZQByAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAtAGEAbgBhAGwAeQB0AGkAYwBzAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC8AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtAC4AYwBuAC8AOwAgAGMAaABpAGwAZAAtAHMAcgBjACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUAdABhAGcAbQBhAG4AYQBnAGUAcgAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AZwBvAG8AZwBsAGUALQBhAG4AYQBsAHkAdABpAGMAcwAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AeQBvAHUAdAB1AGIAZQAtAG4AbwBjAG8AbwBrAGkAZQAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAHQAcgBhAGMAawBlAHIAdABlAHMAdAAuAG8AcgBnACAAaAB0AHQAcABzADoALwAvAHcAdwB3AC4AcwB1AHIAdgBlAHkAZwBpAHoAbQBvAC4AYwBvAG0AIABoAHQAdABwAHMAOgAvAC8AYQBjAGMAbwB1AG4AdABzAC4AZgBpAHIAZQBmAG8AeAAuAGMAbwBtACAAaAB0AHQAcABzADoALwAvAGEAYwBjAG8AdQBuAHQAcwAuAGYAaQByAGUAZgBvAHgALgBjAG8AbQAuAGMAbgAgAGgAdAB0AHAAcwA6AC8ALwB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQAA", + output: { + URISpec: "https://www.mozilla.org/en-US/firefox/accounts/", + originAttributes: { + firstPartyDomain: "", + inIsolatedMozBrowser: false, + privateBrowsingId: 0, + userContextId: 0, + geckoViewSessionContextId: "", + partitionKey: "", + }, + // Within Bug 965637 we removed CSP from Principals. Already serialized Principals however should still deserialize correctly (just without the CSP). + // "cspJSON": "{\"csp-policies\":[{\"child-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"connect-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://accounts.firefox.com/\",\"https://accounts.firefox.com.cn/\"],\"default-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\"],\"frame-src\":[\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://www.youtube-nocookie.com\",\"https://trackertest.org\",\"https://www.surveygizmo.com\",\"https://accounts.firefox.com\",\"https://accounts.firefox.com.cn\",\"https://www.youtube.com\"],\"img-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"data:\",\"https://mozilla.org\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://adservice.google.com\",\"https://adservice.google.de\",\"https://adservice.google.dk\",\"https://creativecommons.org\",\"https://ad.doubleclick.net\"],\"report-only\":false,\"script-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\",\"'unsafe-eval'\",\"https://www.googletagmanager.com\",\"https://www.google-analytics.com\",\"https://tagmanager.google.com\",\"https://www.youtube.com\",\"https://s.ytimg.com\"],\"style-src\":[\"'self'\",\"https://*.mozilla.net\",\"https://*.mozilla.org\",\"https://*.mozilla.com\",\"'unsafe-inline'\"]}]}", + }, + }, + ]; + + for (let test of serializedPrincipalsFromFirefox) { + let principal = E10SUtils.deserializePrincipal(test.input); + + for (let key in principal.originAttributes) { + is( + principal.originAttributes[key], + test.output.originAttributes[key], + `Ensure value of ${key} is ${test.output.originAttributes[key]}` + ); + } + + if ("URI" in test.output && test.output.URI === false) { + is( + principal.isContentPrincipal, + false, + "Should have not have a URI for system" + ); + } else { + is( + principal.spec, + test.output.URISpec, + `Should have spec ${test.output.URISpec}` + ); + } + } +}); diff --git a/src/zen/tests/mochitests/caps/browser_principalSerialization_json.js b/src/zen/tests/mochitests/caps/browser_principalSerialization_json.js new file mode 100644 index 000000000..f79f269fd --- /dev/null +++ b/src/zen/tests/mochitests/caps/browser_principalSerialization_json.js @@ -0,0 +1,161 @@ +"use strict"; + +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + This test file exists to ensure whenever changes to principal serialization happens, + we guarantee that the data can be restored and generated into a new principal. + + The tests are written to be brittle so we encode all versions of the changes into the tests. +*/ + +add_task(async function test_nullPrincipal() { + const nullId = "0"; + // fields + const uri = 0; + const suffix = 1; + + const nullReplaceRegex = + /moz-nullprincipal:{[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}}/; + const NULL_REPLACE = "NULL_PRINCIPAL_URL"; + + /* + This test should NOT be resilient to changes in versioning, + however it exists purely to verify the code doesn't unintentionally change without updating versioning and migration code. + */ + let tests = [ + { + input: { OA: {} }, + expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`, + }, + { + input: { OA: {} }, + expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`, + }, + { + input: { OA: { userContextId: 0 } }, + expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`, + }, + { + input: { OA: { userContextId: 2 } }, + expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}","${suffix}":"^userContextId=2"}}`, + }, + { + input: { OA: { privateBrowsingId: 1 } }, + expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}","${suffix}":"^privateBrowsingId=1"}}`, + }, + { + input: { OA: { privateBrowsingId: 0 } }, + expected: `{"${nullId}":{"${uri}":"${NULL_REPLACE}"}}`, + }, + ]; + + for (let test of tests) { + let p = Services.scriptSecurityManager.createNullPrincipal(test.input.OA); + let sp = E10SUtils.serializePrincipal(p); + // Not sure why cppjson is adding a \n here + let spr = sp.replace(nullReplaceRegex, NULL_REPLACE); + is( + test.expected, + spr, + "Expected serialized object for " + JSON.stringify(test.input) + ); + let dp = E10SUtils.deserializePrincipal(sp); + + // Check all the origin attributes + for (let key in test.input.OA) { + is( + dp.originAttributes[key], + test.input.OA[key], + "Ensure value of " + key + " is " + test.input.OA[key] + ); + } + } +}); + +add_task(async function test_contentPrincipal() { + const contentId = "1"; + // fields + const content = 0; + // const domain = 1; + const suffix = 2; + // const csp = 3; + + /* + This test should NOT be resilient to changes in versioning, + however it exists purely to verify the code doesn't unintentionally change without updating versioning and migration code. + */ + let tests = [ + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + input: { uri: "http://example.com/", OA: {} }, + expected: `{"${contentId}":{"${content}":"http://example.com/"}}`, + }, + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + input: { uri: "http://mozilla1.com/", OA: {} }, + expected: `{"${contentId}":{"${content}":"http://mozilla1.com/"}}`, + }, + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + input: { uri: "http://mozilla2.com/", OA: { userContextId: 0 } }, + expected: `{"${contentId}":{"${content}":"http://mozilla2.com/"}}`, + }, + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + input: { uri: "http://mozilla3.com/", OA: { userContextId: 2 } }, + expected: `{"${contentId}":{"${content}":"http://mozilla3.com/","${suffix}":"^userContextId=2"}}`, + }, + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + input: { uri: "http://mozilla4.com/", OA: { privateBrowsingId: 1 } }, + expected: `{"${contentId}":{"${content}":"http://mozilla4.com/","${suffix}":"^privateBrowsingId=1"}}`, + }, + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + input: { uri: "http://mozilla5.com/", OA: { privateBrowsingId: 0 } }, + expected: `{"${contentId}":{"${content}":"http://mozilla5.com/"}}`, + }, + ]; + + for (let test of tests) { + let uri = Services.io.newURI(test.input.uri); + let p = Services.scriptSecurityManager.createContentPrincipal( + uri, + test.input.OA + ); + let sp = E10SUtils.serializePrincipal(p); + is(test.expected, sp, "Expected serialized object for " + test.input.uri); + let dp = E10SUtils.deserializePrincipal(sp); + is(dp.URI.spec, test.input.uri, "Ensure spec is the same"); + + // Check all the origin attributes + for (let key in test.input.OA) { + is( + dp.originAttributes[key], + test.input.OA[key], + "Ensure value of " + key + " is " + test.input.OA[key] + ); + } + } +}); + +add_task(async function test_systemPrincipal() { + const systemId = "3"; + /* + This test should NOT be resilient to changes in versioning, + however it exists purely to verify the code doesn't unintentionally change without updating versioning and migration code. + */ + const expected = `{"${systemId}":{}}`; + + let p = Services.scriptSecurityManager.getSystemPrincipal(); + let sp = E10SUtils.serializePrincipal(p); + is(expected, sp, "Expected serialized object for system principal"); + let dp = E10SUtils.deserializePrincipal(sp); + is( + dp, + Services.scriptSecurityManager.getSystemPrincipal(), + "Deserialized the system principal" + ); +}); diff --git a/src/zen/tests/mochitests/caps/browser_principalSerialization_version1.js b/src/zen/tests/mochitests/caps/browser_principalSerialization_version1.js new file mode 100644 index 000000000..6c4a41e91 --- /dev/null +++ b/src/zen/tests/mochitests/caps/browser_principalSerialization_version1.js @@ -0,0 +1,159 @@ +"use strict"; + +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + This test file exists to ensure whenever changes to principal serialization happens, + we guarantee that the data can be restored and generated into a new principal. + + The tests are written to be brittle so we encode all versions of the changes into the tests. +*/ + +add_task(function test_nullPrincipal() { + /* + As Null principals are designed to be non deterministic we just need to ensure that + a previous serialized version matches what it was generated as. + + This test should be resilient to changes in versioning, however it should also be duplicated for a new serialization change. + */ + // Principal created with: E10SUtils.serializePrincipal(Services.scriptSecurityManager.createNullPrincipal({ })); + let p = E10SUtils.deserializePrincipal( + "vQZuXxRvRHKDMXv9BbHtkAAAAAAAAAAAwAAAAAAAAEYAAAA4bW96LW51bGxwcmluY2lwYWw6ezU2Y2FjNTQwLTg2NGQtNDdlNy04ZTI1LTE2MTRlYWI1MTU1ZX0AAAAA" + ); + is( + "moz-nullprincipal:{56cac540-864d-47e7-8e25-1614eab5155e}", + p.URI.spec, + "Deserialized principal doesn't have the correct URI" + ); + + // Principal created with: E10SUtils.serializePrincipal(Services.scriptSecurityManager.createNullPrincipal({ userContextId: 2 })); + let p2 = E10SUtils.deserializePrincipal( + "vQZuXxRvRHKDMXv9BbHtkAAAAAAAAAAAwAAAAAAAAEYAAAA4bW96LW51bGxwcmluY2lwYWw6ezA1ZjllN2JhLWIwODMtNDJhMi1iNDdkLTZiODRmNmYwYTM3OX0AAAAQXnVzZXJDb250ZXh0SWQ9Mg==" + ); + is( + "moz-nullprincipal:{05f9e7ba-b083-42a2-b47d-6b84f6f0a379}", + p2.URI.spec, + "Deserialized principal doesn't have the correct URI" + ); + is(p2.originAttributes.userContextId, 2, "Expected a userContextId of 2"); +}); + +add_task(async function test_realHistoryCheck() { + /* + This test should be resilient to changes in principal serialization, if these are failing then it's likely the code will break session storage. + To recreate this for another version, copy the function into the browser console, browse some pages and printHistory. + + Generated with: + function printHistory() { + let tests = []; + let entries = SessionStore.getSessionHistory(gBrowser.selectedTab).entries.map((entry) => { return entry.triggeringPrincipal_base64 }); + entries.push(E10SUtils.serializePrincipal(gBrowser.selectedTab.linkedBrowser._contentPrincipal)); + for (let entry of entries) { + console.log(entry); + let testData = {}; + testData.input = entry; + let principal = E10SUtils.deserializePrincipal(testData.input); + testData.output = {}; + if (principal.URI === null) { + testData.output.URI = false; + } else { + testData.output.URISpec = principal.URI.spec; + } + testData.output.originAttributes = principal.originAttributes; + + tests.push(testData); + } + return tests; + } + printHistory(); // Copy this into: serializedPrincipalsFromFirefox + */ + + let serializedPrincipalsFromFirefox = [ + { + input: "SmIS26zLEdO3ZQBgsLbOywAAAAAAAAAAwAAAAAAAAEY=", + output: { + URI: false, + originAttributes: { + firstPartyDomain: "", + inIsolatedMozBrowser: false, + privateBrowsingId: 0, + userContextId: 0, + geckoViewSessionContextId: "", + partitionKey: "", + }, + }, + }, + { + input: + "ZT4OTT7kRfqycpfCC8AeuAAAAAAAAAAAwAAAAAAAAEYB3pRy0IA0EdOTmQAQS6D9QJIHOlRteE8wkTq4cYEyCMYAAAAC/////wAAAbsBAAAAe2h0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTLz91dG1fc291cmNlPXd3dy5tb3ppbGxhLm9yZyZ1dG1fbWVkaXVtPXJlZmVycmFsJnV0bV9jYW1wYWlnbj1uYXYmdXRtX2NvbnRlbnQ9ZGV2ZWxvcGVycwAAAAAAAAAFAAAACAAAABUAAAAA/////wAAAAD/////AAAACAAAABUAAAAdAAAAXgAAAB0AAAAHAAAAHQAAAAcAAAAkAAAAAAAAAAD/////AAAAAP////8AAAAlAAAAVgAAAAD/////AQAAAAAAAAAAAAAAAA==", + output: { + URISpec: + "https://developer.mozilla.org/en-US/?utm_source=www.mozilla.org&utm_medium=referral&utm_campaign=nav&utm_content=developers", + originAttributes: { + firstPartyDomain: "", + inIsolatedMozBrowser: false, + privateBrowsingId: 0, + userContextId: 0, + geckoViewSessionContextId: "", + partitionKey: "", + }, + }, + }, + { + input: "SmIS26zLEdO3ZQBgsLbOywAAAAAAAAAAwAAAAAAAAEY=", + output: { + URI: false, + originAttributes: { + firstPartyDomain: "", + inIsolatedMozBrowser: false, + privateBrowsingId: 0, + userContextId: 0, + geckoViewSessionContextId: "", + partitionKey: "", + }, + }, + }, + { + input: + "vQZuXxRvRHKDMXv9BbHtkAAAAAAAAAAAwAAAAAAAAEYAAAA4bW96LW51bGxwcmluY2lwYWw6ezA0NWNhMThkLTQzNmMtNDc0NC1iYmI2LWIxYTE1MzY2ZGY3OX0AAAAA", + output: { + URISpec: "moz-nullprincipal:{045ca18d-436c-4744-bbb6-b1a15366df79}", + originAttributes: { + firstPartyDomain: "", + inIsolatedMozBrowser: false, + privateBrowsingId: 0, + userContextId: 0, + geckoViewSessionContextId: "", + partitionKey: "", + }, + }, + }, + ]; + + for (let test of serializedPrincipalsFromFirefox) { + let principal = E10SUtils.deserializePrincipal(test.input); + + for (let key in principal.originAttributes) { + is( + principal.originAttributes[key], + test.output.originAttributes[key], + `Ensure value of ${key} is ${test.output.originAttributes[key]}` + ); + } + + if ("URI" in test.output && test.output.URI === false) { + is( + principal.isContentPrincipal, + false, + "Should have not have a URI for system" + ); + } else { + is( + principal.spec, + test.output.URISpec, + `Should have spec ${test.output.URISpec}` + ); + } + } +}); diff --git a/src/zen/tests/mochitests/gesture/browser.toml b/src/zen/tests/mochitests/gesture/browser.toml new file mode 100644 index 000000000..4e9cc2e0d --- /dev/null +++ b/src/zen/tests/mochitests/gesture/browser.toml @@ -0,0 +1,11 @@ +[DEFAULT] +prefs = [ + "zen.window-sync.enabled=false", +] + +["browser_gesture_navigation.js"] + +["browser_gesture_scroll.js"] +skip-if = [ + "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 1988390 +] diff --git a/src/zen/tests/mochitests/gesture/browser_gesture_navigation.js b/src/zen/tests/mochitests/gesture/browser_gesture_navigation.js new file mode 100644 index 000000000..f92b070bb --- /dev/null +++ b/src/zen/tests/mochitests/gesture/browser_gesture_navigation.js @@ -0,0 +1,231 @@ +"use strict"; + +add_setup(async () => { + // Disable window occlusion. See bug 1733955 / bug 1779559. + if (navigator.platform.indexOf("Win") == 0) { + await SpecialPowers.pushPrefEnv({ + set: [["widget.windows.window_occlusion_tracking.enabled", false]], + }); + } +}); + +add_task(async () => { + // Open a new browser window to make sure there is no navigation history. + const newBrowser = await BrowserTestUtils.openNewBrowserWindow({}); + + let event = { + direction: SimpleGestureEvent.DIRECTION_LEFT, + }; + ok(!newBrowser.gGestureSupport._shouldDoSwipeGesture(event)); + + event = { + direction: SimpleGestureEvent.DIRECTION_RIGHT, + }; + ok(!newBrowser.gGestureSupport._shouldDoSwipeGesture(event)); + + await BrowserTestUtils.closeWindow(newBrowser); +}); + +function createSimpleGestureEvent(type, direction) { + let event = document.createEvent("SimpleGestureEvent"); + event.initSimpleGestureEvent( + type, + false /* canBubble */, + false /* cancelableArg */, + window, + 0 /* detail */, + 0 /* screenX */, + 0 /* screenY */, + 0 /* clientX */, + 0 /* clientY */, + false /* ctrlKey */, + false /* altKey */, + false /* shiftKey */, + false /* metaKey */, + 0 /* button */, + null /* relatedTarget */, + 0 /* allowedDirections */, + direction, + 1 /* delta */ + ); + return event; +} + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["ui.swipeAnimationEnabled", false]], + }); + + // Open a new browser window and load two pages so that the browser can go + // back but can't go forward. + const newWindow = await BrowserTestUtils.openNewBrowserWindow({}); + + // gHistroySwipeAnimation gets initialized in a requestIdleCallback so we need + // to wait for the initialization. + await TestUtils.waitForCondition(() => { + return ( + // There's no explicit notification for the initialization, so we wait + // until `isLTR` matches the browser locale state. + newWindow.gHistorySwipeAnimation.isLTR != Services.locale.isAppLocaleRTL + ); + }); + + BrowserTestUtils.startLoadingURIString( + newWindow.gBrowser.selectedBrowser, + "about:mozilla" + ); + await BrowserTestUtils.browserLoaded( + newWindow.gBrowser.selectedBrowser, + false, + "about:mozilla" + ); + BrowserTestUtils.startLoadingURIString( + newWindow.gBrowser.selectedBrowser, + "about:about" + ); + await BrowserTestUtils.browserLoaded( + newWindow.gBrowser.selectedBrowser, + false, + "about:about" + ); + + let event = createSimpleGestureEvent( + "MozSwipeGestureMayStart", + SimpleGestureEvent.DIRECTION_LEFT + ); + newWindow.gGestureSupport._shouldDoSwipeGesture(event); + + // Assuming we are on LTR environment. + is( + event.allowedDirections, + SimpleGestureEvent.DIRECTION_LEFT, + "Allows only swiping to left, i.e. backward" + ); + + event = createSimpleGestureEvent( + "MozSwipeGestureMayStart", + SimpleGestureEvent.DIRECTION_RIGHT + ); + newWindow.gGestureSupport._shouldDoSwipeGesture(event); + is( + event.allowedDirections, + SimpleGestureEvent.DIRECTION_LEFT, + "Allows only swiping to left, i.e. backward" + ); + + await BrowserTestUtils.closeWindow(newWindow); +}); + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["ui.swipeAnimationEnabled", true]], + }); + + // Open a new browser window and load two pages so that the browser can go + // back but can't go forward. + const newWindow = await BrowserTestUtils.openNewBrowserWindow({}); + + if (!newWindow.gHistorySwipeAnimation._isSupported()) { + await BrowserTestUtils.closeWindow(newWindow); + return; + } + + function sendSwipeSequence(sendEnd) { + let event = createSimpleGestureEvent( + "MozSwipeGestureMayStart", + SimpleGestureEvent.DIRECTION_LEFT + ); + newWindow.gGestureSupport.handleEvent(event); + + event = createSimpleGestureEvent( + "MozSwipeGestureStart", + SimpleGestureEvent.DIRECTION_LEFT + ); + newWindow.gGestureSupport.handleEvent(event); + + event = createSimpleGestureEvent( + "MozSwipeGestureUpdate", + SimpleGestureEvent.DIRECTION_LEFT + ); + newWindow.gGestureSupport.handleEvent(event); + + event = createSimpleGestureEvent( + "MozSwipeGestureUpdate", + SimpleGestureEvent.DIRECTION_LEFT + ); + newWindow.gGestureSupport.handleEvent(event); + + if (sendEnd) { + sendSwipeEnd(); + } + } + function sendSwipeEnd() { + let event = createSimpleGestureEvent( + "MozSwipeGestureEnd", + SimpleGestureEvent.DIRECTION_LEFT + ); + newWindow.gGestureSupport.handleEvent(event); + } + + // gHistroySwipeAnimation gets initialized in a requestIdleCallback so we need + // to wait for the initialization. + await TestUtils.waitForCondition(() => { + return ( + // There's no explicit notification for the initialization, so we wait + // until `isLTR` matches the browser locale state. + newWindow.gHistorySwipeAnimation.isLTR != Services.locale.isAppLocaleRTL + ); + }); + + BrowserTestUtils.startLoadingURIString( + newWindow.gBrowser.selectedBrowser, + "about:mozilla" + ); + await BrowserTestUtils.browserLoaded( + newWindow.gBrowser.selectedBrowser, + false, + "about:mozilla" + ); + BrowserTestUtils.startLoadingURIString( + newWindow.gBrowser.selectedBrowser, + "about:about" + ); + await BrowserTestUtils.browserLoaded( + newWindow.gBrowser.selectedBrowser, + false, + "about:about" + ); + + // Start a swipe that's not enough to navigate + sendSwipeSequence(/* sendEnd = */ true); + + // Wait two frames + await new Promise(r => + window.requestAnimationFrame(() => window.requestAnimationFrame(r)) + ); + + // The transition to fully stopped shouldn't have had enough time yet to + // become fully stopped. + ok( + newWindow.gHistorySwipeAnimation._isStoppingAnimation, + "should be stopping anim" + ); + + // Start another swipe. + sendSwipeSequence(/* sendEnd = */ false); + + // Wait two frames + await new Promise(r => + window.requestAnimationFrame(() => window.requestAnimationFrame(r)) + ); + + // We should have started a new swipe, ie we shouldn't be stopping. + ok( + !newWindow.gHistorySwipeAnimation._isStoppingAnimation, + "should not be stopping anim" + ); + + sendSwipeEnd(); + + await BrowserTestUtils.closeWindow(newWindow); +}); diff --git a/src/zen/tests/mochitests/gesture/browser_gesture_scroll.js b/src/zen/tests/mochitests/gesture/browser_gesture_scroll.js new file mode 100644 index 000000000..ffe998a7b --- /dev/null +++ b/src/zen/tests/mochitests/gesture/browser_gesture_scroll.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = `data:text/html, + + +
+ Some content +
+`; + +function getScrollPos(browser) { + return SpecialPowers.spawn(browser, [], () => { + return { + scrollY: Math.round(content.scrollY), + scrollMaxY: Math.round(content.scrollMaxY), + }; + }); +} + +// Bug 1964901 +add_task(async function test_scroll_command() { + await BrowserTestUtils.withNewTab(URL, async function (browser) { + { + let { scrollY, scrollMaxY } = await getScrollPos(browser); + is(scrollY, 0, "Should be scrolled to the top"); + isnot(scrollMaxY, 0, "Should be scrollable"); + } + let scrollEvent = SpecialPowers.spawn(browser, [], async () => { + await new Promise(r => { + content.addEventListener("scrollend", r, { once: true }); + }); + }); + goDoCommand("cmd_scrollBottom"); + await scrollEvent; + { + let { scrollY, scrollMaxY } = await getScrollPos(browser); + is(scrollY, scrollMaxY, "Should be scrolled to the bottom"); + } + }); +}); diff --git a/src/zen/tests/mochitests/linkHandling/browser.toml b/src/zen/tests/mochitests/linkHandling/browser.toml new file mode 100644 index 000000000..72dd92d42 --- /dev/null +++ b/src/zen/tests/mochitests/linkHandling/browser.toml @@ -0,0 +1,12 @@ +[DEFAULT] + +["browser_contentAreaClick_subframe_javascript.js"] +support-files = [ + "file_contentAreaClick_subframe_javascript.html" +] + +["browser_javascript_links.js"] +disabled="Disabled by import_external_tests.py" +support-files = [ + "file_javascript_links_subframe.html" +] diff --git a/src/zen/tests/mochitests/linkHandling/browser_contentAreaClick_subframe_javascript.js b/src/zen/tests/mochitests/linkHandling/browser_contentAreaClick_subframe_javascript.js new file mode 100644 index 000000000..b8236e073 --- /dev/null +++ b/src/zen/tests/mochitests/linkHandling/browser_contentAreaClick_subframe_javascript.js @@ -0,0 +1,49 @@ +const gExampleComRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" +); +const IFRAME_FILE = "file_contentAreaClick_subframe_javascript.html"; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.alternative_click.block_javascript", false]], + }); + + await BrowserTestUtils.withNewTab( + `data:text/html,`, + async browser => { + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:blank" + ); + let javascriptRanPromise = TestUtils.topicObserved( + "contentAreaClick-javascriptRan" + ); + + // ctrl/cmd-click the link in the subframe. This should cause it to be + // loaded in a new tab. + info("Clicking link"); + let expectedRemoteType = + browser.browsingContext.children[0].currentRemoteType; + await BrowserTestUtils.synthesizeMouseAtCenter( + "a", + { ctrlKey: true, metaKey: true }, + browser.browsingContext.children[0] + ); + + info("Waiting for new tab"); + let newTab = await newTabPromise; + + info("Waiting to be notified that the javascript ran"); + await javascriptRanPromise; + is( + newTab.linkedBrowser.remoteType, + expectedRemoteType, + "new tab was loaded in expected process" + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(newTab); + } + ); +}); diff --git a/src/zen/tests/mochitests/linkHandling/browser_javascript_links.js b/src/zen/tests/mochitests/linkHandling/browser_javascript_links.js new file mode 100644 index 000000000..891e17c17 --- /dev/null +++ b/src/zen/tests/mochitests/linkHandling/browser_javascript_links.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" +); +const IFRAME_PATH = TEST_PATH + "file_javascript_links_subframe.html"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.link.alternative_click.block_javascript", true], + ["browser.tabs.opentabfor.middleclick", true], + ["middlemouse.paste", false], + ["middlemouse.contentLoadURL", false], + ["general.autoScroll", false], + ], + }); +}); + +add_task(async function () { + await BrowserTestUtils.withNewTab( + `data:text/html,click me`, + async browser => { + await BrowserTestUtils.synthesizeMouseAtCenter( + "a", + { button: 0, ctrlKey: true, metaKey: true }, + browser + ); + is( + gBrowser.tabs.length, + 2, + "Accel+click on javascript: link shouldn't open a new tab" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "a", + { button: 1 }, + browser + ); + is( + gBrowser.tabs.length, + 2, + "Middle click on javascript: link shouldn't open a new tab" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "a", + { button: 0, shiftKey: true }, + browser + ); + // This is fragile and might miss the new window, but the test will fail + // anyway when finishing with an extra window left behind. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 200)); + is( + BrowserWindowTracker.windowCount, + 1, + "Shift+click on javascript: link shouldn't open a new window" + ); + } + ); +}); + +add_task(async function iframe_link() { + await BrowserTestUtils.withNewTab( + `data:text/html,`, + async browser => { + // ctrl/cmd-click the link in the subframe. + await BrowserTestUtils.synthesizeMouseAtCenter( + "a", + { ctrlKey: true, metaKey: true }, + browser.browsingContext.children[0] + ); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 200)); + is( + gBrowser.tabs.length, + 2, + "Click on javascript: link in iframe shouldn't open a new tab" + ); + } + ); +}); diff --git a/src/zen/tests/mochitests/linkHandling/file_contentAreaClick_subframe_javascript.html b/src/zen/tests/mochitests/linkHandling/file_contentAreaClick_subframe_javascript.html new file mode 100644 index 000000000..aa4ca9c75 --- /dev/null +++ b/src/zen/tests/mochitests/linkHandling/file_contentAreaClick_subframe_javascript.html @@ -0,0 +1,2 @@ + +link diff --git a/src/zen/tests/mochitests/linkHandling/file_javascript_links_subframe.html b/src/zen/tests/mochitests/linkHandling/file_javascript_links_subframe.html new file mode 100644 index 000000000..e52e169d2 --- /dev/null +++ b/src/zen/tests/mochitests/linkHandling/file_javascript_links_subframe.html @@ -0,0 +1,2 @@ + +click me diff --git a/src/zen/tests/mochitests/moz.build b/src/zen/tests/mochitests/moz.build index d798a5e2e..377de8620 100644 --- a/src/zen/tests/mochitests/moz.build +++ b/src/zen/tests/mochitests/moz.build @@ -8,11 +8,18 @@ BROWSER_CHROME_MANIFESTS += [ + "alerts/browser.toml", + "backforward/browser.toml", + "caps/browser.toml", + "gesture/browser.toml", + "linkHandling/browser.toml", "readermode/browser.toml", "safebrowsing/browser.toml", + "sessionstore/browser.toml", "shell/browser.toml", "tabMediaIndicator/browser.toml", "tooltiptext/browser.toml", + "zoom/browser.toml", ] XPCSHELL_TESTS_MANIFESTS += [ "remote-settings/xpcshell.toml", diff --git a/src/zen/tests/mochitests/sessionstore/SessionStoreTestUtils.sys.mjs b/src/zen/tests/mochitests/sessionstore/SessionStoreTestUtils.sys.mjs new file mode 100644 index 000000000..889245172 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/SessionStoreTestUtils.sys.mjs @@ -0,0 +1,197 @@ +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +export var SessionStoreTestUtils = { + /** + * Running this init allows helpers to access test scope helpers, like Assert + * and SimpleTest. + * Tests should call this init() before using the helpers which rely on properties assign here. + * + * @param {object} scope The global scope where tests are being run. + * @param {DOMWindow} windowGlobal The global window object, for acessing gBrowser etc. + */ + init(scope, windowGlobal) { + if (!scope) { + throw new Error( + "Must initialize SessionStoreTestUtils with a test scope" + ); + } + if (!windowGlobal) { + throw new Error("this.windowGlobal must be defined when we init"); + } + this._scopeRef = new WeakRef(scope); + this._windowGlobalRef = new WeakRef(windowGlobal); + }, + + get info() { + return this._scopeRef.deref()?.info; + }, + + get registerCleanupFunction() { + return this._scopeRef.deref()?.registerCleanupFunction; + }, + + get windowGlobal() { + return this._windowGlobalRef.deref(); + }, + + async closeTab(tab) { + await lazy.TabStateFlusher.flush(tab.linkedBrowser); + let sessionUpdatePromise = + lazy.BrowserTestUtils.waitForSessionStoreUpdate(tab); + lazy.BrowserTestUtils.removeTab(tab); + await sessionUpdatePromise; + }, + + async openAndCloseTab(window, url) { + let { updatePromise } = await lazy.BrowserTestUtils.withNewTab( + { url, gBrowser: window.gBrowser }, + async browser => { + return { + updatePromise: lazy.BrowserTestUtils.waitForSessionStoreUpdate({ + linkedBrowser: browser, + }), + }; + } + ); + await updatePromise; + return lazy.TestUtils.topicObserved("sessionstore-closed-objects-changed"); + }, + + // This assumes that tests will at least have some state/entries + waitForBrowserState(aState, aSetStateCallback) { + if (typeof aState == "string") { + aState = JSON.parse(aState); + } + if (typeof aState != "object") { + throw new TypeError( + "Argument must be an object or a JSON representation of an object" + ); + } + if (!this.windowGlobal) { + throw new Error( + "no windowGlobal defined, please call init() first with the scope and window object" + ); + } + let windows = [this.windowGlobal]; + let tabsRestored = 0; + let expectedTabsRestored = 0; + let expectedWindows = aState.windows.length; + let windowsOpen = 1; + let listening = false; + let windowObserving = false; + let restoreHiddenTabs = Services.prefs.getBoolPref( + "browser.sessionstore.restore_hidden_tabs" + ); + // This should match the |restoreTabsLazily| value that + // SessionStore.restoreWindow() uses. + let restoreTabsLazily = + Services.prefs.getBoolPref("browser.sessionstore.restore_on_demand") && + Services.prefs.getBoolPref("browser.sessionstore.restore_tabs_lazily"); + + aState.windows.forEach(function (winState) { + winState.tabs.forEach(function (tabState) { + if (!restoreTabsLazily && (restoreHiddenTabs || !tabState.hidden)) { + expectedTabsRestored++; + } + }); + }); + + // If there are only hidden tabs and restoreHiddenTabs = false, we still + // expect one of them to be restored because it gets shown automatically. + // Otherwise if lazy tab restore there will only be one tab restored per window. + if (!expectedTabsRestored) { + expectedTabsRestored = 1; + } else if (restoreTabsLazily) { + expectedTabsRestored = aState.windows.length; + } + + function onSSTabRestored() { + if (++tabsRestored == expectedTabsRestored) { + // Remove the event listener from each window + windows.forEach(function (win) { + win.gBrowser.tabContainer.removeEventListener( + "SSTabRestored", + onSSTabRestored, + true + ); + }); + listening = false; + SessionStoreTestUtils.info("running " + aSetStateCallback.name); + lazy.TestUtils.executeSoon(aSetStateCallback); + } + } + + // Used to add our listener to further windows so we can catch SSTabRestored + // coming from them when creating a multi-window state. + function windowObserver(aSubject, aTopic) { + if (aTopic == "domwindowopened") { + let newWindow = aSubject; + newWindow.addEventListener( + "load", + function () { + if (++windowsOpen == expectedWindows) { + Services.ww.unregisterNotification(windowObserver); + windowObserving = false; + } + + // Track this window so we can remove the progress listener later + windows.push(newWindow); + // Add the progress listener + newWindow.gBrowser.tabContainer.addEventListener( + "SSTabRestored", + onSSTabRestored, + true + ); + }, + { once: true } + ); + } + } + + // We only want to register the notification if we expect more than 1 window + if (expectedWindows > 1) { + this.registerCleanupFunction(function () { + if (windowObserving) { + Services.ww.unregisterNotification(windowObserver); + } + }); + windowObserving = true; + Services.ww.registerNotification(windowObserver); + } + + this.registerCleanupFunction(function () { + if (listening) { + windows.forEach(function (win) { + win.gBrowser.tabContainer.removeEventListener( + "SSTabRestored", + onSSTabRestored, + true + ); + }); + } + }); + // Add the event listener for this window as well. + listening = true; + this.windowGlobal.gBrowser.tabContainer.addEventListener( + "SSTabRestored", + onSSTabRestored, + true + ); + + // Ensure setBrowserState() doesn't remove the initial tab. + this.windowGlobal.gBrowser.selectedTab = this.windowGlobal.gBrowser.tabs[0]; + + // Finally, call setBrowserState + lazy.SessionStore.setBrowserState(JSON.stringify(aState)); + }, + + promiseBrowserState(aState) { + return new Promise(resolve => this.waitForBrowserState(aState, resolve)); + }, +}; diff --git a/src/zen/tests/mochitests/sessionstore/browser.toml b/src/zen/tests/mochitests/sessionstore/browser.toml new file mode 100644 index 000000000..f6ab3d6e8 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser.toml @@ -0,0 +1,483 @@ +[DEFAULT] +support-files = [ + "head.js", + "browser_formdata_sample.html", + "browser_formdata_xpath_sample.html", + "browser_frametree_sample.html", + "browser_frametree_sample_frameset.html", + "browser_frametree_sample_iframes.html", + "browser_frame_history_index.html", + "browser_frame_history_index2.html", + "browser_frame_history_index_blank.html", + "browser_frame_history_a.html", + "browser_frame_history_b.html", + "browser_frame_history_c.html", + "browser_frame_history_c1.html", + "browser_frame_history_c2.html", + "browser_formdata_format_sample.html", + "browser_sessionHistory_slow.sjs", + "browser_policy_container_sample.html", + "browser_policy_container_sample_frame.html", + "browser_scrollPositions_sample.html", + "browser_scrollPositions_sample2.html", + "browser_scrollPositions_sample_frameset.html", + "browser_scrollPositions_readerModeArticle.html", + "browser_sessionStorage.html", + "browser_speculative_connect.html", + "coopHeaderCommon.sjs", + "restore_redirect_http.html", + "restore_redirect_http.html^headers^", + "restore_redirect_js.html", + "restore_redirect_target.html", + "empty.html", + "coop_coep.html", + "coop_coep.html^headers^", +] +# remove this after bug 1628486 is landed +prefs = [ + "zen.window-sync.enabled=false", + "network.cookie.cookieBehavior=5", + "gfx.font_rendering.fallback.async=false", + "browser.sessionstore.closedTabsFromAllWindows=true", + "browser.sessionstore.closedTabsFromClosedWindows=true", + # Override the test default, allowing the session restore infobar to be shown if necessary + "browser.startup.couldRestoreSession.count=0", +] + +["browser_1933485_tab_groups_history.js"] +disabled="Disabled by import_external_tests.py" + +["browser_1953801_tab_groups_history_close_other.js"] +disabled="Disabled by import_external_tests.py" + +["browser_aboutPrivateBrowsing.js"] + +["browser_aboutRestartrequired_noRestore.js"] + +["browser_aboutSessionRestore.js"] +disabled="Disabled by import_external_tests.py" + +["browser_async_duplicate_tab.js"] +support-files = ["file_async_duplicate_tab.html"] + +["browser_async_flushes.js"] +support-files = ["file_async_flushes.html"] +run-if = [ + "crashreporter", +] + +["browser_async_remove_tab.js"] +skip-if = [ + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && opt", # Bug 1787024 + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && asan", # Bug 1787024 + "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && asan", # Bug 1787024 +] + +["browser_async_window_flushing.js"] +https_first_disabled = true +skip-if = [ + "os == 'mac' && os_version == '10.15' && arch == 'x86_64' && opt", # Bug 1775616 + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && asan", # Bug 1775616 + "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && asan", # Bug 1775616 + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && opt", # Bug 1775616 + "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && opt", # Bug 1775616 +] + +["browser_attributes.js"] + +["browser_background_tab_crash.js"] +https_first_disabled = true +run-if = [ + "crashreporter", +] +tags = "os_integration" + +["browser_backup_recovery.js"] +https_first_disabled = true +tags = "os_integration" + +["browser_bfcache_telemetry.js"] +disabled="Disabled by import_external_tests.py" + +["browser_broadcast.js"] +https_first_disabled = true + +["browser_capabilities.js"] + +["browser_cleaner.js"] + +["browser_closedId.js"] +disabled="Disabled by import_external_tests.py" + +["browser_closed_objects_changed_notifications_tabs.js"] +disabled="Disabled by import_external_tests.py" + +["browser_closed_objects_changed_notifications_windows.js"] +disabled="Disabled by import_external_tests.py" + +["browser_closed_tabs_closed_windows.js"] +disabled="Disabled by import_external_tests.py" + +["browser_closed_tabs_windows.js"] +disabled="Disabled by import_external_tests.py" + +["browser_cookies.js"] + +["browser_cookies_legacy.js"] + +["browser_cookies_partitioned.js"] +disabled="Disabled by import_external_tests.py" + +["browser_cookies_privacy.js"] + +["browser_cookies_sameSite.js"] + +["browser_crashedTabs.js"] +disabled="Disabled by import_external_tests.py" +https_first_disabled = true +run-if = [ + "crashreporter", +] + +["browser_csp_policy_container_migration.js"] + +["browser_docshell_uuid_consistency.js"] + +["browser_duplicate_history.js"] + +["browser_duplicate_tab_in_new_window.js"] + +["browser_dying_cache.js"] +disabled="Disabled by import_external_tests.py" + +["browser_dynamic_frames.js"] + +["browser_firefoxView_restore.js"] +disabled="Disabled by import_external_tests.py" + +["browser_firefoxView_selected_restore.js"] +disabled="Disabled by import_external_tests.py" + +["browser_focus_after_restore.js"] +tags = "os_integration" + +["browser_forget_async_closings.js"] + +["browser_forget_closed_tab_window_byId.js"] +disabled="Disabled by import_external_tests.py" +https_first_disabled = true + +["browser_formdata.js"] + +["browser_formdata_cc.js"] +skip-if = [ + "asan", # test runs too long +] + +["browser_formdata_face.js"] + +["browser_formdata_format.js"] + +["browser_formdata_max_size.js"] + +["browser_formdata_password.js"] +support-files = ["file_formdata_password.html"] + +["browser_formdata_xpath.js"] + +["browser_frame_history.js"] + +["browser_frametree.js"] +https_first_disabled = true + +["browser_global_store.js"] + +["browser_history_persist.js"] + +["browser_ignore_updates_crashed_tabs.js"] +https_first_disabled = true +run-if = [ + "crashreporter", +] +skip-if = [ + "asan", +] + +["browser_label_and_icon.js"] +https_first_disabled = true +skip-if = [ + "os == 'mac' && os_version == '10.15' && arch == 'x86_64' && opt", # Bug 1638958 + "os == 'mac' && os_version == '14.70' && arch == 'x86_64' && opt && socketprocess_networking", # Bug 1868915 + "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && opt", # Bug 1775605 + "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && opt", # Bug 1775605 +] + +["browser_movePendingTabToNewWindow.js"] +disabled="Disabled by import_external_tests.py" +https_first_disabled = true +tags = "os_integration" + +["browser_multiple_navigateAndRestore.js"] + +["browser_multiple_select_after_load.js"] + +["browser_navigation_api_restore.js"] +support-files = [ + "empty_frame.html", + "entry1.html", + "entry2.html", + "entry3.html", +] + +["browser_newtab_userTypedValue.js"] +disabled="Disabled by import_external_tests.py" + +["browser_not_collect_when_idle.js"] + +["browser_page_title.js"] + +["browser_parentProcessRestoreHash.js"] +https_first_disabled = true +tags = "openUILinkIn" + +["browser_pending_tabs.js"] + +["browser_pinned_tabs.js"] +disabled="Disabled by import_external_tests.py" +skip-if = [ + "ccov", # Bug 1625525 +] + +["browser_policy_container_not_stored.js"] + +["browser_privatetabs.js"] + +["browser_purge_domaindata.js"] +disabled="Disabled by import_external_tests.py" + +["browser_purge_shistory.js"] + +["browser_remoteness_flip_on_restore.js"] + +["browser_reopen_all_windows.js"] +disabled="Disabled by import_external_tests.py" +https_first_disabled = true +skip-if = [ + "asan", # high memory +] +tags = "os_integration" + +["browser_replace_load.js"] +skip-if = [ + "true", # Bug 1646894 +] + +["browser_restoreLastActionCorrectOrder.js"] +disabled="Disabled by import_external_tests.py" + +["browser_restoreLastClosedTabOrWindowOrSession.js"] +disabled="Disabled by import_external_tests.py" + +["browser_restoreTabContainer.js"] + +["browser_restore_container_tabs_oa.js"] +disabled="Disabled by import_external_tests.py" + +["browser_restore_cookies_noOriginAttributes.js"] + +["browser_restore_pageProxyState.js"] + +["browser_restore_private_tab_os.js"] + +["browser_restore_redirect.js"] +https_first_disabled = true + +["browser_restore_reversed_z_order.js"] +skip-if = [ + "true", # Bug 1455602 +] + +["browser_restore_srcdoc.js"] + +["browser_restore_tabless_window.js"] +disabled="Disabled by import_external_tests.py" + +["browser_restore_verticalPinnedTabs.js"] +disabled="Disabled by import_external_tests.py" + +["browser_restored_window_features.js"] +disabled="Disabled by import_external_tests.py" +skip-if = [ + "os == 'win' && os_version == '11.26200' && asan", # Bug 2019745 +] + +["browser_revive_crashed_bg_tabs.js"] +https_first_disabled = true +run-if = [ + "crashreporter", +] + +["browser_scrollPositions.js"] +disabled="Disabled by import_external_tests.py" +https_first_disabled = true +run-if = [ + "fission", +] + +["browser_scrollPositionsReaderMode.js"] +disabled="Disabled by import_external_tests.py" + +["browser_searchModeSwitcher_restore.js"] +disabled="Disabled by import_external_tests.py" + +["browser_sessionHistory.js"] +https_first_disabled = true +support-files = ["file_sessionHistory_hashchange.html"] + +["browser_sessionHistory_partitionedPrincipalToInherit.js"] + +["browser_sessionStorage.js"] +tags = "os_integration" + +["browser_sessionStorage_size.js"] + +["browser_sessionStoreContainer.js"] + +["browser_should_restore_tab.js"] +skip-if = [ + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && debug && socketprocess_networking", # Bug 1848488 + "os == 'win' && os_version == '11.26100' && arch == 'x86' && debug && verify-standalone", + "os == 'win' && os_version == '11.26200' && arch == 'x86' && debug && verify-standalone", + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && debug", # Bug 1848488 + "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # Bug 1848488 +] + +["browser_sizemodeBeforeMinimized.js"] +skip-if = [ + "os == 'win' && os_version == '11.26200' && asan", # Bug 2019745 +] + +["browser_speculative_connect.js"] +disabled="Disabled by import_external_tests.py" + +["browser_splitview_integer_ids.js"] +disabled="Disabled by import_external_tests.py" + +["browser_splitview_restore_in_closed_window.js"] +disabled="Disabled by import_external_tests.py" + +["browser_splitview_string_migration.js"] +disabled="Disabled by import_external_tests.py" + +["browser_swapDocShells.js"] +disabled="Disabled by import_external_tests.py" + +["browser_switch_remoteness.js"] + +["browser_tab_groups_closed.js"] +disabled="Disabled by import_external_tests.py" +skip-if = [ + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && debug && socketprocess_networking", # Bug 1934803 + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && debug", # Bug 1934803 + "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # Bug 1934803 +] + +["browser_tab_groups_closed_groups_in_closed_windows.js"] + +["browser_tab_groups_empty.js"] + +["browser_tab_groups_restore_closed_in_closed_window.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_restore_closed_in_open_window.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_restore_closed_many_tabs.js"] +disabled="Disabled by import_external_tests.py" +skip-if = [ + "os == 'mac' && os_version == '15.30' && arch == 'aarch64' && opt", # Bug 1954300 +] + +["browser_tab_groups_restore_multiple.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_restore_saved.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_restore_simple.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_restore_to_group.js"] + +["browser_tab_groups_save_on_removeAllTabsBut.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_save_on_removeTabsToTheEnd.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_save_on_removeTabsToTheStart.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_save_on_window_close.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_saved.js"] +skip-if = [ + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && opt", # Bug 1945957 +] + +["browser_tab_groups_state.js"] +disabled="Disabled by import_external_tests.py" + +["browser_tab_groups_undo.js"] +disabled="Disabled by import_external_tests.py" + +skip-if = [ + "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 2023967 +] +["browser_tab_label_during_restore.js"] +disabled="Disabled by import_external_tests.py" +https_first_disabled = true + +["browser_tab_notes_canonicalurl.js"] + +["browser_tabicon_after_bg_tab_crash.js"] +run-if = [ + "crashreporter", +] + +["browser_tabs_in_urlbar.js"] +https_first_disabled = true + +["browser_undoCloseById.js"] +disabled="Disabled by import_external_tests.py" +skip-if = [ + "debug", +] + +["browser_undoCloseById_targetWindow.js"] + +["browser_unrestored_crashedTabs.js"] +run-if = [ + "crashreporter", +] + +["browser_upgrade_backup.js"] +skip-if = [ + "asan", + "tsan", +] + +["browser_urlbarSearchMode.js"] +disabled="Disabled by import_external_tests.py" + +["browser_userTyped_restored_after_discard.js"] + +["browser_windowRestore_perwindowpb.js"] + +["browser_windowStateContainer.js"] +disabled="Disabled by import_external_tests.py" + +["browser_wireframe_basic.js"] +disabled="Disabled by import_external_tests.py" diff --git a/src/zen/tests/mochitests/sessionstore/browser_1234021.js b/src/zen/tests/mochitests/sessionstore/browser_1234021.js new file mode 100644 index 000000000..f6a95ad68 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_1234021.js @@ -0,0 +1,22 @@ +"use strict"; + +const PREF = "network.cookie.cookieBehavior"; +const PAGE_URL = + "http://mochi.test:8888/browser/" + + "browser/components/sessionstore/test/browser_1234021_page.html"; +const BEHAVIOR_REJECT = 2; + +add_task(async function test() { + await pushPrefs([PREF, BEHAVIOR_REJECT]); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE_URL, + }, + async function handler(aBrowser) { + await TabStateFlusher.flush(aBrowser); + ok(true, "Flush didn't time out"); + } + ); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_1234021_page.html b/src/zen/tests/mochitests/sessionstore/browser_1234021_page.html new file mode 100644 index 000000000..0c3fca84d --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_1234021_page.html @@ -0,0 +1,6 @@ + + + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab.html b/src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab.html new file mode 100644 index 000000000..ec3edbffd --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab.html @@ -0,0 +1,12 @@ + + + + + +TEST PAGE + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab.js b/src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab.js new file mode 100644 index 000000000..3c6a2c1f2 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); + + let url = "about:robots"; + let tab0 = gBrowser.tabs[0]; + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + const staleAttributes = [ + "activemedia-blocked", + "busy", + "pendingicon", + "progress", + "soundplaying", + ]; + for (let attr of staleAttributes) { + tab0.toggleAttribute(attr, true); + } + gBrowser.discardBrowser(tab0); + ok(!tab0.linkedPanel, "tab0 is suspended"); + for (let attr of staleAttributes) { + ok( + !tab0.hasAttribute(attr), + `discarding browser removes "${attr}" tab attribute` + ); + } + + await BrowserTestUtils.switchTab(gBrowser, tab0); + ok(tab0.linkedPanel, "selecting tab unsuspends it"); + + // Test that active tab is not able to be suspended. + gBrowser.discardBrowser(tab0); + ok(tab0.linkedPanel, "active tab is not able to be suspended"); + + // Test that tab that is closing is not able to be suspended. + gBrowser._beginRemoveTab(tab1); + gBrowser.discardBrowser(tab1); + + ok(tab1.linkedPanel, "cannot suspend a tab that is closing"); + + gBrowser._endRemoveTab(tab1); + + // Open tab containing a page which has a beforeunload handler which shows a prompt. + url = + "http://example.com/browser/browser/components/sessionstore/test/browser_1284886_suspend_tab.html"; + tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await BrowserTestUtils.switchTab(gBrowser, tab0); + + // Test that tab with beforeunload handler which would show a prompt cannot be suspended. + gBrowser.discardBrowser(tab1); + ok( + tab1.linkedPanel, + "cannot suspend a tab with beforeunload handler which would show a prompt" + ); + + // Test that tab with beforeunload handler which would show a prompt will be suspended if forced. + gBrowser.discardBrowser(tab1, true); + ok( + !tab1.linkedPanel, + "force suspending a tab with beforeunload handler which would show a prompt" + ); + + BrowserTestUtils.removeTab(tab1); + + // Open tab containing a page which has a beforeunload handler which does not show a prompt. + url = + "http://example.com/browser/browser/components/sessionstore/test/browser_1284886_suspend_tab_2.html"; + tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await BrowserTestUtils.switchTab(gBrowser, tab0); + + // Test that tab with beforeunload handler which would not show a prompt can be suspended. + gBrowser.discardBrowser(tab1); + ok( + !tab1.linkedPanel, + "can suspend a tab with beforeunload handler which would not show a prompt" + ); + + BrowserTestUtils.removeTab(tab1); + + // Test that non-remote tab is not able to be suspended. + url = "about:robots"; + tab1 = BrowserTestUtils.addTab(gBrowser, url, { forceNotRemote: true }); + await promiseBrowserLoaded(tab1.linkedBrowser, true, url); + await BrowserTestUtils.switchTab(gBrowser, tab1); + await BrowserTestUtils.switchTab(gBrowser, tab0); + + gBrowser.discardBrowser(tab1); + ok(tab1.linkedPanel, "cannot suspend a remote tab"); + + BrowserTestUtils.removeTab(tab1); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab_2.html b/src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab_2.html new file mode 100644 index 000000000..5c4291363 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_1284886_suspend_tab_2.html @@ -0,0 +1,11 @@ + + + + + +TEST PAGE + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_1446343-windowsize.js b/src/zen/tests/mochitests/sessionstore/browser_1446343-windowsize.js new file mode 100644 index 000000000..97f664a46 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_1446343-windowsize.js @@ -0,0 +1,39 @@ +add_task(async function test() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + async function changeSizeMode(mode) { + let promise = BrowserTestUtils.waitForEvent(win, "sizemodechange"); + win[mode](); + await promise; + } + if (win.windowState != win.STATE_NORMAL) { + await changeSizeMode("restore"); + } + + const { outerWidth, outerHeight, screenX, screenY } = win; + function checkCurrentState(sizemode) { + let state = ss.getWindowState(win); + let winState = state.windows[0]; + let msgSuffix = ` should match on ${sizemode} mode`; + is(winState.width, outerWidth, "width" + msgSuffix); + is(winState.height, outerHeight, "height" + msgSuffix); + // The position attributes seem to be affected on macOS when the + // window gets maximized, so skip checking them for now. + if (AppConstants.platform != "macosx" || sizemode == "normal") { + is(winState.screenX, screenX, "screenX" + msgSuffix); + is(winState.screenY, screenY, "screenY" + msgSuffix); + } + is(winState.sizemode, sizemode, "sizemode should match"); + } + + checkCurrentState("normal"); + + await changeSizeMode("maximize"); + checkCurrentState("maximized"); + + await changeSizeMode("minimize"); + checkCurrentState("minimized"); + + // Clean up. + await BrowserTestUtils.closeWindow(win); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_1933485_tab_groups_history.js b/src/zen/tests/mochitests/sessionstore/browser_1933485_tab_groups_history.js new file mode 100644 index 000000000..c4093127b --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_1933485_tab_groups_history.js @@ -0,0 +1,43 @@ +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +// This test ensures that closed tab groups are immediately stored in the +// closedGroups array, even if their history was not stored in the tab state at +// the time the tab group was closed. +add_task( + async function test_browser_1933485_tabGroupImmediatelyStoredInClosedGroups() { + let win = await promiseNewWindowLoaded(); + + const urls = ["about:blank", "about:robots", "https://www.example.com/"]; + const tabs = urls.map(url => BrowserTestUtils.addTab(win.gBrowser, url)); + await Promise.all( + tabs.map((t, i) => + BrowserTestUtils.browserLoaded(t.linkedBrowser, { wantLoad: urls[i] }) + ) + ); + + const tabGroup = win.gBrowser.addTabGroup(tabs); + + let removePromise = BrowserTestUtils.waitForEvent( + tabGroup, + "TabGroupRemoved" + ); + win.gBrowser.removeTabGroup(tabGroup); + await removePromise; + + const closedGroupState = SessionStore.getClosedTabGroups(win); + + Assert.equal( + closedGroupState.length, + 1, + "Tab group is stored in closedGroups" + ); + + await BrowserTestUtils.closeWindow(win); + } +); diff --git a/src/zen/tests/mochitests/sessionstore/browser_1953801_tab_groups_history_close_other.js b/src/zen/tests/mochitests/sessionstore/browser_1953801_tab_groups_history_close_other.js new file mode 100644 index 000000000..f1b2e2ae3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_1953801_tab_groups_history_close_other.js @@ -0,0 +1,55 @@ +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task( + async function test_browser_1953801_tabGroupImmediatelyStoredInSavedGroupsOnCloseOtherTabs() { + let savedGroupState; + let win = await promiseNewWindowLoaded(); + + savedGroupState = SessionStore.getSavedTabGroups(win); + Assert.equal( + savedGroupState.length, + 0, + "Should start with no saved groups" + ); + + const tabs = [ + BrowserTestUtils.addTab(win.gBrowser, "about:robots"), + BrowserTestUtils.addTab(win.gBrowser, "about:robots"), + BrowserTestUtils.addTab(win.gBrowser, "https://www.example.com"), + ]; + await Promise.all(tabs.map(t => promiseBrowserLoaded(t.linkedBrowser))); + + const tabGroup = win.gBrowser.addTabGroup([tabs[1], tabs[2]], { + label: "group-to-save", + }); + + await TestUtils.waitForCondition( + () => SessionStore.getWindowState(win).windows[0].groups.length == 1, + "Waiting for group to appear in session store" + ); + + let removePromise = BrowserTestUtils.waitForEvent( + tabGroup, + "TabGroupRemoved" + ); + win.gBrowser.removeAllTabsBut(win.gBrowser.tabs[0]); + await removePromise; + + savedGroupState = SessionStore.getSavedTabGroups(win); + Assert.equal( + savedGroupState.length, + 1, + "Tab group is stored in saved groups" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + SessionStore.forgetSavedTabGroup(tabGroup.id); + } +); diff --git a/src/zen/tests/mochitests/sessionstore/browser_248970_b_perwindowpb.js b/src/zen/tests/mochitests/sessionstore/browser_248970_b_perwindowpb.js new file mode 100644 index 000000000..ffddaf563 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_248970_b_perwindowpb.js @@ -0,0 +1,199 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + /** Test (B) for Bug 248970 */ + waitForExplicitFinish(); + + let windowsToClose = []; + let file = Services.dirsvc.get("TmpD", Ci.nsIFile); + let filePath = file.path; + let fieldList = { + "//input[@name='input']": Date.now().toString(16), + "//input[@name='spaced 1']": Math.random().toString(), + "//input[3]": "three", + "//input[@type='checkbox']": true, + "//input[@name='uncheck']": false, + "//input[@type='radio'][1]": false, + "//input[@type='radio'][2]": true, + "//input[@type='radio'][3]": false, + "//select": 2, + "//select[@multiple]": [1, 3], + "//textarea[1]": "", + "//textarea[2]": "Some text... " + Math.random(), + "//textarea[3]": "Some more text\n" + new Date(), + "//input[@type='file']": filePath, + }; + + registerCleanupFunction(async function () { + for (let win of windowsToClose) { + await BrowserTestUtils.closeWindow(win); + } + }); + + function checkNoThrow(aLambda) { + try { + return aLambda() || true; + } catch (ex) {} + return false; + } + + function getElementByXPath(aTab, aQuery) { + let doc = aTab.linkedBrowser.contentDocument; + let xptype = doc.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE; + return doc.evaluate(aQuery, doc, null, xptype, null).singleNodeValue; + } + + function setFormValue(aTab, aQuery, aValue) { + let node = getElementByXPath(aTab, aQuery); + if (typeof aValue == "string") { + node.value = aValue; + } else if (typeof aValue == "boolean") { + node.checked = aValue; + } else if (typeof aValue == "number") { + node.selectedIndex = aValue; + } else { + Array.prototype.forEach.call( + node.options, + (aOpt, aIx) => (aOpt.selected = aValue.indexOf(aIx) > -1) + ); + } + } + + function compareFormValue(aTab, aQuery, aValue) { + let node = getElementByXPath(aTab, aQuery); + if (!node) { + return false; + } + if (ChromeUtils.getClassName(node) === "HTMLInputElement") { + return ( + aValue == + (node.type == "checkbox" || node.type == "radio" + ? node.checked + : node.value) + ); + } + if (ChromeUtils.getClassName(node) === "HTMLTextAreaElement") { + return aValue == node.value; + } + if (!node.multiple) { + return aValue == node.selectedIndex; + } + return Array.prototype.every.call( + node.options, + (aOpt, aIx) => aValue.indexOf(aIx) > -1 == aOpt.selected + ); + } + + /** + * Test (B) : Session data restoration between windows + */ + + let rootDir = getRootDirectory(gTestPath); + const testURL = rootDir + "browser_248970_b_sample.html"; + const testURL2 = + "http://mochi.test:8888/browser/" + + "browser/components/sessionstore/test/browser_248970_b_sample.html"; + + whenNewWindowLoaded({ private: false }, function (aWin) { + windowsToClose.push(aWin); + + // get closed tab count + let count = ss.getClosedTabCountForWindow(aWin); + let max_tabs_undo = Services.prefs.getIntPref( + "browser.sessionstore.max_tabs_undo" + ); + ok( + 0 <= count && count <= max_tabs_undo, + "getClosedTabCountForWindow should return zero or at most max_tabs_undo" + ); + + // setup a state for tab (A) so we can check later that is restored + let value = "Value " + Math.random(); + let state = { entries: [{ url: testURL }], extData: { key: value } }; + + // public session, add new tab: (A) + let tab_A = BrowserTestUtils.addTab(aWin.gBrowser, testURL); + ss.setTabState(tab_A, JSON.stringify(state)); + promiseBrowserLoaded(tab_A.linkedBrowser).then(() => { + // make sure that the next closed tab will increase getClosedTabCountForWindow + Services.prefs.setIntPref( + "browser.sessionstore.max_tabs_undo", + max_tabs_undo + 1 + ); + + // populate tab_A with form data + for (let i in fieldList) { + setFormValue(tab_A, i, fieldList[i]); + } + + // public session, close tab: (A) + aWin.gBrowser.removeTab(tab_A); + + // verify that closedTabCount increased + Assert.greater( + ss.getClosedTabCountForWindow(aWin), + count, + "getClosedTabCountForWindow has increased after closing a tab" + ); + + // verify tab: (A), in undo list + let tab_A_restored = checkNoThrow(() => ss.undoCloseTab(aWin, 0)); + ok(tab_A_restored, "a tab is in undo list"); + promiseTabRestored(tab_A_restored).then(() => { + is( + testURL, + tab_A_restored.linkedBrowser.currentURI.spec, + "it's the same tab that we expect" + ); + aWin.gBrowser.removeTab(tab_A_restored); + + whenNewWindowLoaded({ private: true }, function (win) { + windowsToClose.push(win); + + // setup a state for tab (B) so we can check that its duplicated + // properly + let key1 = "key1"; + let value1 = "Value " + Math.random(); + let state1 = { + entries: [{ url: testURL2 }], + extData: { key1: value1 }, + }; + + let tab_B = BrowserTestUtils.addTab(win.gBrowser, testURL2); + promiseTabState(tab_B, state1).then(() => { + // populate tab: (B) with different form data + for (let item in fieldList) { + setFormValue(tab_B, item, fieldList[item]); + } + + // duplicate tab: (B) + let tab_C = win.gBrowser.duplicateTab(tab_B); + promiseTabRestored(tab_C).then(() => { + // verify the correctness of the duplicated tab + is( + ss.getCustomTabValue(tab_C, key1), + value1, + "tab successfully duplicated - correct state" + ); + + for (let item in fieldList) { + ok( + compareFormValue(tab_C, item, fieldList[item]), + 'The value for "' + item + '" was correctly duplicated' + ); + } + + // private browsing session, close tab: (C) and (B) + win.gBrowser.removeTab(tab_C); + win.gBrowser.removeTab(tab_B); + + finish(); + }); + }); + }); + }); + }); + }); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_248970_b_sample.html b/src/zen/tests/mochitests/sessionstore/browser_248970_b_sample.html new file mode 100644 index 000000000..76c3ae1aa --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_248970_b_sample.html @@ -0,0 +1,37 @@ + + +Test for bug 248970 + +

Text Fields

+ + + + +

Checkboxes and Radio buttons

+ Check 1 + Check 2 +

+ Radio 1 + Radio 2 + Radio 3 + +

Selects

+ + + +

Text Areas

+ + + + +

File Selector

+ diff --git a/src/zen/tests/mochitests/sessionstore/browser_339445.js b/src/zen/tests/mochitests/sessionstore/browser_339445.js new file mode 100644 index 000000000..01e784439 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_339445.js @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function test() { + /** Test for Bug 339445 */ + + let testURL = + "http://mochi.test:8888/browser/" + + "browser/components/sessionstore/test/browser_339445_sample.html"; + + let tab = BrowserTestUtils.addTab(gBrowser, testURL); + await promiseBrowserLoaded(tab.linkedBrowser, true, testURL); + + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + let doc = content.document; + is( + doc.getElementById("storageTestItem").textContent, + "PENDING", + "sessionStorage value has been set" + ); + }); + + let tab2 = gBrowser.duplicateTab(tab); + await promiseTabRestored(tab2); + + await SpecialPowers.spawn(tab2.linkedBrowser, [], function () { + let doc2 = content.document; + is( + doc2.getElementById("storageTestItem").textContent, + "SUCCESS", + "sessionStorage value has been duplicated" + ); + }); + + // clean up + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_339445_sample.html b/src/zen/tests/mochitests/sessionstore/browser_339445_sample.html new file mode 100644 index 000000000..ff5b4acd9 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_339445_sample.html @@ -0,0 +1,18 @@ + + +Test for bug 339445 + +storageTestItem = FAIL + + + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_345898.js b/src/zen/tests/mochitests/sessionstore/browser_345898.js new file mode 100644 index 000000000..2a946ad28 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_345898.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + /** Test for Bug 345898 */ + + // all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE + Assert.throws( + () => ss.getWindowState({}), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid window for getWindowState throws" + ); + Assert.throws( + () => ss.setWindowState({}, "", false), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid window for setWindowState throws" + ); + Assert.throws( + () => ss.getTabState({}), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid tab for getTabState throws" + ); + Assert.throws( + () => ss.setTabState({}, "{}"), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid tab state for setTabState throws" + ); + Assert.throws( + () => ss.setTabState({}, JSON.stringify({ entries: [] })), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid tab for setTabState throws" + ); + Assert.throws( + () => ss.duplicateTab({}, {}), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid tab for duplicateTab throws" + ); + Assert.throws( + () => ss.duplicateTab({}, gBrowser.selectedTab), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid window for duplicateTab throws" + ); + Assert.throws( + () => ss.getClosedTabDataForWindow({}), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid window for getClosedTabData throws" + ); + Assert.throws( + () => ss.undoCloseTab({}, 0), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid window for undoCloseTab throws" + ); + Assert.throws( + () => ss.undoCloseTab(window, -1), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid index for undoCloseTab throws" + ); + Assert.throws( + () => ss.getCustomWindowValue({}, ""), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid window for getCustomWindowValue throws" + ); + Assert.throws( + () => ss.setCustomWindowValue({}, "", ""), + /NS_ERROR_ILLEGAL_VALUE/, + "Invalid window for setCustomWindowValue throws" + ); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_350525.js b/src/zen/tests/mochitests/sessionstore/browser_350525.js new file mode 100644 index 000000000..190552a6b --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_350525.js @@ -0,0 +1,136 @@ +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processCount", 1]], + }); +}); + +add_task(async function () { + /** Test for Bug 350525 */ + + function test(aLambda) { + try { + return aLambda() || true; + } catch (ex) {} + return false; + } + + /** + * setCustomWindowValue, et al. + */ + let key = "Unique name: " + Date.now(); + let value = "Unique value: " + Math.random(); + + // test adding + ok( + test(() => ss.setCustomWindowValue(window, key, value)), + "set a window value" + ); + + // test retrieving + is( + ss.getCustomWindowValue(window, key), + value, + "stored window value matches original" + ); + + // test deleting + ok( + test(() => ss.deleteCustomWindowValue(window, key)), + "delete the window value" + ); + + // value should not exist post-delete + is(ss.getCustomWindowValue(window, key), "", "window value was deleted"); + + // test deleting a non-existent value + ok( + test(() => ss.deleteCustomWindowValue(window, key)), + "delete non-existent window value" + ); + + /** + * setCustomTabValue, et al. + */ + key = "Unique name: " + Math.random(); + value = "Unique value: " + Date.now(); + let tab = BrowserTestUtils.addTab(gBrowser); + tab.linkedBrowser.stop(); + + // test adding + ok( + test(() => ss.setCustomTabValue(tab, key, value)), + "store a tab value" + ); + + // test retrieving + is(ss.getCustomTabValue(tab, key), value, "stored tab value match original"); + + // test deleting + ok( + test(() => ss.deleteCustomTabValue(tab, key)), + "delete the tab value" + ); + + // value should not exist post-delete + is(ss.getCustomTabValue(tab, key), "", "tab value was deleted"); + + // test deleting a non-existent value + ok( + test(() => ss.deleteCustomTabValue(tab, key)), + "delete non-existent tab value" + ); + + // clean up + await promiseRemoveTabAndSessionState(tab); + + /** + * getClosedTabCountForWindow, undoCloseTab + */ + + // get closed tab count + let count = ss.getClosedTabCountForWindow(window); + let max_tabs_undo = Services.prefs.getIntPref( + "browser.sessionstore.max_tabs_undo" + ); + ok( + 0 <= count && count <= max_tabs_undo, + "getClosedTabCountForWindow returns zero or at most max_tabs_undo" + ); + + // create a new tab + let testURL = "about:mozilla"; + tab = BrowserTestUtils.addTab(gBrowser, testURL); + await promiseBrowserLoaded(tab.linkedBrowser); + + // make sure that the next closed tab will increase getClosedTabCountForWindow + Services.prefs.setIntPref( + "browser.sessionstore.max_tabs_undo", + max_tabs_undo + 1 + ); + registerCleanupFunction(() => + Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo") + ); + + // remove tab + await promiseRemoveTabAndSessionState(tab); + + // getClosedTabCountForWindow + let newcount = ss.getClosedTabCountForWindow(window); + Assert.greater( + newcount, + count, + "after closing a tab, getClosedTabCountForWindow has been incremented" + ); + + // undoCloseTab + tab = test(() => ss.undoCloseTab(window, 0)); + ok(tab, "undoCloseTab doesn't throw"); + + await promiseTabRestored(tab); + is(tab.linkedBrowser.currentURI.spec, testURL, "correct tab was reopened"); + + // clean up + gBrowser.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_354894_perwindowpb.js b/src/zen/tests/mochitests/sessionstore/browser_354894_perwindowpb.js new file mode 100644 index 000000000..0c5b97003 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_354894_perwindowpb.js @@ -0,0 +1,490 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Checks that restoring the last browser window in session is actually + * working. + * + * @see https://bugzilla.mozilla.org/show_bug.cgi?id=354894 + * Note: It is implicitly tested that restoring the last window works when + * non-browser windows are around. The "Run Tests" window as well as the main + * browser window (wherein the test code gets executed) won't be considered + * browser windows. To achiveve this said main browser window has its windowtype + * attribute modified so that it's not considered a browser window any longer. + * This is crucial, because otherwise there would be two browser windows around, + * said main test window and the one opened by the tests, and hence the new + * logic wouldn't be executed at all. + * Note: Mac only tests the new notifications, as restoring the last window is + * not enabled on that platform (platform shim; the application is kept running + * although there are no windows left) + * Note: There is a difference when closing a browser window with + * BrowserCommands.tryToCloseWindow() as opposed to close(). The former will make + * nsSessionStore restore a window next time it gets a chance and will post + * notifications. The latter won't. + */ + +// The rejection "BrowserWindowTracker.getTopWindow(...) is null" is left +// unhandled in some cases. This bug should be fixed, but for the moment this +// file allows a class of rejections. +// +// NOTE: Allowing a whole class of rejections should be avoided. Normally you +// should use "expectUncaughtRejection" to flag individual failures. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally(/getTopWindow/); + +// Some urls that might be opened in tabs and/or popups +// Do not use about:blank: +// That one is reserved for special purposes in the tests +const TEST_URLS = ["about:mozilla", "about:buildconfig"]; + +// Number of -request notifications to except +// remember to adjust when adding new tests +const NOTIFICATIONS_EXPECTED = 6; + +// Window features of popup windows +const POPUP_FEATURES = "toolbar=no,resizable=no,status=no"; + +// Window features of browser windows +const CHROME_FEATURES = "chrome,all,dialog=no"; + +const IS_MAC = navigator.platform.match(/Mac/); + +/** + * Returns an Object with two properties: + * open (int): + * A count of how many non-closed navigator:browser windows there are. + * winstates (int): + * A count of how many windows there are in the SessionStore state. + */ +function getBrowserWindowsCount() { + let open = 0; + for (let win of Services.wm.getEnumerator("navigator:browser")) { + if (!win.closed) { + ++open; + } + } + + let winstates = JSON.parse(ss.getBrowserState()).windows.length; + + return { open, winstates }; +} + +add_setup(async function () { + // Make sure we've only got one browser window to start with + let { open, winstates } = getBrowserWindowsCount(); + is(open, 1, "Should only be one open window"); + is(winstates, 1, "Should only be one window state in SessionStore"); + + // This test takes some time to run, and it could timeout randomly. + // So we require a longer timeout. See bug 528219. + requestLongerTimeout(3); + + // Make the main test window not count as a browser window any longer + let oldWinType = document.documentElement.getAttribute("windowtype"); + document.documentElement.setAttribute("windowtype", "navigator:testrunner"); + + registerCleanupFunction(() => { + document.documentElement.setAttribute("windowtype", oldWinType); + }); +}); + +/** + * Sets up one of our tests by setting the right preferences, and + * then opening up a browser window preloaded with some tabs. + * + * @param options (Object) + * An object that can contain the following properties: + * + * private: + * Whether or not the opened window should be private. + * + * denyFirst: + * Whether or not the first window that attempts to close + * via closeWindowForRestoration should be denied. + * + * @param testFunction (Function*) + * A generator function that yields Promises to be run + * once the test has been set up. + * + * @returns Promise + * Resolves once the test has been cleaned up. + */ +let setupTest = async function (options, testFunction) { + await pushPrefs( + ["browser.startup.page", 3], + ["browser.tabs.warnOnClose", false] + ); + // SessionStartup caches pref values, but as this test tries to simulate a + // startup scenario, we'll reset them here. + SessionStartup.resetForTest(); + + // Observe these, and also use to count the number of hits + let observing = { + "browser-lastwindow-close-requested": 0, + "browser-lastwindow-close-granted": 0, + }; + + /** + * Helper: Will observe and handle the notifications for us + */ + let hitCount = 0; + function observer(aCancel, aTopic) { + // count so that we later may compare + observing[aTopic]++; + + // handle some tests + if (options.denyFirst && ++hitCount == 1) { + aCancel.QueryInterface(Ci.nsISupportsPRBool).data = true; + } + } + + for (let o in observing) { + Services.obs.addObserver(observer, o); + } + + let newWin = await promiseNewWindowLoaded({ + private: options.private || false, + }); + + await injectTestTabs(newWin); + + await testFunction(newWin, observing); + + let count = getBrowserWindowsCount(); + is(count.open, 0, "Got right number of open windows"); + is(count.winstates, 1, "Got right number of stored window states"); + + for (let o in observing) { + Services.obs.removeObserver(observer, o); + } + + await popPrefs(); + // Act like nothing ever happened. + SessionStartup.resetForTest(); +}; + +/** + * Loads a TEST_URLS into a browser window. + * + * @param win (Window) + * The browser window to load the tabs in + */ +function injectTestTabs(win) { + let promises = TEST_URLS.map(url => + BrowserTestUtils.addTab(win.gBrowser, url) + ).map(tab => BrowserTestUtils.browserLoaded(tab.linkedBrowser)); + return Promise.all(promises); +} + +/** + * Attempts to close a window via BrowserCommands.tryToCloseWindow so that + * we get the browser-lastwindow-close-requested and + * browser-lastwindow-close-granted observer notifications. + * + * @param win (Window) + * The window to try to close + * @returns Promise + * Resolves to true if the window closed, or false if the window + * was denied the ability to close. + */ +function closeWindowForRestoration(win) { + return new Promise(resolve => { + let closePromise = BrowserTestUtils.windowClosed(win); + win.BrowserCommands.tryToCloseWindow(); + if (!win.closed) { + resolve(false); + return; + } + + closePromise.then(() => { + resolve(true); + }); + }); +} + +/** + * Normal in-session restore + * + * Note: Non-Mac only + * + * Should do the following: + * 1. Open a new browser window + * 2. Add some tabs + * 3. Close that window + * 4. Opening another window + * 5. Checks that state is restored + */ +add_task(async function test_open_close_normal() { + if (IS_MAC) { + return; + } + + await setupTest({ denyFirst: true }, async function (newWin, obs) { + let closed = await closeWindowForRestoration(newWin); + ok(!closed, "First close request should have been denied"); + + closed = await closeWindowForRestoration(newWin); + ok(closed, "Second close request should be accepted"); + + newWin = await promiseNewWindowLoaded(); + is( + newWin.gBrowser.browsers.length, + TEST_URLS.length + 2, + "Restored window in-session with otherpopup windows around" + ); + + // Note that this will not result in the the browser-lastwindow-close + // notifications firing for this other newWin. + await BrowserTestUtils.closeWindow(newWin); + + // setupTest gave us a window which was denied for closing once, and then + // closed. + is( + obs["browser-lastwindow-close-requested"], + 2, + "Got expected browser-lastwindow-close-requested notifications" + ); + is( + obs["browser-lastwindow-close-granted"], + 1, + "Got expected browser-lastwindow-close-granted notifications" + ); + }); +}); + +/** + * PrivateBrowsing in-session restore + * + * Note: Non-Mac only + * + * Should do the following: + * 1. Open a new browser window A + * 2. Add some tabs + * 3. Close the window A as the last window + * 4. Open a private browsing window B + * 5. Make sure that B didn't restore the tabs from A + * 6. Close private browsing window B + * 7. Open a new window C + * 8. Make sure that new window C has restored tabs from A + */ +add_task(async function test_open_close_private_browsing() { + if (IS_MAC) { + return; + } + + await setupTest({}, async function (newWin, obs) { + let closed = await closeWindowForRestoration(newWin); + ok(closed, "Should be able to close the window"); + + newWin = await promiseNewWindowLoaded({ private: true }); + is( + newWin.gBrowser.browsers.length, + 1, + "Did not restore in private browsing mode" + ); + + closed = await closeWindowForRestoration(newWin); + ok(closed, "Should be able to close the window"); + + newWin = await promiseNewWindowLoaded(); + is( + newWin.gBrowser.browsers.length, + TEST_URLS.length + 2, + "Restored tabs in a new non-private window" + ); + + // Note that this will not result in the the browser-lastwindow-close + // notifications firing for this other newWin. + await BrowserTestUtils.closeWindow(newWin); + + // We closed two windows with closeWindowForRestoration, and both + // should have been successful. + is( + obs["browser-lastwindow-close-requested"], + 2, + "Got expected browser-lastwindow-close-requested notifications" + ); + is( + obs["browser-lastwindow-close-granted"], + 2, + "Got expected browser-lastwindow-close-granted notifications" + ); + }); +}); + +/** + * Open some popup window to check it isn't restored. Instead nothing at all + * should be restored + * + * Note: Non-Mac only + * + * Should do the following: + * 1. Open a popup + * 2. Add another tab to the popup (so that it gets stored) and close it again + * 3. Open a window + * 4. Check that nothing at all is restored + * 5. Open two browser windows and close them again + * 6. undoCloseWindow() one + * 7. Open another browser window + * 8. Check that nothing at all is restored + */ +add_task(async function test_open_close_only_popup() { + if (IS_MAC) { + return; + } + + await setupTest({}, async function (newWin, obs) { + // We actually don't care about the initial window in this test. + await BrowserTestUtils.closeWindow(newWin); + + // This will cause nsSessionStore to restore a window the next time it + // gets a chance. + let popupPromise = BrowserTestUtils.waitForNewWindow(); + openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]); + let popup = await popupPromise; + + is( + popup.gBrowser.browsers.length, + 1, + "Did not restore the popup window (1)" + ); + + let closed = await closeWindowForRestoration(popup); + ok(closed, "Should be able to close the window"); + + popupPromise = BrowserTestUtils.waitForNewWindow(); + openDialog(location, "popup", POPUP_FEATURES, TEST_URLS[1]); + popup = await popupPromise; + + BrowserTestUtils.addTab(popup.gBrowser, TEST_URLS[0]); + is( + popup.gBrowser.browsers.length, + 2, + "Did not restore to the popup window (2)" + ); + + await BrowserTestUtils.closeWindow(popup); + + newWin = await promiseNewWindowLoaded(); + isnot( + newWin.gBrowser.browsers.length, + 2, + "Did not restore the popup window" + ); + is( + TEST_URLS.indexOf(newWin.gBrowser.browsers[0].currentURI.spec), + -1, + "Did not restore the popup window (2)" + ); + await BrowserTestUtils.closeWindow(newWin); + + // We closed one popup window with closeWindowForRestoration, and popup + // windows should never fire the browser-lastwindow notifications. + is( + obs["browser-lastwindow-close-requested"], + 0, + "Got expected browser-lastwindow-close-requested notifications" + ); + is( + obs["browser-lastwindow-close-granted"], + 0, + "Got expected browser-lastwindow-close-granted notifications" + ); + }); +}); + +/** + * Open some windows and do undoCloseWindow. This should prevent any + * restoring later in the test + * + * Note: Non-Mac only + * + * Should do the following: + * 1. Open two browser windows and close them again + * 2. undoCloseWindow() one + * 3. Open another browser window + * 4. Make sure nothing at all is restored + */ +add_task(async function test_open_close_restore_from_popup() { + if (IS_MAC) { + return; + } + + await setupTest({}, async function (newWin) { + let newWin2 = await promiseNewWindowLoaded(); + await injectTestTabs(newWin2); + + let closed = await closeWindowForRestoration(newWin); + ok(closed, "Should be able to close the window"); + closed = await closeWindowForRestoration(newWin2); + ok(closed, "Should be able to close the window"); + + let counts = getBrowserWindowsCount(); + is(counts.open, 0, "Got right number of open windows"); + is(counts.winstates, 1, "Got right number of window states"); + + newWin = SessionWindowUI.undoCloseWindow(0); + await BrowserTestUtils.waitForEvent(newWin, "load"); + + // Make sure we wait until this window is restored. + await BrowserTestUtils.waitForEvent( + newWin.gBrowser.tabContainer, + "SSTabRestored" + ); + + newWin2 = await promiseNewWindowLoaded(); + + is( + TEST_URLS.indexOf(newWin2.gBrowser.browsers[0].currentURI.spec), + -1, + "Did not restore, as undoCloseWindow() was last called (2)" + ); + + counts = getBrowserWindowsCount(); + is(counts.open, 2, "Got right number of open windows"); + is(counts.winstates, 3, "Got right number of window states"); + + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.closeWindow(newWin2); + + counts = getBrowserWindowsCount(); + is(counts.open, 0, "Got right number of open windows"); + is(counts.winstates, 1, "Got right number of window states"); + }); +}); + +/** + * Test if closing can be denied on Mac. + * + * Note: Mac only + */ +add_task(async function test_mac_notifications() { + if (!IS_MAC) { + return; + } + + await setupTest({ denyFirst: true }, async function (newWin, obs) { + let closed = await closeWindowForRestoration(newWin); + ok(!closed, "First close attempt should be denied"); + closed = await closeWindowForRestoration(newWin); + ok(closed, "Second close attempt should be granted"); + + // We tried closing once, and got denied. Then we tried again and + // succeeded. That means 2 close requests, and 1 close granted. + is( + obs["browser-lastwindow-close-requested"], + 2, + "Got expected browser-lastwindow-close-requested notifications" + ); + is( + obs["browser-lastwindow-close-granted"], + 1, + "Got expected browser-lastwindow-close-granted notifications" + ); + }); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_367052.js b/src/zen/tests/mochitests/sessionstore/browser_367052.js new file mode 100644 index 000000000..413794572 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_367052.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + // make sure that the next closed tab will increase getClosedTabCountForWindow + let max_tabs_undo = Services.prefs.getIntPref( + "browser.sessionstore.max_tabs_undo" + ); + Services.prefs.setIntPref( + "browser.sessionstore.max_tabs_undo", + max_tabs_undo + 1 + ); + registerCleanupFunction(() => + Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo") + ); + + forgetClosedTabs(window); + + // restore a blank tab + let tab = BrowserTestUtils.addTab(gBrowser, "about:mozilla"); + await promiseBrowserLoaded(tab.linkedBrowser); + + let count = await promiseSHistoryCount(tab.linkedBrowser); + Assert.greaterOrEqual( + count, + 1, + "the new tab does have at least one history entry" + ); + + await promiseTabState(tab, { entries: [] }); + + // We may have a different sessionHistory object if the tab + // switched from non-remote to remote. + count = await promiseSHistoryCount(tab.linkedBrowser); + is(count, 0, "the tab was restored without any history whatsoever"); + + await promiseRemoveTabAndSessionState(tab); + is( + ss.getClosedTabCountForWindow(window), + 0, + "The closed blank tab wasn't added to Recently Closed Tabs" + ); +}); + +function promiseSHistoryCount(browser) { + return SpecialPowers.spawn(browser, [], async function () { + return docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.count; + }); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_393716.js b/src/zen/tests/mochitests/sessionstore/browser_393716.js new file mode 100644 index 000000000..383c25e38 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_393716.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = "about:config"; + +add_setup(async function () { + // Make sure that the field of which we restore the state is visible on load. + await SpecialPowers.pushPrefEnv({ + set: [["browser.aboutConfig.showWarning", false]], + }); +}); + +/** + * Bug 393716 - Basic tests for getTabState(), setTabState(), and duplicateTab(). + */ +add_task(async function test_set_tabstate() { + let key = "Unique key: " + Date.now(); + let value = "Unique value: " + Math.random(); + + // create a new tab + let tab = BrowserTestUtils.addTab(gBrowser, URL); + ss.setCustomTabValue(tab, key, value); + await promiseBrowserLoaded(tab.linkedBrowser); + + // get the tab's state + await TabStateFlusher.flush(tab.linkedBrowser); + let state = ss.getTabState(tab); + ok(state, "get the tab's state"); + + // verify the tab state's integrity + state = JSON.parse(state); + ok( + state instanceof Object && + state.entries instanceof Array && + !!state.entries.length, + "state object seems valid" + ); + ok( + state.entries.length == 1 && state.entries[0].url == URL, + "Got the expected state object (test URL)" + ); + ok( + state.extData && state.extData[key] == value, + "Got the expected state object (test manually set tab value)" + ); + + // clean up + gBrowser.removeTab(tab); +}); + +add_task(async function test_set_tabstate_and_duplicate() { + let key2 = "key2"; + let value2 = "Value " + Math.random(); + let value3 = "Another value: " + Date.now(); + let state = { + entries: [{ url: URL, triggeringPrincipal_base64 }], + extData: { key2: value2 }, + }; + + // create a new tab + let tab = BrowserTestUtils.addTab(gBrowser); + // set the tab's state + ss.setTabState(tab, JSON.stringify(state)); + await promiseBrowserLoaded(tab.linkedBrowser); + + // verify the correctness of the restored tab + ok( + ss.getCustomTabValue(tab, key2) == value2 && + tab.linkedBrowser.currentURI.spec == URL, + "the tab's state was correctly restored" + ); + + // add text data + await setPropertyOfFormField( + tab.linkedBrowser, + "#about-config-search", + "value", + value3 + ); + + // duplicate the tab + let tab2 = ss.duplicateTab(window, tab); + await promiseTabRestored(tab2); + + // verify the correctness of the duplicated tab + ok( + ss.getCustomTabValue(tab2, key2) == value2 && + tab2.linkedBrowser.currentURI.spec == URL, + "correctly duplicated the tab's state" + ); + let textbox = await getPropertyOfFormField( + tab2.linkedBrowser, + "#about-config-search", + "value" + ); + is(textbox, value3, "also duplicated text data"); + + // clean up + gBrowser.removeTab(tab2); + gBrowser.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_394759_basic.js b/src/zen/tests/mochitests/sessionstore/browser_394759_basic.js new file mode 100644 index 000000000..cc1c33516 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_394759_basic.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = + "data:text/html;charset=utf-8," + + ""; + +/** + * This test ensures that closing a window is a reversible action. We will + * close the the window, restore it and check that all data has been restored. + * This includes window-specific data as well as form data for tabs. + */ +function test() { + waitForExplicitFinish(); + + let uniqueKey = "bug 394759"; + let uniqueValue = "unik" + Date.now(); + let uniqueText = "pi != " + Math.random(); + + // Clear the list of closed windows. + forgetClosedWindows(); + + provideWindow(function onTestURLLoaded(newWin) { + BrowserTestUtils.addTab(newWin.gBrowser).linkedBrowser.stop(); + + // Mark the window with some unique data to be restored later on. + ss.setCustomWindowValue(newWin, uniqueKey, uniqueValue); + let [txt] = newWin.content.document.querySelectorAll("#txt"); + txt.value = uniqueText; + + let browser = newWin.gBrowser.selectedBrowser; + + setPropertyOfFormField(browser, "#chk", "checked", true).then(() => { + BrowserTestUtils.closeWindow(newWin).then(() => { + is( + ss.getClosedWindowCount(), + 1, + "The closed window was added to Recently Closed Windows" + ); + + let data = SessionStore.getClosedWindowData(); + + // Verify that non JSON serialized data is the same as JSON serialized data. + is( + JSON.stringify(data), + ss.getClosedWindowData(), + "Non-serialized data is the same as serialized data" + ); + + ok( + data[0].title == TEST_URL && + JSON.stringify(data[0]).indexOf(uniqueText) > -1, + "The closed window data was stored correctly" + ); + + // Reopen the closed window and ensure its integrity. + let newWin2 = ss.undoCloseWindow(0); + + ok( + newWin2.isChromeWindow, + "undoCloseWindow actually returned a window" + ); + is( + ss.getClosedWindowCount(), + 0, + "The reopened window was removed from Recently Closed Windows" + ); + + // SSTabRestored will fire more than once, so we need to make sure we count them. + let restoredTabs = 0; + let expectedTabs = data[0].tabs.length; + newWin2.addEventListener( + "SSTabRestored", + function sstabrestoredListener() { + ++restoredTabs; + info("Restored tab " + restoredTabs + "/" + expectedTabs); + if (restoredTabs < expectedTabs) { + return; + } + + is(restoredTabs, expectedTabs, "Correct number of tabs restored"); + newWin2.removeEventListener( + "SSTabRestored", + sstabrestoredListener, + true + ); + + is( + newWin2.gBrowser.tabs.length, + 2, + "The window correctly restored 2 tabs" + ); + is( + newWin2.gBrowser.currentURI.spec, + TEST_URL, + "The window correctly restored the URL" + ); + + let chk; + [txt, chk] = + newWin2.content.document.querySelectorAll("#txt, #chk"); + ok( + txt.value == uniqueText && chk.checked, + "The window correctly restored the form" + ); + is( + ss.getCustomWindowValue(newWin2, uniqueKey), + uniqueValue, + "The window correctly restored the data associated with it" + ); + + // Clean up. + BrowserTestUtils.closeWindow(newWin2).then(finish); + }, + true + ); + }); + }); + }, TEST_URL); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_394759_behavior.js b/src/zen/tests/mochitests/sessionstore/browser_394759_behavior.js new file mode 100644 index 000000000..01217f86c --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_394759_behavior.js @@ -0,0 +1,93 @@ +/** + * Test helper function that opens a series of windows, closes them + * and then checks the closed window data from SessionStore against + * expected results. + * + * @param windowsToOpen (Array) + * An array of Objects, where each object must define a single + * property "isPopup" for whether or not the opened window should + * be a popup. + * @param expectedResults (Array) + * An Object with two properies: mac and other, where each points + * at yet another Object, with the following properties: + * + * popup (int): + * The number of popup windows we expect to be in the closed window + * data. + * normal (int): + * The number of normal windows we expect to be in the closed window + * data. + * @returns Promise + */ +function testWindows(windowsToOpen, expectedResults) { + return (async function () { + let num = 0; + for (let winData of windowsToOpen) { + let features = "chrome,dialog=no," + (winData.isPopup ? "all=no" : "all"); + let url = "http://example.com/?window=" + num; + num = num + 1; + + let openWindowPromise = BrowserTestUtils.waitForNewWindow({ url }); + openDialog(AppConstants.BROWSER_CHROME_URL, "", features, url); + let win = await openWindowPromise; + await BrowserTestUtils.closeWindow(win); + } + + let closedWindowData = ss.getClosedWindowData(); + let numPopups = closedWindowData.filter(function (el) { + return el.isPopup; + }).length; + let numNormal = ss.getClosedWindowCount() - numPopups; + // #ifdef doesn't work in browser-chrome tests, so do a simple regex on platform + let oResults = navigator.platform.match(/Mac/) + ? expectedResults.mac + : expectedResults.other; + is( + numPopups, + oResults.popup, + "There were " + oResults.popup + " popup windows to reopen" + ); + is( + numNormal, + oResults.normal, + "There were " + oResults.normal + " normal windows to reopen" + ); + })(); +} + +add_task(async function test_closed_window_states() { + // This test takes quite some time, and timeouts frequently, so we require + // more time to run. + // See Bug 518970. + requestLongerTimeout(2); + + let windowsToOpen = [ + { isPopup: false }, + { isPopup: true }, + { isPopup: true }, + { isPopup: true }, + { isPopup: true }, + { isPopup: true }, + ]; + let expectedResults = { + mac: { popup: 5, normal: 0 }, + other: { popup: 5, normal: 1 }, + }; + + await testWindows(windowsToOpen, expectedResults); + + let windowsToOpen2 = [ + { isPopup: false }, + { isPopup: false }, + { isPopup: false }, + { isPopup: false }, + { isPopup: false }, + { isPopup: false }, + ]; + let expectedResults2 = { + mac: { popup: 0, normal: 5 }, + other: { popup: 0, normal: 5 }, + }; + + await testWindows(windowsToOpen2, expectedResults2); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_394759_perwindowpb.js b/src/zen/tests/mochitests/sessionstore/browser_394759_perwindowpb.js new file mode 100644 index 000000000..b79527d4c --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_394759_perwindowpb.js @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TESTS = [ + { url: "about:config", key: "bug 394759 Non-PB", value: "uniq" + r() }, + { url: "about:mozilla", key: "bug 394759 PB", value: "uniq" + r() }, +]; + +function promiseTestOpenCloseWindow(aIsPrivate, aTest) { + return (async function () { + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: aIsPrivate, + }); + BrowserTestUtils.startLoadingURIString( + win.gBrowser.selectedBrowser, + aTest.url + ); + await promiseBrowserLoaded(win.gBrowser.selectedBrowser, true, aTest.url); + // Mark the window with some unique data to be restored later on. + ss.setCustomWindowValue(win, aTest.key, aTest.value); + await TabStateFlusher.flushWindow(win); + // Close. + await BrowserTestUtils.closeWindow(win); + })(); +} + +function promiseTestOnWindow(aIsPrivate, aValue) { + return (async function () { + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: aIsPrivate, + }); + await TabStateFlusher.flushWindow(win); + let data = ss.getClosedWindowData()[0]; + is( + ss.getClosedWindowCount(), + 1, + "Check that the closed window count hasn't changed" + ); + Assert.greater( + JSON.stringify(data).indexOf(aValue), + -1, + "Check the closed window data was stored correctly" + ); + registerCleanupFunction(() => BrowserTestUtils.closeWindow(win)); + })(); +} + +add_setup(async function () { + forgetClosedWindows(); + forgetClosedTabs(window); +}); + +add_task(async function main() { + await promiseTestOpenCloseWindow(false, TESTS[0]); + await promiseTestOpenCloseWindow(true, TESTS[1]); + await promiseTestOnWindow(false, TESTS[0].value); + await promiseTestOnWindow(true, TESTS[0].value); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_394759_purge.js b/src/zen/tests/mochitests/sessionstore/browser_394759_purge.js new file mode 100644 index 000000000..9665df0fe --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_394759_purge.js @@ -0,0 +1,250 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let { ForgetAboutSite } = ChromeUtils.importESModule( + "moz-src:///toolkit/components/forgetaboutsite/ForgetAboutSite.sys.mjs" +); + +function promiseClearHistory() { + return new Promise(resolve => { + let observer = { + observe() { + Services.obs.removeObserver( + this, + "browser:purge-session-history-for-domain" + ); + resolve(); + }, + }; + Services.obs.addObserver( + observer, + "browser:purge-session-history-for-domain" + ); + }); +} + +add_task(async function () { + // utility functions + function countClosedTabsByTitle(aClosedTabList, aTitle) { + return aClosedTabList.filter(aData => aData.title == aTitle).length; + } + + function countOpenTabsByTitle(aOpenTabList, aTitle) { + return aOpenTabList.filter(aData => + aData.entries.some(aEntry => aEntry.title == aTitle) + ).length; + } + + // backup old state + let oldState = ss.getBrowserState(); + let oldState_wins = JSON.parse(oldState).windows.length; + if (oldState_wins != 1) { + ok( + false, + "oldState in test_purge has " + oldState_wins + " windows instead of 1" + ); + } + + // create a new state for testing + const REMEMBER = Date.now(), + FORGET = Math.random(); + let testState = { + windows: [ + { + tabs: [ + { + entries: [ + { url: "http://example.com/", triggeringPrincipal_base64 }, + ], + }, + ], + selected: 1, + }, + ], + _closedWindows: [ + // _closedWindows[0] + { + tabs: [ + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: REMEMBER, + }, + ], + }, + { + entries: [ + { + url: "http://mozilla.org/", + triggeringPrincipal_base64, + title: FORGET, + }, + ], + }, + ], + selected: 2, + title: "mozilla.org", + _closedTabs: [], + closedGroups: [], + }, + // _closedWindows[1] + { + tabs: [ + { + entries: [ + { + url: "http://mozilla.org/", + triggeringPrincipal_base64, + title: FORGET, + }, + ], + }, + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: REMEMBER, + }, + ], + }, + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: REMEMBER, + }, + ], + }, + { + entries: [ + { + url: "http://mozilla.org/", + triggeringPrincipal_base64, + title: FORGET, + }, + ], + }, + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: REMEMBER, + }, + ], + }, + ], + selected: 5, + _closedTabs: [], + closedGroups: [], + }, + // _closedWindows[2] + { + tabs: [ + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: REMEMBER, + }, + ], + }, + ], + selected: 1, + _closedTabs: [ + { + state: { + entries: [ + { + url: "http://mozilla.org/", + triggeringPrincipal_base64, + title: FORGET, + }, + { + url: "http://mozilla.org/again", + triggeringPrincipal_base64, + title: "doesn't matter", + }, + ], + }, + pos: 1, + title: FORGET, + }, + { + state: { + entries: [ + { + url: "http://example.com", + triggeringPrincipal_base64, + title: REMEMBER, + }, + ], + }, + title: REMEMBER, + }, + ], + closedGroups: [], + }, + ], + }; + + // set browser to test state + ss.setBrowserState(JSON.stringify(testState)); + + // purge domain & check that we purged correctly for closed windows + let clearHistoryPromise = promiseClearHistory(); + await ForgetAboutSite.removeDataFromBaseDomain("mozilla.org"); + await clearHistoryPromise; + + let closedWindowData = ss.getClosedWindowData(); + + // First set of tests for _closedWindows[0] - tests basics + let win = closedWindowData[0]; + is(win.tabs.length, 1, "1 tab was removed"); + is(countOpenTabsByTitle(win.tabs, FORGET), 0, "The correct tab was removed"); + is( + countOpenTabsByTitle(win.tabs, REMEMBER), + 1, + "The correct tab was remembered" + ); + is(win.selected, 1, "Selected tab has changed"); + is(win.title, REMEMBER, "The window title was correctly updated"); + + // Test more complicated case + win = closedWindowData[1]; + is(win.tabs.length, 3, "2 tabs were removed"); + is( + countOpenTabsByTitle(win.tabs, FORGET), + 0, + "The correct tabs were removed" + ); + is( + countOpenTabsByTitle(win.tabs, REMEMBER), + 3, + "The correct tabs were remembered" + ); + is(win.selected, 3, "Selected tab has changed"); + is(win.title, REMEMBER, "The window title was correctly updated"); + + // Tests handling of _closedTabs + win = closedWindowData[2]; + is( + countClosedTabsByTitle(win._closedTabs, REMEMBER), + 1, + "The correct number of tabs were removed, and the correct ones" + ); + is( + countClosedTabsByTitle(win._closedTabs, FORGET), + 0, + "All tabs to be forgotten were indeed removed" + ); + + // restore pre-test state + ss.setBrowserState(oldState); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_423132.js b/src/zen/tests/mochitests/sessionstore/browser_423132.js new file mode 100644 index 000000000..3a0113d4d --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_423132.js @@ -0,0 +1,52 @@ +"use strict"; + +/** + * Tests that cookies are stored and restored correctly + * by sessionstore (bug 423132). + */ +add_task(async function () { + const testURL = + "http://mochi.test:8888/browser/" + + "browser/components/sessionstore/test/browser_423132_sample.html"; + + Services.cookies.removeAll(); + // make sure that sessionstore.js can be forced to be created by setting + // the interval pref to 0 + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionstore.interval", 0]], + }); + + let tab = BrowserTestUtils.addTab(gBrowser, testURL); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await TabStateFlusher.flush(tab.linkedBrowser); + + // get the sessionstore state for the window + let state = ss.getBrowserState(); + + // verify our cookie got set during pageload + let i = 0; + for (var cookie of Services.cookies.cookies) { + i++; + } + Assert.equal(i, 1, "expected one cookie"); + + // remove the cookie + Services.cookies.removeAll(); + + // restore the window state + await setBrowserState(state); + + // at this point, the cookie should be restored... + for (var cookie2 of Services.cookies.cookies) { + if (cookie.name == cookie2.name) { + break; + } + } + is(cookie.name, cookie2.name, "cookie name successfully restored"); + is(cookie.value, cookie2.value, "cookie value successfully restored"); + is(cookie.path, cookie2.path, "cookie path successfully restored"); + + // clean up + Services.cookies.removeAll(); + BrowserTestUtils.removeTab(gBrowser.tabs[1]); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_423132_sample.html b/src/zen/tests/mochitests/sessionstore/browser_423132_sample.html new file mode 100644 index 000000000..6ff7e7aa3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_423132_sample.html @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_447951.js b/src/zen/tests/mochitests/sessionstore/browser_447951.js new file mode 100644 index 000000000..9daf08268 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_447951.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + /** Test for Bug 447951 */ + + waitForExplicitFinish(); + const baseURL = + "http://mochi.test:8888/browser/" + + "browser/components/sessionstore/test/browser_447951_sample.html#"; + + // Make sure the functionality added in bug 943339 doesn't affect the results + Services.prefs.setIntPref("browser.sessionstore.max_serialize_back", -1); + Services.prefs.setIntPref("browser.sessionstore.max_serialize_forward", -1); + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.sessionstore.max_serialize_back"); + Services.prefs.clearUserPref("browser.sessionstore.max_serialize_forward"); + }); + + let tab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.browserLoaded(tab.linkedBrowser, { + wantLoad: "about:blank", + }).then(() => { + let tabState = { entries: [] }; + let max_entries = Services.prefs.getIntPref( + "browser.sessionhistory.max_entries" + ); + for (let i = 0; i < max_entries; i++) { + tabState.entries.push({ url: baseURL + i, triggeringPrincipal_base64 }); + } + + promiseTabState(tab, tabState) + .then(() => { + return TabStateFlusher.flush(tab.linkedBrowser); + }) + .then(() => { + tabState = JSON.parse(ss.getTabState(tab)); + is( + tabState.entries.length, + max_entries, + "session history filled to the limit" + ); + is(tabState.entries[0].url, baseURL + 0, "... but not more"); + + // visit yet another anchor (appending it to session history) + SpecialPowers.spawn(tab.linkedBrowser, [], function () { + content.window.document.querySelector("a").click(); + }).then(flushAndCheck); + + function flushAndCheck() { + TabStateFlusher.flush(tab.linkedBrowser).then(check); + } + + function check() { + tabState = JSON.parse(ss.getTabState(tab)); + if (tab.linkedBrowser.currentURI.spec != baseURL + "end") { + // It may take a few passes through the event loop before we + // get the right URL. + executeSoon(flushAndCheck); + return; + } + + is( + tab.linkedBrowser.currentURI.spec, + baseURL + "end", + "the new anchor was loaded" + ); + is( + tabState.entries[tabState.entries.length - 1].url, + baseURL + "end", + "... and ignored" + ); + is( + tabState.entries[0].url, + baseURL + 1, + "... and the first item was removed" + ); + + // clean up + gBrowser.removeTab(tab); + finish(); + } + }); + }); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_447951_sample.html b/src/zen/tests/mochitests/sessionstore/browser_447951_sample.html new file mode 100644 index 000000000..00282f25e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_447951_sample.html @@ -0,0 +1,5 @@ + + +Testcase for bug 447951 + +
click me diff --git a/src/zen/tests/mochitests/sessionstore/browser_454908.js b/src/zen/tests/mochitests/sessionstore/browser_454908.js new file mode 100644 index 000000000..415930c32 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_454908.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +if (gFissionBrowser) { + addCoopTask( + "browser_454908_sample.html", + test_dont_save_passwords, + HTTPSROOT + ); +} +addNonCoopTask("browser_454908_sample.html", test_dont_save_passwords, ROOT); +addNonCoopTask( + "browser_454908_sample.html", + test_dont_save_passwords, + HTTPROOT +); +addNonCoopTask( + "browser_454908_sample.html", + test_dont_save_passwords, + HTTPSROOT +); + +const PASS = "pwd-" + Math.random(); + +/** + * Bug 454908 - Don't save/restore values of password fields. + */ +async function test_dont_save_passwords(aURL) { + // Make sure we do save form data. + Services.prefs.clearUserPref("browser.sessionstore.privacy_level"); + + // Add a tab with a password field. + let tab = BrowserTestUtils.addTab(gBrowser, aURL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // Fill in some values. + let usernameValue = "User " + Math.random(); + await setPropertyOfFormField(browser, "#username", "value", usernameValue); + await setPropertyOfFormField(browser, "#passwd", "value", PASS); + + // Close and restore the tab. + await promiseRemoveTabAndSessionState(tab); + tab = ss.undoCloseTab(window, 0); + browser = tab.linkedBrowser; + await promiseTabRestored(tab); + + // Check that password fields aren't saved/restored. + let username = await getPropertyOfFormField(browser, "#username", "value"); + is(username, usernameValue, "username was saved/restored"); + let passwd = await getPropertyOfFormField(browser, "#passwd", "value"); + is(passwd, "", "password wasn't saved/restored"); + + // Write to disk and read our file. + await forceSaveState(); + await promiseForEachSessionRestoreFile((state, key) => + // Ensure that we have not saved our password. + ok(!state.includes(PASS), "password has not been written to file " + key) + ); + + // Cleanup. + gBrowser.removeTab(tab); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_454908_sample.html b/src/zen/tests/mochitests/sessionstore/browser_454908_sample.html new file mode 100644 index 000000000..02f40bf20 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_454908_sample.html @@ -0,0 +1,8 @@ + +Test for bug 454908 + +

Dummy Login

+
+

Username: +

Password: +

diff --git a/src/zen/tests/mochitests/sessionstore/browser_456342.js b/src/zen/tests/mochitests/sessionstore/browser_456342.js new file mode 100644 index 000000000..68dbbbea1 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_456342.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +addCoopTask( + "browser_456342_sample.xhtml", + test_restore_nonstandard_input_values, + HTTPSROOT +); +addNonCoopTask( + "browser_456342_sample.xhtml", + test_restore_nonstandard_input_values, + ROOT +); +addNonCoopTask( + "browser_456342_sample.xhtml", + test_restore_nonstandard_input_values, + HTTPROOT +); +addNonCoopTask( + "browser_456342_sample.xhtml", + test_restore_nonstandard_input_values, + HTTPSROOT +); + +const EXPECTED_IDS = new Set(["searchTerm"]); + +const EXPECTED_XPATHS = new Set([ + "/xhtml:html/xhtml:body/xhtml:form/xhtml:p[2]/xhtml:input", + "/xhtml:html/xhtml:body/xhtml:form/xhtml:p[3]/xhtml:input[@name='fill-in']", + "/xhtml:html/xhtml:body/xhtml:form/xhtml:p[4]/xhtml:input[@name='mistyped']", + "/xhtml:html/xhtml:body/xhtml:form/xhtml:p[5]/xhtml:textarea[@name='textarea_pass']", +]); + +/** + * Bug 456342 - Restore values from non-standard input field types. + */ +async function test_restore_nonstandard_input_values(aURL) { + // Add tab with various non-standard input field types. + let tab = BrowserTestUtils.addTab(gBrowser, aURL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // Fill in form values. + let expectedValue = Math.random(); + + await SpecialPowers.spawn(browser, [expectedValue], valueChild => { + for (let elem of content.document.forms[0].elements) { + elem.value = valueChild; + let event = elem.ownerDocument.createEvent("UIEvents"); + event.initUIEvent("input", true, true, elem.documentGlobal, 0); + elem.dispatchEvent(event); + } + }); + + // Remove tab and check collected form data. + await promiseRemoveTabAndSessionState(tab); + let undoItems = ss.getClosedTabDataForWindow(window); + let savedFormData = undoItems[0].state.formdata; + + let foundIds = 0; + for (let id of Object.keys(savedFormData.id)) { + ok(EXPECTED_IDS.has(id), `Check saved ID "${id}" was expected`); + is( + savedFormData.id[id], + "" + expectedValue, + `Check saved value for #${id}` + ); + foundIds++; + } + + let foundXpaths = 0; + for (let exp of Object.keys(savedFormData.xpath)) { + ok(EXPECTED_XPATHS.has(exp), `Check saved xpath "${exp}" was expected`); + is( + savedFormData.xpath[exp], + "" + expectedValue, + `Check saved value for ${exp}` + ); + foundXpaths++; + } + + is(foundIds, EXPECTED_IDS.size, "Check number of fields saved by ID"); + is( + foundXpaths, + EXPECTED_XPATHS.size, + "Check number of fields saved by xpath" + ); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_456342_sample.xhtml b/src/zen/tests/mochitests/sessionstore/browser_456342_sample.xhtml new file mode 100644 index 000000000..ea8704d17 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_456342_sample.xhtml @@ -0,0 +1,46 @@ + + + +Test for bug 456342 + + +
+

Non-standard <input>s

+

Search

+

Image Search:

+

Autocomplete:

+

Mistyped:

+

Invalid attr: + + + +

File Selector

+ + diff --git a/src/zen/tests/mochitests/sessionstore/browser_frame_history.js b/src/zen/tests/mochitests/sessionstore/browser_frame_history.js new file mode 100644 index 000000000..4e281d261 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_frame_history.js @@ -0,0 +1,247 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/** + Ensure that frameset history works properly when restoring a tab, + provided that the frameset is static. + */ + +// Loading a toplevel frameset +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.navigation.requireUserInteraction", false], + ["dom.navigation.webidl.enabled", true], + ], + }); + + let testURL = + getRootDirectory(gTestPath) + "browser_frame_history_index.html"; + let tab = BrowserTestUtils.addTab(gBrowser, testURL); + gBrowser.selectedTab = tab; + + info("Opening a page with three frames, 4 loads should take place"); + await waitForLoadsInBrowser(tab.linkedBrowser, 4); + + let browser_b = + tab.linkedBrowser.contentDocument.getElementsByTagName("frame")[1]; + let document_b = browser_b.contentDocument; + let links = document_b.getElementsByTagName("a"); + + // We're going to click on the first link, so listen for another load event + info("Clicking on link 1, 1 load should take place"); + let promise = waitForLoadsInBrowser(tab.linkedBrowser, 1); + EventUtils.sendMouseEvent( + { type: "click" }, + links[0], + browser_b.contentWindow + ); + await promise; + + info("Clicking on link 2, 1 load should take place"); + promise = waitForLoadsInBrowser(tab.linkedBrowser, 1); + EventUtils.sendMouseEvent( + { type: "click" }, + links[1], + browser_b.contentWindow + ); + await promise; + + info("Close then un-close page, 4 loads should take place"); + await promiseRemoveTabAndSessionState(tab); + let newTab = ss.undoCloseTab(window, 0); + await waitForLoadsInBrowser(newTab.linkedBrowser, 4); + + info("Go back in time, 1 load should take place"); + gBrowser.goBack(); + await waitForLoadsInBrowser(newTab.linkedBrowser, 1); + + let expectedURLEnds = ["a.html", "b.html", "c1.html"]; + let frames = + newTab.linkedBrowser.contentDocument.getElementsByTagName("frame"); + for (let i = 0; i < frames.length; i++) { + is( + frames[i].contentDocument.location.href, + getRootDirectory(gTestPath) + + "browser_frame_history_" + + expectedURLEnds[i], + "frame " + i + " has the right url" + ); + } + + let expectedUrls = ["c", "c1", "c2"].map( + url => `${getRootDirectory(gTestPath)}browser_frame_history_${url}.html` + ); + let entryUrls = frames[2].contentWindow.navigation + .entries() + .map(entry => entry.url); + for (const [entryUrl, expectedUrl] of expectedUrls.map((url, i) => [ + entryUrls[i], + url, + ])) { + is(entryUrl, expectedUrl, "frame 2 NHE has correct url"); + } + + gBrowser.removeTab(newTab); +}); + +// Loading the frameset inside an iframe +add_task(async function () { + let testURL = + getRootDirectory(gTestPath) + "browser_frame_history_index2.html"; + let tab = BrowserTestUtils.addTab(gBrowser, testURL); + gBrowser.selectedTab = tab; + + info( + "iframe: Opening a page with an iframe containing three frames, 5 loads should take place" + ); + await waitForLoadsInBrowser(tab.linkedBrowser, 5); + + let browser_b = tab.linkedBrowser.contentDocument + .getElementById("iframe") + .contentDocument.getElementsByTagName("frame")[1]; + let document_b = browser_b.contentDocument; + let links = document_b.getElementsByTagName("a"); + + // We're going to click on the first link, so listen for another load event + info("iframe: Clicking on link 1, 1 load should take place"); + let promise = waitForLoadsInBrowser(tab.linkedBrowser, 1); + EventUtils.sendMouseEvent( + { type: "click" }, + links[0], + browser_b.contentWindow + ); + await promise; + + info("iframe: Clicking on link 2, 1 load should take place"); + promise = waitForLoadsInBrowser(tab.linkedBrowser, 1); + EventUtils.sendMouseEvent( + { type: "click" }, + links[1], + browser_b.contentWindow + ); + await promise; + + info("iframe: Close then un-close page, 5 loads should take place"); + await promiseRemoveTabAndSessionState(tab); + let newTab = ss.undoCloseTab(window, 0); + await waitForLoadsInBrowser(newTab.linkedBrowser, 5); + + info("iframe: Go back in time, 1 load should take place"); + gBrowser.goBack(); + await waitForLoadsInBrowser(newTab.linkedBrowser, 1); + + let expectedURLEnds = ["a.html", "b.html", "c1.html"]; + let frames = newTab.linkedBrowser.contentDocument + .getElementById("iframe") + .contentDocument.getElementsByTagName("frame"); + for (let i = 0; i < frames.length; i++) { + is( + frames[i].contentDocument.location.href, + getRootDirectory(gTestPath) + + "browser_frame_history_" + + expectedURLEnds[i], + "frame " + i + " has the right url" + ); + } + gBrowser.removeTab(newTab); +}); + +// Now, test that we don't record history if the iframe is added dynamically +add_task(async function () { + // Start with an empty history + let blankState = JSON.stringify({ + windows: [ + { + tabs: [ + { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] }, + ], + _closedTabs: [], + }, + ], + _closedWindows: [], + }); + await setBrowserState(blankState); + + let testURL = + getRootDirectory(gTestPath) + "browser_frame_history_index_blank.html"; + let tab = BrowserTestUtils.addTab(gBrowser, testURL); + gBrowser.selectedTab = tab; + await waitForLoadsInBrowser(tab.linkedBrowser, 1); + + info( + "dynamic: Opening a page with an iframe containing three frames, 4 dynamic loads should take place" + ); + let doc = tab.linkedBrowser.contentDocument; + let iframe = doc.createElement("iframe"); + iframe.id = "iframe"; + iframe.src = "browser_frame_history_index.html"; + doc.body.appendChild(iframe); + await waitForLoadsInBrowser(tab.linkedBrowser, 4); + + let browser_b = tab.linkedBrowser.contentDocument + .getElementById("iframe") + .contentDocument.getElementsByTagName("frame")[1]; + let document_b = browser_b.contentDocument; + let links = document_b.getElementsByTagName("a"); + + // We're going to click on the first link, so listen for another load event + info("dynamic: Clicking on link 1, 1 load should take place"); + let promise = waitForLoadsInBrowser(tab.linkedBrowser, 1); + EventUtils.sendMouseEvent( + { type: "click" }, + links[0], + browser_b.contentWindow + ); + await promise; + + info("dynamic: Clicking on link 2, 1 load should take place"); + promise = waitForLoadsInBrowser(tab.linkedBrowser, 1); + EventUtils.sendMouseEvent( + { type: "click" }, + links[1], + browser_b.contentWindow + ); + await promise; + + info("Check in the state that we have not stored this history"); + let state = ss.getBrowserState(); + info(JSON.stringify(JSON.parse(state), null, "\t")); + is( + state.indexOf("c1.html"), + -1, + "History entry was not stored in the session state" + ); + gBrowser.removeTab(tab); +}); + +// helper functions +function waitForLoadsInBrowser(aBrowser, aLoadCount) { + return new Promise(resolve => { + let loadCount = 0; + aBrowser.addEventListener( + "load", + function listener() { + if (++loadCount < aLoadCount) { + info( + "Got " + loadCount + " loads, waiting until we have " + aLoadCount + ); + return; + } + + aBrowser.removeEventListener("load", listener, true); + resolve(); + }, + true + ); + }); +} + +function timeout(delay, task) { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(true), delay); + task.then(() => resolve(false), reject); + }); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_frame_history_a.html b/src/zen/tests/mochitests/sessionstore/browser_frame_history_a.html new file mode 100644 index 000000000..8e7b35d7a --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_frame_history_a.html @@ -0,0 +1,5 @@ + + + I'm A! + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_frame_history_b.html b/src/zen/tests/mochitests/sessionstore/browser_frame_history_b.html new file mode 100644 index 000000000..38b43da21 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_frame_history_b.html @@ -0,0 +1,10 @@ + + + I'm B!
+ click me first
+ then click me
+ Close this tab.
+ Restore this tab.
+ Click back.
+ + diff --git a/src/zen/tests/mochitests/sessionstore/browser_frame_history_c.html b/src/zen/tests/mochitests/sessionstore/browser_frame_history_c.html new file mode 100644 index 000000000..0efd7d902 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_frame_history_c.html @@ -0,0 +1,5 @@ + + + I'm C! + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_frame_history_c1.html b/src/zen/tests/mochitests/sessionstore/browser_frame_history_c1.html new file mode 100644 index 000000000..b55c1d45a --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_frame_history_c1.html @@ -0,0 +1,5 @@ + + + I'm C1! + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_frame_history_c2.html b/src/zen/tests/mochitests/sessionstore/browser_frame_history_c2.html new file mode 100644 index 000000000..aec504141 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_frame_history_c2.html @@ -0,0 +1,5 @@ + + + I'm C2! + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_frame_history_index.html b/src/zen/tests/mochitests/sessionstore/browser_frame_history_index.html new file mode 100644 index 000000000..04a44555a --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_frame_history_index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_frame_history_index2.html b/src/zen/tests/mochitests/sessionstore/browser_frame_history_index2.html new file mode 100644 index 000000000..d465abef6 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_frame_history_index2.html @@ -0,0 +1,3 @@ + + + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_global_store.js b/src/zen/tests/mochitests/sessionstore/browser_global_store.js new file mode 100644 index 000000000..99aa67218 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_global_store.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the API for saving global session data. +add_task(async function () { + const key1 = "Unique name 1: " + Date.now(); + const key2 = "Unique name 2: " + Date.now(); + const value1 = "Unique value 1: " + Math.random(); + const value2 = "Unique value 2: " + Math.random(); + + let global = {}; + global[key1] = value1; + + const testState = { + windows: [ + { + tabs: [ + { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] }, + ], + }, + ], + global, + }; + + function testRestoredState() { + is( + ss.getCustomGlobalValue(key1), + value1, + "restored state has global value" + ); + } + + function testGlobalStore() { + is(ss.getCustomGlobalValue(key2), "", "global value initially not set"); + + ss.setCustomGlobalValue(key2, value1); + is(ss.getCustomGlobalValue(key2), value1, "retreived value matches stored"); + + ss.setCustomGlobalValue(key2, value2); + is( + ss.getCustomGlobalValue(key2), + value2, + "previously stored value was overwritten" + ); + + ss.deleteCustomGlobalValue(key2); + is(ss.getCustomGlobalValue(key2), "", "global value was deleted"); + } + + await promiseBrowserState(testState); + testRestoredState(); + testGlobalStore(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_history_persist.js b/src/zen/tests/mochitests/sessionstore/browser_history_persist.js new file mode 100644 index 000000000..59f1a9445 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_history_persist.js @@ -0,0 +1,140 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Ensure that history entries that should not be persisted are restored in the + * same state. + */ +add_task(async function check_history_not_persisted() { + // Create an about:blank tab + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + + // Retrieve the tab state. + await TabStateFlusher.flush(browser); + let state = JSON.parse(ss.getTabState(tab)); + is( + state.entries[0].transient, + true, + "Should have collected the transient state" + ); + BrowserTestUtils.removeTab(tab); + browser = null; + + // Open a new tab to restore into. + tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + browser = tab.linkedBrowser; + + { + let sessionHistory = browser.browsingContext.sessionHistory; + + // addTab sends a message to the child to load about:blank, which sends a + // message to the parent to add the SH entry. + // promiseTabState modifies the SH syncronously, so ensure it is settled. + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + is(sessionHistory.count, 1, "Should have initial entry"); + } + + info("New about:blank loaded, restoring state"); + await promiseTabState(tab, state); + + { + let sessionHistory = browser.browsingContext.sessionHistory; + is(sessionHistory.count, 1, "Should be a single history entry"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:blank", + "Should be the right URL" + ); + } + + // Load a new URL into the tab, it should replace the about:blank history entry + BrowserTestUtils.startLoadingURIString(browser, "about:robots"); + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:robots" }); + + { + let sessionHistory = browser.browsingContext.sessionHistory; + + is(sessionHistory.count, 1, "Should be a single history entry"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:robots", + "Should be the right URL" + ); + } + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); + +/** + * Check that entries default to being persisted when the attribute doesn't + * exist + */ +add_task(async function check_history_default_persisted() { + // Create an about:blank tab + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + + // Retrieve the tab state. + await TabStateFlusher.flush(browser); + let state = JSON.parse(ss.getTabState(tab)); + delete state.entries[0].transient; + BrowserTestUtils.removeTab(tab); + browser = null; + + // Open a new tab to restore into. + tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + browser = tab.linkedBrowser; + + { + let sessionHistory = browser.browsingContext.sessionHistory; + + // addTab sends a message to the child to load about:blank, which sends a + // message to the parent to add the SH entry. + // promiseTabState modifies the SH syncronously, so ensure it is settled. + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + is(sessionHistory.count, 1, "Should have initial entry"); + } + + info("New about:blank loaded, restoring state"); + await promiseTabState(tab, state); + + { + let sessionHistory = browser.browsingContext.sessionHistory; + + is(sessionHistory.count, 1, "Should be a single history entry"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:blank", + "Should be the right URL" + ); + } + + // Load a new URL into the tab, it should NOT replace the about:blank history entry + BrowserTestUtils.startLoadingURIString(browser, "about:robots"); + await promiseBrowserLoaded(browser, false, "about:robots"); + + { + let sessionHistory = browser.browsingContext.sessionHistory; + + is(sessionHistory.count, 2, "Should be two history entries"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:blank", + "Should be the right URL" + ); + is( + sessionHistory.getEntryAtIndex(1).URI.spec, + "about:robots", + "Should be the right URL" + ); + } + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_ignore_updates_crashed_tabs.js b/src/zen/tests/mochitests/sessionstore/browser_ignore_updates_crashed_tabs.js new file mode 100644 index 000000000..b70a3aa39 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_ignore_updates_crashed_tabs.js @@ -0,0 +1,108 @@ +// This test checks that browsers are removed from the SessionStore's +// crashed browser set at a correct time, so that it can stop ignoring update +// events coming from those browsers. + +/** + * Open a tab, crash it, navigate it to a remote uri, and check that it + * is removed from a crashed set. + */ +add_task(async function test_update_crashed_tab_after_navigate_to_remote() { + let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/"); + let browser = tab.linkedBrowser; + gBrowser.selectedTab = tab; + await promiseBrowserLoaded(browser); + ok( + !SessionStore.isBrowserInCrashedSet(browser), + "browser is not in the crashed set" + ); + + await BrowserTestUtils.crashFrame(browser); + ok( + SessionStore.isBrowserInCrashedSet(browser), + "browser is in the crashed set" + ); + + BrowserTestUtils.startLoadingURIString(browser, "https://example.org/"); + await BrowserTestUtils.browserLoaded(browser, false, "https://example.org/"); + ok( + !SessionStore.isBrowserInCrashedSet(browser), + "browser is not in the crashed set" + ); + ok( + !tab.hasAttribute("crashed"), + "Tab shouldn't be marked as crashed anymore." + ); + gBrowser.removeTab(tab); +}); + +/** + * Open a tab, crash it, navigate it to a non-remote uri, and check that it + * is removed from a crashed set. + */ +add_task(async function test_update_crashed_tab_after_navigate_to_non_remote() { + let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/"); + let browser = tab.linkedBrowser; + gBrowser.selectedTab = tab; + await promiseBrowserLoaded(browser); + ok( + !SessionStore.isBrowserInCrashedSet(browser), + "browser is not in the crashed set" + ); + + await BrowserTestUtils.crashFrame(browser); + ok( + SessionStore.isBrowserInCrashedSet(browser), + "browser is in the crashed set" + ); + + BrowserTestUtils.startLoadingURIString(browser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(browser, false, "about:mozilla"); + ok( + !SessionStore.isBrowserInCrashedSet(browser), + "browser is not in the crashed set" + ); + ok( + !gBrowser.selectedTab.hasAttribute("crashed"), + "Tab shouldn't be marked as crashed anymore." + ); + gBrowser.removeTab(tab); +}); + +/** + * Open a tab, crash it, restore it from history, and check that it + * is removed from a crashed set. + */ +add_task(async function test_update_crashed_tab_after_session_restore() { + let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/"); + let browser = tab.linkedBrowser; + gBrowser.selectedTab = tab; + await promiseBrowserLoaded(browser); + ok( + !SessionStore.isBrowserInCrashedSet(browser), + "browser is not in the crashed set" + ); + + await BrowserTestUtils.crashFrame(browser); + ok( + SessionStore.isBrowserInCrashedSet(browser), + "browser is in the crashed set" + ); + + let tabRestoredPromise = promiseTabRestored(tab); + // Click restoreTab button + await SpecialPowers.spawn(browser, [], () => { + let button = content.document.getElementById("restoreTab"); + button.click(); + }); + await tabRestoredPromise; + ok( + !tab.hasAttribute("crashed"), + "Tab shouldn't be marked as crashed anymore." + ); + ok( + !SessionStore.isBrowserInCrashedSet(browser), + "browser is not in the crashed set" + ); + + gBrowser.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_label_and_icon.js b/src/zen/tests/mochitests/sessionstore/browser_label_and_icon.js new file mode 100644 index 000000000..a76db891b --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_label_and_icon.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["test.wait300msAfterTabSwitch", true]], + }); +}); + +/** + * Ensure that a pending tab has label and icon correctly set. + */ +add_task(async function test_label_and_icon() { + // Make sure that tabs are restored on demand as otherwise the tab will start + // loading immediately and we can't check its icon and label. + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionstore.restore_on_demand", true]], + }); + + // Create a new tab. + let tab = BrowserTestUtils.addTab(gBrowser, "about:robots"); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + // Because there is debounce logic in FaviconLoader.sys.mjs to reduce the + // favicon loads, we have to wait some time before checking that icon was + // stored properly. + await TestUtils.waitForCondition( + () => { + return gBrowser.getIcon(tab) != null; + }, + "wait for favicon load to finish", + 100, + 5 + ); + + // Retrieve the tab state. + await TabStateFlusher.flush(browser); + let state = ss.getTabState(tab); + BrowserTestUtils.removeTab(tab); + browser = null; + + // Open a new tab to restore into. + tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + ss.setTabState(tab, state); + await promiseTabRestoring(tab); + + // Check that label and icon are set for the restoring tab. + is( + gBrowser.getIcon(tab), + "chrome://browser/content/robot.ico", + "icon is set" + ); + is(tab.label, "Gort! Klaatu barada nikto!", "label is set"); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_movePendingTabToNewWindow.js b/src/zen/tests/mochitests/sessionstore/browser_movePendingTabToNewWindow.js new file mode 100644 index 000000000..916426cda --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_movePendingTabToNewWindow.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests the behaviour of moving pending tabs to a new window. These + * pending tabs have yet to be restored and should be restored upon opening + * in the new window. This test covers moving a single pending tab at once + * as well as multiple tabs at the same time (using tab multiselection). + */ +add_task(async function test_movePendingTabToNewWindow() { + const TEST_URIS = [ + "http://www.example.com/1", + "http://www.example.com/2", + "http://www.example.com/3", + "http://www.example.com/4", + ]; + + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionstore.restore_on_demand", true]], + }); + + let state = { + windows: [ + { + tabs: [ + { entries: [{ url: TEST_URIS[0], triggeringPrincipal_base64 }] }, + { entries: [{ url: TEST_URIS[1], triggeringPrincipal_base64 }] }, + { entries: [{ url: TEST_URIS[2], triggeringPrincipal_base64 }] }, + { entries: [{ url: TEST_URIS[3], triggeringPrincipal_base64 }] }, + ], + selected: 4, + }, + ], + }; + + await promiseBrowserState(state); + + is( + gBrowser.visibleTabs.length, + 4, + "Three tabs are visible to start the test" + ); + + let tabToSelect = gBrowser.visibleTabs[1]; + ok(tabToSelect.hasAttribute("pending"), "Tab should be pending"); + + gBrowser.addRangeToMultiSelectedTabs(gBrowser.selectedTab, tabToSelect); + ok(!gBrowser.visibleTabs[0].multiselected, "First tab not multiselected"); + ok(gBrowser.visibleTabs[1].multiselected, "Second tab multiselected"); + ok(gBrowser.visibleTabs[2].multiselected, "Third tab multiselected"); + ok(gBrowser.visibleTabs[3].multiselected, "Fourth tab multiselected"); + + let promiseNewWindow = BrowserTestUtils.waitForNewWindow(); + gBrowser.replaceTabsWithWindow(tabToSelect); + + info("Waiting for new window"); + let newWindow = await promiseNewWindow; + isnot(newWindow, gBrowser.documentGlobal, "Tab moved to new window"); + + let newWindowTabs = newWindow.gBrowser.visibleTabs; + await TestUtils.waitForCondition(() => { + return ( + newWindowTabs.length == 3 && + newWindowTabs[0].linkedBrowser.currentURI.spec == TEST_URIS[1] && + newWindowTabs[1].linkedBrowser.currentURI.spec == TEST_URIS[2] && + newWindowTabs[2].linkedBrowser.currentURI.spec == TEST_URIS[3] + ); + }, "Wait for all three tabs to move to new window and load"); + + is(newWindowTabs.length, 3, "Three tabs should be in new window"); + is( + newWindowTabs[0].linkedBrowser.currentURI.spec, + TEST_URIS[1], + "Second tab moved" + ); + is( + newWindowTabs[1].linkedBrowser.currentURI.spec, + TEST_URIS[2], + "Third tab moved" + ); + is( + newWindowTabs[2].linkedBrowser.currentURI.spec, + TEST_URIS[3], + "Fourth tab moved" + ); + + ok( + newWindowTabs[0].hasAttribute("pending"), + "First tab in new window should still be pending" + ); + ok( + newWindowTabs[1].hasAttribute("pending"), + "Second tab in new window should still be pending" + ); + newWindow.gBrowser.clearMultiSelectedTabs(); + ok( + newWindowTabs.every(t => !t.multiselected), + "No multiselection should be present" + ); + + promiseNewWindow = BrowserTestUtils.waitForNewWindow(); + newWindow.gBrowser.replaceTabsWithWindow(newWindowTabs[0]); + + info("Waiting for second new window"); + let secondNewWindow = await promiseNewWindow; + await TestUtils.waitForCondition( + () => + secondNewWindow.gBrowser.selectedBrowser.currentURI.spec == TEST_URIS[1], + "Wait until the URI is updated" + ); + is( + secondNewWindow.gBrowser.visibleTabs.length, + 1, + "Only one tab in second new window" + ); + is( + secondNewWindow.gBrowser.selectedBrowser.currentURI.spec, + TEST_URIS[1], + "First tab moved" + ); + + await BrowserTestUtils.closeWindow(secondNewWindow); + await BrowserTestUtils.closeWindow(newWindow); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_multiple_navigateAndRestore.js b/src/zen/tests/mochitests/sessionstore/browser_multiple_navigateAndRestore.js new file mode 100644 index 000000000..e2ee1b9e3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_multiple_navigateAndRestore.js @@ -0,0 +1,45 @@ +"use strict"; + +const PAGE_1 = + "data:text/html,A%20regular,%20everyday,%20normal%20page."; +const PAGE_2 = + "data:text/html,Another%20regular,%20everyday,%20normal%20page."; + +add_task(async function () { + // Load an empty, non-remote tab at about:blank... + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + forceNotRemote: true, + }); + gBrowser.selectedTab = tab; + let browser = gBrowser.selectedBrowser; + ok(!browser.isRemoteBrowser, "Ensure browser is not remote"); + // Load a remote page, and then another remote page immediately + // after. + BrowserTestUtils.startLoadingURIString(browser, PAGE_1); + browser.stop(); + BrowserTestUtils.startLoadingURIString(browser, PAGE_2); + await BrowserTestUtils.browserLoaded(browser, false, PAGE_2); + + ok(browser.isRemoteBrowser, "Should have switched remoteness"); + await TabStateFlusher.flush(browser); + let state = JSON.parse(ss.getTabState(tab)); + let entries = state.entries; + is(entries.length, 1, "There should only be one entry"); + is(entries[0].url, PAGE_2, "Should have PAGE_2 as the sole history entry"); + is( + browser.currentURI.spec, + PAGE_2, + "Should have PAGE_2 as the browser currentURI" + ); + + await SpecialPowers.spawn(browser, [PAGE_2], async function (expectedURL) { + docShell.QueryInterface(Ci.nsIWebNavigation); + Assert.equal( + docShell.currentURI.spec, + expectedURL, + "Content should have PAGE_2 as the browser currentURI" + ); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_multiple_select_after_load.js b/src/zen/tests/mochitests/sessionstore/browser_multiple_select_after_load.js new file mode 100644 index 000000000..dcb896e43 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_multiple_select_after_load.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = `data:text/html;charset=utf-8, +`; + +const VALUES = ["1", "3"]; + +// Tests that a document that changes a element and select some + // options. + await setPropertyOfFormField(tab.linkedBrowser, "select", "multiple", true); + + for (let v of VALUES) { + await setPropertyOfFormField( + tab.linkedBrowser, + `option[value="${v}"]`, + "selected", + true + ); + } + + // Remove the tab. + await promiseRemoveTabAndSessionState(tab); + + // Verify state of the closed tab. + let tabData = ss.getClosedTabDataForWindow(window); + Assert.deepEqual( + tabData[0].state.formdata.id.select, + VALUES, + "Collected correct formdata" + ); + + // Restore the close tab. + tab = ss.undoCloseTab(window, 0); + await promiseTabRestored(tab); + ok(true, "Didn't crash!"); + + // Cleanup. + gBrowser.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_navigation_api_restore.js b/src/zen/tests/mochitests/sessionstore/browser_navigation_api_restore.js new file mode 100644 index 000000000..51ffe1b8f --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_navigation_api_restore.js @@ -0,0 +1,156 @@ +"use strict"; + +const PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" +); +const EMPTY_URL = PATH + "empty.html"; +const EMPTY_FRAME_URL = PATH + "empty_frame.html"; + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["dom.navigation.webidl.enabled", true]], + }); +}); + +async function checkNavigationEntries(browser, urls, inFrame = null) { + let entryUrls = await SpecialPowers.spawn(browser, [inFrame], aInFrame => + (aInFrame ? content.frames[0].navigation : content.navigation) + .entries() + .map(entry => entry.url) + ); + for (const [entryUrl, expectedUrl] of urls.map((expected, i) => [ + entryUrls[i], + expected, + ])) { + is(entryUrl, expectedUrl); + } +} + +add_task(async function test_toplevel_fragment_navigations() { + let tab = BrowserTestUtils.addTab(gBrowser, EMPTY_URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + let hashes = ["#1", "#2", "#3"]; + for (const hash of hashes) { + await SpecialPowers.spawn(browser, [hash], async url => { + content.history.pushState(null, "", url); + }); + } + await SpecialPowers.spawn(browser, [], async () => { + content.history.back(); + }); + + let expectedUrls = ["", ...hashes].map(hash => `${EMPTY_URL}${hash}`); + await checkNavigationEntries(browser, expectedUrls); + + await TabStateFlusher.flush(browser); + let { entries } = JSON.parse(ss.getTabState(tab)); + is(entries.length, 4); + + await SessionStoreTestUtils.closeTab(tab); + tab = ss.undoCloseTab(window, 0); + await promiseTabRestored(tab); + browser = tab.linkedBrowser; + + await checkNavigationEntries(browser, expectedUrls); + + gBrowser.removeTab(tab); +}); + +add_task(async function test_frame_fragment_navigations() { + let tab = BrowserTestUtils.addTab(gBrowser, EMPTY_FRAME_URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + let hashes = ["#1", "#2", "#3"]; + for (const hash of hashes) { + await SpecialPowers.spawn(browser, [hash], async url => { + content.frames[0].history.pushState(null, "", url); + }); + } + await SpecialPowers.spawn(browser, [], async () => { + content.history.back(); + }); + + let expectedUrls = ["", ...hashes].map(hash => `${EMPTY_URL}${hash}`); + await checkNavigationEntries(browser, expectedUrls, true); + + await TabStateFlusher.flush(browser); + let { entries } = JSON.parse(ss.getTabState(tab)); + is(entries.length, 4); + + await SessionStoreTestUtils.closeTab(tab); + tab = ss.undoCloseTab(window, 0); + await promiseTabRestored(tab); + browser = tab.linkedBrowser; + + await checkNavigationEntries(browser, expectedUrls, true); + + gBrowser.removeTab(tab); +}); + +add_task(async function test_mixed_fragment_navigations() { + let tab = BrowserTestUtils.addTab(gBrowser, EMPTY_FRAME_URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + let hashes = ["#frame1", "#frame2", "#frame3"]; + + await SpecialPowers.spawn(browser, [], async () => { + content.frames[0].history.pushState(null, "", "#frame1"); + content.frames[0].history.pushState(null, "", "#frame2"); + content.history.pushState(null, "", "#top"); + content.frames[0].history.pushState(null, "", "#frame3"); + }); + + let expectedTopUrls = ["", "#top"].map(hash => `${EMPTY_FRAME_URL}${hash}`); + await checkNavigationEntries(browser, expectedTopUrls); + let expectedFrameUrls = ["", ...hashes].map(hash => `${EMPTY_URL}${hash}`); + await checkNavigationEntries(browser, expectedFrameUrls, true); + + await TabStateFlusher.flush(browser); + let { entries } = JSON.parse(ss.getTabState(tab)); + is(entries.length, 5, "Got expected number of history entries"); + + await SessionStoreTestUtils.closeTab(tab); + tab = ss.undoCloseTab(window, 0); + await promiseTabRestored(tab); + browser = tab.linkedBrowser; + + await checkNavigationEntries(browser, expectedTopUrls); + await checkNavigationEntries(browser, expectedFrameUrls, true); + + gBrowser.removeTab(tab); +}); + +add_task(async function test_toplevel_navigations() { + let tab = BrowserTestUtils.addTab(gBrowser, EMPTY_URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + let urls = ["entry1.html", "entry2.html", "entry3.html"].map( + url => `${PATH}${url}` + ); + for (const url of urls) { + BrowserTestUtils.startLoadingURIString(browser, url); + await BrowserTestUtils.browserLoaded(browser); + } + + let expectedUrls = [EMPTY_URL, ...urls]; + await checkNavigationEntries(browser, expectedUrls); + + await TabStateFlusher.flush(browser); + let { entries } = JSON.parse(ss.getTabState(tab)); + is(entries.length, 4); + + await SessionStoreTestUtils.closeTab(tab); + tab = ss.undoCloseTab(window, 0); + await promiseTabRestored(tab); + browser = tab.linkedBrowser; + + await checkNavigationEntries(browser, expectedUrls); + + gBrowser.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_newtab_userTypedValue.js b/src/zen/tests/mochitests/sessionstore/browser_newtab_userTypedValue.js new file mode 100644 index 000000000..441652cb4 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_newtab_userTypedValue.js @@ -0,0 +1,97 @@ +"use strict"; + +requestLongerTimeout(4); + +/** + * Test that when restoring an 'initial page' with session restore, it + * produces an empty URL bar, rather than leaving its URL explicitly + * there as a 'user typed value'. + */ +add_task(async function () { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "https://example.com/" + ); + let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + win.gBrowser, + () => {} + ); + + // This opens about:newtab: + win.BrowserCommands.openTab(); + let tab = await tabOpenedAndSwitchedTo; + is(win.gURLBar.value, "", "URL bar should be empty"); + is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null"); + let state = JSON.parse(SessionStore.getTabState(tab)); + ok( + !state.userTypedValue, + "userTypedValue should be undefined on the tab's state" + ); + tab = null; + + await BrowserTestUtils.closeWindow(win); + + ok(SessionStore.getClosedWindowCount(), "Should have a closed window"); + + await forceSaveState(); + + win = SessionStore.undoCloseWindow(0); + await TestUtils.topicObserved( + "sessionstore-single-window-restored", + subject => subject == win + ); + // Don't wait for load here because it's about:newtab and we may have swapped in + // a preloaded browser. + await TabStateFlusher.flush(win.gBrowser.selectedBrowser); + + is(win.gURLBar.value, "", "URL bar should be empty"); + tab = win.gBrowser.selectedTab; + is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null"); + state = JSON.parse(SessionStore.getTabState(tab)); + ok( + !state.userTypedValue, + "userTypedValue should be undefined on the tab's state" + ); + + BrowserTestUtils.removeTab(tab); + + for (let url of gInitialPages) { + if (url == BROWSER_NEW_TAB_URL || url === "about:opentabs") { + continue; // We tested about:newtab using BrowserCommands.openTab() above. + } + info("Testing " + url + " - " + new Date()); + await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url); + await BrowserTestUtils.closeWindow(win); + + ok(SessionStore.getClosedWindowCount(), "Should have a closed window"); + + await forceSaveState(); + + win = SessionStore.undoCloseWindow(0); + await TestUtils.topicObserved( + "sessionstore-single-window-restored", + subject => subject == win + ); + await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser, { + wantLoad: url, + }); + await TabStateFlusher.flush(win.gBrowser.selectedBrowser); + + is(win.gURLBar.value, "", "URL bar should be empty"); + tab = win.gBrowser.selectedTab; + is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null"); + state = JSON.parse(SessionStore.getTabState(tab)); + ok( + !state.userTypedValue, + "userTypedValue should be undefined on the tab's state" + ); + + info("Removing tab - " + new Date()); + BrowserTestUtils.removeTab(tab); + info("Finished removing tab - " + new Date()); + } + info("Removing window - " + new Date()); + await BrowserTestUtils.closeWindow(win); + info("Finished removing window - " + new Date()); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_not_collect_when_idle.js b/src/zen/tests/mochitests/sessionstore/browser_not_collect_when_idle.js new file mode 100644 index 000000000..ddb088813 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_not_collect_when_idle.js @@ -0,0 +1,118 @@ +/** Test for Bug 1305950 */ + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +// The mock idle service. +var idleService = { + _observers: new Set(), + _activity: { + addCalls: [], + removeCalls: [], + observerFires: [], + }, + + _reset() { + this._observers.clear(); + this._activity.addCalls = []; + this._activity.removeCalls = []; + this._activity.observerFires = []; + }, + + _fireObservers(state) { + for (let observer of this._observers.values()) { + observer.observe(observer, state, null); + this._activity.observerFires.push(state); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIUserIdleService"]), + idleTime: 19999, + + addIdleObserver(observer, time) { + this._observers.add(observer); + this._activity.addCalls.push(time); + }, + + removeIdleObserver(observer, time) { + this._observers.delete(observer); + this._activity.removeCalls.push(time); + }, +}; + +add_task(async function testIntervalChanges() { + const PREF_SS_INTERVAL = 2000; + + // We speed up the interval between session saves to ensure that the test + // runs quickly. + Services.prefs.setIntPref("browser.sessionstore.interval", PREF_SS_INTERVAL); + + // Increase `idleDelay` to 1 day to update the pre-registered idle observer + // in "real" idle service to avoid possible interference, especially for the + // CI server environment. + Services.prefs.setIntPref("browser.sessionstore.idleDelay", 86400); + + // Mock an idle service. + let fakeIdleService = MockRegistrar.register( + "@mozilla.org/widget/useridleservice;1", + idleService + ); + idleService._reset(); + + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.sessionstore.interval"); + MockRegistrar.unregister(fakeIdleService); + }); + + // Hook idle/active observer to mock idle service by changing pref `idleDelay` + // to a whatever value, which will not be used. + Services.prefs.setIntPref("browser.sessionstore.idleDelay", 5000); + + // Wait a `sessionstore-state-write-complete` event from any previous + // scheduled state write. This is needed since the `_lastSaveTime` in + // runDelayed() should be set at least once, or the `_isIdle` flag will not + // become effective. + info("Waiting for sessionstore-state-write-complete notification"); + await TestUtils.topicObserved("sessionstore-state-write-complete"); + + info( + "Got the sessionstore-state-write-complete notification, now testing idle mode" + ); + + // Enter the "idle mode" (raise the `_isIdle` flag) by firing idle + // observer of mock idle service. + idleService._fireObservers("idle"); + + // Cancel any possible state save, which is not related with this test to + // avoid interference. + SessionSaver.cancel(); + + let p1 = promiseSaveState(); + + // Schedule a state write, which is expeced to be postponed after about + // `browser.sessionstore.interval.idle` ms, since the idle flag was just set. + SessionSaver.runDelayed(0); + + // We expect `p1` hits the timeout. + await Assert.rejects( + p1, + /Save state timeout/, + "[Test 1A] No state write during idle." + ); + + // Test again for better reliability. Same, we expect following promise hits + // the timeout. + await Assert.rejects( + promiseSaveState(), + /Save state timeout/, + "[Test 1B] Again: No state write during idle." + ); + + // Back to the active mode. + info("Start to test active mode..."); + idleService._fireObservers("active"); + + info("[Test 2] Waiting for sessionstore-state-write-complete during active"); + await TestUtils.topicObserved("sessionstore-state-write-complete"); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_oldformat.toml b/src/zen/tests/mochitests/sessionstore/browser_oldformat.toml new file mode 100644 index 000000000..54842390e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_oldformat.toml @@ -0,0 +1,301 @@ +[DEFAULT] +support-files = [ + "head.js", + "browser_formdata_sample.html", + "browser_formdata_xpath_sample.html", + "browser_frametree_sample.html", + "browser_frametree_sample_frameset.html", + "browser_frametree_sample_iframes.html", + "browser_frame_history_index.html", + "browser_frame_history_index2.html", + "browser_frame_history_index_blank.html", + "browser_frame_history_a.html", + "browser_frame_history_b.html", + "browser_frame_history_c.html", + "browser_frame_history_c1.html", + "browser_frame_history_c2.html", + "browser_formdata_format_sample.html", + "browser_sessionHistory_slow.sjs", + "browser_scrollPositions_sample.html", + "browser_scrollPositions_sample2.html", + "browser_scrollPositions_sample_frameset.html", + "browser_scrollPositions_readerModeArticle.html", + "browser_sessionStorage.html", + "browser_speculative_connect.html", + "browser_248970_b_sample.html", + "browser_339445_sample.html", + "browser_423132_sample.html", + "browser_447951_sample.html", + "browser_454908_sample.html", + "browser_456342_sample.xhtml", + "browser_463205_sample.html", + "browser_463206_sample.html", + "browser_466937_sample.html", + "browser_485482_sample.html", + "browser_637020_slow.sjs", + "browser_662743_sample.html", + "browser_739531_sample.html", + "browser_739531_frame.html", + "coopHeaderCommon.sjs", + "restore_redirect_http.html", + "restore_redirect_http.html^headers^", + "restore_redirect_js.html", + "restore_redirect_target.html", + "browser_1234021_page.html", + "browser_1284886_suspend_tab.html", + "browser_1284886_suspend_tab_2.html", + "empty.html", + "coop_coep.html", + "coop_coep.html^headers^", +] +# remove this after bug 1628486 is landed +prefs = [ + "network.cookie.cookieBehavior=5", + "gfx.font_rendering.fallback.async=false", + "browser.sessionstore.closedTabsFromAllWindows=true", + "browser.sessionstore.closedTabsFromClosedWindows=true", +] + +#NB: the following are disabled +# browser_464620_a.html +# browser_464620_b.html +# browser_464620_xd.html + +#disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html +#disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html +#disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html + +["browser_1234021.js"] + +["browser_1284886_suspend_tab.js"] + +["browser_1446343-windowsize.js"] +skip-if = [ + "os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland'", # Bug 1600180 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11'", # Bug 1600180 +] + +["browser_248970_b_perwindowpb.js"] +# Disabled because of leaks. +# Re-enabling and rewriting this test is tracked in bug 936919. +skip-if = [ + "true", +] + +["browser_339445.js"] + +["browser_345898.js"] + +["browser_350525.js"] + +["browser_354894_perwindowpb.js"] +skip-if = [ + "os == 'win' && os_version == '11.26200' && asan", # Bug 2019745 +] + +["browser_367052.js"] + +["browser_393716.js"] +skip-if = [ + "debug", # Bug 1507747 +] + +["browser_394759_basic.js"] +# Disabled for intermittent failures, bug 944372. +skip-if = [ + "true", +] + +["browser_394759_behavior.js"] +https_first_disabled = true + +["browser_394759_perwindowpb.js"] + +["browser_394759_purge.js"] + +["browser_423132.js"] + +["browser_447951.js"] + +["browser_454908.js"] + +["browser_456342.js"] +skip-if = [ + "verify-standalone", +] + +["browser_461634.js"] + +["browser_463205.js"] + +["browser_463206.js"] + +["browser_464199.js"] +# Disabled for frequent intermittent failures + +["browser_464620_a.js"] +skip-if = [ + "true", +] + +["browser_464620_b.js"] +skip-if = [ + "true", +] + +["browser_465215.js"] + +["browser_465223.js"] + +["browser_466937.js"] + +["browser_467409-backslashplosion.js"] + +["browser_477657.js"] +skip-if = [ + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11'", # Bug 1610668 +] + +["browser_480893.js"] + +["browser_485482.js"] + +["browser_485563.js"] + +["browser_490040.js"] + +["browser_491168.js"] + +["browser_491577.js"] + +["browser_495495.js"] + +["browser_500328.js"] + +["browser_514751.js"] + +["browser_522375.js"] + +["browser_522545.js"] +skip-if = [ + "true", # Bug 1380968 +] + +["browser_524745.js"] + +["browser_528776.js"] + +["browser_579868.js"] + +["browser_579879.js"] + +["browser_581937.js"] + +["browser_586068-apptabs.js"] + +["browser_586068-apptabs_ondemand.js"] + +["browser_586068-browser_state_interrupted.js"] + +["browser_586068-cascade.js"] + +["browser_586068-multi_window.js"] + +["browser_586068-reload.js"] +https_first_disabled = true + +["browser_586068-select.js"] + +["browser_586068-window_state.js"] + +["browser_586068-window_state_override.js"] + +["browser_586147.js"] + +["browser_588426.js"] + +["browser_590268.js"] + +["browser_590563.js"] +skip-if = [ + "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", +] + +["browser_595601-restore_hidden.js"] + +["browser_597071.js"] + +["browser_600545.js"] + +["browser_601955.js"] + +["browser_607016.js"] + +["browser_615394-SSWindowState_events_duplicateTab.js"] + +["browser_615394-SSWindowState_events_setBrowserState.js"] + +["browser_615394-SSWindowState_events_setTabState.js"] + +["browser_615394-SSWindowState_events_setWindowState.js"] +https_first_disabled = true + +["browser_615394-SSWindowState_events_undoCloseTab.js"] + +["browser_615394-SSWindowState_events_undoCloseWindow.js"] + +["browser_618151.js"] + +["browser_623779.js"] + +["browser_624727.js"] + +["browser_625016.js"] +run-if = [ + "os != 'mac'", # not running on macOS where closing all windows doesn't quit the application +] + +["browser_628270.js"] + +["browser_635418.js"] + +["browser_636279.js"] + +["browser_637020.js"] + +["browser_645428.js"] + +["browser_659591.js"] + +["browser_662743.js"] + +["browser_662812.js"] + +["browser_665702-state_session.js"] + +["browser_682507.js"] + +["browser_687710.js"] + +["browser_687710_2.js"] +https_first_disabled = true + +["browser_694378.js"] + +["browser_701377.js"] + +["browser_705597.js"] + +["browser_707862.js"] + +["browser_739531.js"] + +["browser_739805.js"] + +["browser_819510_perwindowpb.js"] +skip-if = [ + "true", # Bug 1284312, Bug 1341980, bug 1381451 +] + +["browser_906076_lazy_tabs.js"] +https_first_disabled = true diff --git a/src/zen/tests/mochitests/sessionstore/browser_page_title.js b/src/zen/tests/mochitests/sessionstore/browser_page_title.js new file mode 100644 index 000000000..9f84e67a9 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_page_title.js @@ -0,0 +1,54 @@ +"use strict"; + +const URL = "data:text/html,initial title"; + +add_task(async function () { + // Create a new tab. + let tab = BrowserTestUtils.addTab(gBrowser, URL); + await promiseBrowserLoaded(tab.linkedBrowser); + + // Remove the tab. + await promiseRemoveTabAndSessionState(tab); + + // Check the title. + let [ + { + state: { entries }, + }, + ] = ss.getClosedTabDataForWindow(window); + is(entries[0].title, "initial title", "correct title"); +}); + +add_task(async function () { + // Create a new tab. + let tab = BrowserTestUtils.addTab(gBrowser, URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // Flush to ensure we collected the initial title. + await TabStateFlusher.flush(browser); + + // Set a new title. + await SpecialPowers.spawn(browser, [], async function () { + return new Promise(resolve => { + docShell.chromeEventHandler.addEventListener( + "DOMTitleChanged", + () => resolve(), + { once: true } + ); + + content.document.title = "new title"; + }); + }); + + // Remove the tab. + await promiseRemoveTabAndSessionState(tab); + + // Check the title. + let [ + { + state: { entries }, + }, + ] = ss.getClosedTabDataForWindow(window); + is(entries[0].title, "new title", "correct title"); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_parentProcessRestoreHash.js b/src/zen/tests/mochitests/sessionstore/browser_parentProcessRestoreHash.js new file mode 100644 index 000000000..ad8144f86 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_parentProcessRestoreHash.js @@ -0,0 +1,115 @@ +"use strict"; + +const SELFCHROMEURL = + "chrome://mochitests/content/browser/browser/" + + "components/sessionstore/test/browser_parentProcessRestoreHash.js"; + +const Cm = Components.manager; + +const TESTCLASSID = "78742c04-3630-448c-9be3-6c5070f062de"; + +const TESTURL = "about:testpageforsessionrestore#foo"; + +let TestAboutPage = { + QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), + getURIFlags() { + // No CAN_ or MUST_LOAD_IN_CHILD means this loads in the parent: + return ( + Ci.nsIAboutModule.ALLOW_SCRIPT | + Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT | + Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT + ); + }, + + newChannel(aURI, aLoadInfo) { + // about: page inception! + let newURI = Services.io.newURI(SELFCHROMEURL); + let channel = Services.io.newChannelFromURIWithLoadInfo(newURI, aLoadInfo); + channel.originalURI = aURI; + return channel; + }, + + createInstance(iid) { + return this.QueryInterface(iid); + }, + + register() { + Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory( + Components.ID(TESTCLASSID), + "Only here for a test", + "@mozilla.org/network/protocol/about;1?what=testpageforsessionrestore", + this + ); + }, + + unregister() { + Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory( + Components.ID(TESTCLASSID), + this + ); + }, +}; + +/** + * Test that switching from a remote to a parent process browser + * correctly clears the userTypedValue + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.skip_about_page_has_csp_assert", true]], + }); + + TestAboutPage.register(); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/", + true, + true + ); + ok(tab.linkedBrowser.isRemoteBrowser, "Browser should be remote"); + + let resolveLocationChangePromise; + let locationChangePromise = new Promise( + r => (resolveLocationChangePromise = r) + ); + let wpl = { + onStateChange(listener, request, state, _status) { + let location = request.QueryInterface(Ci.nsIChannel).originalURI; + // Ignore about:blank loads. + let docStop = + Ci.nsIWebProgressListener.STATE_STOP | + Ci.nsIWebProgressListener.STATE_IS_NETWORK; + if (location.spec == "about:blank" || (state & docStop) != docStop) { + return; + } + is(location.spec, TESTURL, "Got the expected URL"); + resolveLocationChangePromise(); + }, + }; + gBrowser.addProgressListener(wpl); + + gURLBar.value = TESTURL; + gURLBar.select(); + EventUtils.sendKey("return"); + + await locationChangePromise; + + ok(!tab.linkedBrowser.isRemoteBrowser, "Browser should no longer be remote"); + + is(gURLBar.value, TESTURL, "URL bar visible value should be correct."); + is(gURLBar.untrimmedValue, TESTURL, "URL bar value should be correct."); + is( + gURLBar.getAttribute("pageproxystate"), + "valid", + "URL bar is in valid page proxy state" + ); + + ok( + !tab.linkedBrowser.userTypedValue, + "No userTypedValue should be on the browser." + ); + + BrowserTestUtils.removeTab(tab); + gBrowser.removeProgressListener(wpl); + TestAboutPage.unregister(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_pending_tabs.js b/src/zen/tests/mochitests/sessionstore/browser_pending_tabs.js new file mode 100644 index 000000000..20f3c4e8c --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_pending_tabs.js @@ -0,0 +1,38 @@ +"use strict"; + +const TAB_STATE = { + entries: [ + { url: "about:mozilla", triggeringPrincipal_base64 }, + { url: "about:robots", triggeringPrincipal_base64 }, + ], + index: 1, +}; + +add_task(async function () { + // Create a background tab. + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + + // The tab shouldn't be restored right away. + Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true); + + // Prepare the tab state. + let promise = promiseTabRestoring(tab); + ss.setTabState(tab, JSON.stringify(TAB_STATE)); + ok(tab.hasAttribute("pending"), "tab is pending"); + await promise; + + // Flush to ensure the parent has all data. + await TabStateFlusher.flush(browser); + + // Check that the shistory index is the one we restored. + let tabState = TabState.collect(tab, ss.getInternalObjectState(tab)); + is(tabState.index, TAB_STATE.index, "correct shistory index"); + + // Check we don't collect userTypedValue when we shouldn't. + ok(!tabState.userTypedValue, "tab didn't have a userTypedValue"); + + // Cleanup. + gBrowser.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_pinned_tabs.js b/src/zen/tests/mochitests/sessionstore/browser_pinned_tabs.js new file mode 100644 index 000000000..7a51da7cc --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_pinned_tabs.js @@ -0,0 +1,324 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const REMOTE_URL = "https://www.example.com/"; +const ABOUT_ROBOTS_URL = "about:robots"; +const NO_TITLE_URL = "data:text/plain,foo"; + +const BACKUP_STATE = SessionStore.getBrowserState(); +registerCleanupFunction(() => promiseBrowserState(BACKUP_STATE)); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.sessionstore.restore_on_demand", true], + ["browser.sessionstore.restore_tabs_lazily", true], + ], + }); +}); + +/** + * When implementing batch insertion of tabs as part of session restore, + * we started reversing the insertion order of pinned tabs (bug 1607441). + * This test checks we don't regress that again. + */ +add_task(async function test_pinned_tabs_order() { + // we expect 3 pinned tabs plus the selected tab get content restored. + let allTabsRestored = promiseSessionStoreLoads(4); + await promiseBrowserState({ + windows: [ + { + selected: 4, // SessionStore uses 1-based indexing. + tabs: [ + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: ABOUT_ROBOTS_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: NO_TITLE_URL, triggeringPrincipal_base64 }], + }, + { entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }] }, + { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] }, + ], + }, + ], + }); + await allTabsRestored; + let [tab1, tab2, tab3, tab4, tab5] = gBrowser.tabs; + ok(tab1.pinned, "First tab is pinned"); + ok(tab2.pinned, "Second tab is pinned"); + ok(tab3.pinned, "Third tab is pinned"); + ok(!tab4.pinned, "Fourth tab is not pinned"); + ok(!tab5.pinned, "Fifth tab is not pinned"); + + ok(tab4.selected, "Fourth tab is selected"); + is( + tab1.linkedBrowser.currentURI.spec, + REMOTE_URL, + "First tab has matching URL" + ); + is( + tab2.linkedBrowser.currentURI.spec, + ABOUT_ROBOTS_URL, + "Second tab has matching URL" + ); + is( + tab3.linkedBrowser.currentURI.spec, + NO_TITLE_URL, + "Third tab has matching URL" + ); + // Clean up for the next task. + await promiseBrowserState(BACKUP_STATE); +}); + +/** + * When fixing the previous regression, pinned tabs started disappearing out + * of sessions with selected pinned tabs. This test checks that case. + */ +add_task(async function test_selected_pinned_tab_dataloss() { + // we expect 3 pinned tabs (one of which is selected) get content restored. + let allTabsRestored = promiseSessionStoreLoads(3); + await promiseBrowserState({ + windows: [ + { + selected: 1, // SessionStore uses 1-based indexing. + tabs: [ + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: ABOUT_ROBOTS_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: NO_TITLE_URL, triggeringPrincipal_base64 }], + }, + { entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }] }, + { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] }, + ], + }, + ], + }); + await allTabsRestored; + let [tab1, tab2, tab3, tab4, tab5] = gBrowser.tabs; + ok(tab5, "Should have 5 tabs"); + ok(tab1.pinned, "First tab is pinned"); + ok(tab2.pinned, "Second tab is pinned"); + ok(tab3.pinned, "Third tab is pinned"); + ok(tab4 && !tab4.pinned, "Fourth tab is not pinned"); + ok(tab5 && !tab5.pinned, "Fifth tab is not pinned"); + + ok(tab1 && tab1.selected, "First (pinned) tab is selected"); + is( + tab1.linkedBrowser.currentURI.spec, + REMOTE_URL, + "First tab has matching URL" + ); + is( + tab2.linkedBrowser.currentURI.spec, + ABOUT_ROBOTS_URL, + "Second tab has matching URL" + ); + is( + tab3.linkedBrowser.currentURI.spec, + NO_TITLE_URL, + "Third tab has matching URL" + ); + // Clean up for the next task. + await promiseBrowserState(BACKUP_STATE); +}); + +/** + * While we're here, it seems useful to have a test for mixed pinned and + * unpinned tabs in session store state, as well as userContextId. + */ +add_task(async function test_mixed_pinned_unpinned() { + // we expect 3 pinned tabs plus the selected tab get content restored. + let allTabsRestored = promiseSessionStoreLoads(4); + await promiseBrowserState({ + windows: [ + { + selected: 4, // SessionStore uses 1-based indexing. + tabs: [ + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }] }, + { + pinned: true, + entries: [{ url: ABOUT_ROBOTS_URL, triggeringPrincipal_base64 }], + }, + { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] }, + { + pinned: true, + entries: [{ url: NO_TITLE_URL, triggeringPrincipal_base64 }], + }, + ], + }, + ], + }); + await allTabsRestored; + let [tab1, tab2, tab3, tab4, tab5] = gBrowser.tabs; + ok(tab1.pinned, "First tab is pinned"); + ok(tab2.pinned, "Second tab is pinned"); + ok(tab3.pinned, "Third tab is pinned"); + ok(!tab4.pinned, "Fourth tab is not pinned"); + ok(!tab5.pinned, "Fifth tab is not pinned"); + + // This is confusing to read - the 4th entry in the session data is + // selected. But the 5th entry is pinned, so it moves to the start of the + // tabstrip, so when we fetch `gBrowser.tabs`, the 4th entry in the list + // is actually the 5th tab. + ok(tab5.selected, "Fifth tab is selected"); + is( + tab1.linkedBrowser.currentURI.spec, + REMOTE_URL, + "First tab has matching URL" + ); + is( + tab2.linkedBrowser.currentURI.spec, + ABOUT_ROBOTS_URL, + "Second tab has matching URL" + ); + is( + tab3.linkedBrowser.currentURI.spec, + NO_TITLE_URL, + "Third tab has matching URL" + ); + // Clean up for the next task. + await promiseBrowserState(BACKUP_STATE); +}); + +/** + * After session restore, if we crash an unpinned tab, we noticed pinned tabs + * created in the same process would lose all data (Bug 1624511). This test + * checks that case. + */ +add_task(async function test_pinned_tab_dataloss() { + // We do not run if there are no crash reporters to avoid + // problems with the intentional crash. + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + // If we end up increasing the process count limit in future, + // we want to ensure that we don't stop testing this case + // of pinned tab data loss. + if (SpecialPowers.getIntPref("dom.ipc.processCount") > 8) { + ok( + false, + "Process count is greater than 8, update the number of pinned tabs in test." + ); + } + + // We expect 17 pinned tabs plus the selected tab get content restored. + // Given that the default process count is currently 8, we need this + // number of pinned tabs to reproduce the data loss. If this changes, + // please add more pinned tabs. + let allTabsRestored = promiseSessionStoreLoads(18); + await promiseBrowserState({ + windows: [ + { + selected: 18, // SessionStore uses 1-based indexing. + tabs: [ + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { + pinned: true, + entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }], + }, + { entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }] }, + ], + }, + ], + }); + await allTabsRestored; + + let tabs = gBrowser.tabs; + await BrowserTestUtils.crashFrame(tabs[17].linkedBrowser); + + await TestUtils.topicObserved("sessionstore-state-write-complete"); + + for (let i = 0; i < tabs.length; i++) { + let tab = tabs[i]; + is( + tab.linkedBrowser.currentURI.spec, + REMOTE_URL, + `Tab ${i + 1} should have matching URL` + ); + } + + // Clean up for the next task. + await promiseBrowserState(BACKUP_STATE); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_policy_container_not_stored.js b/src/zen/tests/mochitests/sessionstore/browser_policy_container_not_stored.js new file mode 100644 index 000000000..850299ebf --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_policy_container_not_stored.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * A session history entry only needs to retain the policy container for loads + * that inherit their policies (about:blank, about:srcdoc, blob:, data:, ...). + * Anything fetched over the network gets its policies from the response again, + * so storing them would only bloat the session store (bug 2011236). + */ + +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const PARENT_URL = TEST_ROOT + "browser_policy_container_sample.html"; +const FRAME_URL = TEST_ROOT + "browser_policy_container_sample_frame.html"; + +add_task(async function test_policy_container_only_for_inheriting_loads() { + const tab = BrowserTestUtils.addTab(gBrowser, PARENT_URL); + gBrowser.selectedTab = tab; + await promiseBrowserLoaded(tab.linkedBrowser, true, PARENT_URL); + await TabStateFlusher.flush(tab.linkedBrowser); + + const state = JSON.parse(ss.getTabState(tab)); + const topEntry = state.entries.at(-1); + is(topEntry.url, PARENT_URL, "collected the parent entry"); + + const children = topEntry.children ?? []; + const networkFrame = children.find(child => child.url === FRAME_URL); + const blankFrame = children.find(child => child.url === "about:blank"); + + ok(networkFrame, "collected the network-scheme subframe entry"); + ok(blankFrame, "collected the about:blank subframe entry"); + + // The parent has a CSP, so both subframes inherit a policy container onto + // their load state. Only the one that cannot recover it from a response + // should keep it in session history. + ok( + !("policyContainer" in networkFrame), + "https subframe entry does not store a policyContainer" + ); + ok( + "policyContainer" in blankFrame, + "about:blank subframe entry still stores its inherited policyContainer" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_policy_container_sample.html b/src/zen/tests/mochitests/sessionstore/browser_policy_container_sample.html new file mode 100644 index 000000000..1b8ad8d29 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_policy_container_sample.html @@ -0,0 +1,12 @@ + + + + + + policyContainer session store sample + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_policy_container_sample_frame.html b/src/zen/tests/mochitests/sessionstore/browser_policy_container_sample_frame.html new file mode 100644 index 000000000..4675a0cd4 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_policy_container_sample_frame.html @@ -0,0 +1,10 @@ + + + + + policyContainer session store sample frame + + + frame + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_privatetabs.js b/src/zen/tests/mochitests/sessionstore/browser_privatetabs.js new file mode 100644 index 000000000..237d9ff03 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_privatetabs.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(function cleanup() { + info("Forgetting closed tabs"); + forgetClosedTabs(window); +}); + +add_task(async function test_restore_pbm() { + // Clear the list of closed windows. + forgetClosedWindows(); + + // Create a new window to attach our frame script to. + let win = await promiseNewWindowLoaded({ private: true }); + + // Create a new tab in the new window that will load the frame script. + let tab = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // Check that we consider the tab as private. + let state = JSON.parse(ss.getTabState(tab)); + ok(state.isPrivate, "tab considered private"); + + // Ensure that closed tabs in a private windows can be restored. + await SessionStoreTestUtils.closeTab(tab); + is(ss.getClosedTabCountForWindow(win), 1, "there is a single tab to restore"); + + // Ensure that closed private windows can never be restored. + await BrowserTestUtils.closeWindow(win); + is(ss.getClosedWindowCount(), 0, "no windows to restore"); +}); + +/** + * Tests the purgeDataForPrivateWindow SessionStore method. + */ +add_task(async function test_purge_pbm() { + info("Clear the list of closed windows."); + forgetClosedWindows(); + + info("Create a new window to attach our frame script to."); + let win = await promiseNewWindowLoaded({ private: true }); + + info("Create a new tab in the new window that will load the frame script."); + let tab = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + info("Ensure that closed tabs in a private windows can be restored."); + await SessionStoreTestUtils.closeTab(tab); + is(ss.getClosedTabCountForWindow(win), 1, "there is a single tab to restore"); + + info("Call purgeDataForPrivateWindow"); + ss.purgeDataForPrivateWindow(win); + + is(ss.getClosedTabCountForWindow(win), 0, "there is no tab to restore"); + + // Cleanup + await BrowserTestUtils.closeWindow(win); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_purge_domaindata.js b/src/zen/tests/mochitests/sessionstore/browser_purge_domaindata.js new file mode 100644 index 000000000..662db006d --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_purge_domaindata.js @@ -0,0 +1,196 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); +const SITE = "https://example.com/"; + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +// Test for closed tabs and tab groups in an open window +add_task(async function test_closedTabsPurgeDomainData() { + let win = await promiseNewWindowLoaded(); + + let tabs = [ + BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"), + BrowserTestUtils.addTab(win.gBrowser, "about:robots"), + BrowserTestUtils.addTab(win.gBrowser, SITE), + ]; + let tabsInGroup = [ + BrowserTestUtils.addTab(win.gBrowser, "about:robots"), + BrowserTestUtils.addTab(win.gBrowser, SITE), + ]; + + let allTabs = [...tabs, ...tabsInGroup]; + await Promise.all( + allTabs.map(async t => { + await promiseBrowserLoaded(t.linkedBrowser); + }) + ); + let tabGroup = win.gBrowser.addTabGroup(tabsInGroup); + + await TabStateFlusher.flushWindow(win); + + tabs.forEach(t => { + win.gBrowser.removeTab(t); + }); + let removePromise = BrowserTestUtils.waitForEvent( + tabGroup, + "TabGroupRemoved" + ); + win.gBrowser.removeTabGroup(tabGroup); + await removePromise; + + let state = ss.getWindowState(win).windows[0]; + Assert.equal( + state._closedTabs.length, + 3, + "Closed tabs state has all closed tabs" + ); + Assert.equal( + state.closedGroups[0].tabs.length, + 2, + "Closed tab group state has all closed tabs" + ); + + // Purge session history for domain + Services.obs.notifyObservers( + null, + "browser:purge-session-history-for-domain", + "example.com" + ); + + state = ss.getWindowState(win).windows[0]; + Assert.equal(state._closedTabs.length, 2, "Closed tab list has one less tab"); + Assert.equal( + state.closedGroups[0].tabs.length, + 1, + "Closed tab group tab list has one less tab" + ); + + state._closedTabs.forEach((tab, index) => { + Assert.notEqual( + tab.state.entries[0].url, + SITE, + `Closed tab ${index} does not contain purged site` + ); + }); + state.closedGroups[0].tabs.forEach((tab, index) => { + Assert.notEqual( + tab.state.entries[0].url, + SITE, + `Closed tab ${index} in group does not contain purged site` + ); + }); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_closedTabsInClosedWindowsPurgeDomainData() { + let win = await promiseNewWindowLoaded(); + + let openTabs = [ + BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"), + BrowserTestUtils.addTab(win.gBrowser, "about:robots"), + BrowserTestUtils.addTab(win.gBrowser, SITE), + ]; + let tabsInOpenGroup = [ + BrowserTestUtils.addTab(win.gBrowser, "about:robots"), + BrowserTestUtils.addTab(win.gBrowser, SITE), + ]; + let closedTabs = [ + BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"), + BrowserTestUtils.addTab(win.gBrowser, "about:robots"), + BrowserTestUtils.addTab(win.gBrowser, SITE), + ]; + let tabsInClosedGroup = [ + BrowserTestUtils.addTab(win.gBrowser, "about:robots"), + BrowserTestUtils.addTab(win.gBrowser, SITE), + ]; + + let allTabs = [ + ...openTabs, + ...tabsInOpenGroup, + ...closedTabs, + ...tabsInClosedGroup, + ]; + await Promise.all( + allTabs.map(async t => { + await promiseBrowserLoaded(t.linkedBrowser); + }) + ); + win.gBrowser.addTabGroup(tabsInOpenGroup); + let closedTabGroup = win.gBrowser.addTabGroup(tabsInClosedGroup); + + await TabStateFlusher.flushWindow(win); + + closedTabs.forEach(t => { + win.gBrowser.removeTab(t); + }); + let removePromise = BrowserTestUtils.waitForEvent( + closedTabGroup, + "TabGroupRemoved" + ); + win.gBrowser.removeTabGroup(closedTabGroup); + await removePromise; + + await BrowserTestUtils.closeWindow(win); + + let state = ss.getClosedWindowData()[0]; + Assert.equal(state.tabs.length, 6, "Open tabs state has all tabs"); + Assert.equal( + state._closedTabs.length, + 3, + "Closed tabs state has all closed tabs" + ); + Assert.equal( + state.closedGroups[0].tabs.length, + 2, + "Closed tab group state has all closed tabs" + ); + + // Purge session history for domain + Services.obs.notifyObservers( + null, + "browser:purge-session-history-for-domain", + "example.com" + ); + + state = ss.getClosedWindowData()[0]; + Assert.equal( + state.tabs.length, + 4, + "Open tabs state loses ungrouped and grouped site tab" + ); + Assert.equal(state._closedTabs.length, 2, "Closed tabs state loses a tab"); + Assert.equal( + state.closedGroups[0].tabs.length, + 1, + "Closed tab group state loses a tab" + ); + + state.tabs.forEach((tab, index) => { + Assert.notEqual( + tab.entries[0].url, + SITE, + `Open tab ${index} does not contain purged site` + ); + }); + state._closedTabs.forEach((tab, index) => { + Assert.notEqual( + tab.state.entries[0].url, + SITE, + `Closed tab ${index} does not contain purged site` + ); + }); + state.closedGroups[0].tabs.forEach((tab, index) => { + Assert.notEqual( + tab.state.entries[0].url, + SITE, + `Closed tab ${index} in group does not contain purged site` + ); + }); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_purge_shistory.js b/src/zen/tests/mochitests/sessionstore/browser_purge_shistory.js new file mode 100644 index 000000000..617df0455 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_purge_shistory.js @@ -0,0 +1,65 @@ +"use strict"; + +/** + * This test checks that pending tabs are treated like fully loaded tabs when + * purging session history. Just like for fully loaded tabs we want to remove + * every but the current shistory entry. + */ + +const TAB_STATE = { + entries: [ + { url: "about:mozilla", triggeringPrincipal_base64 }, + { url: "about:robots", triggeringPrincipal_base64 }, + ], + index: 1, +}; + +function checkTabContents(browser) { + return SpecialPowers.spawn(browser, [], async function () { + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); + let history = webNavigation.sessionHistory; + Assert.ok( + history && + history.count == 1 && + content.document.documentURI == "about:mozilla", + "expected tab contents found" + ); + }); +} + +add_task(async function () { + // Create a new tab. + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + await promiseTabState(tab, TAB_STATE); + + // Create another new tab. + let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser2 = tab2.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser2, { wantLoad: "about:blank" }); + + // The tab shouldn't be restored right away. + Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true); + + // Prepare the tab state. + let promise = promiseTabRestoring(tab2); + ss.setTabState(tab2, JSON.stringify(TAB_STATE)); + ok(tab2.hasAttribute("pending"), "tab is pending"); + await promise; + + // Purge session history. + Services.obs.notifyObservers(null, "browser:purge-session-history"); + await checkTabContents(browser); + ok(tab2.hasAttribute("pending"), "tab is still pending"); + + // Kick off tab restoration. + gBrowser.selectedTab = tab2; + await promiseTabRestored(tab2); + await checkTabContents(browser2); + ok(!tab2.hasAttribute("pending"), "tab is not pending anymore"); + + // Cleanup. + gBrowser.removeTab(tab2); + gBrowser.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_remoteness_flip_on_restore.js b/src/zen/tests/mochitests/sessionstore/browser_remoteness_flip_on_restore.js new file mode 100644 index 000000000..fb799fc92 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_remoteness_flip_on_restore.js @@ -0,0 +1,311 @@ +"use strict"; + +/** + * This set of tests checks that the remoteness is properly + * set for each browser in a window when that window has + * session state loaded into it. + */ + +/** + * Takes a SessionStore window state object for a single + * window, sets the selected tab for it, and then returns + * the object to be passed to SessionStore.setWindowState. + * + * @param state (object) + * The state to prepare to be sent to a window. This is + * state should just be for a single window. + * @param selected (int) + * The 1-based index of the selected tab. Note that + * If this is 0, then the selected tab will not change + * from what's already selected in the window that we're + * sending state to. + * @returns (object) + * The JSON encoded string to call + * SessionStore.setWindowState with. + */ +function prepareState(state, selected) { + // We'll create a copy so that we don't accidentally + // modify the caller's selected property. + let copy = {}; + Object.assign(copy, state); + copy.selected = selected; + + return { + windows: [copy], + }; +} + +const SIMPLE_STATE = { + tabs: [ + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: "title", + }, + ], + }, + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: "title", + }, + ], + }, + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: "title", + }, + ], + }, + ], + title: "", + _closedTabs: [], +}; + +const PINNED_STATE = { + tabs: [ + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: "title", + }, + ], + pinned: true, + }, + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: "title", + }, + ], + pinned: true, + }, + { + entries: [ + { + url: "http://example.com/", + triggeringPrincipal_base64, + title: "title", + }, + ], + }, + ], + title: "", + _closedTabs: [], +}; + +/** + * This is where most of the action is happening. This function takes + * an Array of "test scenario" Objects and runs them. For each scenario, a + * window is opened, put into some state, and then a new state is + * loaded into that window. We then check to make sure that the + * right things have happened in that window wrt remoteness flips. + * + * The schema for a testing scenario Object is as follows: + * + * initialRemoteness: + * an Array that represents the starting window. Each bool + * in the Array represents the window tabs in order. A "true" + * indicates that that tab should be remote. "false" if the tab + * should be non-remote. + * + * initialSelectedTab: + * The 1-based index of the tab that we want to select for the + * restored window. This is 1-based to avoid confusion with the + * selectedTab property described down below, though you probably + * want to set this to be greater than 0, since the initial window + * needs to have a defined initial selected tab. Because of this, + * the test will throw if initialSelectedTab is 0. + * + * stateToRestore: + * A JS Object for the state to send down to the window. + * + * selectedTab: + * The 1-based index of the tab that we want to select for the + * restored window. Leave this at 0 if you don't want to change + * the selection from the initial window state. + * + * expectedRemoteness: + * an Array that represents the window that we end up with after + * restoring state. Each bool in the Array represents the window + * tabs in order. A "true" indicates that the tab be remote, and + * a "false" indicates that the tab should be "non-remote". We + * need this Array in order to test pinned tabs which will also + * be loaded by default, and therefore should end up remote. + * + */ +async function runScenarios(scenarios) { + for (let [scenarioIndex, scenario] of scenarios.entries()) { + info("Running scenario " + scenarioIndex); + Assert.greater( + scenario.initialSelectedTab, + 0, + "You must define an initially selected tab" + ); + + // First, we need to create the initial conditions, so we + // open a new window to put into our starting state... + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tabbrowser = win.gBrowser; + Assert.ok( + tabbrowser.selectedBrowser.isRemoteBrowser, + "The initial browser should be remote." + ); + // Now put the window into the expected initial state. + for (let i = 0; i < scenario.initialRemoteness.length; ++i) { + let tab; + if (i > 0) { + // The window starts with one tab, so we need to create + // any of the additional ones required by this test. + info("Opening a new tab"); + tab = await BrowserTestUtils.openNewForegroundTab(tabbrowser); + } else { + info("Using the selected tab"); + tab = tabbrowser.selectedTab; + } + let browser = tab.linkedBrowser; + let remotenessState = scenario.initialRemoteness[i] + ? E10SUtils.DEFAULT_REMOTE_TYPE + : E10SUtils.NOT_REMOTE; + tabbrowser.updateBrowserRemoteness(browser, { + remoteType: remotenessState, + }); + } + + // And select the requested tab. + let tabToSelect = tabbrowser.tabs[scenario.initialSelectedTab - 1]; + if (tabbrowser.selectedTab != tabToSelect) { + await BrowserTestUtils.switchTab(tabbrowser, tabToSelect); + } + + // Okay, time to test! + let state = prepareState(scenario.stateToRestore, scenario.selectedTab); + + await setWindowState(win, state, true); + + for (let i = 0; i < scenario.expectedRemoteness.length; ++i) { + let expectedRemoteness = scenario.expectedRemoteness[i]; + let tab = tabbrowser.tabs[i]; + + Assert.equal( + tab.linkedBrowser.isRemoteBrowser, + expectedRemoteness, + "Should have gotten the expected remoteness " + + `for the tab at index ${i}` + ); + } + + await BrowserTestUtils.closeWindow(win); + } +} + +/** + * Tests that if we restore state to browser windows with + * a variety of initial remoteness states. For this particular + * set of tests, we assume that tabs are restoring on demand. + */ +add_task(async function () { + // This test opens and closes windows, which might bog down + // a debug build long enough to time out the test, so we + // extend the tolerance on timeouts. + requestLongerTimeout(5); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionstore.restore_on_demand", true]], + }); + + const TEST_SCENARIOS = [ + // Only one tab in the new window, and it's remote. This + // is the common case, since this is how restoration occurs + // when the restored window is being opened. + { + initialRemoteness: [true], + initialSelectedTab: 1, + stateToRestore: SIMPLE_STATE, + selectedTab: 3, + // All tabs should now be remote. + expectedRemoteness: [true, true, true], + }, + + // A single remote tab, and this is the one that's going + // to be selected once state is restored. + { + initialRemoteness: [true], + initialSelectedTab: 1, + stateToRestore: SIMPLE_STATE, + selectedTab: 1, + // All tabs should now be remote. + expectedRemoteness: [true, true, true], + }, + + // A single remote tab which starts selected. We set the + // selectedTab to 0 which is equivalent to "don't change + // the tab selection in the window". + { + initialRemoteness: [true], + initialSelectedTab: 1, + stateToRestore: SIMPLE_STATE, + selectedTab: 0, + // All tabs should now be remote. + expectedRemoteness: [true, true, true], + }, + + // An initially remote tab, but we're going to load + // some pinned tabs now, and the pinned tabs should load + // right away. + { + initialRemoteness: [true], + initialSelectedTab: 1, + stateToRestore: PINNED_STATE, + selectedTab: 3, + // Both pinned tabs and the selected tabs should all + // end up being remote. + expectedRemoteness: [true, true, true], + }, + + // A single non-remote tab. + { + initialRemoteness: [false], + initialSelectedTab: 1, + stateToRestore: SIMPLE_STATE, + selectedTab: 2, + // All tabs should now be remote. + expectedRemoteness: [true, true, true], + }, + + // A mixture of remote and non-remote tabs. + { + initialRemoteness: [true, false, true], + initialSelectedTab: 1, + stateToRestore: SIMPLE_STATE, + selectedTab: 3, + // All tabs should now be remote. + expectedRemoteness: [true, true, true], + }, + + // An initially non-remote tab, but we're going to load + // some pinned tabs now, and the pinned tabs should load + // right away. + { + initialRemoteness: [false], + initialSelectedTab: 1, + stateToRestore: PINNED_STATE, + selectedTab: 3, + // All tabs should now be remote. + expectedRemoteness: [true, true, true], + }, + ]; + + await runScenarios(TEST_SCENARIOS); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_reopen_all_windows.js b/src/zen/tests/mochitests/sessionstore/browser_reopen_all_windows.js new file mode 100644 index 000000000..6857d2af7 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_reopen_all_windows.js @@ -0,0 +1,154 @@ +"use strict"; + +const PATH = "browser/browser/components/sessionstore/test/empty.html"; +var URLS_WIN1 = [ + "https://example.com/" + PATH, + "https://example.org/" + PATH, + "http://test1.mochi.test:8888/" + PATH, + "http://test1.example.com/" + PATH, +]; +var EXPECTED_URLS_WIN1 = ["about:blank", ...URLS_WIN1]; + +var URLS_WIN2 = [ + "http://sub1.test1.mochi.test:8888/" + PATH, + "http://sub2.xn--lt-uia.mochi.test:8888/" + PATH, + "http://test2.mochi.test:8888/" + PATH, + "http://sub1.test2.example.org/" + PATH, + "http://sub2.test1.example.org/" + PATH, + "http://test2.example.com/" + PATH, +]; +var EXPECTED_URLS_WIN2 = ["about:blank", ...URLS_WIN2]; + +requestLongerTimeout(4); + +function allTabsRestored(win, expectedUrls) { + return new Promise(resolve => { + let tabsRestored = 0; + function handler(event) { + let spec = event.target.linkedBrowser.currentURI.spec; + if (expectedUrls.includes(spec)) { + tabsRestored++; + } + info(`Got SSTabRestored for ${spec}, tabsRestored=${tabsRestored}`); + if (tabsRestored === expectedUrls.length) { + win.gBrowser.tabContainer.removeEventListener( + "SSTabRestored", + handler, + true + ); + resolve(); + } + } + win.gBrowser.tabContainer.addEventListener("SSTabRestored", handler, true); + }); +} + +async function windowAndTabsRestored(win, expectedUrls) { + await TestUtils.topicObserved( + "browser-window-before-show", + subject => subject === win + ); + return allTabsRestored(win, expectedUrls); +} + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + // Set the pref to true so we know exactly how many tabs should be restoring at + // any given time. This guarantees that a finishing load won't start another. + ["browser.sessionstore.restore_on_demand", false], + ], + }); + + forgetClosedWindows(); + is(SessionStore.getClosedWindowCount(), 0, "starting with no closed windows"); + + // Open window 1, with different tabs + let win1 = await BrowserTestUtils.openNewBrowserWindow(); + for (let url of URLS_WIN1) { + await BrowserTestUtils.openNewForegroundTab(win1.gBrowser, url); + } + + // Open window 2, with different tabs + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + for (let url of URLS_WIN2) { + await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, url); + } + + await TabStateFlusher.flushWindow(win1); + await TabStateFlusher.flushWindow(win2); + + // Close both windows + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); + await forceSaveState(); + + // Verify both windows were accounted for by session store + is( + ss.getClosedWindowCount(), + 2, + "The closed windows was added to Recently Closed Windows" + ); + + // We previously used to manually navigate the Library menu to click the + // "Reopen all Windows" button, but that reopens all windows at once without + // returning a reference to each window. Since we need to attach listeners to + // these windows *before* they start restoring tabs, we now manually call + // undoCloseWindow() here, which has the same effect, but also gives us the + // window references. + info("Reopening windows"); + let restoredWindows = []; + while (SessionStore.getClosedWindowCount() > 0) { + restoredWindows.unshift(SessionWindowUI.undoCloseWindow()); + } + is(restoredWindows.length, 2, "Reopened correct number of windows"); + + let win1Restored = windowAndTabsRestored( + restoredWindows[0], + EXPECTED_URLS_WIN1 + ); + let win2Restored = windowAndTabsRestored( + restoredWindows[1], + EXPECTED_URLS_WIN2 + ); + + info("About to wait for tabs to be restored"); + await Promise.all([win1Restored, win2Restored]); + + const sessionRestoreInitialized = + Glean.sessionRestore.startupTimeline.sessionRestoreInitialized.testGetValue(); + Assert.greater( + sessionRestoreInitialized, + 0, + "Session store initialize recorded." + ); + + is( + restoredWindows[0].gBrowser.tabs.length, + EXPECTED_URLS_WIN1.length, + "All tabs restored" + ); + is( + restoredWindows[1].gBrowser.tabs.length, + EXPECTED_URLS_WIN2.length, + "All tabs restored" + ); + + // Verify that tabs opened as expected + Assert.deepEqual( + restoredWindows[0].gBrowser.tabs.map( + tab => tab.linkedBrowser.currentURI.spec + ), + EXPECTED_URLS_WIN1 + ); + Assert.deepEqual( + restoredWindows[1].gBrowser.tabs.map( + tab => tab.linkedBrowser.currentURI.spec + ), + EXPECTED_URLS_WIN2 + ); + + info("About to close windows"); + await BrowserTestUtils.closeWindow(restoredWindows[0]); + await BrowserTestUtils.closeWindow(restoredWindows[1]); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_replace_load.js b/src/zen/tests/mochitests/sessionstore/browser_replace_load.js new file mode 100644 index 000000000..21bec044a --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_replace_load.js @@ -0,0 +1,56 @@ +"use strict"; + +const STATE = { + entries: [{ url: "about:robots" }, { url: "about:mozilla" }], + selected: 2, +}; + +/** + * Bug 1100223. Calling browser.loadURI() while a tab is loading causes + * sessionstore to override the desired target URL. This test ensures that + * calling loadURI() on a pending tab causes the tab to no longer be marked + * as pending and correctly finish the instructed load while keeping the + * restored history around. + */ +add_task(async function () { + await testSwitchToTab("about:mozilla#fooobar", { + ignoreFragment: "whenComparingAndReplace", + }); + await testSwitchToTab("about:mozilla?foo=bar", { replaceQueryString: true }); +}); + +var testSwitchToTab = async function (url, options) { + // Create a background tab. + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // The tab shouldn't be restored right away. + Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true); + + // Prepare the tab state. + let promise = promiseTabRestoring(tab); + ss.setTabState(tab, JSON.stringify(STATE)); + ok(tab.hasAttribute("pending"), "tab is pending"); + await promise; + + options.triggeringPrincipal = + Services.scriptSecurityManager.getSystemPrincipal(); + + // Switch-to-tab with a similar URI. + switchToTabHavingURI(url, false, options); + + // Tab should now restore + await promiseTabRestored(tab); + is(browser.currentURI.spec, url, "correct URL loaded"); + + // Check that we didn't lose any history entries. + await SpecialPowers.spawn(browser, [], async function () { + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); + let history = webNavigation.sessionHistory; + Assert.equal(history && history.count, 3, "three history entries"); + }); + + // Cleanup. + gBrowser.removeTab(tab); +}; diff --git a/src/zen/tests/mochitests/sessionstore/browser_restoreLastActionCorrectOrder.js b/src/zen/tests/mochitests/sessionstore/browser_restoreLastActionCorrectOrder.js new file mode 100644 index 000000000..bd660a4bd --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restoreLastActionCorrectOrder.js @@ -0,0 +1,101 @@ +"use strict"; + +const { _LastSession, _lastClosedActions } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionStore.sys.mjs" +); + +/** + * Tests that the _lastClosedAction list is truncated correctly + * by removing oldest actions in SessionStore._addClosedAction + */ +add_task(async function test_undo_last_action_correct_order() { + SpecialPowers.pushPrefEnv({ + set: [ + ["browser.sessionstore.max_tabs_undo", 3], + ["browser.sessionstore.max_windows_undo", 1], + ], + }); + + gBrowser.removeAllTabsBut(gBrowser.tabs[0]); + await TabStateFlusher.flushWindow(window); + + forgetClosedTabs(window); + + const state = { + windows: [ + { + tabs: [ + { + entries: [ + { + title: "example.org", + url: "https://example.org/", + triggeringPrincipal_base64, + }, + ], + }, + { + entries: [ + { + title: "example.com", + url: "https://example.com/", + triggeringPrincipal_base64, + }, + ], + }, + { + entries: [ + { + title: "mozilla", + url: "https://www.mozilla.org/", + triggeringPrincipal_base64, + }, + ], + }, + { + entries: [ + { + title: "mozilla privacy policy", + url: "https://www.mozilla.org/privacy", + triggeringPrincipal_base64, + }, + ], + }, + ], + selected: 3, + }, + ], + }; + + _LastSession.setState(state); + SessionStore.resetLastClosedActions(); + + let sessionRestored = promiseSessionStoreLoads(4 /* total restored tabs */); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + await sessionRestored; + + Assert.equal(window.gBrowser.tabs.length, 4, "4 tabs have been restored"); + + BrowserTestUtils.removeTab(window.gBrowser.tabs[3]); + BrowserTestUtils.removeTab(window.gBrowser.tabs[2]); + Assert.equal(window.gBrowser.tabs.length, 2, "Window has one open tab"); + + // open and close a window + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + Assert.equal(win2.gBrowser.tabs.length, 1, "Second window has one open tab"); + BrowserTestUtils.startLoadingURIString( + win2.gBrowser.selectedBrowser, + "https://example.com/" + ); + await BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser); + await BrowserTestUtils.closeWindow(win2); + + // close one tab and reopen it + BrowserTestUtils.removeTab(window.gBrowser.tabs[1]); + Assert.equal(window.gBrowser.tabs.length, 1, "Window has one open tabs"); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + Assert.equal(window.gBrowser.tabs.length, 2, "Window now has two open tabs"); + + await SpecialPowers.popPrefEnv(); + gBrowser.removeAllTabsBut(gBrowser.tabs[0]); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_restoreLastClosedTabOrWindowOrSession.js b/src/zen/tests/mochitests/sessionstore/browser_restoreLastClosedTabOrWindowOrSession.js new file mode 100644 index 000000000..f7e2f38e4 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restoreLastClosedTabOrWindowOrSession.js @@ -0,0 +1,352 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { _LastSession, _lastClosedActions } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionStore.sys.mjs" +); + +async function testLastClosedActionsEntries() { + SessionStore.resetLastClosedActions(); + + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + BrowserTestUtils.startLoadingURIString( + win2.gBrowser.selectedBrowser, + "https://www.mozilla.org/" + ); + + await BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser); + await openAndCloseTab(win2, "https://example.org/"); + + Assert.equal( + SessionStore.lastClosedActions.length, + 1, + `1 closed action has been recorded` + ); + + await BrowserTestUtils.closeWindow(win2); + + Assert.equal( + SessionStore.lastClosedActions.length, + 2, + `2 closed actions have been recorded` + ); +} + +add_setup(() => { + forgetClosedTabs(window); + registerCleanupFunction(() => { + forgetClosedTabs(window); + }); + + // needed for verify tests so that forgetting tabs isn't recorded + SessionStore.resetLastClosedActions(); +}); + +/** + * Tests that if the user invokes restoreLastClosedTabOrWindowOrSession it will + * result in either the last session will be restored, if possible, the last + * tab (or multiple selected tabs) that was closed is reopened, or the last + * window that is closed is reopened. + */ +add_task(async function test_undo_last_action() { + const state = { + windows: [ + { + tabs: [ + { + entries: [ + { + title: "example.org", + url: "https://example.org/", + triggeringPrincipal_base64, + }, + ], + }, + { + entries: [ + { + title: "example.com", + url: "https://example.com/", + triggeringPrincipal_base64, + }, + ], + }, + { + entries: [ + { + title: "mozilla", + url: "https://www.mozilla.org/", + triggeringPrincipal_base64, + }, + ], + }, + ], + selected: 3, + }, + ], + }; + + _LastSession.setState(state); + + let sessionRestored = promiseSessionStoreLoads(3 /* total restored tabs */); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + await sessionRestored; + Assert.equal( + window.gBrowser.tabs.length, + 3, + "Window has three tabs after session is restored" + ); + + // open and close a window, then reopen it + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + Assert.equal(win2.gBrowser.tabs.length, 1, "Second window has one open tab"); + BrowserTestUtils.startLoadingURIString( + win2.gBrowser.selectedBrowser, + "https://example.com/" + ); + await BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser); + await BrowserTestUtils.closeWindow(win2); + let restoredWinPromise = BrowserTestUtils.waitForNewWindow({ + url: "https://example.com/", + }); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + let restoredWin = await restoredWinPromise; + Assert.equal( + restoredWin.gBrowser.tabs.length, + 1, + "First tab in the second window has been reopened" + ); + await BrowserTestUtils.closeWindow(restoredWin); + SessionStore.forgetClosedWindow(restoredWin.index); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + + // close one tab and reopen it + BrowserTestUtils.removeTab(window.gBrowser.tabs[2]); + Assert.equal(window.gBrowser.tabs.length, 2, "Window has two open tabs"); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + Assert.equal( + window.gBrowser.tabs.length, + 3, + "Window now has three open tabs" + ); + + // select 2 tabs and close both via the 'close 2 tabs' context menu option + let tab2 = window.gBrowser.tabs[1]; + let tab3 = window.gBrowser.tabs[2]; + await triggerClickOn(tab2, { ctrlKey: true }); + Assert.equal(tab2.multiselected, true); + Assert.equal(tab3.multiselected, true); + + let menu = await openTabMenuFor(tab3); + let menuItemCloseTab = document.getElementById("context_closeTab"); + let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2); + let tab3Closing = BrowserTestUtils.waitForTabClosing(tab3); + menu.activateItem(menuItemCloseTab); + await tab2Closing; + await tab3Closing; + Assert.equal(window.gBrowser.tabs[0].selected, true); + await TestUtils.waitForCondition(() => window.gBrowser.tabs.length == 1); + Assert.equal( + window.gBrowser.tabs.length, + 1, + "Window now has one open tab after closing two multi-selected tabs" + ); + + // ensure both tabs are reopened with a single click + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + Assert.equal( + window.gBrowser.tabs.length, + 3, + "Window now has three open tabs after reopening closed tabs" + ); + + // close one tab and forget it - it should not be reopened + BrowserTestUtils.removeTab(window.gBrowser.tabs[2]); + Assert.equal(window.gBrowser.tabs.length, 2, "Window has two open tabs"); + SessionStore.forgetClosedTab(window, 0); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + Assert.equal( + window.gBrowser.tabs.length, + 2, + "Window still has two open tabs" + ); + + // create a tab group, delete it, and re-open it + const groupedTab = BrowserTestUtils.addTab( + window.gBrowser, + "https://example.com" + ); + await BrowserTestUtils.browserLoaded(groupedTab.linkedBrowser); + const tabGroup = window.gBrowser.addTabGroup([groupedTab]); + Assert.equal( + window.gBrowser.tabs.length, + 3, + "Window has three tabs after creating tab group" + ); + + await TabStateFlusher.flushWindow(window); + let removePromise = BrowserTestUtils.waitForEvent( + tabGroup, + "TabGroupRemoved" + ); + window.gBrowser.removeTabGroup(tabGroup); + await removePromise; + + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + Assert.equal( + window.gBrowser.tabs.length, + 3, + "Window has three tabs after restoring tab group" + ); + Assert.equal( + window.gBrowser.tabGroups.length, + 1, + "Tab group exists on the tab strip after restore" + ); + + // Save and close the tab group, and then re-open it + const groupedTabToSave = BrowserTestUtils.addTab( + window.gBrowser, + "https://example.com" + ); + await BrowserTestUtils.browserLoaded(groupedTabToSave.linkedBrowser); + const tabGroupToSave = window.gBrowser.addTabGroup([groupedTabToSave]); + Assert.equal( + window.gBrowser.tabs.length, + 4, + "Window has four tabs after creating tab group" + ); + + await TabStateFlusher.flushWindow(window); + tabGroupToSave.save(); + removePromise = BrowserTestUtils.waitForEvent( + tabGroupToSave, + "TabGroupRemoved" + ); + window.gBrowser.removeTabGroup(tabGroupToSave); + await removePromise; + + await TabStateFlusher.flushWindow(window); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + Assert.equal( + window.gBrowser.tabs.length, + 4, + "Window has four tabs after restoring tab group" + ); + Assert.equal( + window.gBrowser.tabGroups.length, + 2, + "Tab group exists on the tab strip after restore" + ); + + gBrowser.removeAllTabsBut(gBrowser.tabs[0], { animate: false }); +}); + +add_task(async function test_forget_closed_window() { + await testLastClosedActionsEntries(); + SessionStore.forgetClosedWindow(); + + // both the forgotten window and its closed tab has been removed from the list + Assert.ok( + !SessionStore.lastClosedActions.length, + `0 closed actions have been recorded` + ); +}); + +add_task(async function test_user_clears_history() { + await testLastClosedActionsEntries(); + Services.obs.notifyObservers(null, "browser:purge-session-history"); + + // both the forgotten window and its closed tab has been removed from the list + Assert.ok( + !SessionStore.lastClosedActions.length, + `0 closed actions have been recorded` + ); +}); + +/** + * It the browser has restarted and the closed actions list is empty, we + * should fallback to re-opening the last closed tab if one exists. + */ +add_task(async function test_reopen_last_tab_if_no_closed_actions() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + async () => { + const TEST_URL = "https://example.com/"; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + let update = BrowserTestUtils.waitForSessionStoreUpdate(tab); + BrowserTestUtils.removeTab(tab); + await update; + + SessionStore.resetLastClosedActions(); + + let promiseTab = BrowserTestUtils.waitForNewTab(gBrowser, TEST_URL); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + let newTab = await promiseTab; + Assert.equal(newTab.linkedBrowser.currentURI.spec, TEST_URL); + } + ); +}); + +/** + * It the browser has restarted and the closed actions list is empty, and + * no closed tabs exist for the window, we should fallback to re-opening + * the last session if one exists. + */ +add_task(async function test_reopen_last_session_if_no_closed_actions() { + gBrowser.removeAllTabsBut(gBrowser.tabs[0], { animate: false }); + await TabStateFlusher.flushWindow(window); + + forgetClosedTabs(window); + + const state = { + windows: [ + { + tabs: [ + { + entries: [ + { + title: "example.org", + url: "https://example.org/", + triggeringPrincipal_base64, + }, + ], + }, + { + entries: [ + { + title: "example.com", + url: "https://example.com/", + triggeringPrincipal_base64, + }, + ], + }, + { + entries: [ + { + title: "mozilla", + url: "https://www.mozilla.org/", + triggeringPrincipal_base64, + }, + ], + }, + ], + selected: 3, + }, + ], + }; + + _LastSession.setState(state); + SessionStore.resetLastClosedActions(); + + let sessionRestored = promiseSessionStoreLoads(3 /* total restored tabs */); + SessionWindowUI.restoreLastClosedTabOrWindowOrSession(window); + await sessionRestored; + Assert.equal(gBrowser.tabs.length, 4, "Got the expected number of tabs"); + gBrowser.removeAllTabsBut(gBrowser.tabs[0], { animate: false }); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_restoreTabContainer.js b/src/zen/tests/mochitests/sessionstore/browser_restoreTabContainer.js new file mode 100644 index 000000000..a38dca386 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restoreTabContainer.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +add_task(async function () { + const testUserContextId = 2; + const testCases = [ + { + url: `${TEST_PATH}empty.html`, + crossOriginIsolated: false, + }, + { + url: `${TEST_PATH}coop_coep.html`, + crossOriginIsolated: true, + }, + ]; + + for (const testCase of testCases) { + let tab = BrowserTestUtils.addTab(gBrowser, testCase.url, { + userContextId: testUserContextId, + }); + await promiseBrowserLoaded(tab.linkedBrowser); + + is( + tab.userContextId, + testUserContextId, + `The tab was opened with the expected userContextId` + ); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [testCase.crossOriginIsolated], + async expectedCrossOriginIsolated => { + is( + content.window.crossOriginIsolated, + expectedCrossOriginIsolated, + `The tab was opened in the expected crossOriginIsolated environment` + ); + } + ); + + let sessionPromise = BrowserTestUtils.waitForSessionStoreUpdate(tab); + BrowserTestUtils.removeTab(tab); + await sessionPromise; + + let restoredTab = SessionStore.undoCloseTab(window, 0); + + // TODO: also check that `promiseTabRestored` is fulfilled. This currently + // doesn't happen correctly in some cases, as the content restore is aborted + // when the process switch occurs to load a cross-origin-isolated document + // into a different process. + await promiseBrowserLoaded(restoredTab.linkedBrowser); + + is( + restoredTab.userContextId, + testUserContextId, + `The tab was restored with the expected userContextId` + ); + + await SpecialPowers.spawn( + restoredTab.linkedBrowser, + [testCase.crossOriginIsolated], + async expectedCrossOriginIsolated => { + is( + content.window.crossOriginIsolated, + expectedCrossOriginIsolated, + `The tab was restored in the expected crossOriginIsolated environment` + ); + } + ); + + BrowserTestUtils.removeTab(restoredTab); + } +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_restore_container_tabs_oa.js b/src/zen/tests/mochitests/sessionstore/browser_restore_container_tabs_oa.js new file mode 100644 index 000000000..5df11566b --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restore_container_tabs_oa.js @@ -0,0 +1,272 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { UrlbarProviderOpenTabs } = ChromeUtils.importESModule( + "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs" +); + +const PATH = "browser/browser/components/sessionstore/test/empty.html"; + +/* import-globals-from ../../tabbrowser/test/browser/tabs/helper_origin_attrs_testing.js */ +loadTestSubscript( + "../../tabbrowser/test/browser/tabs/helper_origin_attrs_testing.js" +); + +var TEST_CASES = [ + "https://example.com/" + PATH, + "https://example.org/" + PATH, + "about:preferences", + "about:config", +]; + +var remoteTypes; + +var xulFrameLoaderCreatedCounter = {}; + +function handleEventLocal(aEvent) { + if (aEvent.type != "XULFrameLoaderCreated") { + return; + } + // Ignore element in about:preferences and any other special pages + if ("gBrowser" in aEvent.target.documentGlobal) { + xulFrameLoaderCreatedCounter.numCalledSoFar++; + } +} + +var NUM_DIFF_TAB_MODES = NUM_USER_CONTEXTS + 1; /** regular tab */ + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Set the pref to true so we know exactly how many tabs should be restoring at + // any given time. This guarantees that a finishing load won't start another. + ["browser.sessionstore.restore_on_demand", true], + // Don't restore tabs lazily. + ["browser.sessionstore.restore_tabs_lazily", true], + // don't preload tabs so we don't have extra XULFrameLoaderCreated events + // firing + ["browser.newtab.preload", false], + ], + }); + + requestLongerTimeout(14); +}); + +function setupRemoteTypes(isolateEverything) { + if (isolateEverything) { + remoteTypes = [ + "webIsolated=https://example.com", + "webIsolated=https://example.com^userContextId=1", + "webIsolated=https://example.com^userContextId=2", + "webIsolated=https://example.com^userContextId=3", + "webIsolated=https://example.org", + "webIsolated=https://example.org^userContextId=1", + "webIsolated=https://example.org^userContextId=2", + "webIsolated=https://example.org^userContextId=3", + ]; + } else { + remoteTypes = [ + "web", + "web=^userContextId=1", + "web=^userContextId=2", + "web=^userContextId=3", + "web", + "web=^userContextId=1", + "web=^userContextId=2", + "web=^userContextId=3", + ]; + } + remoteTypes.push(...Array(NUM_DIFF_TAB_MODES * 2).fill(null)); // remote types for about: pages + + forgetClosedWindows(); + is(SessionStore.getClosedWindowCount(), 0, "starting with no closed windows"); +} + +/* + * 1. Open several tabs in different containers and in regular tabs + [page1, page2, page3] [ [(page1 - work) (page1 - home)] [(page2 - work) (page2 - home)] ] + * 2. Close the window + * 3. Restore session, window will have the following tabs + * [initial blank page, page1, page1-work, page1-home, page2, page2-work, page2-home] + * 4. Verify correct remote types and that XULFrameLoaderCreated gets fired correct number of times + */ +async function testRestoreCommon(isolateEverything) { + await SpecialPowers.pushPrefEnv({ + set: [["fission.webContentIsolationStrategy", isolateEverything ? 1 : 0]], + }); + setupRemoteTypes(isolateEverything); + + let newWin = await promiseNewWindowLoaded(); + var regularPages = []; + var containerPages = {}; + // Go through all the test cases and open same set of urls in regular tabs and in container tabs + for (const uri of TEST_CASES) { + // Open a url in a regular tab + let regularPage = await openURIInRegularTab(uri, newWin); + regularPages.push(regularPage); + + // Open the same url in different user contexts + for ( + var user_context_id = 1; + user_context_id <= NUM_USER_CONTEXTS; + user_context_id++ + ) { + let containerPage = await openURIInContainer( + uri, + newWin, + user_context_id + ); + containerPages[uri] = containerPage; + } + } + await TabStateFlusher.flushWindow(newWin); + + // Close the window + await BrowserTestUtils.closeWindow(newWin); + await forceSaveState(); + + is( + SessionStore.getClosedWindowCount(), + 1, + "Should have restore data for the closed window" + ); + + Assert.equal( + 0, + (await UrlbarProviderOpenTabs.getDatabaseRegisteredOpenTabsForTests()) + .length, + "No registered open pages should be left" + ); + + // Now restore the window + newWin = SessionStore.undoCloseWindow(0); + + // Make sure to wait for the window to be restored. + await Promise.all([ + BrowserTestUtils.waitForEvent(newWin, "SSWindowStateReady"), + ]); + await BrowserTestUtils.waitForEvent( + newWin.gBrowser.tabContainer, + "SSTabRestored" + ); + + var nonblank_pages_len = + TEST_CASES.length + NUM_USER_CONTEXTS * TEST_CASES.length; + is( + newWin.gBrowser.tabs.length, + nonblank_pages_len + 1 /* initial page */, + "Correct number of tabs restored" + ); + + // Now we have pages opened in the following manner + // [blank page, page1, page1-work, page1-home, page2, page2-work, page2-home] + + info(`Number of tabs restored: ${newWin.gBrowser.tabs.length}`); + var currRemoteType, expectedRemoteType; + let loaded; + for (var tab_idx = 1; tab_idx < nonblank_pages_len; ) { + info(`Accessing regular tab at index ${tab_idx}`); + var test_page_data = regularPages.shift(); + let regular_tab = newWin.gBrowser.tabs[tab_idx]; + let regular_browser = regular_tab.linkedBrowser; + + // I would have used browserLoaded but for about:config it doesn't work + let ready = TestUtils.waitForCondition(async () => { + // Catch an error because the browser might change remoteness in between + // calls, so we will just wait for the document to finish loadig. + return SpecialPowers.spawn(regular_browser, [], () => { + return content.document.readyState == "complete"; + }).catch(console.error); + }); + newWin.gBrowser.selectedTab = regular_tab; + await TabStateFlusher.flush(regular_browser); + await ready; + + currRemoteType = regular_browser.remoteType; + expectedRemoteType = remoteTypes.shift(); + is( + currRemoteType, + expectedRemoteType, + `correct remote type for regular tab with uri ${test_page_data.uri}` + ); + + let page_uri = regular_browser.currentURI.spec; + info(`Current uri = ${page_uri}`); + + // Iterate over container pages, starting after the regular page and ending before the next regular page + var userContextId = 1; + for ( + var container_tab_idx = tab_idx + 1; + container_tab_idx < tab_idx + 1 + NUM_USER_CONTEXTS; + container_tab_idx++, userContextId++ + ) { + info(`Accessing container tab at index ${container_tab_idx}`); + let container_tab = newWin.gBrowser.tabs[container_tab_idx]; + + initXulFrameLoaderCreatedCounter(xulFrameLoaderCreatedCounter); + container_tab.documentGlobal.gBrowser.addEventListener( + "XULFrameLoaderCreated", + handleEventLocal + ); + + loaded = BrowserTestUtils.browserLoaded( + container_tab.linkedBrowser, + false, + test_page_data.uri + ); + + newWin.gBrowser.selectedTab = container_tab; + await TabStateFlusher.flush(container_tab.linkedBrowser); + await loaded; + let uri = container_tab.linkedBrowser.currentURI.spec; + + // Verify XULFrameLoaderCreated was fired once + is( + xulFrameLoaderCreatedCounter.numCalledSoFar, + 1, + `XULFrameLoaderCreated was fired once, when restoring ${uri} in container ${userContextId} ` + ); + container_tab.documentGlobal.gBrowser.removeEventListener( + "XULFrameLoaderCreated", + handleEventLocal + ); + + // Verify correct remote type for container tab + currRemoteType = container_tab.linkedBrowser.remoteType; + expectedRemoteType = remoteTypes.shift(); + info( + `Remote type for container tab ${userContextId} is ${currRemoteType}` + ); + is( + currRemoteType, + expectedRemoteType, + "correct remote type for container tab" + ); + } + // Advance to the next regular page in our tabs list + tab_idx = container_tab_idx; + } + + await BrowserTestUtils.closeWindow(newWin); + + Assert.equal( + 0, + (await UrlbarProviderOpenTabs.getDatabaseRegisteredOpenTabsForTests()) + .length, + "No registered open pages should be left" + ); +} + +if (gFissionBrowser) { + // This will have no impact if fission is disabled, so we skip this test. + add_task(async function testRestoreIsolateEverything() { + await testRestoreCommon(/* isolateEverything */ true); + }); +} + +add_task(async function testRestoreIsolateNothing() { + await testRestoreCommon(/* isolateEverything */ false); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_restore_cookies_noOriginAttributes.js b/src/zen/tests/mochitests/sessionstore/browser_restore_cookies_noOriginAttributes.js new file mode 100644 index 000000000..3973c7c73 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restore_cookies_noOriginAttributes.js @@ -0,0 +1,194 @@ +/* + * Bug 1267910 - The regression test case for session cookies. + */ + +"use strict"; + +const TEST_HOST = "www.example.com"; +const COOKIE = { + name: "test1", + value: "yes1", + path: "/browser/browser/components/sessionstore/test/", + sameSite: Ci.nsICookie.SAMESITE_UNSET, +}; +const SESSION_DATA = JSON.stringify({ + version: ["sessionrestore", 1], + windows: [ + { + tabs: [ + { + entries: [], + lastAccessed: 1463893009797, + hidden: false, + attributes: {}, + image: null, + }, + { + entries: [ + { + url: "http://www.example.com/browser/browser/components/sessionstore/test/browser_1267910_page.html", + triggeringPrincipal_base64, + charset: "UTF-8", + ID: 0, + docshellID: 2, + originalURI: + "http://www.example.com/browser/browser/components/sessionstore/test/browser_1267910_page.html", + docIdentifier: 0, + persist: true, + }, + ], + lastAccessed: 1463893009321, + hidden: false, + attributes: {}, + userContextId: 0, + index: 1, + image: "http://www.example.com/favicon.ico", + }, + ], + selected: 1, + _closedTabs: [], + busy: false, + width: 1024, + height: 768, + screenX: 4, + screenY: 23, + sizemode: "normal", + cookies: [ + { + host: "www.example.com", + value: "yes1", + path: "/browser/browser/components/sessionstore/test/", + name: "test1", + sameSite: Ci.nsICookie.SAMESITE_UNSET, + }, + ], + }, + ], + selectedWindow: 1, + _closedWindows: [], + session: { + lastUpdate: 1463893009801, + startTime: 1463893007134, + recentCrashes: 0, + }, + global: {}, +}); + +const SESSION_DATA_OA = JSON.stringify({ + version: ["sessionrestore", 1], + windows: [ + { + tabs: [ + { + entries: [], + lastAccessed: 1463893009797, + hidden: false, + attributes: {}, + image: null, + }, + { + entries: [ + { + url: "http://www.example.com/browser/browser/components/sessionstore/test/browser_1267910_page.html", + triggeringPrincipal_base64, + charset: "UTF-8", + ID: 0, + docshellID: 2, + originalURI: + "http://www.example.com/browser/browser/components/sessionstore/test/browser_1267910_page.html", + docIdentifier: 0, + persist: true, + }, + ], + lastAccessed: 1463893009321, + hidden: false, + attributes: {}, + userContextId: 0, + index: 1, + image: "http://www.example.com/favicon.ico", + }, + ], + selected: 1, + _closedTabs: [], + busy: false, + width: 1024, + height: 768, + screenX: 4, + screenY: 23, + sizemode: "normal", + cookies: [ + { + host: "www.example.com", + value: "yes1", + path: "/browser/browser/components/sessionstore/test/", + name: "test1", + sameSite: Ci.nsICookie.SAMESITE_UNSET, + originAttributes: { + addonId: "", + inIsolatedMozBrowser: false, + userContextId: 0, + }, + }, + ], + }, + ], + selectedWindow: 1, + _closedWindows: [], + session: { + lastUpdate: 1463893009801, + startTime: 1463893007134, + recentCrashes: 0, + }, + global: {}, +}); + +add_task(async function run_test() { + // Wait until initialization is complete. + await SessionStore.promiseInitialized; + + // Clear cookies. + Services.cookies.removeAll(); + + // Open a new window. + let win = await promiseNewWindowLoaded(); + + // Restore window with session cookies that have no originAttributes. + await setWindowState(win, SESSION_DATA, true); + + let cookieCount = 0; + for (var cookie of Services.cookies.getCookiesFromHost(TEST_HOST, {})) { + cookieCount++; + } + + // Check that the cookie is restored successfully. + is(cookieCount, 1, "expected one cookie"); + is(cookie.name, COOKIE.name, "cookie name successfully restored"); + is(cookie.value, COOKIE.value, "cookie value successfully restored"); + is(cookie.path, COOKIE.path, "cookie path successfully restored"); + + // Clear cookies. + Services.cookies.removeAll(); + + // In real usage, the event loop would get to spin between setWindowState + // uses. Without a spin, we can defer handling the STATE_STOP that + // removes the progress listener until after the mozbrowser has been + // destroyed, causing a window leak. + await new Promise(resolve => win.setTimeout(resolve, 0)); + + // Restore window with session cookies that have originAttributes within. + await setWindowState(win, SESSION_DATA_OA, true); + + cookieCount = 0; + for (cookie of Services.cookies.getCookiesFromHost(TEST_HOST, {})) { + cookieCount++; + } + + // Check that the cookie is restored successfully. + is(cookieCount, 1, "expected one cookie"); + is(cookie.name, COOKIE.name, "cookie name successfully restored"); + is(cookie.value, COOKIE.value, "cookie value successfully restored"); + is(cookie.path, COOKIE.path, "cookie path successfully restored"); + + // Close our window. + await BrowserTestUtils.closeWindow(win); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_restore_pageProxyState.js b/src/zen/tests/mochitests/sessionstore/browser_restore_pageProxyState.js new file mode 100644 index 000000000..f98237c7e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restore_pageProxyState.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const BACKUP_STATE = SessionStore.getBrowserState(); +registerCleanupFunction(() => promiseBrowserState(BACKUP_STATE)); + +// The pageproxystate of the restored tab controls whether the identity +// information in the URL bar will display correctly. See bug 1766951 for more +// context. +async function test_pageProxyState(url1, url2) { + info(`urls: "${url1}", "${url2}"`); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.sessionstore.restore_on_demand", true], + ["browser.sessionstore.restore_tabs_lazily", true], + ], + }); + + await promiseBrowserState({ + windows: [ + { + tabs: [ + { + entries: [ + { + url: url1, + triggeringPrincipal_base64, + }, + ], + }, + { + entries: [ + { + url: url2, + triggeringPrincipal_base64, + }, + ], + }, + ], + selected: 1, + }, + ], + }); + + // The first tab isn't lazy and should be initialized. + ok(gBrowser.tabs[0].linkedPanel, "first tab is not lazy"); + is(gBrowser.selectedTab, gBrowser.tabs[0], "first tab is selected"); + is(gBrowser.userTypedValue, null, "no user typed value"); + is( + gURLBar.getAttribute("pageproxystate"), + "valid", + "has valid page proxy state" + ); + + // The second tab is lazy until selected. + ok(!gBrowser.tabs[1].linkedPanel, "second tab should be lazy"); + gBrowser.selectedTab = gBrowser.tabs[1]; + await promiseTabRestored(gBrowser.tabs[1]); + is(gBrowser.userTypedValue, null, "no user typed value"); + is( + gURLBar.getAttribute("pageproxystate"), + "valid", + "has valid page proxy state" + ); +} + +add_task(async function test_system() { + await test_pageProxyState("about:support", "about:addons"); +}); + +add_task(async function test_http() { + await test_pageProxyState( + "https://example.com/document-builder.sjs?html=tab1", + "https://example.com/document-builder.sjs?html=tab2" + ); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_restore_private_tab_os.js b/src/zen/tests/mochitests/sessionstore/browser_restore_private_tab_os.js new file mode 100644 index 000000000..3041fe6f3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restore_private_tab_os.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URI = + "https://example.com/" + + "browser/browser/components/sessionstore/test/empty.html"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Set the pref to true so we know exactly how many tabs should be restoring at + // any given time. This guarantees that a finishing load won't start another. + ["browser.sessionstore.restore_on_demand", true], + // Don't restore tabs lazily. + ["browser.sessionstore.restore_tabs_lazily", true], + // don't preload tabs so we don't have extra XULFrameLoaderCreated events + // firing + ["browser.newtab.preload", false], + ], + }); +}); + +add_task(async function testRestore() { + // Clear the list of closed windows. + forgetClosedWindows(); + + // Create a new private window + let win = await promiseNewWindowLoaded({ private: true }); + + // Create a new private tab + let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_URI); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + await TabStateFlusher.flush(browser); + + // Ensure that closed tabs in a private windows can be restored. + win.gBrowser.removeTab(tab); + is(ss.getClosedTabCountForWindow(win), 1, "there is a single tab to restore"); + + tab = SessionStore.undoCloseTab(win, 0); + info(`Undo close tab`); + browser = tab.linkedBrowser; + await promiseTabRestored(tab); + info(`Private tab restored`); + + let expectedRemoteType = gFissionBrowser + ? "webIsolated=https://example.com^privateBrowsingId=1" + : "web"; + is(browser.remoteType, expectedRemoteType, "correct remote type"); + + await BrowserTestUtils.closeWindow(win); + + // Cleanup + info("Forgetting closed tabs"); + forgetClosedTabs(window); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_restore_redirect.js b/src/zen/tests/mochitests/sessionstore/browser_restore_redirect.js new file mode 100644 index 000000000..a1d4ae0a3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restore_redirect.js @@ -0,0 +1,84 @@ +"use strict"; + +const BASE = "http://example.com/browser/browser/components/sessionstore/test/"; +const TARGET = BASE + "restore_redirect_target.html"; + +/** + * Ensure that a http redirect leaves a working tab. + */ +add_task(async function check_http_redirect() { + let state = { + entries: [ + { url: BASE + "restore_redirect_http.html", triggeringPrincipal_base64 }, + ], + }; + + // Open a new tab to restore into. + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + + // addTab sends a message to the child to load about:blank, which sends a + // message to the parent to add the SH entry. + // promiseTabState modifies the SH syncronously, so ensure it is settled. + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + + await promiseTabState(tab, state); + + info("Restored tab"); + + await TabStateFlusher.flush(browser); + let data = TabState.collect(tab); + is(data.entries.length, 1, "Should be one entry in session history"); + is(data.entries[0].url, TARGET, "Should be the right session history entry"); + + ok( + !ss.getInternalObjectState(browser), + "Temporary restore data should have been cleared" + ); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); + +/** + * Ensure that a js redirect leaves a working tab. + */ +add_task(async function check_js_redirect() { + let state = { + entries: [ + { url: BASE + "restore_redirect_js.html", triggeringPrincipal_base64 }, + ], + }; + + // Open a new tab to restore into. + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + + // addTab sends a message to the child to load about:blank, which sends a + // message to the parent to add the SH entry. + // promiseTabState modifies the SH syncronously, so ensure it is settled. + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + + let loadPromise = BrowserTestUtils.browserLoaded(browser, true, url => + url.endsWith("restore_redirect_target.html") + ); + + await promiseTabState(tab, state); + + info("Restored tab"); + + await loadPromise; + + await TabStateFlusher.flush(browser); + let data = TabState.collect(tab); + is(data.entries.length, 1, "Should be one entry in session history"); + is(data.entries[0].url, TARGET, "Should be the right session history entry"); + + ok( + !ss.getInternalObjectState(browser), + "Temporary restore data should have been cleared" + ); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_restore_reversed_z_order.js b/src/zen/tests/mochitests/sessionstore/browser_restore_reversed_z_order.js new file mode 100644 index 000000000..69745148a --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restore_reversed_z_order.js @@ -0,0 +1,128 @@ +"use strict"; + +const PRIMARY_WINDOW = window; + +let gTestURLsMap = new Map([ + ["about:about", null], + ["about:license", null], + ["about:robots", null], + ["about:mozilla", null], +]); +let gBrowserState; + +add_setup(async function () { + let windows = []; + let count = 0; + for (let url of gTestURLsMap.keys()) { + let window = !count + ? PRIMARY_WINDOW + : await BrowserTestUtils.openNewBrowserWindow(); + let browserLoaded = BrowserTestUtils.browserLoaded( + window.gBrowser.selectedBrowser + ); + BrowserTestUtils.startLoadingURIString( + window.gBrowser.selectedBrowser, + url + ); + await browserLoaded; + // Capture the title. + gTestURLsMap.set(url, window.gBrowser.selectedTab.label); + // Minimize the before-last window, to have a different window feature added + // to the test. + if (count == gTestURLsMap.size - 1) { + let activated = BrowserTestUtils.waitForEvent( + windows[count - 1], + "activate" + ); + window.minimize(); + await activated; + } + windows.push(window); + ++count; + } + + // Wait until we get the lastest history from all windows. + await Promise.all(windows.map(window => TabStateFlusher.flushWindow(window))); + + gBrowserState = ss.getBrowserState(); + + await promiseAllButPrimaryWindowClosed(); +}); + +add_task(async function test_z_indices_are_saved_correctly() { + let state = JSON.parse(gBrowserState); + Assert.equal( + state.windows.length, + gTestURLsMap.size, + "Correct number of windows saved" + ); + + // Check if we saved state in correct order of creation. + let idx = 0; + for (let url of gTestURLsMap.keys()) { + Assert.equal( + state.windows[idx].tabs[0].entries[0].url, + url, + `Window #${idx} is stored in correct creation order` + ); + ++idx; + } + + // Check if we saved a valid zIndex (no null, no undefined or no 0). + for (let window of state.windows) { + Assert.ok(window.zIndex, "A valid zIndex is stored"); + } + + Assert.equal( + state.windows[0].zIndex, + 3, + "Window #1 should have the correct z-index" + ); + Assert.equal( + state.windows[1].zIndex, + 2, + "Window #2 should have correct z-index" + ); + Assert.equal( + state.windows[2].zIndex, + 1, + "Window #3 should be the topmost window" + ); + Assert.equal( + state.windows[3].zIndex, + 4, + "Minimized window should be the last window to restore" + ); +}); + +add_task(async function test_windows_are_restored_in_reversed_z_order() { + await promiseBrowserState(gBrowserState); + + let indexedTabLabels = [...gTestURLsMap.values()]; + let tabsRestoredLabels = BrowserWindowTracker.orderedWindows.map( + window => window.gBrowser.selectedTab.label + ); + + Assert.equal( + tabsRestoredLabels[0], + indexedTabLabels[2], + "First restored tab should be last used tab" + ); + Assert.equal( + tabsRestoredLabels[1], + indexedTabLabels[1], + "Second restored tab is correct" + ); + Assert.equal( + tabsRestoredLabels[2], + indexedTabLabels[0], + "Third restored tab is correct" + ); + Assert.equal( + tabsRestoredLabels[3], + indexedTabLabels[3], + "Last restored tab should be a minimized window" + ); + + await promiseAllButPrimaryWindowClosed(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_restore_srcdoc.js b/src/zen/tests/mochitests/sessionstore/browser_restore_srcdoc.js new file mode 100644 index 000000000..9e670100e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_restore_srcdoc.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function makeURL(srcdocValue) { + return `data:text/html;charset=utf-8," + + "clickme" + + "clickme"; + + // Create a new tab. + let tab = BrowserTestUtils.addTab(gBrowser, URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // Check that we have a single shistory entry. + await TabStateFlusher.flush(browser); + let { entries } = JSON.parse(ss.getTabState(tab)); + is(entries.length, 1, "there is one shistory entry"); + is(entries[0].children.length, 1, "the entry has one child"); + + // Navigate the subframe. + await SpecialPowers.spawn(browser, [], async function () { + content.document.notifyUserGestureActivation(); + content.document.querySelector("#a1").click(); + }); + await promiseBrowserLoaded( + browser, + false /* wait for subframe load only */, + "http://example.com/1" + ); + + // Check shistory. + await TabStateFlusher.flush(browser); + ({ entries } = JSON.parse(ss.getTabState(tab))); + is(entries.length, 2, "there now are two shistory entries"); + is(entries[1].children.length, 1, "the second entry has one child"); + + // Go back in history. + let goneBack = promiseBrowserLoaded( + browser, + false /* wait for subframe load only */, + "http://example.com/" + ); + info("About to go back in history"); + browser.goBack(); + await goneBack; + + // Navigate the subframe again. + let eventPromise = BrowserTestUtils.waitForContentEvent( + browser, + "hashchange", + true + ); + await SpecialPowers.spawn(browser, [], async function () { + content.document.querySelector("#a2").click(); + }); + await eventPromise; + + // Check shistory. + await TabStateFlusher.flush(browser); + ({ entries } = JSON.parse(ss.getTabState(tab))); + is(entries.length, 2, "there now are two shistory entries"); + is(entries[1].children.length, 1, "the second entry has one child"); + + // Cleanup. + gBrowser.removeTab(tab); +}); + +/** + * Ensure that navigating from an about page invalidates shistory. + */ +add_task(async function test_about_page_navigate() { + // Create a new tab. + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + + // Check that we have a single shistory entry. + await TabStateFlusher.flush(browser); + let { entries } = JSON.parse(ss.getTabState(tab)); + is(entries.length, 1, "there is one shistory entry"); + is(entries[0].url, "about:blank", "url is correct"); + + // Verify that the title is also recorded. + is(entries[0].title, "about:blank", "title is correct"); + + BrowserTestUtils.startLoadingURIString(browser, "about:robots"); + await promiseBrowserLoaded(browser); + + // Check that we have changed the history entry. + await TabStateFlusher.flush(browser); + ({ entries } = JSON.parse(ss.getTabState(tab))); + is(entries.length, 1, "there is one shistory entry"); + is(entries[0].url, "about:robots", "url is correct"); + + // Cleanup. + gBrowser.removeTab(tab); +}); + +/** + * Ensure that history.pushState and history.replaceState invalidate shistory. + */ +add_task(async function test_pushstate_replacestate() { + // Create a new tab. + let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com/1"); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // Check that we have a single shistory entry. + await TabStateFlusher.flush(browser); + let { entries } = JSON.parse(ss.getTabState(tab)); + is(entries.length, 1, "there is one shistory entry"); + is(entries[0].url, "http://example.com/1", "url is correct"); + + await SpecialPowers.spawn(browser, [], async function () { + content.window.history.pushState({}, "", "test-entry/"); + }); + + // Check that we have added the history entry. + await TabStateFlusher.flush(browser); + ({ entries } = JSON.parse(ss.getTabState(tab))); + is(entries.length, 2, "there is another shistory entry"); + is(entries[1].url, "http://example.com/test-entry/", "url is correct"); + + await SpecialPowers.spawn(browser, [], async function () { + content.window.history.replaceState({}, "", "test-entry2/"); + }); + + // Check that we have modified the history entry. + await TabStateFlusher.flush(browser); + ({ entries } = JSON.parse(ss.getTabState(tab))); + is(entries.length, 2, "there is still two shistory entries"); + is( + entries[1].url, + "http://example.com/test-entry/test-entry2/", + "url is correct" + ); + + // Cleanup. + gBrowser.removeTab(tab); +}); + +/** + * Ensure that slow loading subframes will invalidate shistory. + */ +add_task(async function test_slow_subframe_load() { + const SLOW_URL = + "http://mochi.test:8888/browser/browser/components/" + + "sessionstore/test/browser_sessionHistory_slow.sjs"; + + const URL = + "data:text/html;charset=utf-8," + + "" + + "" + + ""; + + // Add a new tab with a slow loading subframe + let tab = BrowserTestUtils.addTab(gBrowser, URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + await TabStateFlusher.flush(browser); + let { entries } = JSON.parse(ss.getTabState(tab)); + + // Check the number of children. + is(entries.length, 1, "there is one root entry ..."); + is(entries[0].children.length, 1, "... with one child entries"); + + // Check URLs. + ok(entries[0].url.startsWith("data:text/html"), "correct root url"); + is(entries[0].children[0].url, SLOW_URL, "correct url for subframe"); + + // Cleanup. + gBrowser.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_sessionHistory_partitionedPrincipalToInherit.js b/src/zen/tests/mochitests/sessionstore/browser_sessionHistory_partitionedPrincipalToInherit.js new file mode 100644 index 000000000..5caf022d1 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_sessionHistory_partitionedPrincipalToInherit.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_partitioned_principal_to_inherit() { + // Create a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + let browser = tab.linkedBrowser; + + // Get the last entry in the session history. + let sh = browser.browsingContext.sessionHistory; + let entry = sh.getEntryAtIndex(sh.count - 1); + let partitionedPrincipalToInherit = entry.partitionedPrincipalToInherit; + + // Check that the partitioned principal to inherit is properly set. + ok(partitionedPrincipalToInherit, "partitionedPrincipalToInherit is set"); + is( + partitionedPrincipalToInherit.originAttributes.partitionKey, + "(https,example.com)", + "correct partitionKey" + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_partitioned_Principal_to_inherit_in_iframe() { + // Create a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + let browser = tab.linkedBrowser; + + // Load a same-origin iframe + await SpecialPowers.spawn(browser, [], async _ => { + let iframe = content.document.createElement("iframe"); + iframe.src = "https://example.com"; + + await new content.Promise(resolve => { + iframe.onload = resolve; + content.document.body.appendChild(iframe); + }); + }); + + // Get the last child entry in the session history for the same-origin iframe. + let sh = browser.browsingContext.sessionHistory; + let entry = sh.getEntryAtIndex(sh.count - 1); + let childEntry = entry.GetChildAt(entry.childCount - 1); + let partitionedPrincipalToInherit = childEntry.partitionedPrincipalToInherit; + + // Check that the partitioned principal to inherit is properly set. + ok(partitionedPrincipalToInherit, "partitionedPrincipalToInherit is set"); + is( + partitionedPrincipalToInherit.originNoSuffix, + "https://example.com", + "correct originNoSuffix in the same-origin iframe" + ); + is( + partitionedPrincipalToInherit.originAttributes.partitionKey, + "(https,example.com)", + "correct partitionKey in the same-origin iframe" + ); + + // Load a cross-site iframe. + await SpecialPowers.spawn(browser, [], async _ => { + let iframe = content.document.createElement("iframe"); + iframe.src = "https://example.net"; + + await new content.Promise(resolve => { + iframe.onload = resolve; + content.document.body.appendChild(iframe); + }); + }); + + // Get the last child entry in the session history for the cross-site iframe. + entry = sh.getEntryAtIndex(sh.count - 1); + childEntry = entry.GetChildAt(entry.childCount - 1); + partitionedPrincipalToInherit = childEntry.partitionedPrincipalToInherit; + + // Check that the partitioned principal to inherit is properly set. + ok(partitionedPrincipalToInherit, "partitionedPrincipalToInherit is set"); + is( + partitionedPrincipalToInherit.originNoSuffix, + "https://example.net", + "correct originNoSuffix in the cross-site iframe" + ); + is( + partitionedPrincipalToInherit.originAttributes.partitionKey, + "(https,example.com)", + "correct partitionKey in the cross-site iframe" + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_partitioned_Principal_to_inherit_in_ABA_iframe() { + // Create a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + let browser = tab.linkedBrowser; + + // Load a ABA iframe + await SpecialPowers.spawn(browser, [], async () => { + let iframe = content.document.createElement("iframe"); + iframe.src = "https://example.net"; + + await new content.Promise(resolve => { + iframe.onload = resolve; + content.document.body.appendChild(iframe); + }); + + await SpecialPowers.spawn(iframe, [], async () => { + let nestedIframe = content.document.createElement("iframe"); + nestedIframe.src = "https://example.com"; + + await new content.Promise(resolve => { + nestedIframe.onload = resolve; + content.document.body.appendChild(nestedIframe); + }); + }); + }); + + // Get the last child entry in the session history for the same-origin iframe. + let sh = browser.browsingContext.sessionHistory; + let entry = sh.getEntryAtIndex(sh.count - 1); + let childEntry = entry.GetChildAt(entry.childCount - 1); + let nestedChildEntry = childEntry.GetChildAt(childEntry.childCount - 1); + let partitionedPrincipalToInherit = + nestedChildEntry.partitionedPrincipalToInherit; + + // Check that the partitioned principal to inherit is properly set. + ok(partitionedPrincipalToInherit, "partitionedPrincipalToInherit is set"); + is( + partitionedPrincipalToInherit.originNoSuffix, + "https://example.com", + "correct originNoSuffix in the ABA iframe" + ); + is( + partitionedPrincipalToInherit.originAttributes.partitionKey, + "(https,example.com,f)", + "correct partitionKey in the ABA iframe" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_sessionHistory_slow.sjs b/src/zen/tests/mochitests/sessionstore/browser_sessionHistory_slow.sjs new file mode 100644 index 000000000..abb1dee82 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_sessionHistory_slow.sjs @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const DELAY_MS = "2000"; + +let timer; + +function handleRequest(req, resp) { + resp.processAsync(); + resp.setHeader("Cache-Control", "no-cache", false); + resp.setHeader("Content-Type", "text/html;charset=utf-8", false); + + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + () => { + resp.write("hi"); + resp.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_sessionStorage.html b/src/zen/tests/mochitests/sessionstore/browser_sessionStorage.html new file mode 100644 index 000000000..afc1cf1de --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_sessionStorage.html @@ -0,0 +1,35 @@ + + + + + browser_sessionStorage.html + + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_sessionStorage.js b/src/zen/tests/mochitests/sessionstore/browser_sessionStorage.js new file mode 100644 index 000000000..43af6c47e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_sessionStorage.js @@ -0,0 +1,308 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const RAND = Math.random(); +const URL = + "http://mochi.test:8888/browser/" + + "browser/components/sessionstore/test/browser_sessionStorage.html" + + "?" + + RAND; + +const HAS_FIRST_PARTY_DOMAIN = [ + Ci.nsICookieService.BEHAVIOR_PARTITION_FOREIGN, +].includes(Services.prefs.getIntPref("network.cookie.cookieBehavior")); +const OUTER_ORIGIN = "http://mochi.test:8888"; +const FIRST_PARTY_DOMAIN = escape("(http,mochi.test)"); +const INNER_ORIGIN = HAS_FIRST_PARTY_DOMAIN + ? `http://example.com^partitionKey=${FIRST_PARTY_DOMAIN}` + : "http://example.com"; +const SECURE_INNER_ORIGIN = HAS_FIRST_PARTY_DOMAIN + ? `https://example.com^partitionKey=${FIRST_PARTY_DOMAIN}` + : "https://example.com"; + +const OUTER_VALUE = "outer-value-" + RAND; +const INNER_VALUE = "inner-value-" + RAND; + +/** + * This test ensures that setting, modifying and restoring sessionStorage data + * works as expected. + */ +add_task(async function session_storage() { + let tab = BrowserTestUtils.addTab(gBrowser, URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // Flush to make sure chrome received all data. + await TabStateFlusher.flush(browser); + + let { storage } = JSON.parse(ss.getTabState(tab)); + is( + storage[INNER_ORIGIN].test, + INNER_VALUE, + "sessionStorage data for example.com has been serialized correctly" + ); + is( + storage[OUTER_ORIGIN].test, + OUTER_VALUE, + "sessionStorage data for mochi.test has been serialized correctly" + ); + + // Ensure that modifying sessionStore values works for the inner frame only. + await modifySessionStorage(browser, { test: "modified1" }, { frameIndex: 0 }); + await TabStateFlusher.flush(browser); + + ({ storage } = JSON.parse(ss.getTabState(tab))); + is( + storage[INNER_ORIGIN].test, + "modified1", + "sessionStorage data for example.com has been serialized correctly" + ); + is( + storage[OUTER_ORIGIN].test, + OUTER_VALUE, + "sessionStorage data for mochi.test has been serialized correctly" + ); + + // Ensure that modifying sessionStore values works for both frames. + await modifySessionStorage(browser, { test: "modified" }); + await modifySessionStorage(browser, { test: "modified2" }, { frameIndex: 0 }); + await TabStateFlusher.flush(browser); + + ({ storage } = JSON.parse(ss.getTabState(tab))); + is( + storage[INNER_ORIGIN].test, + "modified2", + "sessionStorage data for example.com has been serialized correctly" + ); + is( + storage[OUTER_ORIGIN].test, + "modified", + "sessionStorage data for mochi.test has been serialized correctly" + ); + + // Test that duplicating a tab works. + let tab2 = gBrowser.duplicateTab(tab); + let browser2 = tab2.linkedBrowser; + // See bug 2001322, need to wait for iframe of tab2 to finish loading + const subframeLoaded = BrowserTestUtils.browserLoaded(tab2.linkedBrowser, { + includeSubFrames: true, + wantLoad: url => url.startsWith("http://example.com"), + }); + await Promise.all([promiseTabRestored(tab2), subframeLoaded]); + + // Flush to make sure chrome received all data. + await TabStateFlusher.flush(browser2); + + ({ storage } = JSON.parse(ss.getTabState(tab2))); + is( + storage[INNER_ORIGIN].test, + "modified2", + "sessionStorage data for example.com has been duplicated correctly" + ); + is( + storage[OUTER_ORIGIN].test, + "modified", + "sessionStorage data for mochi.test has been duplicated correctly" + ); + + // Ensure that the content script retains restored data + // (by e.g. duplicateTab) and sends it along with new data. + await modifySessionStorage(browser2, { test: "modified3" }); + await TabStateFlusher.flush(browser2); + + ({ storage } = JSON.parse(ss.getTabState(tab2))); + is( + storage[INNER_ORIGIN].test, + "modified2", + "sessionStorage data for example.com has been duplicated correctly" + ); + is( + storage[OUTER_ORIGIN].test, + "modified3", + "sessionStorage data for mochi.test has been duplicated correctly" + ); + + // Check that loading a new URL discards data. + BrowserTestUtils.startLoadingURIString(browser2, "http://mochi.test:8888/"); + await promiseBrowserLoaded(browser2); + await TabStateFlusher.flush(browser2); + + ({ storage } = JSON.parse(ss.getTabState(tab2))); + is( + storage[OUTER_ORIGIN].test, + "modified3", + "navigating retains correct storage data" + ); + + is( + storage[INNER_ORIGIN].test, + "modified2", + "sessionStorage data for example.com wasn't discarded after top-level same-site navigation" + ); + + // Test that clearing the data in the first tab works properly within + // the subframe + await modifySessionStorage(browser, {}, { frameIndex: 0 }); + await TabStateFlusher.flush(browser); + ({ storage } = JSON.parse(ss.getTabState(tab))); + is( + storage[INNER_ORIGIN], + undefined, + "sessionStorage data for example.com has been cleared correctly" + ); + + // Test that clearing the data in the first tab works properly within + // the top-level frame + await modifySessionStorage(browser, {}); + await TabStateFlusher.flush(browser); + ({ storage } = JSON.parse(ss.getTabState(tab))); + ok( + storage === null || storage === undefined, + "sessionStorage data for the entire tab has been cleared correctly" + ); + + // Clean up. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +/** + * This test ensures that purging domain data also purges data from the + * sessionStorage data collected for tabs. + */ +add_task(async function purge_domain() { + let tab = BrowserTestUtils.addTab(gBrowser, URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // Purge data for "mochi.test". + await purgeDomainData(browser, "mochi.test"); + + // Flush to make sure chrome received all data. + await TabStateFlusher.flush(browser); + + let { storage } = JSON.parse(ss.getTabState(tab)); + ok( + !storage[OUTER_ORIGIN], + "sessionStorage data for mochi.test has been purged" + ); + is( + storage[INNER_ORIGIN].test, + INNER_VALUE, + "sessionStorage data for example.com has been preserved" + ); + + BrowserTestUtils.removeTab(tab); +}); + +/** + * This test ensures that collecting sessionStorage data respects the privacy + * levels as set by the user. + */ +add_task(async function respect_privacy_level() { + let tab = BrowserTestUtils.addTab(gBrowser, URL + "&secure"); + await promiseBrowserLoaded(tab.linkedBrowser); + await TabStateFlusher.flush(tab.linkedBrowser); + await promiseRemoveTabAndSessionState(tab); + + let [ + { + state: { storage }, + }, + ] = ss.getClosedTabDataForWindow(window); + is( + storage[OUTER_ORIGIN].test, + OUTER_VALUE, + "http sessionStorage data has been saved" + ); + is( + storage[SECURE_INNER_ORIGIN].test, + INNER_VALUE, + "https sessionStorage data has been saved" + ); + + // Disable saving data for encrypted sites. + Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1); + + tab = BrowserTestUtils.addTab(gBrowser, URL + "&secure"); + await promiseBrowserLoaded(tab.linkedBrowser); + await TabStateFlusher.flush(tab.linkedBrowser); + await promiseRemoveTabAndSessionState(tab); + + [ + { + state: { storage }, + }, + ] = ss.getClosedTabDataForWindow(window); + is( + storage[OUTER_ORIGIN].test, + OUTER_VALUE, + "http sessionStorage data has been saved" + ); + ok( + !storage[SECURE_INNER_ORIGIN], + "https sessionStorage data has *not* been saved" + ); + + // Disable saving data for any site. + Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2); + + // Check that duplicating a tab copies all private data. + tab = BrowserTestUtils.addTab(gBrowser, URL + "&secure"); + await promiseBrowserLoaded(tab.linkedBrowser); + let tab2 = gBrowser.duplicateTab(tab); + // See bug 2001322, need to wait for iframe of tab2 to finish loading + const subframeLoaded = BrowserTestUtils.browserLoaded(tab2.linkedBrowser, { + includeSubFrames: true, + wantLoad: url => url.startsWith("https://example.com"), + }); + await Promise.all([promiseTabRestored(tab2), subframeLoaded]); + await promiseRemoveTabAndSessionState(tab); + + // With privacy_level=2 the |tab| shouldn't have any sessionStorage data. + [ + { + state: { storage }, + }, + ] = ss.getClosedTabDataForWindow(window); + ok(!storage, "sessionStorage data has *not* been saved"); + + // Remove all closed tabs before continuing with the next test. + // As Date.now() isn't monotonic we might sometimes check + // the wrong closedTabData entry. + forgetClosedTabs(window); + + // Restore the default privacy level and close the duplicated tab. + Services.prefs.clearUserPref("browser.sessionstore.privacy_level"); + await promiseRemoveTabAndSessionState(tab2); + + // With privacy_level=0 the duplicated |tab2| should persist all data. + [ + { + state: { storage }, + }, + ] = ss.getClosedTabDataForWindow(window); + is( + storage[OUTER_ORIGIN].test, + OUTER_VALUE, + "http sessionStorage data has been saved" + ); + is( + storage[SECURE_INNER_ORIGIN].test, + INNER_VALUE, + "https sessionStorage data has been saved" + ); +}); + +function purgeDomainData(browser, domain) { + return new Promise(resolve => { + Services.clearData.deleteDataFromHost( + domain, + true, + Services.clearData.CLEAR_HISTORY, + resolve + ); + }); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_sessionStorage_size.js b/src/zen/tests/mochitests/sessionstore/browser_sessionStorage_size.js new file mode 100644 index 000000000..104548281 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_sessionStorage_size.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const RAND = Math.random(); +const URL = + "http://mochi.test:8888/browser/" + + "browser/components/sessionstore/test/browser_sessionStorage.html" + + "?" + + RAND; + +const OUTER_VALUE = "outer-value-" + RAND; + +// Lower the size limit for DOM Storage content. Check that DOM Storage +// is not updated, but that other things remain updated. +add_task(async function test_large_content() { + Services.prefs.setIntPref("browser.sessionstore.dom_storage_limit", 5); + + let tab = BrowserTestUtils.addTab(gBrowser, URL); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + // Flush to make sure chrome received all data. + await TabStateFlusher.flush(browser); + + let state = JSON.parse(ss.getTabState(tab)); + info(JSON.stringify(state, null, "\t")); + Assert.equal(state.storage, null, "We have no storage for the tab"); + Assert.equal(state.entries[0].title, OUTER_VALUE); + BrowserTestUtils.removeTab(tab); + + Services.prefs.clearUserPref("browser.sessionstore.dom_storage_limit"); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_sessionStoreContainer.js b/src/zen/tests/mochitests/sessionstore/browser_sessionStoreContainer.js new file mode 100644 index 000000000..c656df7ec --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_sessionStoreContainer.js @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + for (let i = 0; i < 3; ++i) { + let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com/", { + userContextId: i, + }); + let browser = tab.linkedBrowser; + + await promiseBrowserLoaded(browser); + + let tab2 = gBrowser.duplicateTab(tab); + Assert.equal(tab2.getAttribute("usercontextid") || "", i); + let browser2 = tab2.linkedBrowser; + await promiseTabRestored(tab2); + + await SpecialPowers.spawn( + browser2, + [{ expectedId: i }], + async function (args) { + let loadContext = docShell.QueryInterface(Ci.nsILoadContext); + Assert.equal( + loadContext.originAttributes.userContextId, + args.expectedId, + "The docShell has the correct userContextId" + ); + } + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); + } +}); + +add_task(async function () { + let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com/", { + userContextId: 1, + }); + let browser = tab.linkedBrowser; + + await promiseBrowserLoaded(browser); + + gBrowser.selectedTab = tab; + + let tab2 = gBrowser.duplicateTab(tab); + let browser2 = tab2.linkedBrowser; + await promiseTabRestored(tab2); + + await SpecialPowers.spawn( + browser2, + [{ expectedId: 1 }], + async function (args) { + Assert.equal( + docShell.getOriginAttributes().userContextId, + args.expectedId, + "The docShell has the correct userContextId" + ); + } + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +add_task(async function () { + let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com/", { + userContextId: 1, + }); + let browser = tab.linkedBrowser; + + await promiseBrowserLoaded(browser); + + gBrowser.removeTab(tab); + + let tab2 = ss.undoCloseTab(window, 0); + Assert.equal(tab2.getAttribute("usercontextid"), 1); + await promiseTabRestored(tab2); + await SpecialPowers.spawn( + tab2.linkedBrowser, + [{ expectedId: 1 }], + async function (args) { + Assert.equal( + docShell.getOriginAttributes().userContextId, + args.expectedId, + "The docShell has the correct userContextId" + ); + } + ); + + BrowserTestUtils.removeTab(tab2); +}); + +// Opens "uri" in a new tab with the provided userContextId and focuses it. +// Returns the newly opened tab. +async function openTabInUserContext(userContextId) { + // Open the tab in the correct userContextId. + let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com", { + userContextId, + }); + + // Select tab and make sure its browser is focused. + gBrowser.selectedTab = tab; + tab.documentGlobal.focus(); + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + return { tab, browser }; +} + +function waitForNewCookie() { + return new Promise(resolve => { + Services.obs.addObserver(function observer(subj, topic) { + let notification = subj.QueryInterface(Ci.nsICookieNotification); + if (notification.action == Ci.nsICookieNotification.COOKIE_ADDED) { + Services.obs.removeObserver(observer, topic); + resolve(); + } + }, "session-cookie-changed"); + }); +} + +add_task(async function test() { + const USER_CONTEXTS = ["default", "personal", "work"]; + + // Make sure userContext is enabled. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); + + Services.cookies.removeAll(); + + for (let userContextId of Object.keys(USER_CONTEXTS)) { + // Load the page in 3 different contexts and set a cookie + // which should only be visible in that context. + let cookie = USER_CONTEXTS[userContextId]; + + // Open our tab in the given user context. + let { tab, browser } = await openTabInUserContext(userContextId); + + await Promise.all([ + waitForNewCookie(), + SpecialPowers.spawn( + browser, + [cookie], + passedCookie => (content.document.cookie = passedCookie) + ), + ]); + + // Ensure the tab's session history is up-to-date. + await TabStateFlusher.flush(browser); + + // Remove the tab. + gBrowser.removeTab(tab); + } + + let state = JSON.parse(SessionStore.getBrowserState()); + is( + state.cookies.length, + USER_CONTEXTS.length, + "session restore should have each container's cookie" + ); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_should_restore_tab.js b/src/zen/tests/mochitests/sessionstore/browser_should_restore_tab.js new file mode 100644 index 000000000..07c699c31 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_should_restore_tab.js @@ -0,0 +1,139 @@ +const NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed"; + +add_setup(async () => { + registerCleanupFunction(async () => { + Services.obs.notifyObservers(null, "browser:purge-session-history"); + }); +}); + +async function check_tab_close_notification(openedTab, expectNotification) { + let tabUrl = openedTab.linkedBrowser.currentURI.spec; + let win = openedTab.documentGlobal; + let initialTabCount = SessionStore.getClosedTabCountForWindow(win); + + let tabClosed = BrowserTestUtils.waitForTabClosing(openedTab); + let notified = false; + function topicObserver() { + notified = true; + } + Services.obs.addObserver(topicObserver, NOTIFY_CLOSED_OBJECTS_CHANGED); + + BrowserTestUtils.removeTab(openedTab); + await tabClosed; + // SessionStore does a setTimeout(notify, 0) to notifyObservers when it handles TabClose + // We need to wait long enough to be confident the observer would have been notified + // if it was going to be. + let ticks = 0; + await TestUtils.waitForCondition(() => { + return ++ticks > 1; + }); + + Services.obs.removeObserver(topicObserver, NOTIFY_CLOSED_OBJECTS_CHANGED); + if (expectNotification) { + Assert.ok( + notified, + `Expected ${NOTIFY_CLOSED_OBJECTS_CHANGED} when the ${tabUrl} tab closed` + ); + Assert.equal( + SessionStore.getClosedTabCountForWindow(win), + initialTabCount + 1, + "Expected closed tab count to have incremented" + ); + } else { + Assert.ok( + !notified, + `Expected no ${NOTIFY_CLOSED_OBJECTS_CHANGED} when the ${tabUrl} tab closed` + ); + Assert.equal( + SessionStore.getClosedTabCountForWindow(win), + initialTabCount, + "Expected closed tab count to have not changed" + ); + } +} + +add_task(async function test_control_case() { + let win = window; + let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + win.gBrowser, + () => {} + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "https://example.com/" + ); + await tabOpenedAndSwitchedTo; + await check_tab_close_notification(tab, true); +}); + +add_task(async function test_about_new_tab() { + let win = window; + let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + win.gBrowser, + () => {} + ); + // This opens about:newtab: + win.BrowserCommands.openTab(); + let tab = await tabOpenedAndSwitchedTo; + await check_tab_close_notification(tab, false); +}); + +add_task(async function test_about_home() { + let win = window; + let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + win.gBrowser, + () => {} + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "about:home" + ); + await tabOpenedAndSwitchedTo; + await check_tab_close_notification(tab, false); +}); + +add_task(async function test_navigated_about_home() { + let win = window; + let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + win.gBrowser, + () => {} + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "https://example.com/" + ); + await tabOpenedAndSwitchedTo; + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:home"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + // even if we end up on an ignorable URL, + // if there's meaningful history, we should save this tab + await check_tab_close_notification(tab, true); +}); + +add_task(async function test_about_blank() { + let win = window; + let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + win.gBrowser, + () => {} + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "about:blank" + ); + await tabOpenedAndSwitchedTo; + await check_tab_close_notification(tab, false); +}); + +add_task(async function test_about_privatebrowsing() { + let win = window; + let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + win.gBrowser, + () => {} + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "about:privatebrowsing" + ); + await tabOpenedAndSwitchedTo; + await check_tab_close_notification(tab, false); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_sizemodeBeforeMinimized.js b/src/zen/tests/mochitests/sessionstore/browser_sizemodeBeforeMinimized.js new file mode 100644 index 000000000..a832e71bc --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_sizemodeBeforeMinimized.js @@ -0,0 +1,44 @@ +add_task(async function test() { + // Test for bugfix 384278. Confirms that sizemodeBeforeMinimized is set properly when window state is saved. + let win = await BrowserTestUtils.openNewBrowserWindow(); + + async function changeSizeMode(mode) { + let promise = BrowserTestUtils.waitForEvent(win, "sizemodechange"); + win[mode](); + await promise; + } + + function checkCurrentState(sizemodeBeforeMinimized) { + let state = ss.getWindowState(win); + let winState = state.windows[0]; + is( + winState.sizemodeBeforeMinimized, + sizemodeBeforeMinimized, + "sizemodeBeforeMinimized should match" + ); + } + + // Note: Uses ss.getWindowState(win); as a more time efficient alternative to forceSaveState(); (causing timeouts). + // Simulates FF restart. + + if (win.windowState != win.STATE_NORMAL) { + await changeSizeMode("restore"); + } + ss.getWindowState(win); + await changeSizeMode("minimize"); + checkCurrentState("normal"); + + // Need to create new window or test will timeout on linux. + await BrowserTestUtils.closeWindow(win); + win = await BrowserTestUtils.openNewBrowserWindow(); + + if (win.windowState != win.STATE_MAXIMIZED) { + await changeSizeMode("maximize"); + } + ss.getWindowState(win); + await changeSizeMode("minimize"); + checkCurrentState("maximized"); + + // Clean up. + await BrowserTestUtils.closeWindow(win); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_speculative_connect.html b/src/zen/tests/mochitests/sessionstore/browser_speculative_connect.html new file mode 100644 index 000000000..a0fb88e0a --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_speculative_connect.html @@ -0,0 +1,8 @@ + +
+ Dummy html page to test speculative connect +
+ + Hello Speculative Connect + + diff --git a/src/zen/tests/mochitests/sessionstore/browser_speculative_connect.js b/src/zen/tests/mochitests/sessionstore/browser_speculative_connect.js new file mode 100644 index 000000000..01d2d0b61 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_speculative_connect.js @@ -0,0 +1,145 @@ +const TEST_URLS = [ + "about:buildconfig", + "http://mochi.test:8888/browser/browser/components/sessionstore/test/browser_speculative_connect.html", + "", +]; + +/** + * This will open tabs in browser. This will also make the last tab + * inserted to be the selected tab. + */ +async function openTabs(win) { + for (let i = 0; i < TEST_URLS.length; ++i) { + await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URLS[i]); + } +} + +add_task(async function speculative_connect_restore_on_demand() { + Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true); + is( + Services.prefs.getBoolPref("browser.sessionstore.restore_on_demand"), + true, + "We're restoring on demand" + ); + forgetClosedWindows(); + + // Open a new window and populate with tabs. + let win = await promiseNewWindowLoaded(); + await openTabs(win); + + // Close the window. + await BrowserTestUtils.closeWindow(win); + + // Reopen a window. + let newWin = SessionWindowUI.undoCloseWindow(0); + // Make sure we wait until this window is restored. + await promiseWindowRestored(newWin); + + let tabs = newWin.gBrowser.tabs; + is(tabs.length, TEST_URLS.length + 1, "Restored right number of tabs"); + + let e = new MouseEvent("mouseover"); + + // First tab should be ignored, since it's the default blank tab when we open a new window. + + // Trigger a mouse enter on second tab. + tabs[1].dispatchEvent(e); + ok( + !tabs[1].__test_connection_prepared, + "Second tab doesn't have a connection prepared" + ); + is(tabs[1].__test_connection_url, TEST_URLS[0], "Second tab has correct url"); + ok( + SessionStore.getLazyTabValue(tabs[1], "connectionPrepared"), + "Second tab should have connectionPrepared flag after hovered" + ); + + // Trigger a mouse enter on third tab. + tabs[2].dispatchEvent(e); + ok(tabs[2].__test_connection_prepared, "Third tab has a connection prepared"); + is(tabs[2].__test_connection_url, TEST_URLS[1], "Third tab has correct url"); + ok( + SessionStore.getLazyTabValue(tabs[2], "connectionPrepared"), + "Third tab should have connectionPrepared flag after hovered" + ); + + // Last tab is the previously selected tab. + tabs[3].dispatchEvent(e); + is( + SessionStore.getLazyTabValue(tabs[3], "connectionPrepared"), + undefined, + "Previous selected tab shouldn't have connectionPrepared flag" + ); + is( + tabs[3].__test_connection_prepared, + undefined, + "Previous selected tab should not have a connection prepared" + ); + is( + tabs[3].__test_connection_url, + undefined, + "Previous selected tab should not have a connection prepared" + ); + + await BrowserTestUtils.closeWindow(newWin); +}); + +add_task(async function speculative_connect_restore_automatically() { + Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false); + is( + Services.prefs.getBoolPref("browser.sessionstore.restore_on_demand"), + false, + "We're restoring automatically" + ); + forgetClosedWindows(); + + // Open a new window and populate with tabs. + let win = await promiseNewWindowLoaded(); + await openTabs(win); + + // Close the window. + await BrowserTestUtils.closeWindow(win); + + // Reopen a window. + let newWin = SessionWindowUI.undoCloseWindow(0); + // Make sure we wait until this window is restored. + await promiseWindowRestored(newWin); + + let tabs = newWin.gBrowser.tabs; + is(tabs.length, TEST_URLS.length + 1, "Restored right number of tabs"); + + // First tab is ignored, since it's the default tab open when we open new window + + // Second tab. + ok( + !tabs[1].__test_connection_prepared, + "Second tab doesn't have a connection prepared" + ); + is( + tabs[1].__test_connection_url, + TEST_URLS[0], + "Second tab has correct host url" + ); + + // Third tab. + ok(tabs[2].__test_connection_prepared, "Third tab has a connection prepared"); + is( + tabs[2].__test_connection_url, + TEST_URLS[1], + "Third tab has correct host url" + ); + + // Last tab is the previously selected tab. + is( + tabs[3].__test_connection_prepared, + undefined, + "Selected tab should not have a connection prepared" + ); + is( + tabs[3].__test_connection_url, + undefined, + "Selected tab should not have a connection prepared" + ); + + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_splitview_integer_ids.js b/src/zen/tests/mochitests/sessionstore/browser_splitview_integer_ids.js new file mode 100644 index 000000000..4173f6d63 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_splitview_integer_ids.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_fresh_session_sequential_integer_ids() { + SessionStore.resetNextClosedId(); + + let tab1 = gBrowser.tabs[0]; + let tab2 = BrowserTestUtils.addTab(gBrowser, "about:robots"); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser); + + let splitview1 = gBrowser.addTabSplitView([tab1, tab2]); + + Assert.strictEqual( + typeof splitview1.splitViewId, + "number", + "splitViewId should be a number" + ); + Assert.strictEqual( + splitview1.splitViewId, + 1, + "First split view ID should be 1" + ); + + let tab3 = BrowserTestUtils.addTab(gBrowser, "about:mozilla"); + let tab4 = BrowserTestUtils.addTab(gBrowser, "about:about"); + await Promise.all([ + BrowserTestUtils.browserLoaded(tab3.linkedBrowser), + BrowserTestUtils.browserLoaded(tab4.linkedBrowser), + ]); + + let splitview2 = gBrowser.addTabSplitView([tab3, tab4]); + + Assert.strictEqual( + typeof splitview2.splitViewId, + "number", + "splitViewId should be a number" + ); + Assert.strictEqual( + splitview2.splitViewId, + 2, + "Second split view ID should be 2" + ); + + Assert.notEqual( + splitview1.splitViewId, + splitview2.splitViewId, + "Split view IDs should be unique" + ); + + gBrowser.removeTab(tab4); + gBrowser.removeTab(tab3); + gBrowser.removeTab(tab2); +}); + +add_task(async function test_counter_persists_across_sessions() { + let tab1 = gBrowser.tabs[0]; + let tab2 = BrowserTestUtils.addTab(gBrowser, "about:robots"); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser); + + gBrowser.addTabSplitView([tab1, tab2]); + + let tab3 = BrowserTestUtils.addTab(gBrowser, "about:mozilla"); + let tab4 = BrowserTestUtils.addTab(gBrowser, "about:about"); + await Promise.all([ + BrowserTestUtils.browserLoaded(tab3.linkedBrowser), + BrowserTestUtils.browserLoaded(tab4.linkedBrowser), + ]); + + let splitview2 = gBrowser.addTabSplitView([tab3, tab4]); + let secondId = splitview2.splitViewId; + + await TabStateFlusher.flushWindow(window); + + let state = JSON.parse(SessionStore.getBrowserState()); + + Assert.ok(state.maxSplitViewId, "maxSplitViewId should be in saved state"); + Assert.strictEqual( + state.maxSplitViewId, + secondId, + "maxSplitViewId should match the highest ID used" + ); + + await SessionStoreTestUtils.promiseBrowserState(state); + + let tab5 = BrowserTestUtils.addTab(gBrowser, "about:config"); + let tab6 = BrowserTestUtils.addTab(gBrowser, "about:support"); + await Promise.all([ + BrowserTestUtils.browserLoaded(tab5.linkedBrowser), + BrowserTestUtils.browserLoaded(tab6.linkedBrowser), + ]); + + let splitview3 = gBrowser.addTabSplitView([tab5, tab6]); + + Assert.strictEqual( + typeof splitview3.splitViewId, + "number", + "New splitViewId should be a number" + ); + Assert.strictEqual( + splitview3.splitViewId, + secondId + 1, + "New split view ID should be one more than persisted counter" + ); + + gBrowser.removeTab(tab6); + gBrowser.removeTab(tab5); +}); + +add_task(async function test_counter_initialized_from_persisted_value() { + let state = { + windows: [ + { + tabs: [ + { + entries: [ + { + url: "about:mozilla", + triggeringPrincipal_base64, + }, + ], + splitViewId: 42, + }, + { + entries: [ + { + url: "about:robots", + triggeringPrincipal_base64, + }, + ], + splitViewId: 42, + }, + ], + selected: 1, + splitViews: [ + { + id: 42, + numberOfTabs: 2, + }, + ], + }, + ], + maxSplitViewId: 42, + }; + + await SessionStoreTestUtils.promiseBrowserState(state); + + Assert.strictEqual( + gBrowser.tabs[0].splitview.splitViewId, + 42, + "Restored split view should have ID 42" + ); + + let tab3 = BrowserTestUtils.addTab(gBrowser, "about:config"); + let tab4 = BrowserTestUtils.addTab(gBrowser, "about:support"); + await Promise.all([ + BrowserTestUtils.browserLoaded(tab3.linkedBrowser), + BrowserTestUtils.browserLoaded(tab4.linkedBrowser), + ]); + + let newSplitview = gBrowser.addTabSplitView([tab3, tab4]); + + Assert.strictEqual( + newSplitview.splitViewId, + 43, + "New split view ID should be maxSplitViewId + 1" + ); + + gBrowser.removeTab(tab4); + gBrowser.removeTab(tab3); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_splitview_restore_in_closed_window.js b/src/zen/tests/mochitests/sessionstore/browser_splitview_restore_in_closed_window.js new file mode 100644 index 000000000..428050a74 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_splitview_restore_in_closed_window.js @@ -0,0 +1,203 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); +forgetClosedWindows(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_restoreSplitViewInClosedWindow() { + let win = await promiseNewWindowLoaded(); + let tab1 = win.gBrowser.tabs[0]; + let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + const splitview = win.gBrowser.addTabSplitView([tab1, tab2]); + const splitViewId = splitview.splitViewId; + await BrowserTestUtils.browserLoaded( + tab2.linkedBrowser, + false, + "about:robots" + ); + + Assert.equal( + splitview.tabs.length, + 2, + "A splitview is present before closing the window." + ); + + await TabStateFlusher.flushWindow(win); + await BrowserTestUtils.closeWindow(win); + + const closedWindowData = SessionStore.getClosedWindowData(); + Assert.equal(closedWindowData.length, 1, "only `win` should be closed."); + + const restoredWin = SessionStore.undoCloseWindow(0); + await promiseDelayedStartupFinished(restoredWin); + + Assert.equal( + restoredWin.gBrowser.tabs.length, + 2, + "There should be 2 tabs restored," + ); + Assert.equal( + restoredWin.gBrowser.tabs[1].splitview.tabs.length, + 2, + "Both tabs in the splitview have been restored to the splitview." + ); + Assert.equal( + restoredWin.gBrowser.tabs[1].splitview.splitViewId, + splitViewId, + "The correct splitview id is assigned." + ); + + await BrowserTestUtils.closeWindow(restoredWin); + forgetClosedWindows(); +}); + +add_task( + async function test_restoreClosedTabGroupWithSplitViewFromClosedWindow() { + let win = await promiseNewWindowLoaded(); + let aboutMozillaTab = BrowserTestUtils.addTab( + win.gBrowser, + "about:mozilla" + ); + let aboutRobotsTab = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let aboutAboutTab = BrowserTestUtils.addTab(win.gBrowser, "about:about"); + const splitview = win.gBrowser.addTabSplitView([ + aboutRobotsTab, + aboutAboutTab, + ]); + const splitViewId = splitview.splitViewId; + const tabGroupToClose = win.gBrowser.addTabGroup([splitview], { + color: "blue", + label: "about pages", + }); + const tabGroupToCloseId = tabGroupToClose.id; + await Promise.all([ + BrowserTestUtils.browserLoaded( + aboutMozillaTab.linkedBrowser, + false, + "about:mozilla" + ), + BrowserTestUtils.browserLoaded( + aboutRobotsTab.linkedBrowser, + false, + "about:robots" + ), + BrowserTestUtils.browserLoaded( + aboutAboutTab.linkedBrowser, + false, + "about:about" + ), + ]); + + await TabStateFlusher.flushWindow(win); + + let removePromise = BrowserTestUtils.waitForEvent( + tabGroupToClose, + "TabGroupRemoved" + ); + win.gBrowser.removeTabGroup(tabGroupToClose); + await removePromise; + + Assert.ok( + !win.gBrowser.tabGroups.length, + "closed tab group should not be in the tab strip" + ); + Assert.ok( + !win.gBrowser.tabs.includes(aboutRobotsTab), + "about:robots tab should not be in the tab strip" + ); + Assert.ok( + !win.gBrowser.tabs.includes(aboutAboutTab), + "about:about tab should not be in the tab strip" + ); + + await TabStateFlusher.flushWindow(win); + + const closedTabGroups = SessionStore.getClosedTabGroups(win); + Assert.equal( + closedTabGroups.length, + 1, + "there should be a closed tab group" + ); + Assert.equal( + closedTabGroups[0].id, + tabGroupToCloseId, + "the closed tab group should be the one we just closed" + ); + + let win2 = await promiseNewWindowLoaded(); + await BrowserTestUtils.closeWindow(win); + + const closedWindowData = SessionStore.getClosedWindowData(); + Assert.equal(closedWindowData.length, 1, "only `win` should be closed"); + + let restorePromise = BrowserTestUtils.waitForEvent( + win2, + "SSWindowStateReady" + ); + const restoredTabGroup = SessionStore.undoCloseTabGroup( + { sourceClosedId: closedWindowData[0].closedId }, + tabGroupToCloseId, + win2 + ); + await restorePromise; + + Assert.equal( + win2.gBrowser.tabs.length, + 3, + "there should be 2 tabs restored + 1 initial tab from the new window" + ); + Assert.equal( + win2.gBrowser.tabGroups.length, + 1, + "there should be 1 tab group restored" + ); + Assert.equal( + win2.gBrowser.selectedTab, + restoredTabGroup.tabs[0], + "first tab of the group is selected" + ); + Assert.equal( + restoredTabGroup.tabs[0].splitview.tabs.length, + 2, + "first tab of the group is should have a splitview with 2 tabs associated" + ); + Assert.equal( + restoredTabGroup.tabs[0].splitview.splitViewId, + splitViewId, + "splitview ID should be restored" + ); + Assert.equal( + restoredTabGroup.tabs.length, + 2, + "the 2 restored tabs should be in the restored tab group" + ); + Assert.equal( + restoredTabGroup.label, + "about pages", + "tab group name should be restored" + ); + Assert.equal( + restoredTabGroup.id, + tabGroupToCloseId, + "tab group ID should be restored" + ); + Assert.equal( + restoredTabGroup.color, + "blue", + "tab group color should be restored" + ); + Assert.ok( + !restoredTabGroup.collapsed, + "tab group collapsed state should be restored" + ); + + await BrowserTestUtils.closeWindow(win2); + forgetClosedWindows(); + } +); diff --git a/src/zen/tests/mochitests/sessionstore/browser_splitview_string_migration.js b/src/zen/tests/mochitests/sessionstore/browser_splitview_string_migration.js new file mode 100644 index 000000000..7f1aef0e0 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_splitview_string_migration.js @@ -0,0 +1,328 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); +forgetClosedWindows(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_string_id_migration_preserves_grouping() { + let state = { + windows: [ + { + tabs: [ + { + entries: [ + { + url: "about:mozilla", + triggeringPrincipal_base64, + }, + ], + splitViewId: "1706789123456-42", + }, + { + entries: [ + { + url: "about:robots", + triggeringPrincipal_base64, + }, + ], + splitViewId: "1706789123456-42", + }, + { + entries: [ + { + url: "about:about", + triggeringPrincipal_base64, + }, + ], + splitViewId: "1706789123457-99", + }, + { + entries: [ + { + url: "about:config", + triggeringPrincipal_base64, + }, + ], + splitViewId: "1706789123457-99", + }, + ], + selected: 1, + splitViews: [ + { + id: "1706789123456-42", + numberOfTabs: 2, + }, + { + id: "1706789123457-99", + numberOfTabs: 2, + }, + ], + }, + ], + }; + + await SessionStoreTestUtils.promiseBrowserState(state); + + info("State restored, checking tabs"); + Assert.strictEqual(gBrowser.tabs.length, 4, "Should have 4 tabs"); + + let tab0 = gBrowser.tabs[0]; + let tab1 = gBrowser.tabs[1]; + let tab2 = gBrowser.tabs[2]; + let tab3 = gBrowser.tabs[3]; + + info(`tab0.splitview: ${tab0.splitview}`); + + if (!tab0.splitview) { + Assert.ok(false, "tab0 should have a splitview"); + return; + } + + Assert.strictEqual( + typeof tab0.splitview.splitViewId, + "number", + "Migrated splitViewId should be a number" + ); + Assert.strictEqual( + typeof tab2.splitview.splitViewId, + "number", + "Migrated splitViewId should be a number" + ); + + Assert.strictEqual( + tab0.splitview.splitViewId, + tab1.splitview.splitViewId, + "Tabs with same string ID should have same integer ID" + ); + Assert.strictEqual( + tab2.splitview.splitViewId, + tab3.splitview.splitViewId, + "Tabs with same string ID should have same integer ID" + ); + + Assert.notEqual( + tab0.splitview.splitViewId, + tab2.splitview.splitViewId, + "Different split views should have different integer IDs" + ); + + let tab4 = BrowserTestUtils.addTab(gBrowser, "about:preferences"); + let tab5 = BrowserTestUtils.addTab(gBrowser, "about:addons"); + await Promise.all([ + BrowserTestUtils.browserLoaded(tab4.linkedBrowser), + BrowserTestUtils.browserLoaded(tab5.linkedBrowser), + ]); + + let newSplitview = gBrowser.addTabSplitView([tab4, tab5]); + + Assert.greater( + newSplitview.splitViewId, + tab0.splitview.splitViewId, + "New split view ID should be greater than migrated IDs" + ); + Assert.greater( + newSplitview.splitViewId, + tab2.splitview.splitViewId, + "New split view ID should be greater than migrated IDs" + ); + + gBrowser.removeTab(tab5); + gBrowser.removeTab(tab4); +}); + +add_task(async function test_string_id_migration_in_closed_windows() { + let state = { + windows: [ + { + tabs: [ + { + entries: [ + { + url: "about:blank", + triggeringPrincipal_base64, + }, + ], + }, + ], + selected: 1, + }, + ], + _closedWindows: [ + { + tabs: [ + { + entries: [ + { + url: "about:mozilla", + triggeringPrincipal_base64, + }, + ], + splitViewId: "1706789123456-42", + }, + { + entries: [ + { + url: "about:robots", + triggeringPrincipal_base64, + }, + ], + splitViewId: "1706789123456-42", + }, + ], + splitViews: [ + { + id: "1706789123456-42", + numberOfTabs: 2, + }, + ], + selected: 1, + closedId: 1, + closedAt: Date.now(), + title: "Test Window", + _closedTabs: [], + }, + ], + }; + + await SessionStoreTestUtils.promiseBrowserState(state); + + let closedWindowData = SessionStore.getClosedWindowData(); + Assert.strictEqual(closedWindowData.length, 1, "Should have 1 closed window"); + + let closedWinTabs = closedWindowData[0].tabs; + Assert.strictEqual( + closedWinTabs.length, + 2, + "Closed window should have 2 tabs" + ); + + Assert.strictEqual( + typeof closedWinTabs[0].splitViewId, + "number", + "Closed window tab splitViewId should be migrated to number" + ); + Assert.strictEqual( + typeof closedWinTabs[1].splitViewId, + "number", + "Closed window tab splitViewId should be migrated to number" + ); + + Assert.strictEqual( + closedWinTabs[0].splitViewId, + closedWinTabs[1].splitViewId, + "Closed window tabs with same string ID should have same integer ID" + ); + + let restoredWin = SessionStore.undoCloseWindow(0); + await promiseDelayedStartupFinished(restoredWin); + + Assert.strictEqual( + restoredWin.gBrowser.tabs.length, + 2, + "Restored window should have 2 tabs" + ); + + let restoredTab0 = restoredWin.gBrowser.tabs[0]; + let restoredTab1 = restoredWin.gBrowser.tabs[1]; + + Assert.strictEqual( + typeof restoredTab0.splitview.splitViewId, + "number", + "Restored window tab should have integer splitViewId" + ); + Assert.strictEqual( + restoredTab0.splitview.splitViewId, + restoredTab1.splitview.splitViewId, + "Restored split view should maintain grouping" + ); + + await BrowserTestUtils.closeWindow(restoredWin); + forgetClosedWindows(); +}); + +add_task(async function test_old_session_without_maxSplitViewId() { + let state = { + windows: [ + { + tabs: [ + { + entries: [ + { + url: "about:mozilla", + triggeringPrincipal_base64, + }, + ], + splitViewId: "1706789123456-42", + }, + { + entries: [ + { + url: "about:robots", + triggeringPrincipal_base64, + }, + ], + splitViewId: "1706789123456-42", + }, + ], + selected: 1, + splitViews: [ + { + id: "1706789123456-42", + numberOfTabs: 2, + }, + ], + }, + ], + }; + + await SessionStoreTestUtils.promiseBrowserState(state); + + Assert.strictEqual( + typeof gBrowser.tabs[0].splitview.splitViewId, + "number", + "String ID should be migrated even without maxSplitViewId" + ); + Assert.strictEqual( + gBrowser.tabs[0].splitview.splitViewId, + gBrowser.tabs[1].splitview.splitViewId, + "Grouping should be preserved" + ); + + let tab2 = BrowserTestUtils.addTab(gBrowser, "about:config"); + let tab3 = BrowserTestUtils.addTab(gBrowser, "about:support"); + await Promise.all([ + BrowserTestUtils.browserLoaded(tab2.linkedBrowser), + BrowserTestUtils.browserLoaded(tab3.linkedBrowser), + ]); + + let splitview = gBrowser.addTabSplitView([tab2, tab3]); + + Assert.strictEqual( + typeof splitview.splitViewId, + "number", + "New split view should get integer ID" + ); + Assert.greater(splitview.splitViewId, 0, "Counter should generate valid IDs"); + + await TabStateFlusher.flushWindow(window); + let newState = JSON.parse(SessionStore.getBrowserState()); + + Assert.ok( + newState.maxSplitViewId, + "maxSplitViewId should be persisted after first save" + ); + Assert.strictEqual( + typeof newState.maxSplitViewId, + "number", + "Persisted maxSplitViewId should be a number" + ); + + gBrowser.removeTab(tab3); + gBrowser.removeTab(tab2); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_swapDocShells.js b/src/zen/tests/mochitests/sessionstore/browser_swapDocShells.js new file mode 100644 index 000000000..047a36c51 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_swapDocShells.js @@ -0,0 +1,40 @@ +"use strict"; + +add_task(async function () { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:mozilla" + )); + await promiseBrowserLoaded(gBrowser.selectedBrowser); + + let win = gBrowser.replaceTabWithWindow(tab); + await promiseDelayedStartupFinished(win); + await promiseBrowserHasURL(win.gBrowser.browsers[0], "about:mozilla"); + + win.duplicateTabIn(win.gBrowser.selectedTab, "tab"); + await promiseTabRestored(win.gBrowser.tabs[1]); + + let browser = win.gBrowser.browsers[1]; + is(browser.currentURI.spec, "about:mozilla", "tab was duplicated"); + + await BrowserTestUtils.closeWindow(win); +}); + +function promiseDelayedStartupFinished(win) { + return new Promise(resolve => { + whenDelayedStartupFinished(win, resolve); + }); +} + +function promiseBrowserHasURL(browser, url) { + let promise = Promise.resolve(); + + if ( + browser.contentDocument.readyState === "complete" && + browser.currentURI.spec === url + ) { + return promise; + } + + return promise.then(() => promiseBrowserHasURL(browser, url)); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_switch_remoteness.js b/src/zen/tests/mochitests/sessionstore/browser_switch_remoteness.js new file mode 100644 index 000000000..51c801d10 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_switch_remoteness.js @@ -0,0 +1,57 @@ +"use strict"; + +const URL = "http://example.com/browser_switch_remoteness_"; + +function countHistoryEntries(browser, expected) { + return SpecialPowers.spawn(browser, [{ expected }], async function (args) { + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); + let history = webNavigation.sessionHistory; + Assert.equal( + history && history.count, + args.expected, + "correct number of shistory entries" + ); + }); +} + +add_task(async function () { + // Open a new window. + let win = await promiseNewWindowLoaded(); + + // Add a new tab. + let tab = BrowserTestUtils.addTab(win.gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); + ok(browser.isRemoteBrowser, "browser is remote"); + + // Get the maximum number of preceding entries to save. + const MAX_BACK = Services.prefs.getIntPref( + "browser.sessionstore.max_serialize_back" + ); + Assert.greater( + MAX_BACK, + -1, + "check that the default has a value that caps data" + ); + + // Load more pages than we would save to disk on a clean shutdown. + for (let i = 0; i < MAX_BACK + 2; i++) { + BrowserTestUtils.startLoadingURIString(browser, URL + i); + await promiseBrowserLoaded(browser); + ok(browser.isRemoteBrowser, "browser is still remote"); + } + + // Check we have the right number of shistory entries. + await countHistoryEntries(browser, MAX_BACK + 2); + + // Load a non-remote page. + BrowserTestUtils.startLoadingURIString(browser, "about:robots"); + await promiseBrowserLoaded(browser); + ok(!browser.isRemoteBrowser, "browser is not remote anymore"); + + // Check that we didn't lose any shistory entries. + await countHistoryEntries(browser, MAX_BACK + 3); + + // Cleanup. + await BrowserTestUtils.closeWindow(win); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_closed.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_closed.js new file mode 100644 index 000000000..5a77264d9 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_closed.js @@ -0,0 +1,197 @@ +"use strict"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + TabGroupTestUtils: "resource://testing-common/TabGroupTestUtils.sys.mjs", +}); + +const ORIG_STATE = SessionStore.getBrowserState(); + +add_task(async function test_closingTabGroupAddsClosedGroup() { + let win = await promiseNewWindowLoaded(); + + let tabs = [ + BrowserTestUtils.addTab(win.gBrowser, "https://example.com"), + BrowserTestUtils.addTab(win.gBrowser, "https://example.com"), + BrowserTestUtils.addTab(win.gBrowser, "https://example.com"), + ]; + let group = win.gBrowser.addTabGroup(tabs); + await Promise.all( + tabs.map(async t => { + await BrowserTestUtils.browserLoaded(t.linkedBrowser); + await TabStateFlusher.flush(t.linkedBrowser); + }) + ); + + let windowState = ss.getWindowState(win).windows[0]; + Assert.equal( + windowState.closedGroups.length, + 0, + "Window state starts with no closed groups" + ); + + await lazy.TabGroupTestUtils.removeTabGroup(group); + + windowState = ss.getWindowState(win).windows[0]; + Assert.equal( + windowState.closedGroups.length, + 1, + "Window state gains a closed group" + ); + Assert.equal( + windowState.closedGroups[0].tabs.length, + 3, + "Closed group has 3 tabs" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_closedTabGroupSkipsNotWorthSavingTabs() { + let win = await promiseNewWindowLoaded(); + + const urls = ["https://example.com/", "about:blank"]; + const tabs = urls.map(url => BrowserTestUtils.addTab(win.gBrowser, url)); + let group = win.gBrowser.addTabGroup(tabs); + await Promise.all( + tabs.map(async (t, i) => { + await BrowserTestUtils.browserLoaded(t.linkedBrowser, { + wantLoad: urls[i], + }); + await TabStateFlusher.flush(t.linkedBrowser); + }) + ); + + let windowState = ss.getWindowState(win).windows[0]; + Assert.equal( + windowState.closedGroups.length, + 0, + "Window state starts with no closed groups" + ); + + await lazy.TabGroupTestUtils.removeTabGroup(group); + + windowState = ss.getWindowState(win).windows[0]; + Assert.equal( + windowState.closedGroups.length, + 1, + "Window state gains a closed group" + ); + Assert.equal( + windowState.closedGroups[0].tabs.length, + 1, + "Closed group has only 1 tab" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_closedTabCountsRespectTabGroups() { + let win = await promiseNewWindowLoaded(); + + Assert.equal( + SessionStore.getClosedTabCount(), + 0, + "Session store starts with 0 closed tabs" + ); + Assert.equal( + SessionStore.getLastClosedTabCount(win), + 0, + "Session store starts with 0 last closed tab count" + ); + + await SessionStoreTestUtils.openAndCloseTab(win, "https://example.com"); + + Assert.equal( + SessionStore.getClosedTabCount(), + 1, + "Session store registers closed tab" + ); + Assert.equal( + SessionStore.getLastClosedTabCount(win), + 1, + "Session store reports last closed tab count as 1" + ); + + // We need to have at least one "worth saving" tab open in the window at time + // of close, or else the window will not be added to _closedWindows + BrowserTestUtils.addTab(win.gBrowser, "https://example.com"); + let tabs = [ + BrowserTestUtils.addTab(win.gBrowser, "https://example.com"), + BrowserTestUtils.addTab(win.gBrowser, "https://example.com"), + BrowserTestUtils.addTab(win.gBrowser, "https://example.com"), + ]; + let group = win.gBrowser.addTabGroup(tabs); + await Promise.all( + tabs.map(async t => { + await BrowserTestUtils.browserLoaded(t.linkedBrowser); + await TabStateFlusher.flush(t.linkedBrowser); + }) + ); + + await lazy.TabGroupTestUtils.removeTabGroup(group); + + Assert.equal( + SessionStore.getClosedTabCount(), + 4, + "Session store registers tabs from closed group" + ); + Assert.equal( + SessionStore.getLastClosedTabCount(win), + 3, + "Session store reports last closed tab count as 3" + ); + + Assert.equal( + SessionStore.getClosedTabCountForWindow(win), + 4, + "Session store correctly reports closed tab count for window" + ); + + await BrowserTestUtils.closeWindow(win); + + Assert.equal( + SessionStore.getClosedTabCountFromClosedWindows(), + 4, + "Session store correctly reports closed tab count for closed windows" + ); + + forgetClosedWindows(); + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_purgingSessionHistoryClearsClosedTabGroups() { + let win = await promiseNewWindowLoaded(); + + let tab = BrowserTestUtils.addTab(win.gBrowser, "https://example.com"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await TabStateFlusher.flush(tab.linkedBrowser); + + let group = win.gBrowser.addTabGroup([tab]); + await lazy.TabGroupTestUtils.removeTabGroup(group); + + let windowState = ss.getWindowState(win).windows[0]; + Assert.equal( + windowState.closedGroups.length, + 1, + "Window state gains a closed group" + ); + + Services.obs.notifyObservers(null, "browser:purge-session-history"); + + windowState = ss.getWindowState(win).windows[0]; + Assert.equal( + windowState.closedGroups.length, + 0, + "Session history purge removes closed groups" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_closed_groups_in_closed_windows.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_closed_groups_in_closed_windows.js new file mode 100644 index 000000000..0f1f288bb --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_closed_groups_in_closed_windows.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_closedTabGroupsInClosedWindows() { + forgetClosedWindows(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionstore.closedTabsFromClosedWindows", true]], + }); + + let win = await promiseNewWindowLoaded(); + + // Window needs at least one worth-saving tab opened in order to be saved + BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + + let groupedTab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let groupedTab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + + let tabGroup = win.gBrowser.addTabGroup([groupedTab1, groupedTab2]); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getClosedTabGroups(win).length, + 0, + "should start with no closed tab groups" + ); + Assert.equal( + SessionStore.getClosedTabDataForWindow(win).length, + 0, + "should start with no closed tabs" + ); + Assert.equal( + SessionStore.getClosedWindowData().length, + 0, + "should start with no closed windows" + ); + + let removePromise = BrowserTestUtils.waitForEvent( + tabGroup, + "TabGroupRemoved" + ); + win.gBrowser.removeTabGroup(tabGroup); + await removePromise; + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getClosedTabGroups().length, + 1, + "should have one closed tab group" + ); + + await BrowserTestUtils.closeWindow(win); + + Assert.equal( + SessionStore.getClosedWindowData().length, + 1, + "should have one closed window" + ); + Assert.equal( + SessionStore.getClosedTabGroups().length, + 1, + "should still have one closed tab group, even when the window is closed" + ); + Assert.equal( + SessionStore.getClosedTabDataFromClosedWindows().length, + 2, + "should report 2 closed tabs from the closed tab group in the closed window" + ); + await SpecialPowers.popPrefEnv(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionstore.closedTabsFromClosedWindows", false]], + }); + Assert.equal( + SessionStore.getClosedTabGroups().length, + 0, + "should report no closed tabs if closedTabsFromClosedWindows is false" + ); + await SpecialPowers.popPrefEnv(); + + forgetClosedWindows(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_empty.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_empty.js new file mode 100644 index 000000000..5817c3a29 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_empty.js @@ -0,0 +1,31 @@ +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_ZeroTabGroups() { + let win = await promiseNewWindowLoaded(); + const state = ss.getWindowState(win); + + Assert.equal(state.windows.length, 1, "should have state from 1 window"); + const windowState = state.windows[0]; + + Assert.ok(windowState.groups, "window state should have a `groups` property"); + Assert.equal( + windowState.groups.length, + 0, + "`groups` property should be 0 since there are no tab groups" + ); + + const countOfGroupedTabs = windowState.tabs.filter(tab => tab.groupId).length; + Assert.equal( + countOfGroupedTabs, + 0, + "none of the tabs should refer to a tab group" + ); + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_in_closed_window.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_in_closed_window.js new file mode 100644 index 000000000..6b3bb7ee8 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_in_closed_window.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); +forgetClosedWindows(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_restoreClosedTabGroupFromClosedWindow() { + let win = await promiseNewWindowLoaded(); + let aboutMozillaTab = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + let aboutRobotsTab = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let aboutAboutTab = BrowserTestUtils.addTab(win.gBrowser, "about:about"); + const tabGroupToClose = win.gBrowser.addTabGroup( + [aboutRobotsTab, aboutAboutTab], + { + color: "blue", + label: "about pages", + } + ); + const tabGroupToCloseId = tabGroupToClose.id; + await Promise.all([ + BrowserTestUtils.browserLoaded( + aboutMozillaTab.linkedBrowser, + false, + "about:mozilla" + ), + BrowserTestUtils.browserLoaded( + aboutRobotsTab.linkedBrowser, + false, + "about:robots" + ), + BrowserTestUtils.browserLoaded( + aboutAboutTab.linkedBrowser, + false, + "about:about" + ), + ]); + + await TabStateFlusher.flushWindow(win); + + let removePromise = BrowserTestUtils.waitForEvent( + tabGroupToClose, + "TabGroupRemoved" + ); + win.gBrowser.removeTabGroup(tabGroupToClose); + await removePromise; + + Assert.ok( + !win.gBrowser.tabGroups.length, + "closed tab group should not be in the tab strip" + ); + Assert.ok( + !win.gBrowser.tabs.includes(aboutRobotsTab), + "about:robots tab should not be in the tab strip" + ); + Assert.ok( + !win.gBrowser.tabs.includes(aboutAboutTab), + "about:about tab should not be in the tab strip" + ); + + await TabStateFlusher.flushWindow(win); + + const closedTabGroups = SessionStore.getClosedTabGroups(win); + Assert.equal(closedTabGroups.length, 1, "there should be a closed tab group"); + Assert.equal( + closedTabGroups[0].id, + tabGroupToCloseId, + "the closed tab group should be the one we just closed" + ); + + let win2 = await promiseNewWindowLoaded(); + await BrowserTestUtils.closeWindow(win); + + const closedWindowData = SessionStore.getClosedWindowData(); + Assert.equal(closedWindowData.length, 1, "only `win` should be closed"); + + let restorePromise = BrowserTestUtils.waitForEvent( + win2, + "SSWindowStateReady" + ); + const restoredTabGroup = SessionStore.undoCloseTabGroup( + { sourceClosedId: closedWindowData[0].closedId }, + tabGroupToCloseId, + win2 + ); + await restorePromise; + + Assert.equal( + win2.gBrowser.tabs.length, + 3, + "there should be 2 tabs restored + 1 initial tab from the new window" + ); + Assert.equal( + win2.gBrowser.tabGroups.length, + 1, + "there should be 1 tab group restored" + ); + Assert.equal( + win2.gBrowser.selectedTab, + restoredTabGroup.tabs[0], + "first tab of the group is selected" + ); + Assert.equal( + restoredTabGroup.tabs.length, + 2, + "the 2 restored tabs should be in the restored tab group" + ); + Assert.equal( + restoredTabGroup.label, + "about pages", + "tab group name should be restored" + ); + Assert.equal( + restoredTabGroup.id, + tabGroupToCloseId, + "tab group ID should be restored" + ); + Assert.equal( + restoredTabGroup.color, + "blue", + "tab group color should be restored" + ); + Assert.ok( + !restoredTabGroup.collapsed, + "tab group collapsed state should be restored" + ); + + await BrowserTestUtils.closeWindow(win2); + forgetClosedWindows(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_in_open_window.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_in_open_window.js new file mode 100644 index 000000000..075438927 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_in_open_window.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_restoreClosedTabGroupFromSameWindow() { + let win = await promiseNewWindowLoaded(); + let aboutRobotsTab = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let aboutAboutTab = BrowserTestUtils.addTab(win.gBrowser, "about:about"); + const tabGroupToClose = win.gBrowser.addTabGroup( + [aboutRobotsTab, aboutAboutTab], + { + color: "blue", + label: "about pages", + } + ); + const tabGroupToCloseId = tabGroupToClose.id; + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + win.gBrowser.tabs.length, + 3, + "there should be 2 new tabs + 1 initial tab from the new window" + ); + + let removePromise = BrowserTestUtils.waitForEvent( + tabGroupToClose, + "TabGroupRemoved" + ); + win.gBrowser.removeTabGroup(tabGroupToClose); + await removePromise; + + Assert.ok( + !win.gBrowser.tabGroups.length, + "closed tab group should not be in the tab strip" + ); + Assert.ok( + !win.gBrowser.tabs.includes(aboutRobotsTab), + "about:robots tab should not be in the tab strip" + ); + Assert.ok( + !win.gBrowser.tabs.includes(aboutAboutTab), + "about:about tab should not be in the tab strip" + ); + + await TabStateFlusher.flushWindow(win); + + let restorePromise = BrowserTestUtils.waitForEvent(win, "SSWindowStateReady"); + const restoredTabGroup = SessionStore.undoCloseTabGroup( + win, + tabGroupToCloseId, + win + ); + await restorePromise; + + Assert.equal( + win.gBrowser.tabs.length, + 3, + "there should be 2 tabs restored + 1 initial tab from the new window" + ); + Assert.equal( + win.gBrowser.tabGroups.length, + 1, + "there should be 1 tab group restored" + ); + Assert.equal( + win.gBrowser.selectedTab, + restoredTabGroup.tabs[0], + "first tab of the group is selected" + ); + Assert.equal( + restoredTabGroup.tabs.length, + 2, + "the 2 restored tabs should be in the restored tab group" + ); + Assert.equal( + restoredTabGroup.label, + "about pages", + "tab group name should be restored" + ); + Assert.equal( + restoredTabGroup.id, + tabGroupToCloseId, + "tab group ID should be restored" + ); + Assert.equal( + restoredTabGroup.color, + "blue", + "tab group color should be restored" + ); + Assert.ok( + !restoredTabGroup.collapsed, + "tab group collapsed state should be restored" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_many_tabs.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_many_tabs.js new file mode 100644 index 000000000..c671d6110 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_closed_many_tabs.js @@ -0,0 +1,202 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TabGroupTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TabGroupTestUtils.sys.mjs" +); + +// Ensure that a closed tab group will record all of its tabs into session state +// without being constrained by the `browser.sessionstore.max_tabs_undo` pref +// which normally limits the number of closed tabs that we retain in state. + +const ORIG_STATE = SessionStore.getBrowserState(); + +const maxTabsUndo = Services.prefs.getIntPref( + "browser.sessionstore.max_tabs_undo" +); +const numberOfTabsToTest = maxTabsUndo + 5; + +registerCleanupFunction(async () => { + forgetClosedWindows(); + forgetSavedTabGroups(); + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_restoreClosedTabGroupWithManyTabs() { + let win = await promiseNewWindowLoaded(); + let tabs = []; + let tabUrls = []; + for (let i = 1; i <= numberOfTabsToTest; i++) { + let tab = BrowserTestUtils.addTab( + win.gBrowser, + `https://example.com/?${i}` + ); + tabUrls.push(`https://example.com/?${i}`); + tabs.push(tab); + } + await Promise.all( + tabs.map(tab => BrowserTestUtils.browserLoaded(tab.linkedBrowser)) + ); + + const tabGroupToClose = win.gBrowser.addTabGroup(tabs, { + color: "blue", + label: "many tabs", + }); + const tabGroupToCloseId = tabGroupToClose.id; + + Assert.equal( + win.gBrowser.tabs.length, + numberOfTabsToTest + 1, + `there should be ${numberOfTabsToTest} new tabs + 1 initial tab from the new window` + ); + for (let i = 1; i <= numberOfTabsToTest; i++) { + Assert.equal( + tabGroupToClose.tabs[i - 1].linkedBrowser.currentURI.spec, + tabUrls[i - 1], + `confirmed order of tab ${i} in the group` + ); + } + + await TabStateFlusher.flushWindow(win); + + await TabGroupTestUtils.removeTabGroup(tabGroupToClose); + + Assert.ok( + !win.gBrowser.tabGroups.length, + "closed tab group should not be in the tab strip" + ); + + let closedTabGroups = ss.getClosedTabGroups(win); + Assert.equal( + closedTabGroups.length, + 1, + "one closed tab group should be in session state" + ); + let closedTabGroup = closedTabGroups[0]; + Assert.equal( + closedTabGroup.id, + tabGroupToCloseId, + "the tab group we closed should be in session state" + ); + Assert.equal( + closedTabGroup.tabs.length, + numberOfTabsToTest, + "the closed tab group should contain all of the tabs" + ); + for (let i = 1; i <= numberOfTabsToTest; i++) { + Assert.equal( + closedTabGroup.tabs[i - 1].state.entries[0].url, + tabUrls[i - 1], + `the closed tab group tab ${i} should preserve its order in the group` + ); + } + + let restorePromise = BrowserTestUtils.waitForEvent(win, "SSWindowStateReady"); + let undoTabGroup = SessionStore.undoCloseTabGroup( + win, + tabGroupToCloseId, + win + ); + await restorePromise; + await Promise.all( + undoTabGroup.tabs.map(tab => + BrowserTestUtils.browserLoaded(tab.linkedBrowser) + ) + ); + + Assert.equal( + win.gBrowser.tabs.length, + numberOfTabsToTest + 1, + `there should be ${numberOfTabsToTest} tabs restored + 1 initial tab from the new window` + ); + for (let i = 1; i <= numberOfTabsToTest; i++) { + Assert.equal( + undoTabGroup.tabs[i - 1].linkedBrowser.currentURI.spec, + tabUrls[i - 1], + `the undo tab ${i} should preserve its order in the group` + ); + } + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_restoreSavedTabGroupWithManyTabs() { + let win = await promiseNewWindowLoaded(); + let tabs = []; + let tabUrls = []; + for (let i = 1; i <= numberOfTabsToTest; i++) { + let tab = BrowserTestUtils.addTab( + win.gBrowser, + `https://example.com/?${i}` + ); + tabUrls.push(`https://example.com/?${i}`); + tabs.push(tab); + } + await Promise.all( + tabs.map(tab => BrowserTestUtils.browserLoaded(tab.linkedBrowser)) + ); + + const tabGroupToSaveAndClose = win.gBrowser.addTabGroup(tabs, { + color: "blue", + label: "many tabs", + }); + const tabGroupToSaveAndCloseId = tabGroupToSaveAndClose.id; + + Assert.equal( + win.gBrowser.tabs.length, + numberOfTabsToTest + 1, + `there should be ${numberOfTabsToTest} new tabs + 1 initial tab from the new window` + ); + for (let i = 1; i <= numberOfTabsToTest; i++) { + Assert.equal( + tabGroupToSaveAndClose.tabs[i - 1].linkedBrowser.currentURI.spec, + tabUrls[i - 1], + `confirmed order of tab ${i} in the group` + ); + } + + await TabGroupTestUtils.saveAndCloseTabGroup(tabGroupToSaveAndClose); + + Assert.ok( + !win.gBrowser.tabGroups.length, + "closed tab group should not be in the tab strip" + ); + + let savedGroupData = SessionStore.getSavedTabGroup(tabGroupToSaveAndCloseId); + for (let i = 1; i <= numberOfTabsToTest; i++) { + Assert.equal( + savedGroupData.tabs[i - 1].state.entries[0].url, + tabUrls[i - 1], + `the saved tab group tab ${i} should preserve its order in the group` + ); + } + + let restorePromise = BrowserTestUtils.waitForEvent(win, "SSWindowStateReady"); + let savedTabGroup = SessionStore.openSavedTabGroup( + tabGroupToSaveAndCloseId, + win + ); + await restorePromise; + await Promise.all( + savedTabGroup.tabs.map(tab => + BrowserTestUtils.browserLoaded(tab.linkedBrowser) + ) + ); + + Assert.equal( + win.gBrowser.tabs.length, + numberOfTabsToTest + 1, + `there should be ${numberOfTabsToTest} tabs restored + 1 initial tab from the new window` + ); + for (let i = 1; i <= numberOfTabsToTest; i++) { + Assert.equal( + savedTabGroup.tabs[i - 1].linkedBrowser.currentURI.spec, + tabUrls[i - 1], + `restored tab ${i} should preserve its order in the group` + ); + } + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_multiple.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_multiple.js new file mode 100644 index 000000000..56ed6b643 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_multiple.js @@ -0,0 +1,135 @@ +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_RestoreMultipleGroups() { + let win = await promiseNewWindowLoaded(); + BrowserTestUtils.addTab(win.gBrowser, "about:about"); + + let aboutMozillaTab = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + let aboutBlankTab = BrowserTestUtils.addTab(win.gBrowser, "about:blank"); + let mozillaTabGroup = win.gBrowser.addTabGroup( + [aboutMozillaTab, aboutBlankTab], + { color: "red", label: "mozilla stuff" } + ); + const mozillaTabGroupId = mozillaTabGroup.id; + BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + + let aboutAboutTab = BrowserTestUtils.addTab(win.gBrowser, "about:about"); + let aboutMemoryTab = BrowserTestUtils.addTab(win.gBrowser, "about:memory"); + let systemTabGroup = win.gBrowser.addTabGroup( + [aboutAboutTab, aboutMemoryTab], + { color: "blue", label: "system stuff" } + ); + systemTabGroup.collapsed = true; + const systemTabGroupId = systemTabGroup.id; + + BrowserTestUtils.addTab(win.gBrowser, "about:license"); + + await TabStateFlusher.flushWindow(win); + await BrowserTestUtils.closeWindow(win); + + // Now restore the window + win = SessionStore.undoCloseWindow(0); + await BrowserTestUtils.waitForEvent(win, "SSWindowStateReady"); + await BrowserTestUtils.waitForEvent( + win.gBrowser.tabContainer, + "SSTabRestored" + ); + + Assert.equal( + win.gBrowser.tabs.length, + 8, + "there should be 7 tabs restored + 1 initial tab from the new window" + ); + Assert.equal( + win.gBrowser.tabGroups.length, + 2, + "there should be 2 tab groups restored" + ); + + Assert.ok( + !win.gBrowser.tabs[0].group, + "initial tab from the new window should not be in a group" + ); + Assert.ok( + !win.gBrowser.tabs[1].group, + "about:about tab should not be in a group" + ); + Assert.equal( + win.gBrowser.tabs[2].group.id, + mozillaTabGroupId, + "about:mozilla tab should be in the mozilla stuff group" + ); + Assert.equal( + win.gBrowser.tabs[3].group.id, + mozillaTabGroupId, + "about:blank tab should be in the mozilla stuff group" + ); + Assert.ok( + !win.gBrowser.tabs[4].group, + "about:robots tab should not be in a group" + ); + Assert.equal( + win.gBrowser.tabs[5].group.id, + systemTabGroupId, + "about:about tab should be in the system stuff group" + ); + Assert.equal( + win.gBrowser.tabs[6].group.id, + systemTabGroupId, + "about:memory tab should be in the system stuff group" + ); + Assert.ok( + !win.gBrowser.tabs[7].group, + "about:license tab should not be in a group" + ); + + [mozillaTabGroup, systemTabGroup] = win.gBrowser.tabGroups; + Assert.equal( + mozillaTabGroup.label, + "mozilla stuff", + "tab group name should be restored" + ); + Assert.equal( + mozillaTabGroup.id, + mozillaTabGroupId, + "tab group ID should be restored" + ); + Assert.equal( + mozillaTabGroup.color, + "red", + "tab group color should be restored" + ); + Assert.ok( + !mozillaTabGroup.collapsed, + "tab group collapsed state should be restored" + ); + + Assert.equal( + systemTabGroup.label, + "system stuff", + "tab group name should be restored" + ); + Assert.equal( + systemTabGroup.id, + systemTabGroupId, + "tab group ID should be restored" + ); + Assert.equal( + systemTabGroup.color, + "blue", + "tab group color should be restored" + ); + Assert.ok( + systemTabGroup.collapsed, + "tab group collapsed state should be restored" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_saved.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_saved.js new file mode 100644 index 000000000..a2cff8c05 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_saved.js @@ -0,0 +1,183 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_restoreSavedTabGroupToSameWindow() { + let win = await promiseNewWindowLoaded(); + let aboutRobotsTab = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let aboutAboutTab = BrowserTestUtils.addTab(win.gBrowser, "about:about"); + const tabGroupToSave = win.gBrowser.addTabGroup( + [aboutRobotsTab, aboutAboutTab], + { + color: "blue", + label: "about pages", + } + ); + const tabGroupToSaveId = tabGroupToSave.id; + + await TabStateFlusher.flushWindow(win); + + tabGroupToSave.save(); + let removePromise = BrowserTestUtils.waitForEvent( + tabGroupToSave, + "TabGroupRemoved" + ); + win.gBrowser.removeTabGroup(tabGroupToSave); + await removePromise; + + Assert.ok( + !win.gBrowser.tabGroups.length, + "saved tab group should not be in the tab strip" + ); + Assert.ok( + !win.gBrowser.tabs.includes(aboutRobotsTab), + "about:robots tab should not be in the tab strip" + ); + Assert.ok( + !win.gBrowser.tabs.includes(aboutAboutTab), + "about:about tab should not be in the tab strip" + ); + + await TabStateFlusher.flushWindow(win); + + let restorePromise = BrowserTestUtils.waitForEvent(win, "SSWindowStateReady"); + const restoredTabGroup = SessionStore.openSavedTabGroup( + tabGroupToSaveId, + win + ); + await restorePromise; + + Assert.equal( + win.gBrowser.tabs.length, + 3, + "there should be 2 tabs restored + 1 initial tab from the new window" + ); + Assert.equal( + win.gBrowser.tabGroups.length, + 1, + "there should be 1 tab group restored" + ); + Assert.equal( + win.gBrowser.selectedTab, + restoredTabGroup.tabs[0], + "first tab of the group is selected" + ); + Assert.equal( + restoredTabGroup.tabs.length, + 2, + "the 2 restored tabs should be in the restored tab group" + ); + Assert.equal( + restoredTabGroup.label, + "about pages", + "tab group name should be restored" + ); + Assert.equal( + restoredTabGroup.id, + tabGroupToSaveId, + "tab group ID should be restored" + ); + Assert.equal( + restoredTabGroup.color, + "blue", + "tab group color should be restored" + ); + Assert.ok( + !restoredTabGroup.collapsed, + "tab group collapsed state should be restored" + ); + + await BrowserTestUtils.closeWindow(win); + forgetSavedTabGroups(); +}); + +add_task(async function test_restoreSavedTabGroupToAnotherWindow() { + let win = await promiseNewWindowLoaded(); + let aboutRobotsTab = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let aboutAboutTab = BrowserTestUtils.addTab(win.gBrowser, "about:about"); + const tabGroupToSave = win.gBrowser.addTabGroup( + [aboutRobotsTab, aboutAboutTab], + { + color: "blue", + label: "about pages", + } + ); + const tabGroupToSaveId = tabGroupToSave.id; + + await Promise.allSettled([ + TabStateFlusher.flush(aboutRobotsTab.linkedBrowser), + TabStateFlusher.flush(aboutAboutTab.linkedBrowser), + ]); + tabGroupToSave.save(); + + await TabStateFlusher.flushWindow(win); + + let removePromise = BrowserTestUtils.waitForEvent( + tabGroupToSave, + "TabGroupRemoved" + ); + win.gBrowser.removeTabGroup(tabGroupToSave); + await removePromise; + + await TabStateFlusher.flushWindow(win); + + let win2 = await promiseNewWindowLoaded(); + + let restorePromise = BrowserTestUtils.waitForEvent( + win2, + "SSWindowStateReady" + ); + const restoredTabGroup = SessionStore.openSavedTabGroup( + tabGroupToSaveId, + win2 + ); + await restorePromise; + + Assert.equal( + win2.gBrowser.tabs.length, + 3, + "there should be 2 tabs restored + 1 initial tab from the new window" + ); + Assert.equal( + win2.gBrowser.tabGroups.length, + 1, + "there should be 1 tab group restored" + ); + + Assert.equal( + restoredTabGroup.tabs.length, + 2, + "the 2 restored tabs should be in the restored tab group" + ); + Assert.equal( + restoredTabGroup.label, + "about pages", + "tab group name should be restored" + ); + Assert.equal( + restoredTabGroup.id, + tabGroupToSaveId, + "tab group ID should be restored" + ); + Assert.equal( + restoredTabGroup.color, + "blue", + "tab group color should be restored" + ); + Assert.ok( + !restoredTabGroup.collapsed, + "tab group collapsed state should be restored" + ); + + await BrowserTestUtils.closeWindow(win2); + await BrowserTestUtils.closeWindow(win); + forgetSavedTabGroups(); + forgetClosedWindows(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_simple.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_simple.js new file mode 100644 index 000000000..8c603645f --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_simple.js @@ -0,0 +1,68 @@ +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_RestoreSingleGroup() { + let win = await promiseNewWindowLoaded(); + let aboutRobotsTab = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let aboutAboutTab = BrowserTestUtils.addTab(win.gBrowser, "about:about"); + const { id: originalTabGroupId } = win.gBrowser.addTabGroup( + [aboutRobotsTab, aboutAboutTab], + { + color: "blue", + label: "about pages", + } + ); + + await TabStateFlusher.flushWindow(win); + await BrowserTestUtils.closeWindow(win); + + // Now restore the window + win = SessionStore.undoCloseWindow(0); + await BrowserTestUtils.waitForEvent(win, "SSWindowStateReady"); + await BrowserTestUtils.waitForEvent( + win.gBrowser.tabContainer, + "SSTabRestored" + ); + + Assert.equal( + win.gBrowser.tabs.length, + 3, + "there should be 2 tabs restored + 1 initial tab from the new window" + ); + Assert.equal( + win.gBrowser.tabGroups.length, + 1, + "there should be 1 tab group restored" + ); + + let tabGroup = win.gBrowser.tabGroups[0]; + Assert.equal( + tabGroup.tabs.length, + 2, + "the 2 restored tabs should be in the restored tab group" + ); + Assert.equal( + tabGroup.label, + "about pages", + "tab group name should be restored" + ); + Assert.equal( + tabGroup.id, + originalTabGroupId, + "tab group ID should be restored" + ); + Assert.equal(tabGroup.color, "blue", "tab group color should be restored"); + Assert.ok( + !tabGroup.collapsed, + "tab group collapsed state should be restored" + ); + + await win.gBrowser.removeTabGroup(tabGroup); + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_to_group.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_to_group.js new file mode 100644 index 000000000..6bb6a6e0d --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_restore_to_group.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_restoreTabToGroup() { + let groupedTabs = [ + BrowserTestUtils.addTab(gBrowser, "about:mozilla"), + BrowserTestUtils.addTab(gBrowser, "about:robots"), + ]; + let tabToRestore = groupedTabs[1]; + await Promise.all( + groupedTabs.map(g => promiseBrowserLoaded(g.linkedBrowser)) + ); + let tabGroup = gBrowser.addTabGroup(groupedTabs); + + await SessionStoreTestUtils.closeTab(tabToRestore); + + Assert.equal(tabGroup.tabs.length, 1, "Tab group only has one tab"); + + await TabStateFlusher.flushWindow(window); + + let tabRestoredEvent = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "SSTabRestored" + ); + SessionStore.undoCloseTab(window); + await tabRestoredEvent; + + Assert.equal(tabGroup.tabs.length, 2, "Tab group has two tabs"); + let restoredTab = tabGroup.tabs.at(-1); + Assert.equal( + restoredTab.linkedBrowser.currentURI.spec, + "about:robots", + "Closed tab exists in group" + ); + BrowserTestUtils.removeTab(restoredTab); + + // remove the other tab in the group so it gets deleted + await SessionStoreTestUtils.closeTab(tabGroup.tabs[0]); + + tabRestoredEvent = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "SSTabRestored" + ); + SessionStore.undoCloseTab(window, 0); + await tabRestoredEvent; + + restoredTab = gBrowser.tabs.at(-1); + Assert.ok( + !restoredTab.group, + "Tab was not restored to the group because the group was removed" + ); + + forgetClosedTabs(window); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeAllTabsBut.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeAllTabsBut.js new file mode 100644 index 000000000..842f0a006 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeAllTabsBut.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_removeAllTabsBut_default_save_tab_groups() { + let win = await promiseNewWindowLoaded(); + let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + + let tabGroup = win.gBrowser.addTabGroup([tab2, tab3]); + let tabGroupId = tabGroup.id; + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getSavedTabGroups().length, + 0, + "should not be any saved tab groups to start" + ); + Assert.equal( + SessionStore.getClosedTabGroups(win).length, + 0, + "should not be any closed tab groups to start" + ); + Assert.equal( + SessionStore.getClosedTabDataForWindow(win).length, + 0, + "should not be any closed tabs" + ); + + win.gBrowser.removeAllTabsBut(tab1); + + await TestUtils.waitForCondition( + () => win.gBrowser.tabs.length == 1, + "waiting for other tabs to close" + ); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getSavedTabGroups().length, + 1, + "should have saved a tab group" + ); + Assert.ok( + SessionStore.getSavedTabGroup(tabGroupId), + "should have saved the tab group that was closed" + ); + Assert.equal( + SessionStore.getClosedTabGroups(win).length, + 0, + "should only have saved the tab group, not deleted it" + ); + Assert.equal( + SessionStore.getClosedTabDataForWindow(win).length, + 0, + "should not be any closed tabs" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + forgetSavedTabGroups(); +}); + +add_task(async function test_removeAllTabsBut_suppress_saving_tab_groups() { + let win = await promiseNewWindowLoaded(); + let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + + win.gBrowser.addTabGroup([tab2, tab3]); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getSavedTabGroups().length, + 0, + "should not be any saved tab groups to start" + ); + Assert.equal( + SessionStore.getClosedTabGroups(win).length, + 0, + "should not be any closed tab groups to start" + ); + Assert.equal( + SessionStore.getClosedTabDataForWindow(win).length, + 0, + "should not be any closed tabs" + ); + + win.gBrowser.removeAllTabsBut(tab1, { skipSessionStore: true }); + + await TestUtils.waitForCondition( + () => win.gBrowser.tabs.length == 1, + "waiting for other tabs to close" + ); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getSavedTabGroups().length, + 0, + "should not have saved the tab group" + ); + Assert.equal( + SessionStore.getClosedTabGroups(win), + 0, + "should still not have any deleted tab groups" + ); + Assert.equal( + SessionStore.getClosedTabDataForWindow(win).length, + 0, + "should be 0 closed tabs" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + forgetSavedTabGroups(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeTabsToTheEnd.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeTabsToTheEnd.js new file mode 100644 index 000000000..44d97f93c --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeTabsToTheEnd.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_removeTabsToTheEnd_saves_tab_groups() { + let win = await promiseNewWindowLoaded(); + let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + + let tabGroup = win.gBrowser.addTabGroup([tab2, tab3], { insertBefore: tab2 }); + let tabGroupId = tabGroup.id; + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + win.gBrowser.tabs.length, + 4, + "should be 4 tabs: 1 tab from new window and 3 tabs just created" + ); + Assert.equal( + SessionStore.getSavedTabGroups().length, + 0, + "should not be any saved tab groups to start" + ); + + win.gBrowser.removeTabsToTheEndFrom(tab1); + + await TestUtils.waitForCondition( + () => win.gBrowser.tabs.length == 2, + "waiting for tabs to the end to close" + ); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getSavedTabGroups().length, + 1, + "should have saved a tab group" + ); + Assert.ok( + SessionStore.getSavedTabGroup(tabGroupId), + "should have saved the tab group that was closed" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + forgetSavedTabGroups(); +}); + +add_task( + async function test_removeTabsToTheEnd_only_saves_when_whole_tab_group_removed() { + let win = await promiseNewWindowLoaded(); + let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + + let tabGroup = win.gBrowser.addTabGroup([tab1, tab2], { + insertBefore: tab1, + }); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + win.gBrowser.tabs.length, + 4, + "should be 4 tabs: 1 tab from new window and 3 tabs just created" + ); + Assert.equal( + SessionStore.getSavedTabGroups().length, + 0, + "should not be any saved tab groups to start" + ); + + win.gBrowser.removeTabsToTheEndFrom(tab1); + + await TestUtils.waitForCondition( + () => win.gBrowser.tabs.length == 2, + "waiting for tabs to the end to close" + ); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getSavedTabGroups().length, + 0, + "should not have saved a tab group because one did not close" + ); + Assert.deepEqual( + win.gBrowser.tabGroups, + [tabGroup], + "tab group should still exist in the tab strip" + ); + Assert.deepEqual( + tabGroup.tabs, + [tab1], + "tab group should just have one tab left" + ); + + await win.gBrowser.removeTabGroup(tabGroup); + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + forgetSavedTabGroups(); + } +); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeTabsToTheStart.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeTabsToTheStart.js new file mode 100644 index 000000000..bcbf1538d --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_removeTabsToTheStart.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_removeTabsToTheStart_saves_tab_groups() { + let win = await promiseNewWindowLoaded(); + let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + + let tabGroup = win.gBrowser.addTabGroup([tab1, tab2], { insertBefore: tab1 }); + let tabGroupId = tabGroup.id; + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + win.gBrowser.tabs.length, + 4, + "should be 4 tabs: 1 tab from new window and 3 tabs just created" + ); + Assert.equal( + SessionStore.getSavedTabGroups().length, + 0, + "should not be any saved tab groups to start" + ); + + win.gBrowser.removeTabsToTheStartFrom(tab3); + + await TestUtils.waitForCondition( + () => win.gBrowser.tabs.length == 1, + "waiting for other tabs to close" + ); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getSavedTabGroups().length, + 1, + "should have saved a tab group" + ); + Assert.ok( + SessionStore.getSavedTabGroup(tabGroupId), + "should have saved the tab group that was closed" + ); + + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + forgetSavedTabGroups(); +}); + +add_task( + async function test_removeTabsToTheStart_only_saves_when_whole_tab_group_removed() { + let win = await promiseNewWindowLoaded(); + BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + + let tabGroup = win.gBrowser.addTabGroup([tab2, tab3], { + insertBefore: tab2, + }); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + win.gBrowser.tabs.length, + 4, + "should be 4 tabs: 1 tab from new window and 3 tabs just created" + ); + Assert.equal( + SessionStore.getSavedTabGroups().length, + 0, + "should not be any saved tab groups to start" + ); + + win.gBrowser.removeTabsToTheStartFrom(tab3); + + await TestUtils.waitForCondition( + () => win.gBrowser.tabs.length == 1, + "waiting for tabs to the end to close" + ); + + await TabStateFlusher.flushWindow(win); + + Assert.equal( + SessionStore.getSavedTabGroups().length, + 0, + "should not have saved a tab group because one did not close" + ); + Assert.deepEqual( + win.gBrowser.tabGroups, + [tabGroup], + "tab group should still exist in the tab strip" + ); + Assert.deepEqual( + tabGroup.tabs, + [tab3], + "tab group should just have one tab left" + ); + + await win.gBrowser.removeTabGroup(tabGroup); + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); + forgetSavedTabGroups(); + } +); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_window_close.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_window_close.js new file mode 100644 index 000000000..04e7ae363 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_save_on_window_close.js @@ -0,0 +1,254 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +forgetClosedWindows(); +forgetSavedTabGroups(); + +add_task(async function test_saveOpenTabGroupsOnWindowClose() { + Assert.equal( + ss.getSavedTabGroups().length, + 0, + "should start with no saved tab groups" + ); + Assert.equal( + ss.getClosedWindowData().length, + 0, + "should start with no closed windows" + ); + + info("open a new window with 1 standalone tab and 2 tabs in a tab group"); + let win = await promiseNewWindowLoaded(); + let aboutMozillaTab = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + let aboutRobotsTab = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let aboutAboutTab = BrowserTestUtils.addTab(win.gBrowser, "about:about"); + const tabGroupToClose = win.gBrowser.addTabGroup( + [aboutRobotsTab, aboutAboutTab], + { + color: "blue", + label: "about pages", + } + ); + const tabGroupToCloseId = tabGroupToClose.id; + await Promise.all([ + BrowserTestUtils.browserLoaded( + aboutMozillaTab.linkedBrowser, + false, + "about:mozilla" + ), + BrowserTestUtils.browserLoaded( + aboutRobotsTab.linkedBrowser, + false, + "about:robots" + ), + BrowserTestUtils.browserLoaded( + aboutAboutTab.linkedBrowser, + false, + "about:about" + ), + ]); + + await TabStateFlusher.flushWindow(win); + + info("close the window to make sure the tab group gets saved automatically"); + await BrowserTestUtils.closeWindow(win); + + const closedWindowData = SessionStore.getClosedWindowData(); + Assert.equal(closedWindowData.length, 1, "`win` should now be closed"); + Assert.equal( + closedWindowData[0].groups.length, + 1, + "should be one tab group in the closed window" + ); + const tabGroupInClosedWindow = closedWindowData[0].groups[0]; + Assert.equal( + tabGroupInClosedWindow.id, + tabGroupToCloseId, + "closed window tab group should be the one that was in the window" + ); + Assert.equal(ss.getSavedTabGroups().length, 1, "should be 1 saved tab group"); + Assert.equal( + ss.getSavedTabGroup(tabGroupToCloseId).id, + tabGroupInClosedWindow.id, + "saved tab group should be the same as the tab group in the closed window" + ); + + info( + "reopen the closed window to make sure that the tab group reopens in the window and is no longer saved" + ); + win = SessionStore.undoCloseWindow(0); + await BrowserTestUtils.waitForEvent(win, "SSWindowStateReady"); + await BrowserTestUtils.waitForEvent( + win.gBrowser.tabContainer, + "SSTabRestored" + ); + + Assert.equal( + win.gBrowser.tabGroups.length, + 1, + "tab group should have been restored" + ); + Assert.equal( + win.gBrowser.tabGroups[0].id, + tabGroupToCloseId, + "restored tab group should be the one from the closed window" + ); + Assert.equal( + ss.getSavedTabGroups().length, + 0, + "saved tab group should no longer be saved because it should be restored into the reopened window" + ); + Assert.equal( + ss.getClosedWindowData().length, + 0, + "the closed window should have been reopened" + ); + + await TabStateFlusher.flushWindow(win); + + info("close the window again"); + await BrowserTestUtils.closeWindow(win); + + Assert.equal( + ss.getClosedWindowData()[0].tabs.length, + 4, + "re-closed window should have 1 new tab from the new window, 1 standalone tab, and 2 tabs in the tab group" + ); + Assert.equal( + ss.getClosedWindowData()[0].groups.length, + 1, + "tab group should still be in the closed window" + ); + + info( + "open the saved tab group into a different window, which should remove it from the closed window" + ); + let savedGroupReopened = BrowserTestUtils.waitForEvent( + window, + "SSWindowStateReady" + ); + await SessionStore.openSavedTabGroup(tabGroupToCloseId, window); + await savedGroupReopened; + + await TabStateFlusher.flushWindow(window); + + Assert.equal( + ss.getClosedWindowData()[0].tabs.length, + 2, + "re-closed window should now just have 1 new tab from the new window and 1 standalone tab" + ); + Assert.equal( + ss.getClosedWindowData()[0].groups.length, + 0, + "re-closed window should no longer have tab groups because we opened the saved group into a different window" + ); + Assert.equal( + ss.getSavedTabGroups().length, + 0, + "saved tab group should no longer be saved because it should be restored into the main window" + ); + + info( + "reopen the closed window, which should no longer have the saved tab group in it since it moved into a different window" + ); + win = SessionStore.undoCloseWindow(0); + await BrowserTestUtils.waitForEvent(win, "SSWindowStateReady"); + await BrowserTestUtils.waitForEvent( + win.gBrowser.tabContainer, + "SSTabRestored" + ); + + Assert.equal( + win.gBrowser.openTabs.length, + 2, + "the re-restored window should have 1 new tab and 1 standalone tab" + ); + Assert.equal( + win.gBrowser.tabGroups.length, + 0, + "the re-restored window should have no tab group" + ); + + info( + "move the tab group back into the window that we've been opening + closing" + ); + let savedTabGroup = window.gBrowser.tabGroups[0]; + let savedTabRemoval = BrowserTestUtils.waitForEvent( + savedTabGroup, + "TabGroupRemoved" + ); + win.gBrowser.adoptTabGroup(window.gBrowser.tabGroups[0], { elementIndex: 2 }); + await savedTabRemoval; + + Assert.equal( + win.gBrowser.openTabs.length, + 4, + "the re-restored window should have the tab group again after adopting it" + ); + + await TabStateFlusher.flushWindow(win); + + info( + "close the window and forget it to make sure that the tab group will still be saved even if we forget about the closed window" + ); + await BrowserTestUtils.closeWindow(win); + ss.forgetClosedWindow(0); + + Assert.equal( + ss.getClosedWindowData().length, + 0, + "the closed window should have been forgotten" + ); + Assert.equal( + ss.getSavedTabGroups().length, + 1, + "but the automatically saved tab group that was in the closed window should remain saved" + ); + + forgetClosedWindows(); + forgetSavedTabGroups(); +}); + +/** + * A closing window should not save its tab group if the window was closed + * in response to moving that tab group + */ +add_task(async function test_dontSaveAfterAdoptingGroup() { + let win = await promiseNewWindowLoaded(); + let state = ss.getCurrentState(); + Assert.ok(!state.savedGroups.length, "savedGroups starts empty"); + + let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(tab1.linkedBrowser); + await TabStateFlusher.flush(tab1.linkedBrowser); + let group1 = win.gBrowser.addTabGroup([tab1]); + await BrowserTestUtils.removeTab(win.gBrowser.tabs[0]); + let windowUnloaded = BrowserTestUtils.waitForEvent(win, "unload"); + gBrowser.adoptTabGroup(group1); + await windowUnloaded; + state = ss.getCurrentState(); + Assert.ok(!state.savedGroups.length, "savedGroups is still empty"); +}); + +add_task(async function test_dontSaveAfterDeletingLastGroupInWindow() { + let win = await promiseNewWindowLoaded(); + let state = ss.getCurrentState(); + Assert.ok(!state.savedGroups.length, "savedGroups starts empty"); + let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(tab1.linkedBrowser); + await TabStateFlusher.flush(tab1.linkedBrowser); + let group1 = win.gBrowser.addTabGroup([tab1]); + await BrowserTestUtils.removeTab(win.gBrowser.tabs[0]); + let windowUnloaded = BrowserTestUtils.waitForEvent(win, "unload"); + win.gBrowser.removeTabGroup(group1); + await windowUnloaded; + state = ss.getCurrentState(); + Assert.ok(!state.savedGroups.length, "savedGroups is still empty"); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_saved.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_saved.js new file mode 100644 index 000000000..7a51e33f1 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_saved.js @@ -0,0 +1,94 @@ +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_SaveTabGroups() { + let win = await promiseNewWindowLoaded(); + let state = ss.getCurrentState(win); + Assert.equal( + state.savedGroups.length, + 0, + "savedGroups starts in initial state" + ); + + let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla?foo"); + await BrowserTestUtils.browserLoaded(tab1.linkedBrowser); + await TabStateFlusher.flush(tab1.linkedBrowser); + let group1 = win.gBrowser.addTabGroup([tab1]); + ss.addSavedTabGroup(group1); + + let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla?bar"); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser); + await TabStateFlusher.flush(tab2.linkedBrowser); + let group2 = win.gBrowser.addTabGroup([tab2]); + ss.addSavedTabGroup(group2); + + state = ss.getCurrentState(); + + Assert.equal(state.savedGroups.length, 2, "savedGroups has 2 groups"); + Assert.equal(state.savedGroups[0].id, group1.id, "group1 is in savedGroups"); + Assert.equal(state.savedGroups[0].tabs.length, 1, "group1 has 1 tab"); + Assert.equal(state.savedGroups[1].id, group2.id, "group2 is in savedGroups"); + Assert.equal(state.savedGroups[1].tabs.length, 1, "group2 has 1 tab"); + + ss.forgetSavedTabGroup(group1.id); + await win.gBrowser.removeTabGroup(group1); + state = ss.getCurrentState(); + + Assert.equal(state.savedGroups.length, 1, "savedGroups has 1 group"); + Assert.equal(state.savedGroups[0].id, group2.id, "group2 is in savedGroups"); + Assert.equal(state.savedGroups[0].tabs.length, 1, "group2 still has 1 tab"); + + ss.forgetSavedTabGroup(group2.id); + await win.gBrowser.removeTabGroup(group2); + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); +}); + +/** + * Newtabs with no history are considered unimportant and are ignored by + * sessionstore. Groups of unimportant tabs should also be ignored. + */ +add_task(async function test_ignoreUnimportantTabGroups() { + let win = await promiseNewWindowLoaded(); + let state = ss.getCurrentState(win); + Assert.equal(state.savedGroups.length, 0, "savedGroups starts empty"); + + let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:newtab"); + await BrowserTestUtils.browserLoaded(tab1.linkedBrowser); + await TabStateFlusher.flush(tab1.linkedBrowser); + let group1 = win.gBrowser.addTabGroup([tab1]); + ss.addSavedTabGroup(group1); + + state = ss.getCurrentState(); + Assert.equal(state.savedGroups.length, 0, "savedGroups is still empty"); + + await win.gBrowser.removeTabGroup(group1); + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); +}); + +add_task(async function test_refuseToSaveGroupsInPrivateWindows() { + const privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let tab = BrowserTestUtils.addTab(privateWin.gBrowser, "about:newtab"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await TabStateFlusher.flush(tab.linkedBrowser); + let group = privateWin.gBrowser.addTabGroup([tab]); + + Assert.throws( + () => { + ss.addSavedTabGroup(group); + }, + /Refusing to save tab group/, + "addSavedTabGroup throws exception when trying to save a tab group from a private window" + ); + await BrowserTestUtils.closeWindow(privateWin); + forgetClosedWindows(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_state.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_state.js new file mode 100644 index 000000000..0a645a86e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_state.js @@ -0,0 +1,101 @@ +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +/** + * @param {WindowStateData} windowState + * @returns {TabStateData|undefined} + */ +function findTabStateByUrl(windowState, url) { + return windowState.tabs.find(tabState => tabState.userTypedValue == url); +} + +add_task(async function test_TabGroupsInState() { + let win = await promiseNewWindowLoaded(); + let aboutRobotsTab = BrowserTestUtils.addTab(win.gBrowser, "about:robots"); + let aboutMozillaTab = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + BrowserTestUtils.addTab(win.gBrowser, "about:about"); + + let group = win.gBrowser.addTabGroup([aboutRobotsTab, aboutMozillaTab], { + label: "non-meta about pages", + }); + + let state = ss.getWindowState(win); + + Assert.equal(state.windows.length, 1, "should have state from 1 window"); + let windowState = state.windows[0]; + + Assert.ok(windowState.groups, "window state should have a `groups` property"); + Assert.equal(windowState.groups.length, 1, "there should be one tab group"); + let groupState = windowState.groups[0]; + + Assert.equal( + groupState.id, + group.id, + "tab group ID should be recorded in state" + ); + Assert.equal( + groupState.name, + group.label, + "tab group name should be recorded in state" + ); + Assert.equal( + groupState.color, + group.color, + "tab group color should be recorded in state" + ); + Assert.equal( + groupState.collapsed, + group.collapsed, + "tab group collapsed state should be recorded in state" + ); + + Assert.equal( + windowState.tabs.length, + 4, + "there should be 3 tabs in session state + 1 initial tab" + ); + + const aboutRobotsTabState = findTabStateByUrl(windowState, "about:robots"); + Assert.ok(aboutRobotsTabState, "about:robots tab should be in session state"); + Assert.equal( + aboutRobotsTabState.groupId, + group.id, + "about:robots tab should be part of the tab group" + ); + + const aboutMozillaTabState = findTabStateByUrl(windowState, "about:mozilla"); + Assert.ok( + aboutMozillaTabState, + "about:mozilla tab should be in session state" + ); + Assert.equal( + aboutMozillaTabState.groupId, + group.id, + "about:mozilla tab should be part of the tab group" + ); + + const aboutAboutTabState = findTabStateByUrl(windowState, "about:about"); + Assert.ok(aboutAboutTabState, "about:about tab should be in session state"); + Assert.ok( + !aboutAboutTabState.groupId, + "about:about tab should NOT be part of the tab group" + ); + + // collapse the tab group and make sure the tab group data updates + group.collapsed = true; + + state = ss.getWindowState(win); + groupState = state.windows[0].groups[0]; + Assert.equal( + groupState.collapsed, + group.collapsed, + "updated tab group collapsed state should be recorded in state" + ); + await BrowserTestUtils.closeWindow(win); + forgetClosedWindows(); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_groups_undo.js b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_undo.js new file mode 100644 index 000000000..c54d18287 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_groups_undo.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ORIG_STATE = SessionStore.getBrowserState(); + +registerCleanupFunction(async () => { + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); +}); + +add_task(async function test_tabGroupsUndo() { + Assert.equal( + SessionStore.getLastClosedTabGroupId(window), + null, + "Should start without a last closed tab group id" + ); + Assert.equal( + window.gBrowser.tabGroups.length, + 0, + "Should start with no tab groups" + ); + + let groupedTabs = [ + BrowserTestUtils.addTab(gBrowser, "https://example.com"), + BrowserTestUtils.addTab(gBrowser, "https://example.com"), + ]; + await Promise.all( + groupedTabs.map(g => promiseBrowserLoaded(g.linkedBrowser)) + ); + + info("Adding to tab group"); + let tabGroup = gBrowser.addTabGroup(groupedTabs); + let tabGroupId = tabGroup.id; + + info("Waiting for tab group removed"); + let removePromise = BrowserTestUtils.waitForEvent( + tabGroup, + "TabGroupRemoved" + ); + await gBrowser.removeTabGroup(tabGroup); + await removePromise; + + await TabStateFlusher.flushWindow(window); + + info("Waiting for getLastClosedTabGroupId"); + await TestUtils.waitForCondition( + () => SessionStore.getLastClosedTabGroupId(window) !== null + ); + + Assert.equal( + SessionStore.getLastClosedTabGroupId(window), + tabGroupId, + "SessionStore saves the ID of the last closed tab group" + ); + + SessionWindowUI.undoCloseTab(window); + + Assert.equal(window.gBrowser.tabGroups.length, 1, "Tab group was restored"); + Assert.equal( + SessionStore.getLastClosedTabGroupId(window), + null, + "SessionStore reset the ID of the last closed tab group" + ); + + BrowserTestUtils.removeTab(gBrowser.tabGroups[0].tabs[0]); + Assert.equal( + SessionStore.getLastClosedTabGroupId(window), + null, + "SessionStore last closed group ID is null after closing a tab within a group" + ); + + let ungroupedTab = BrowserTestUtils.addTab(gBrowser, "https://example.com"); + await promiseBrowserLoaded(ungroupedTab.linkedBrowser); + BrowserTestUtils.removeTab(ungroupedTab); + + Assert.equal( + SessionStore.getLastClosedTabGroupId(window), + null, + "SessionStore last closed group ID is null after closing a tab not in a group" + ); + + let savedGroupedTabs = [ + BrowserTestUtils.addTab(gBrowser, "https://example.com"), + BrowserTestUtils.addTab(gBrowser, "https://example.com"), + ]; + await Promise.all( + savedGroupedTabs.map(g => promiseBrowserLoaded(g.linkedBrowser)) + ); + info("Adding to tab group"); + let savedTabGroup = gBrowser.addTabGroup(savedGroupedTabs); + let savedTabGroupId = savedTabGroup.id; + + savedTabGroup.save(); + info("Waiting for tab group removed"); + removePromise = BrowserTestUtils.waitForEvent( + savedTabGroup, + "TabGroupRemoved" + ); + await gBrowser.removeTabGroup(savedTabGroup); + await removePromise; + + await TabStateFlusher.flushWindow(window); + + info("Waiting for getLastClosedTabGroupId"); + await TestUtils.waitForCondition( + () => SessionStore.getLastClosedTabGroupId(window) !== null + ); + + Assert.equal( + SessionStore.getLastClosedTabGroupId(window), + savedTabGroupId, + "SessionStore saves the ID of the last saved and closed tab group" + ); + + gBrowser.removeAllTabsBut(gBrowser.tabs[0]); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_label_during_restore.js b/src/zen/tests/mochitests/sessionstore/browser_tab_label_during_restore.js new file mode 100644 index 000000000..ef590b89e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_label_during_restore.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we don't do unnecessary tab label changes while restoring a tab. + */ + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.sessionstore.restore_on_demand", true], + ["browser.sessionstore.restore_tabs_lazily", true], + ], + }); + + const BACKUP_STATE = SessionStore.getBrowserState(); + const REMOTE_URL = "http://www.example.com/"; + const ABOUT_ROBOTS_URI = "about:robots"; + const ABOUT_ROBOTS_TITLE = "Gort! Klaatu barada nikto!"; + const NO_TITLE_URL = "data:text/plain,foo"; + const EMPTY_TAB_TITLE = gBrowser.tabContainer.emptyTabTitle; + + function observeLabelChanges(tab, expectedLabels) { + let seenLabels = [tab.label]; + function TabAttrModifiedListener(event) { + if (event.detail.changed.some(attr => attr == "label")) { + seenLabels.push(tab.label); + } + } + tab.addEventListener("TabAttrModified", TabAttrModifiedListener); + return async () => { + await TestUtils.waitForCondition( + () => seenLabels.length == expectedLabels.length, + "saw " + seenLabels.length + " TabAttrModified events" + ); + tab.removeEventListener("TabAttrModified", TabAttrModifiedListener); + is( + JSON.stringify(seenLabels), + JSON.stringify(expectedLabels || []), + "observed tab label changes" + ); + }; + } + + info("setting test browser state"); + let browserLoadedPromise = BrowserTestUtils.firstBrowserLoaded(window, false); + await promiseBrowserState({ + windows: [ + { + tabs: [ + { entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }] }, + { entries: [{ url: ABOUT_ROBOTS_URI, triggeringPrincipal_base64 }] }, + { entries: [{ url: REMOTE_URL, triggeringPrincipal_base64 }] }, + { entries: [{ url: NO_TITLE_URL, triggeringPrincipal_base64 }] }, + { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] }, + ], + }, + ], + }); + let [tab1, tab2, tab3, tab4, tab5] = gBrowser.tabs; + is(gBrowser.selectedTab, tab1, "first tab is selected"); + + await browserLoadedPromise; + const REMOTE_TITLE = tab1.linkedBrowser.contentTitle; + is( + tab1.linkedBrowser.currentURI.spec, + REMOTE_URL, + "correct URL loaded in first tab" + ); + is(typeof REMOTE_TITLE, "string", "content title is a string"); + isnot(REMOTE_TITLE.length, 0, "content title isn't empty"); + isnot(REMOTE_TITLE, REMOTE_URL, "content title is different from the URL"); + is(tab1.label, REMOTE_TITLE, "first tab displays content title"); + ok( + document.title.startsWith(REMOTE_TITLE), + "title bar displays content title" + ); + ok(tab2.hasAttribute("pending"), "second tab is pending"); + ok(tab3.hasAttribute("pending"), "third tab is pending"); + ok(tab4.hasAttribute("pending"), "fourth tab is pending"); + is(tab5.label, EMPTY_TAB_TITLE, "fifth tab dislpays empty tab title"); + + info("selecting the second tab"); + // The fix for bug 1364127 caused about: pages' initial tab titles to show + // their about: URIs until their actual page titles are known, e.g. + // "about:addons" -> "Add-ons Manager". This is bug 1371896. Previously, + // about: pages' initial tab titles were blank until the page title was known. + let finishObservingLabelChanges = observeLabelChanges(tab2, [ + ABOUT_ROBOTS_URI, + ABOUT_ROBOTS_TITLE, + ]); + browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab2.linkedBrowser, + false, + ABOUT_ROBOTS_URI + ); + gBrowser.selectedTab = tab2; + await browserLoadedPromise; + ok(!tab2.hasAttribute("pending"), "second tab isn't pending anymore"); + await finishObservingLabelChanges(); + ok( + document.title.startsWith(ABOUT_ROBOTS_TITLE), + "title bar displays content title" + ); + + info("selecting the third tab"); + finishObservingLabelChanges = observeLabelChanges(tab3, [ + "example.com/", + REMOTE_TITLE, + ]); + browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab3.linkedBrowser, + false, + REMOTE_URL + ); + gBrowser.selectedTab = tab3; + await browserLoadedPromise; + ok(!tab3.hasAttribute("pending"), "third tab isn't pending anymore"); + await finishObservingLabelChanges(); + ok( + document.title.startsWith(REMOTE_TITLE), + "title bar displays content title" + ); + + info("selecting the fourth tab"); + finishObservingLabelChanges = observeLabelChanges(tab4, [NO_TITLE_URL]); + browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab4.linkedBrowser, + false, + NO_TITLE_URL + ); + gBrowser.selectedTab = tab4; + await browserLoadedPromise; + ok(!tab4.hasAttribute("pending"), "fourth tab isn't pending anymore"); + await finishObservingLabelChanges(); + is( + document.title, + document.getElementById("bundle_brand").getString("brandFullName"), + "title bar doesn't display content title since page doesn't have one" + ); + + info("restoring the modified browser state"); + gBrowser.selectedTab = tab3; + await TabStateFlusher.flushWindow(window); + await promiseBrowserState(SessionStore.getBrowserState()); + [tab1, tab2, tab3, tab4, tab5] = gBrowser.tabs; + is(tab3, gBrowser.selectedTab, "third tab is selected after restoring"); + ok( + document.title.startsWith(REMOTE_TITLE), + "title bar displays content title" + ); + ok(tab1.hasAttribute("pending"), "first tab is pending after restoring"); + ok(tab2.hasAttribute("pending"), "second tab is pending after restoring"); + is(tab2.label, ABOUT_ROBOTS_TITLE, "second tab displays content title"); + ok(!tab3.hasAttribute("pending"), "third tab is not pending after restoring"); + is( + tab3.label, + REMOTE_TITLE, + "third tab displays content title in pending state" + ); + ok(tab4.hasAttribute("pending"), "fourth tab is pending after restoring"); + is(tab4.label, NO_TITLE_URL, "fourth tab displays URL"); + is(tab5.label, EMPTY_TAB_TITLE, "fifth tab still displays empty tab title"); + + info("selecting the first tab"); + finishObservingLabelChanges = observeLabelChanges(tab1, [REMOTE_TITLE]); + let tabContentRestored = TestUtils.topicObserved( + "sessionstore-debug-tab-restored" + ); + gBrowser.selectedTab = tab1; + ok( + document.title.startsWith(REMOTE_TITLE), + "title bar displays content title" + ); + await tabContentRestored; + ok(!tab1.hasAttribute("pending"), "first tab isn't pending anymore"); + await finishObservingLabelChanges(); + + await promiseBrowserState(BACKUP_STATE); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tab_notes_canonicalurl.js b/src/zen/tests/mochitests/sessionstore/browser_tab_notes_canonicalurl.js new file mode 100644 index 000000000..18a44fc1b --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tab_notes_canonicalurl.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TabNotes } = ChromeUtils.importESModule( + "moz-src:///browser/components/tabnotes/TabNotes.sys.mjs" +); + +const BACKUP_STATE = SessionStore.getBrowserState(); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.notes.enabled", true]], + }); +}); + +registerCleanupFunction(async () => { + await TabNotes.reset(); + await promiseBrowserState(BACKUP_STATE); +}); + +add_task(async function test_canonicalUrl_respects_privacy_level() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionstore.privacy_level", 2]], + }); + + const CANONICAL_URL = "https://example.com/canonical-private"; + + let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.canonicalUrl = CANONICAL_URL; + + let state = JSON.parse(ss.getTabState(tab)); + ok( + !("canonicalUrl" in state), + "canonicalUrl should not be saved when privacy level blocks it" + ); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_canonicalUrl_saved_in_closed_tab_state() { + const CANONICAL_URL = "https://example.com/canonical-undoclose"; + + let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.canonicalUrl = CANONICAL_URL; + + let tabState = JSON.parse(ss.getTabState(tab)); + is( + tabState.canonicalUrl, + CANONICAL_URL, + "canonicalUrl should be present in tab state before closing" + ); + + await promiseRemoveTabAndSessionState(tab); + + let closedTabData = SessionStore.getClosedTabDataForWindow(window); + is( + closedTabData[0].state.canonicalUrl, + CANONICAL_URL, + "canonicalUrl should be preserved in closed tab data" + ); + + SessionStore.forgetClosedTab(window, 0); +}); + +add_task(async function test_canonicalUrl_in_session_state_on_navigation() { + const CANONICAL_URL = "https://example.com/canonical-nav"; + + let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.canonicalUrl = CANONICAL_URL; + + let tabState = JSON.parse(ss.getTabState(tab)); + is( + tabState.canonicalUrl, + CANONICAL_URL, + "canonicalUrl should be in session state before navigation" + ); + + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + "https://example.org/" + ); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + tabState = JSON.parse(ss.getTabState(tab)); + isnot( + tabState.canonicalUrl, + CANONICAL_URL, + "canonicalUrl should change after navigation to different URL" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tabicon_after_bg_tab_crash.js b/src/zen/tests/mochitests/sessionstore/browser_tabicon_after_bg_tab_crash.js new file mode 100644 index 000000000..f33255867 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tabicon_after_bg_tab_crash.js @@ -0,0 +1,63 @@ +"use strict"; + +const { ImageTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ImageTestUtils.sys.mjs" +); + +const FAVICON = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAARElEQVQYV2NkYLj3nwEnUGKEMxkYGBghilEFIQBTHKqYOIDFZHQ+DNz7j0MxdoDDGThNRgfoNsEATsXYARbFuAHtFAMAuvMbOrNomdAAAAAASUVORK5CYII="; +const PAGE_URL = `data:text/html, + + + + + + Favicon! + +`; + +/** + * Tests that if a background tab crashes that it doesn't + * lose the favicon in the tab. + */ +add_task(async function test_tabicon_after_bg_tab_crash() { + let originalTab = gBrowser.selectedTab; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE_URL, + }, + async function (browser) { + // Because there is debounce logic in FaviconLoader.sys.mjs to reduce the + // favicon loads, we have to wait some time before checking that icon was + // stored properly. + await TestUtils.waitForCondition( + () => { + return gBrowser.getIcon() != null; + }, + "wait for favicon load to finish", + 100, + 5 + ); + await ImageTestUtils.assertEqualImage( + window, + browser.mIconURL, + FAVICON, + "Favicon is correctly set." + ); + + await BrowserTestUtils.switchTab(gBrowser, originalTab); + await BrowserTestUtils.crashFrame( + browser, + false /* shouldShowTabCrashPage */ + ); + await ImageTestUtils.assertEqualImage( + window, + browser.mIconURL, + FAVICON, + "Favicon is still set after crash." + ); + } + ); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_tabs_in_urlbar.js b/src/zen/tests/mochitests/sessionstore/browser_tabs_in_urlbar.js new file mode 100644 index 000000000..d971da6d6 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_tabs_in_urlbar.js @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that tabs which aren't displayed yet (i.e. need to be reloaded) are + * still displayed in the address bar results. + */ + +const RESTRICT_TOKEN_OPENPAGE = "%"; + +const { PlacesTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PlacesTestUtils.sys.mjs" +); + +const { UrlbarProviderOpenTabs } = ChromeUtils.importESModule( + "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs" +); + +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); + +var stateBackup = ss.getBrowserState(); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Set the pref to true so we know exactly how many tabs should be restoring at + // any given time. This guarantees that a finishing load won't start another. + ["browser.sessionstore.restore_on_demand", true], + // Don't restore tabs lazily. + ["browser.sessionstore.restore_tabs_lazily", false], + ], + }); + + registerCleanupFunction(() => { + ss.setBrowserState(stateBackup); + }); + + info("Waiting for the Places DB to be initialized"); + await PlacesUtils.promiseLargeCacheDBConnection(); + await UrlbarProviderOpenTabs.promiseDBPopulated; +}); + +add_task(async function test_unrestored_tabs_listed() { + const state = { + windows: [ + { + tabs: [ + { + entries: [ + { url: "http://example.org/#1", triggeringPrincipal_base64 }, + ], + }, + { + entries: [ + { url: "http://example.org/#2", triggeringPrincipal_base64 }, + ], + }, + { + entries: [ + { url: "http://example.org/#3", triggeringPrincipal_base64 }, + ], + }, + { + entries: [ + { url: "http://example.org/#4", triggeringPrincipal_base64 }, + ], + }, + ], + selected: 1, + }, + ], + }; + + const tabsForEnsure = new Set(); + state.windows[0].tabs.forEach(function (tab) { + tabsForEnsure.add(tab.entries[0].url); + }); + + let tabsRestoring = 0; + let tabsRestored = 0; + + await new Promise(resolve => { + function handleEvent(aEvent) { + if (aEvent.type == "SSTabRestoring") { + tabsRestoring++; + } else { + tabsRestored++; + } + + if (tabsRestoring < state.windows[0].tabs.length || tabsRestored < 1) { + return; + } + + gBrowser.tabContainer.removeEventListener( + "SSTabRestoring", + handleEvent, + true + ); + gBrowser.tabContainer.removeEventListener( + "SSTabRestored", + handleEvent, + true + ); + executeSoon(resolve); + } + + // currentURI is set before SSTabRestoring is fired, so we can sucessfully check + // after that has fired for all tabs. Since 1 tab will be restored though, we + // also need to wait for 1 SSTabRestored since currentURI will be set, unset, then set. + gBrowser.tabContainer.addEventListener("SSTabRestoring", handleEvent, true); + gBrowser.tabContainer.addEventListener("SSTabRestored", handleEvent, true); + ss.setBrowserState(JSON.stringify(state)); + }); + + // Ensure any database statements started by UrlbarProviderOpenTabs are + // complete before continuing. + await PlacesTestUtils.promiseAsyncUpdates(); + + // Remove the current tab from tabsForEnsure, because switch to tab doesn't + // suggest it. + tabsForEnsure.delete(gBrowser.currentURI.spec); + info("Searching open pages."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: RESTRICT_TOKEN_OPENPAGE, + }); + const total = UrlbarTestUtils.getResultCount(window); + info(`Found ${total} matches`); + + // Check to see the expected uris and titles match up (in any order) + for (let i = 0; i < total; i++) { + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.heuristic) { + info("Skip heuristic match"); + continue; + } + const url = result.url; + Assert.ok( + tabsForEnsure.has(url), + `Should have the found result '${url}' in the expected list of entries` + ); + // Remove the found entry from expected results. + tabsForEnsure.delete(url); + } + // Make sure there is no reported open page that is not open. + Assert.equal(tabsForEnsure.size, 0, "Should have found all the tabs"); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_undoCloseById.js b/src/zen/tests/mochitests/sessionstore/browser_undoCloseById.js new file mode 100644 index 000000000..f1c49ae22 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_undoCloseById.js @@ -0,0 +1,185 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +/** + * This test is for the undoCloseById function. + */ + +async function openWindow(url) { + let win = await promiseNewWindowLoaded(); + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY; + BrowserTestUtils.startLoadingURIString(win.gBrowser.selectedBrowser, url, { + flags, + }); + await promiseBrowserLoaded(win.gBrowser.selectedBrowser, true, url); + return win; +} + +async function closeWindow(win) { + await BrowserTestUtils.closeWindow(win); + // Wait 20 ms to allow SessionStorage a chance to register the closed window. + await new Promise(resolve => setTimeout(resolve, 20)); +} + +function getLastClosedTabData(win) { + const closedTabs = SessionStore.getClosedTabData(win); + return closedTabs[closedTabs.length - 1]; +} + +add_task(async function test_undoCloseById() { + // Clear the lists of closed windows and tabs. + forgetClosedWindows(); + for (const win of SessionStore.getWindows()) { + while (SessionStore.getClosedTabCountForWindow(win)) { + SessionStore.forgetClosedTab(win, 0); + } + } + SessionStore.resetNextClosedId(); + + // Open a new window. + let win = await openWindow("about:robots"); + + // Open and close a tab. + await openAndCloseTab(win, "about:mozilla"); + is( + SessionStore.lastClosedObjectType, + "tab", + "The last closed object is a tab" + ); + + // Record the first closedId created. + is(1, SessionStore.getClosedTabCount(), "We have 1 closed tab"); + let initialClosedId = SessionStore.getClosedTabDataForWindow(win)[0].closedId; + + // Open and close another window. + let win2 = await openWindow("about:mozilla"); + await closeWindow(win2); // closedId == initialClosedId + 1 + is( + SessionStore.lastClosedObjectType, + "window", + "The last closed object is a window" + ); + + // Open and close another tab in the first window. + await openAndCloseTab(win, "about:robots"); // closedId == initialClosedId + 2 + is( + SessionStore.lastClosedObjectType, + "tab", + "The last closed object is a tab" + ); + + // Undo closing the second tab. + let tab = SessionStore.undoCloseById(initialClosedId + 2); + await promiseBrowserLoaded(tab.linkedBrowser); + is( + tab.linkedBrowser.currentURI.spec, + "about:robots", + "The expected tab was re-opened" + ); + + let notTab = SessionStore.undoCloseById(initialClosedId + 2); + is(notTab, undefined, "Re-opened tab cannot be unClosed again by closedId"); + + // Now the last closed object should be a window again. + is( + SessionStore.lastClosedObjectType, + "window", + "The last closed object is a window" + ); + + // Undo closing the first tab. + let tab2 = SessionStore.undoCloseById(initialClosedId); + await promiseBrowserLoaded(tab2.linkedBrowser); + is( + tab2.linkedBrowser.currentURI.spec, + "about:mozilla", + "The expected tab was re-opened" + ); + + // Close the two tabs we re-opened. + await promiseRemoveTabAndSessionState(tab); // closedId == initialClosedId + 3 + is( + SessionStore.lastClosedObjectType, + "tab", + "The last closed object is a tab" + ); + await promiseRemoveTabAndSessionState(tab2); // closedId == initialClosedId + 4 + is( + SessionStore.lastClosedObjectType, + "tab", + "The last closed object is a tab" + ); + + // Open another new window. + let win3 = await openWindow("about:mozilla"); + + // Close both windows. + await closeWindow(win); // closedId == initialClosedId + 5 + is( + SessionStore.lastClosedObjectType, + "window", + "The last closed object is a window" + ); + await closeWindow(win3); // closedId == initialClosedId + 6 + is( + SessionStore.lastClosedObjectType, + "window", + "The last closed object is a window" + ); + + // Undo closing the second window. + win = SessionStore.undoCloseById(initialClosedId + 6); + await BrowserTestUtils.waitForEvent(win, "load"); + + // Make sure we wait until this window is restored. + await BrowserTestUtils.waitForEvent( + win.gBrowser.tabContainer, + "SSTabRestored" + ); + + is( + win.gBrowser.selectedBrowser.currentURI.spec, + "about:mozilla", + "The expected window was re-opened" + ); + + let notWin = SessionStore.undoCloseById(initialClosedId + 6); + is( + notWin, + undefined, + "Re-opened window cannot be unClosed again by closedId" + ); + + // Close the window again. + await closeWindow(win); + is( + SessionStore.lastClosedObjectType, + "window", + "The last closed object is a window" + ); + + // Undo closing the first window. + win = SessionStore.undoCloseById(initialClosedId + 5); + + await BrowserTestUtils.waitForEvent(win, "load"); + + // Make sure we wait until this window is restored. + await BrowserTestUtils.waitForEvent( + win.gBrowser.tabContainer, + "SSTabRestored" + ); + + is( + win.gBrowser.selectedBrowser.currentURI.spec, + "about:robots", + "The expected window was re-opened" + ); + + // Close the window again. + await closeWindow(win); + is( + SessionStore.lastClosedObjectType, + "window", + "The last closed object is a window" + ); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_undoCloseById_targetWindow.js b/src/zen/tests/mochitests/sessionstore/browser_undoCloseById_targetWindow.js new file mode 100644 index 000000000..62e3da89e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_undoCloseById_targetWindow.js @@ -0,0 +1,93 @@ +"use strict"; + +/** + * This test verifies SessionStore.undoCloseById behavior when passed the targetWindow argument + */ + +async function openWindow(url) { + let win = await promiseNewWindowLoaded(); + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY; + BrowserTestUtils.startLoadingURIString(win.gBrowser.selectedBrowser, url, { + flags, + }); + await promiseBrowserLoaded(win.gBrowser.selectedBrowser, true, url); + return win; +} + +async function closeWindow(win) { + TestUtils.waitForTick(); + let sessionStoreUpdated = TestUtils.topicObserved( + "sessionstore-closed-objects-changed" + ); + await BrowserTestUtils.closeWindow(win); + await sessionStoreUpdated; +} + +function forgetTabsAndWindows() { + // Clear the lists of closed windows and tabs. + forgetClosedWindows(); + while (SessionStore.getClosedTabCount(window)) { + SessionStore.forgetClosedTab(window, 0); + } +} + +add_task(async function test_undoCloseById_with_targetWindow() { + forgetTabsAndWindows(); + // Test that a tab closed in (currently open) window B, will correctly be opened in target window A. + // And that the closed record should be correctly removed from window B + const winA = window; + // Open a new window. + const winB = await openWindow("about:robots"); + await SimpleTest.promiseFocus(winB); + // Open and close a tab in the 2nd window + await openAndCloseTab(winB, "about:mozilla"); + is( + SessionStore.lastClosedObjectType, + "tab", + "The last closed object is a tab" + ); + // Record the first closedId created. + const closedId = SessionStore.getClosedTabData(winB)[0].closedId; + let tabRestored = BrowserTestUtils.waitForNewTab( + winA.gBrowser, + "about:mozilla" + ); + + // Restore the tab into the first window, not the window it was closed in + SessionStore.undoCloseById(closedId, undefined, winA); + await tabRestored; + is(winA.gBrowser.selectedBrowser.currentURI.spec, "about:mozilla"); + + // Verify the closed tab data is removed from the source window + is( + SessionStore.getClosedTabData(winB).length, + 0, + "Record removed from the source window's closed tab data" + ); + + BrowserTestUtils.removeTab(winA.gBrowser.selectedTab); + await closeWindow(winB); +}); + +add_task(async function test_undoCloseById_with_nonExistent_targetWindow() { + // Test that restoring a tab to a non-existent targetWindow throws + forgetTabsAndWindows(); + await openAndCloseTab(window, "about:mozilla"); + is( + SessionStore.lastClosedObjectType, + "tab", + "The last closed object is a tab" + ); + // Record the first closedId created. + const closedId = SessionStore.getClosedTabData(window)[0].closedId; + + // get a reference to a window that will be closed + const newWin = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(newWin); + await BrowserTestUtils.closeWindow(newWin); + + // Expect an exception trying to restore a tab to a non-existent window + Assert.throws(() => { + SessionStore.undoCloseById(closedId, undefined, newWin); + }, /NS_ERROR_ILLEGAL_VALUE/); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_unrestored_crashedTabs.js b/src/zen/tests/mochitests/sessionstore/browser_unrestored_crashedTabs.js new file mode 100644 index 000000000..51e54af12 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_unrestored_crashedTabs.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if we have tabs that are still in the "click to + * restore" state, that if their browsers crash, that we don't + * show the crashed state for those tabs (since selecting them + * should restore them anyway). + */ + +const PREF = "browser.sessionstore.restore_on_demand"; +const PAGE = + "data:text/html,A%20regular,%20everyday,%20normal%20page."; + +add_task(async function test() { + await pushPrefs([PREF, true]); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + async function (browser) { + await TabStateFlusher.flush(browser); + + // We'll create a second "pending" tab. This is the one we'll + // ensure doesn't go to about:tabcrashed. We start it non-remote + // since this is how SessionStore creates all browsers before + // they are restored. + let unrestoredTab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + skipAnimation: true, + forceNotRemote: true, + }); + + let state = { + entries: [{ url: PAGE, triggeringPrincipal_base64 }], + }; + + ss.setTabState(unrestoredTab, JSON.stringify(state)); + + ok(!unrestoredTab.hasAttribute("crashed"), "tab is not crashed"); + ok(unrestoredTab.hasAttribute("pending"), "tab is pending"); + + // Now crash the selected browser. + await BrowserTestUtils.crashFrame(browser); + + ok(!unrestoredTab.hasAttribute("crashed"), "tab is still not crashed"); + ok(unrestoredTab.hasAttribute("pending"), "tab is still pending"); + + // Selecting the tab should now restore it. + gBrowser.selectedTab = unrestoredTab; + await promiseTabRestored(unrestoredTab); + + ok(!unrestoredTab.hasAttribute("crashed"), "tab is still not crashed"); + ok(!unrestoredTab.hasAttribute("pending"), "tab is no longer pending"); + + // The original tab should still be crashed + let originalTab = gBrowser.getTabForBrowser(browser); + ok(originalTab.hasAttribute("crashed"), "original tab is crashed"); + ok(!originalTab.isRemoteBrowser, "Should not be remote"); + + // We'd better be able to restore it still. + gBrowser.selectedTab = originalTab; + SessionStore.reviveCrashedTab(originalTab); + await promiseTabRestored(originalTab); + + // Clean up. + BrowserTestUtils.removeTab(unrestoredTab); + } + ); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_upgrade_backup.js b/src/zen/tests/mochitests/sessionstore/browser_upgrade_backup.js new file mode 100644 index 000000000..dd803a163 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_upgrade_backup.js @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const Paths = SessionFile.Paths; +const PREF_UPGRADE = "browser.sessionstore.upgradeBackup.latestBuildID"; +const PREF_MAX_UPGRADE_BACKUPS = + "browser.sessionstore.upgradeBackup.maxUpgradeBackups"; + +/** + * Prepares tests by retrieving the current platform's build ID, clearing the + * build where the last backup was created and creating arbitrary JSON data + * for a new backup. + */ +function prepareTest() { + let result = {}; + + result.buildID = Services.appinfo.platformBuildID; + Services.prefs.setCharPref(PREF_UPGRADE, ""); + result.contents = { + "browser_upgrade_backup.js": Math.random(), + }; + + return result; +} + +/** + * Retrieves all upgrade backups and returns them in an array. + */ +async function getUpgradeBackups() { + let children = await IOUtils.getChildren(Paths.backups); + + return children.filter(path => path.startsWith(Paths.upgradeBackupPrefix)); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["test.wait300msAfterTabSwitch", true]], + }); + + // Wait until initialization is complete + await SessionStore.promiseInitialized; +}); + +add_task(async function test_upgrade_backup() { + let test = prepareTest(); + info("Let's check if we create an upgrade backup"); + await SessionFile.wipe(); + await IOUtils.writeJSON(Paths.clean, test.contents, { + compress: true, + }); + info("Call `SessionFile.read()` to set state to 'clean'"); + await SessionFile.read(); + await SessionFile.write(""); // First call to write() triggers the backup + + Assert.equal( + Services.prefs.getCharPref(PREF_UPGRADE), + test.buildID, + "upgrade backup should be set" + ); + + Assert.ok( + await IOUtils.exists(Paths.upgradeBackup), + "upgrade backup file has been created" + ); + + let data = await IOUtils.readJSON(Paths.upgradeBackup, { decompress: true }); + Assert.deepEqual( + test.contents, + data, + "upgrade backup contains the expected contents" + ); + + info("Let's check that we don't overwrite this upgrade backup"); + let newContents = { + "something else entirely": Math.random(), + }; + await IOUtils.writeJSON(Paths.clean, newContents, { + compress: true, + }); + await SessionFile.write(""); // Next call to write() shouldn't trigger the backup + data = await IOUtils.readJSON(Paths.upgradeBackup, { decompress: true }); + Assert.deepEqual(test.contents, data, "upgrade backup hasn't changed"); +}); + +add_task(async function test_upgrade_backup_removal() { + let test = prepareTest(); + let maxUpgradeBackups = Services.prefs.getIntPref( + PREF_MAX_UPGRADE_BACKUPS, + 3 + ); + info("Let's see if we remove backups if there are too many"); + await SessionFile.wipe(); + await IOUtils.writeJSON(Paths.clean, test.contents, { + compress: true, + }); + info("Call `SessionFile.read()` to set state to 'clean'"); + await SessionFile.read(); + + // create dummy backups + await IOUtils.writeUTF8(Paths.upgradeBackupPrefix + "20080101010101", "", { + compress: true, + }); + await IOUtils.writeUTF8(Paths.upgradeBackupPrefix + "20090101010101", "", { + compress: true, + }); + await IOUtils.writeUTF8(Paths.upgradeBackupPrefix + "20100101010101", "", { + compress: true, + }); + await IOUtils.writeUTF8(Paths.upgradeBackupPrefix + "20110101010101", "", { + compress: true, + }); + await IOUtils.writeUTF8(Paths.upgradeBackupPrefix + "20120101010101", "", { + compress: true, + }); + await IOUtils.writeUTF8(Paths.upgradeBackupPrefix + "20130101010101", "", { + compress: true, + }); + + // get currently existing backups + let backups = await getUpgradeBackups(); + + info("Write the session to disk and perform a backup"); + await SessionFile.write(""); // First call to write() triggers the backup and the cleanup + + // a new backup should have been created (and still exist) + is( + Services.prefs.getCharPref(PREF_UPGRADE), + test.buildID, + "upgrade backup should be set" + ); + Assert.ok( + await IOUtils.exists(Paths.upgradeBackup), + "upgrade backup file has been created" + ); + + // get currently existing backups and check their count + let newBackups = await getUpgradeBackups(); + is( + newBackups.length, + maxUpgradeBackups, + "expected number of backups are present after removing old backups" + ); + + // find all backups that were created during the last call to `SessionFile.write("");` + // ie, filter out all the backups that have already been present before the call + newBackups = newBackups.filter(function (backup) { + return !backups.includes(backup); + }); + + // check that exactly one new backup was created + is(newBackups.length, 1, "one new backup was created that was not removed"); + + await SessionFile.write(""); // Second call to write() should not trigger anything + + backups = await getUpgradeBackups(); + is( + backups.length, + maxUpgradeBackups, + "second call to SessionFile.write() didn't create or remove more backups" + ); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_urlbarSearchMode.js b/src/zen/tests/mochitests/sessionstore/browser_urlbarSearchMode.js new file mode 100644 index 000000000..ffba47d98 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_urlbarSearchMode.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test makes sure that the urlbar's search mode is correctly preserved. + */ + +ChromeUtils.defineESModuleGetters(this, { + UrlbarShared: "chrome://browser/content/urlbar/UrlbarShared.mjs", +}); + +ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.scotchBonnet.enableOverride", false]], + }); + // Open the urlbar view and enter search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarShared.RESULT_SOURCE.HISTORY, + }); + + // The search mode should be in the tab state. + let state = JSON.parse(ss.getTabState(gBrowser.selectedTab)); + Assert.ok( + "searchMode" in state, + "state.searchMode is present after entering search mode" + ); + Assert.deepEqual( + state.searchMode, + { + source: UrlbarShared.RESULT_SOURCE.HISTORY, + entry: "oneoff", + isPreview: false, + }, + "state.searchMode is correct" + ); + + // Exit search mode. + await UrlbarTestUtils.exitSearchMode(window); + + // The search mode should not be in the tab state. + let newState = JSON.parse(ss.getTabState(gBrowser.selectedTab)); + Assert.ok( + !newState.searchMode, + "state.searchMode is not present after exiting search mode" + ); + + await UrlbarTestUtils.promisePopupClose(window); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_userTyped_restored_after_discard.js b/src/zen/tests/mochitests/sessionstore/browser_userTyped_restored_after_discard.js new file mode 100644 index 000000000..171197a74 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_userTyped_restored_after_discard.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function testDiscardWithNotLoadedUserTypedValue() { + let tab1 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + + // Make sure we flushed the state at least once (otherwise the fix + // for Bug 1422588 would make SessionStore.resetBrowserToLazyState + // to still store the user typed value into the tab state cache + // even when the user typed value was not yet being loading when + // the tab got discarded). + await TabStateFlusher.flush(tab1.linkedBrowser); + + tab1.linkedBrowser.userTypedValue = "mockUserTypedValue"; + + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:robots" + ); + + let waitForTabDiscarded = BrowserTestUtils.waitForEvent( + tab1, + "TabBrowserDiscarded" + ); + gBrowser.discardBrowser(tab1); + await waitForTabDiscarded; + + const promiseTabLoaded = BrowserTestUtils.browserLoaded( + tab1.linkedBrowser, + false, + "https://example.com/" + ); + await BrowserTestUtils.switchTab(gBrowser, tab1); + info("Wait for the restored tab to load https://example.com"); + await promiseTabLoaded; + is( + tab1.linkedBrowser.currentURI.spec, + "https://example.com/", + "Restored discarded tab has loaded the expected url" + ); + + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab1); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_windowRestore_perwindowpb.js b/src/zen/tests/mochitests/sessionstore/browser_windowRestore_perwindowpb.js new file mode 100644 index 000000000..73568cb34 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_windowRestore_perwindowpb.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This test checks that closed private windows can't be restored + +function test() { + waitForExplicitFinish(); + + // Purging the list of closed windows + forgetClosedWindows(); + + // Load a private window, then close it + // and verify it doesn't get remembered for restoring + whenNewWindowLoaded({ private: true }, function (win) { + info("The private window got loaded"); + win.addEventListener( + "SSWindowClosing", + function () { + executeSoon(function () { + is( + ss.getClosedWindowCount(), + 0, + "The private window should not have been stored" + ); + }); + }, + { once: true } + ); + BrowserTestUtils.closeWindow(win).then(finish); + }); +} diff --git a/src/zen/tests/mochitests/sessionstore/browser_windowStateContainer.js b/src/zen/tests/mochitests/sessionstore/browser_windowStateContainer.js new file mode 100644 index 000000000..5b0fb6393 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_windowStateContainer.js @@ -0,0 +1,168 @@ +"use strict"; + +requestLongerTimeout(2); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processCount", 1]], + }); +}); + +function promiseTabsRestored(win, nExpected) { + return new Promise(resolve => { + let nReceived = 0; + function handler() { + if (++nReceived === nExpected) { + win.gBrowser.tabContainer.removeEventListener( + "SSTabRestored", + handler, + true + ); + resolve(); + } + } + win.gBrowser.tabContainer.addEventListener("SSTabRestored", handler, true); + }); +} + +add_task(async function () { + let win = await BrowserTestUtils.openNewBrowserWindow(); + + // Create 4 tabs with different userContextId. + for (let userContextId = 1; userContextId < 5; userContextId++) { + let tab = BrowserTestUtils.addTab(win.gBrowser, "http://example.com/", { + userContextId, + }); + await promiseBrowserLoaded(tab.linkedBrowser); + await TabStateFlusher.flush(tab.linkedBrowser); + } + + // Move the default tab of window to the end. + // We want the 1st tab to have non-default userContextId, so later when we + // restore into win2 we can test restore into an existing tab with different + // userContextId. + win.gBrowser.moveTabToEnd(win.gBrowser.tabs[0]); + + let winState = ss.getWindowState(win); + + for (let i = 0; i < 4; i++) { + Assert.equal( + winState.windows[0].tabs[i].userContextId, + i + 1, + "1st Window: tabs[" + i + "].userContextId should exist." + ); + } + + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + + // Create tabs with different userContextId, but this time we create them with + // fewer tabs and with different order with win. + for (let userContextId = 3; userContextId > 0; userContextId--) { + let tab = BrowserTestUtils.addTab(win2.gBrowser, "http://example.com/", { + userContextId, + }); + await promiseBrowserLoaded(tab.linkedBrowser); + await TabStateFlusher.flush(tab.linkedBrowser); + } + + let tabsRestored = promiseTabsRestored(win2, 5); + await setWindowState(win2, winState, true); + await tabsRestored; + + for (let i = 0; i < 4; i++) { + let browser = win2.gBrowser.tabs[i].linkedBrowser; + await SpecialPowers.spawn(browser, [i + 1], async function (expectedId) { + Assert.equal( + docShell.getOriginAttributes().userContextId, + expectedId, + "The docShell has the correct userContextId" + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.userContextId, + expectedId, + "The document has the correct userContextId" + ); + }); + } + + // Test the last tab, which doesn't have userContextId. + let browser = win2.gBrowser.tabs[4].linkedBrowser; + await SpecialPowers.spawn(browser, [0], async function (expectedId) { + Assert.equal( + docShell.getOriginAttributes().userContextId, + expectedId, + "The docShell has the correct userContextId" + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.userContextId, + expectedId, + "The document has the correct userContextId" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await BrowserTestUtils.closeWindow(win2); +}); + +add_task(async function () { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await TabStateFlusher.flush(win.gBrowser.selectedBrowser); + + let tab = BrowserTestUtils.addTab(win.gBrowser, "http://example.com/", { + userContextId: 1, + }); + await promiseBrowserLoaded(tab.linkedBrowser); + await TabStateFlusher.flush(tab.linkedBrowser); + + // win should have 1 default tab, and 1 container tab. + Assert.equal(win.gBrowser.tabs.length, 2, "win should have 2 tabs"); + + let winState = ss.getWindowState(win); + + for (let i = 0; i < 2; i++) { + Assert.equal( + winState.windows[0].tabs[i].userContextId, + i, + "1st Window: tabs[" + i + "].userContextId should be " + i + ); + } + + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + + let tab2 = BrowserTestUtils.addTab(win2.gBrowser, "http://example.com/", { + userContextId: 1, + }); + await promiseBrowserLoaded(tab2.linkedBrowser); + await TabStateFlusher.flush(tab2.linkedBrowser); + + // Move the first normal tab to end, so the first tab of win2 will be a + // container tab. + win2.gBrowser.moveTabToEnd(win2.gBrowser.tabs[0]); + await TabStateFlusher.flush(win2.gBrowser.tabs[0].linkedBrowser); + + let tabsRestored = promiseTabsRestored(win2, 2); + await setWindowState(win2, winState, true); + await tabsRestored; + + for (let i = 0; i < 2; i++) { + let browser = win2.gBrowser.tabs[i].linkedBrowser; + await SpecialPowers.spawn(browser, [i], async function (expectedId) { + Assert.equal( + docShell.getOriginAttributes().userContextId, + expectedId, + "The docShell has the correct userContextId" + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.userContextId, + expectedId, + "The document has the correct userContextId" + ); + }); + } + + await BrowserTestUtils.closeWindow(win); + await BrowserTestUtils.closeWindow(win2); +}); diff --git a/src/zen/tests/mochitests/sessionstore/browser_wireframe_basic.js b/src/zen/tests/mochitests/sessionstore/browser_wireframe_basic.js new file mode 100644 index 000000000..7e35aa257 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/browser_wireframe_basic.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["test.wait300msAfterTabSwitch", true]], + }); +}); + +/** + * Ensure that document wireframes are persisted when enabled, + * and that we can generate previews for them. + */ +add_task(async function thumbnails_wireframe_basic() { + // Wireframes only works when Fission is enabled. + if (!Services.appinfo.fissionAutostart) { + ok(true, "Skipping test_wireframes when Fission is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [["browser.history.collectWireframes", true]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://www.example.com/" + ); + await TabStateFlusher.flush(tab.linkedBrowser); + info("Checking a loaded tab"); + checkWireframeForTab(tab); + + await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]); + gBrowser.discardBrowser(tab, true); + + info("Checking a discarded tab"); + checkWireframeForTab(tab); + + gBrowser.removeTab(tab); +}); + +function checkWireframeForTab(tab) { + let wireframe = PageWireframes.getWireframeState(tab); + ok(wireframe, "After load: Got wireframe state"); + Assert.greater(wireframe.rects.length, 0, "After load: Got wireframe rects"); + let wireframeElement = PageWireframes.getWireframeElementForTab(tab); + is(wireframeElement.tagName, "svg", "Got wireframe element"); +} diff --git a/src/zen/tests/mochitests/sessionstore/coopHeaderCommon.sjs b/src/zen/tests/mochitests/sessionstore/coopHeaderCommon.sjs new file mode 100644 index 000000000..be4607b2c --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/coopHeaderCommon.sjs @@ -0,0 +1,32 @@ +function handleRequest(request, response) { + let { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" + ); + let query = new URLSearchParams(request.queryString); + + response.setHeader("Cross-Origin-Opener-Policy", "same-origin", false); + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp", false); + + var fileRoot = query.get("fileRoot"); + + // Get the desired file + var file; + getObjectState("SERVER_ROOT", function (serverRoot) { + file = serverRoot.getFile(fileRoot); + }); + + // Set up the file streams to read in the file as UTF-8 + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + + fstream.init(file, -1, 0, 0); + + // Read the file + let available = fstream.available(); + let data = + available > 0 ? NetUtil.readInputStreamToString(fstream, available) : ""; + fstream.close(); + + response.write(data); +} diff --git a/src/zen/tests/mochitests/sessionstore/coop_coep.html b/src/zen/tests/mochitests/sessionstore/coop_coep.html new file mode 100644 index 000000000..9fe6f7a03 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/coop_coep.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/coop_coep.html^headers^ b/src/zen/tests/mochitests/sessionstore/coop_coep.html^headers^ new file mode 100644 index 000000000..5f8621ef8 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/coop_coep.html^headers^ @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Opener-Policy: same-origin diff --git a/src/zen/tests/mochitests/sessionstore/empty.html b/src/zen/tests/mochitests/sessionstore/empty.html new file mode 100644 index 000000000..ba0056bc3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/empty.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/empty_frame.html b/src/zen/tests/mochitests/sessionstore/empty_frame.html new file mode 100644 index 000000000..9b65f2c24 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/empty_frame.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/entry1.html b/src/zen/tests/mochitests/sessionstore/entry1.html new file mode 100644 index 000000000..ba0056bc3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/entry1.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/entry2.html b/src/zen/tests/mochitests/sessionstore/entry2.html new file mode 100644 index 000000000..ba0056bc3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/entry2.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/entry3.html b/src/zen/tests/mochitests/sessionstore/entry3.html new file mode 100644 index 000000000..ba0056bc3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/entry3.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/file_async_duplicate_tab.html b/src/zen/tests/mochitests/sessionstore/file_async_duplicate_tab.html new file mode 100644 index 000000000..76bf22127 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/file_async_duplicate_tab.html @@ -0,0 +1 @@ +clickme diff --git a/src/zen/tests/mochitests/sessionstore/file_async_flushes.html b/src/zen/tests/mochitests/sessionstore/file_async_flushes.html new file mode 100644 index 000000000..76bf22127 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/file_async_flushes.html @@ -0,0 +1 @@ +clickme diff --git a/src/zen/tests/mochitests/sessionstore/file_formdata_password.html b/src/zen/tests/mochitests/sessionstore/file_formdata_password.html new file mode 100644 index 000000000..0f072c31e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/file_formdata_password.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/file_sessionHistory_hashchange.html b/src/zen/tests/mochitests/sessionstore/file_sessionHistory_hashchange.html new file mode 100644 index 000000000..3cbe3e353 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/file_sessionHistory_hashchange.html @@ -0,0 +1 @@ +clickme diff --git a/src/zen/tests/mochitests/sessionstore/head.js b/src/zen/tests/mochitests/sessionstore/head.js new file mode 100644 index 000000000..78f777a42 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/head.js @@ -0,0 +1,680 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL; + +const TAB_STATE_NEEDS_RESTORE = 1; +const TAB_STATE_RESTORING = 2; + +const ROOT = getRootDirectory(gTestPath); +const HTTPROOT = ROOT.replace( + "chrome://mochitests/content/", + "http://example.com/" +); +const HTTPSROOT = ROOT.replace( + "chrome://mochitests/content/", + "https://example.com/" +); + +const { SessionSaver } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionSaver.sys.mjs" +); +const { SessionFile } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionFile.sys.mjs" +); +const { TabState } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabState.sys.mjs" +); +const { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" +); +const { SessionStoreTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SessionStoreTestUtils.sys.mjs" +); + +const { PageWireframes } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/PageWireframes.sys.mjs" +); + +const ss = SessionStore; +SessionStoreTestUtils.init(this, window); + +// Some tests here assume that all restored tabs are loaded without waiting for +// the user to bring them to the foreground. We ensure this by resetting the +// related preference (see the "firefox.js" defaults file for details). +Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false); +registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand"); +}); + +// Obtain access to internals +Services.prefs.setBoolPref("browser.sessionstore.debug", true); +registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.sessionstore.debug"); +}); + +// This kicks off the search service used on about:home and allows the +// session restore tests to be run standalone without triggering errors. +Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs; + +function provideWindow(aCallback, aURL, aFeatures) { + function callbackSoon(aWindow) { + executeSoon(function executeCallbackSoon() { + aCallback(aWindow); + }); + } + + let win = openDialog( + AppConstants.BROWSER_CHROME_URL, + "", + aFeatures || "chrome,all,dialog=no", + aURL || "about:blank" + ); + whenWindowLoaded(win, function onWindowLoaded(aWin) { + if (!aURL) { + info("Loaded a blank window."); + callbackSoon(aWin); + return; + } + + aWin.gBrowser.selectedBrowser.addEventListener( + "load", + function () { + callbackSoon(aWin); + }, + { capture: true, once: true } + ); + }); +} + +// This assumes that tests will at least have some state/entries +function waitForBrowserState(aState, aSetStateCallback) { + return SessionStoreTestUtils.waitForBrowserState(aState, aSetStateCallback); +} + +function promiseBrowserState(aState) { + return SessionStoreTestUtils.promiseBrowserState(aState); +} + +function promiseTabState(tab, state) { + if (typeof state != "string") { + state = JSON.stringify(state); + } + + let promise = promiseTabRestored(tab); + ss.setTabState(tab, state); + return promise; +} + +function promiseWindowRestoring(win) { + return new Promise(resolve => + win.addEventListener("SSWindowRestoring", resolve, { once: true }) + ); +} + +function promiseWindowRestored(win) { + return new Promise(resolve => + win.addEventListener("SSWindowRestored", resolve, { once: true }) + ); +} + +async function setBrowserState(state, win = window) { + ss.setBrowserState(typeof state != "string" ? JSON.stringify(state) : state); + await promiseWindowRestored(win); +} + +async function setWindowState(win, state, overwrite = false) { + ss.setWindowState( + win, + typeof state != "string" ? JSON.stringify(state) : state, + overwrite + ); + await promiseWindowRestored(win); +} + +function waitForTopic(aTopic, aTimeout, aCallback) { + let observing = false; + function removeObserver() { + if (!observing) { + return; + } + Services.obs.removeObserver(observer, aTopic); + observing = false; + } + + let timeout = setTimeout(function () { + removeObserver(); + aCallback(false); + }, aTimeout); + + function observer() { + removeObserver(); + timeout = clearTimeout(timeout); + executeSoon(() => aCallback(true)); + } + + registerCleanupFunction(function () { + removeObserver(); + if (timeout) { + clearTimeout(timeout); + } + }); + + observing = true; + Services.obs.addObserver(observer, aTopic); +} + +/** + * Wait until session restore has finished collecting its data and is + * has written that data ("sessionstore-state-write-complete"). + * + * @param {function} aCallback If sessionstore-state-write-complete is sent + * within buffering interval + 100 ms, the callback is passed |true|, + * otherwise, it is passed |false|. + */ +function waitForSaveState(aCallback) { + let timeout = + 100 + Services.prefs.getIntPref("browser.sessionstore.interval"); + return waitForTopic("sessionstore-state-write-complete", timeout, aCallback); +} +function promiseSaveState() { + return new Promise((resolve, reject) => { + waitForSaveState(isSuccessful => { + if (!isSuccessful) { + reject(new Error("Save state timeout")); + } else { + resolve(); + } + }); + }); +} +function forceSaveState() { + return SessionSaver.run(); +} + +function promiseRecoveryFileContents() { + let promise = forceSaveState(); + return promise.then(function () { + return IOUtils.readUTF8(SessionFile.Paths.recovery, { + decompress: true, + }); + }); +} + +var promiseForEachSessionRestoreFile = async function (cb) { + for (let key of SessionFile.Paths.loadOrder) { + let data = ""; + try { + data = await IOUtils.readUTF8(SessionFile.Paths[key], { + decompress: true, + }); + } catch (ex) { + // Ignore missing files + if (!(DOMException.isInstance(ex) && ex.name == "NotFoundError")) { + throw ex; + } + } + cb(data, key); + } +}; + +function promiseBrowserLoaded( + aBrowser, + ignoreSubFrames = true, + wantLoad = null +) { + return BrowserTestUtils.browserLoaded(aBrowser, !ignoreSubFrames, wantLoad); +} + +function whenWindowLoaded(aWindow, aCallback) { + aWindow.addEventListener( + "load", + function () { + executeSoon(function executeWhenWindowLoaded() { + aCallback(aWindow); + }); + }, + { once: true } + ); +} +function promiseWindowLoaded(aWindow) { + return new Promise(resolve => whenWindowLoaded(aWindow, resolve)); +} + +var gUniqueCounter = 0; +function r() { + return Date.now() + "-" + ++gUniqueCounter; +} + +function* BrowserWindowIterator() { + for (let currentWindow of Services.wm.getEnumerator("navigator:browser")) { + if (!currentWindow.closed) { + yield currentWindow; + } + } +} + +var gWebProgressListener = { + _callback: null, + + setCallback(aCallback) { + if (!this._callback) { + window.gBrowser.addTabsProgressListener(this); + } + this._callback = aCallback; + }, + + unsetCallback() { + if (this._callback) { + this._callback = null; + window.gBrowser.removeTabsProgressListener(this); + } + }, + + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, _aStatus) { + if ( + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW + ) { + this._callback(aBrowser); + } + }, +}; + +registerCleanupFunction(function () { + gWebProgressListener.unsetCallback(); +}); + +var gProgressListener = { + _callback: null, + + setCallback(callback) { + Services.obs.addObserver(this, "sessionstore-debug-tab-restored"); + this._callback = callback; + }, + + unsetCallback() { + if (this._callback) { + this._callback = null; + Services.obs.removeObserver(this, "sessionstore-debug-tab-restored"); + } + }, + + observe(browser) { + gProgressListener.onRestored(browser); + }, + + onRestored(browser) { + if (ss.getInternalObjectState(browser) == TAB_STATE_RESTORING) { + let args = [browser].concat(gProgressListener._countTabs()); + gProgressListener._callback.apply(gProgressListener, args); + } + }, + + _countTabs() { + let needsRestore = 0, + isRestoring = 0, + wasRestored = 0; + + for (let win of BrowserWindowIterator()) { + for (let i = 0; i < win.gBrowser.tabs.length; i++) { + let browser = win.gBrowser.tabs[i].linkedBrowser; + let state = ss.getInternalObjectState(browser); + if (browser.isConnected && !state) { + wasRestored++; + } else if (state == TAB_STATE_RESTORING) { + isRestoring++; + } else if (state == TAB_STATE_NEEDS_RESTORE || !browser.isConnected) { + needsRestore++; + } + } + } + return [needsRestore, isRestoring, wasRestored]; + }, +}; + +registerCleanupFunction(function () { + gProgressListener.unsetCallback(); +}); + +// Close all but our primary window. +function promiseAllButPrimaryWindowClosed() { + let windows = []; + for (let win of BrowserWindowIterator()) { + if (win != window) { + windows.push(win); + } + } + + return Promise.all(windows.map(BrowserTestUtils.closeWindow)); +} + +// Forget all closed windows. +function forgetClosedWindows() { + while (ss.getClosedWindowCount() > 0) { + ss.forgetClosedWindow(0); + } +} + +// Forget all closed tabs for a window +function forgetClosedTabs(win) { + const closedTabCount = ss.getClosedTabCountForWindow(win); + for (let i = 0; i < closedTabCount; i++) { + try { + ss.forgetClosedTab(win, 0); + } catch (err) { + // This will fail if there are tab groups in here + } + } +} + +function forgetSavedTabGroups() { + const tabGroups = ss.getSavedTabGroups(); + tabGroups.forEach(tabGroup => ss.forgetSavedTabGroup(tabGroup.id)); +} + +function forgetClosedTabGroups(win) { + const tabGroups = ss.getClosedTabGroups(win); + tabGroups.forEach(tabGroup => ss.forgetClosedTabGroup(win, tabGroup.id)); +} + +/** + * When opening a new window it is not sufficient to wait for its load event. + * We need to use whenDelayedStartupFinshed() here as the browser window's + * delayedStartup() routine is executed one tick after the window's load event + * has been dispatched. browser-delayed-startup-finished might be deferred even + * further if parts of the window's initialization process take more time than + * expected (e.g. reading a big session state from disk). + */ +function whenNewWindowLoaded(aOptions, aCallback) { + let features = ""; + let url = "about:blank"; + + if ((aOptions && aOptions.private) || false) { + features = ",private"; + url = "about:privatebrowsing"; + } + + let win = openDialog( + AppConstants.BROWSER_CHROME_URL, + "", + "chrome,all,dialog=no" + features, + url + ); + let delayedStartup = promiseDelayedStartupFinished(win); + + let browserLoaded = new Promise(resolve => { + if (url == "about:blank") { + resolve(); + return; + } + + win.addEventListener( + "load", + function () { + let browser = win.gBrowser.selectedBrowser; + promiseBrowserLoaded(browser).then(resolve); + }, + { once: true } + ); + }); + + Promise.all([delayedStartup, browserLoaded]).then(() => aCallback(win)); +} +function promiseNewWindowLoaded(aOptions) { + return new Promise(resolve => whenNewWindowLoaded(aOptions, resolve)); +} + +/** + * This waits for the browser-delayed-startup-finished notification of a given + * window. It indicates that the windows has loaded completely and is ready to + * be used for testing. + */ +function whenDelayedStartupFinished(aWindow, aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + executeSoon(aCallback); + } + }, "browser-delayed-startup-finished"); +} +function promiseDelayedStartupFinished(aWindow) { + return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve)); +} + +function promiseTabRestored(tab) { + return BrowserTestUtils.waitForEvent(tab, "SSTabRestored"); +} + +function promiseTabRestoring(tab) { + return BrowserTestUtils.waitForEvent(tab, "SSTabRestoring"); +} + +// Removes the given tab immediately and returns a promise that resolves when +// all pending status updates (messages) of the closing tab have been received. +function promiseRemoveTabAndSessionState(tab) { + let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab); + BrowserTestUtils.removeTab(tab); + return sessionUpdatePromise; +} + +// Write DOMSessionStorage data to the given browser. +function modifySessionStorage(browser, storageData, storageOptions = {}) { + let browsingContext = browser.browsingContext; + if (storageOptions && "frameIndex" in storageOptions) { + browsingContext = browsingContext.children[storageOptions.frameIndex]; + } + + return SpecialPowers.spawn( + browsingContext, + [[storageData, storageOptions]], + async function ([data]) { + let frame = content; + let keys = new Set(Object.keys(data)); + let isClearing = !keys.size; + let storage = frame.sessionStorage; + + return new Promise(resolve => { + docShell.chromeEventHandler.addEventListener( + "MozSessionStorageChanged", + function onStorageChanged(event) { + if (event.storageArea == storage) { + keys.delete(event.key); + } + + if (keys.size == 0) { + docShell.chromeEventHandler.removeEventListener( + "MozSessionStorageChanged", + onStorageChanged, + true + ); + resolve(); + } + }, + true + ); + + if (isClearing) { + storage.clear(); + } else { + for (let key of keys) { + frame.sessionStorage[key] = data[key]; + } + } + }); + } + ); +} + +function pushPrefs(...aPrefs) { + return SpecialPowers.pushPrefEnv({ set: aPrefs }); +} + +function popPrefs() { + return SpecialPowers.popPrefEnv(); +} + +function setScrollPosition(bc, x, y) { + return SpecialPowers.spawn(bc, [x, y], (childX, childY) => { + return new Promise(resolve => { + content.addEventListener( + "mozvisualscroll", + function onScroll(event) { + if (content.document.documentGlobal.visualViewport == event.target) { + content.removeEventListener("mozvisualscroll", onScroll, { + mozSystemGroup: true, + }); + resolve(); + } + }, + { mozSystemGroup: true } + ); + content.scrollTo(childX, childY); + }); + }); +} + +async function checkScroll(tab, expected, msg) { + let browser = tab.linkedBrowser; + await TabStateFlusher.flush(browser); + + let scroll = JSON.parse(ss.getTabState(tab)).scroll || null; + is(JSON.stringify(scroll), JSON.stringify(expected), msg); +} + +function whenDomWindowClosedHandled(aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic) { + Services.obs.removeObserver(observer, aTopic); + aCallback(); + }, "sessionstore-debug-domwindowclosed-handled"); +} + +function getPropertyOfFormField(browserContext, selector, propName) { + return SpecialPowers.spawn( + browserContext, + [selector, propName], + (selectorChild, propNameChild) => { + return content.document.querySelector(selectorChild)[propNameChild]; + } + ); +} + +function setPropertyOfFormField(browserContext, selector, propName, newValue) { + return SpecialPowers.spawn( + browserContext, + [selector, propName, newValue], + (selectorChild, propNameChild, newValueChild) => { + let node = content.document.querySelector(selectorChild); + node[propNameChild] = newValueChild; + + let event = node.ownerDocument.createEvent("UIEvents"); + event.initUIEvent("input", true, true, node.documentGlobal, 0); + node.dispatchEvent(event); + } + ); +} + +function promiseOnHistoryReplaceEntry(browser) { + return new Promise((resolve, reject) => { + let sessionHistory = browser.browsingContext?.sessionHistory; + if (!sessionHistory) { + reject("No session history?"); + } + + var historyListener = { + OnHistoryNewEntry() {}, + OnHistoryGotoIndex() {}, + OnHistoryPurge() {}, + OnHistoryReload() { + return true; + }, + + OnHistoryReplaceEntry() { + resolve(); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + + sessionHistory.addSHistoryListener(historyListener); + }); +} + +function loadTestSubscript(filePath) { + Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this); +} + +function addCoopTask(aFile, aTest, aUrlRoot) { + async function taskToBeAdded() { + info(`File ${aFile} has COOP headers enabled`); + let filePath = `browser/browser/components/sessionstore/test/${aFile}`; + let url = aUrlRoot + `coopHeaderCommon.sjs?fileRoot=${filePath}`; + await aTest(url); + } + Object.defineProperty(taskToBeAdded, "name", { value: aTest.name }); + add_task(taskToBeAdded); +} + +function addNonCoopTask(aFile, aTest, aUrlRoot) { + async function taskToBeAdded() { + await aTest(aUrlRoot + aFile); + } + Object.defineProperty(taskToBeAdded, "name", { value: aTest.name }); + add_task(taskToBeAdded); +} + +function openAndCloseTab(window, url) { + return SessionStoreTestUtils.openAndCloseTab(window, url); +} + +/** + * This is regrettable, but when `promiseBrowserState` resolves, we're still + * midway through loading the tabs. To avoid race conditions in URLs for tabs + * being available, wait for all the loads to finish: + */ +function promiseSessionStoreLoads(numberOfLoads) { + let loadsSeen = 0; + return new Promise(resolve => { + Services.obs.addObserver(function obs(browser) { + loadsSeen++; + if (loadsSeen == numberOfLoads) { + resolve(); + } + // The typeof check is here to avoid one test messing with everything else by + // keeping the observer indefinitely. + if (typeof info == "undefined" || loadsSeen >= numberOfLoads) { + Services.obs.removeObserver(obs, "sessionstore-debug-tab-restored"); + } + info("Saw load for " + browser.currentURI.spec); + }, "sessionstore-debug-tab-restored"); + }); +} + +function triggerClickOn(target, options) { + let promise = BrowserTestUtils.waitForEvent(target, "click"); + if (AppConstants.platform == "macosx") { + options.metaKey = options.ctrlKey; + delete options.ctrlKey; + } + EventUtils.synthesizeMouseAtCenter(target, options); + return promise; +} + +async function openTabMenuFor(tab) { + let tabMenu = tab.ownerDocument.getElementById("tabContextMenu"); + + let tabMenuShown = BrowserTestUtils.waitForEvent(tabMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + tab, + { type: "contextmenu" }, + tab.documentGlobal + ); + await tabMenuShown; + + return tabMenu; +} diff --git a/src/zen/tests/mochitests/sessionstore/marionette/manifest.toml b/src/zen/tests/mochitests/sessionstore/marionette/manifest.toml new file mode 100644 index 000000000..e66c364ca --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/manifest.toml @@ -0,0 +1,65 @@ +[DEFAULT] +subsuite = "integration" +tags = "local os_integration" + +["test_log_files.py"] + +["test_new_tab_on_restore.py"] +skip-if = [ + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11'", # Bug 2054369 +] + +["test_persist_closed_tabs_restore_manually.py"] + +["test_restore_aiwindow.py"] + +["test_restore_loading_tab.py"] + +["test_restore_manually.py"] +skip-if = [ + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11'", # Bug 1968578 +] + +["test_restore_manually_with_pinned_tabs.py"] + +["test_restore_manually_with_tab_groups.py"] + +["test_restore_sidebar.py"] + +["test_restore_sidebar_automatic.py"] + +["test_restore_windows_after_close_last_tabs.py"] +skip-if = [ + "os == 'mac' && os_version == '10.15' && arch == 'x86_64'", + "os == 'mac' && os_version == '14.70' && arch == 'x86_64'", + "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", +] + +["test_restore_windows_after_restart_and_quit.py"] + +["test_restore_windows_after_windows_shutdown.py"] +run-if = [ + "os == 'win'", +] +skip-if = [ + "os == 'win' && os_version == '11.26100' && arch == 'x86'", # Bug 1727691 + "os == 'win' && os_version == '11.26100' && arch == 'x86_64'", # Bug 1727691 + "os == 'win' && os_version == '11.26200' && arch == 'x86'", # Bug 1727691 + "os == 'win' && os_version == '11.26200' && arch == 'x86_64'", # Bug 1727691 +] + +["test_splitview_restore.py"] + +["test_tabgroups_restore.py"] + +["test_tabnotes_canonicalurl_restore.py"] + +["test_taskbartab_restore.py"] +run-if = [ + "os == 'win'", +] + +["test_taskbartab_sessionstate.py"] +run-if = [ + "os == 'win'", +] diff --git a/src/zen/tests/mochitests/sessionstore/marionette/session_store_test_case.py b/src/zen/tests/mochitests/sessionstore/marionette/session_store_test_case.py new file mode 100644 index 000000000..8c76be10e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/session_store_test_case.py @@ -0,0 +1,473 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from urllib.parse import quote + +from marionette_driver import Wait, errors +from marionette_driver.keys import Keys +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +def inline(doc): + return f"data:text/html;charset=utf-8,{quote(doc)}" + + +# Each list element represents a window of tabs loaded at +# some testing URL +DEFAULT_WINDOWS = set([ + # Window 1. Note the comma after the inline call - + # this is Python's way of declaring a 1 item tuple. + (inline("""Lorem"""),), + # Window 2 + ( + inline("""ipsum"""), + inline("""dolor"""), + ), + # Window 3 + ( + inline("""sit"""), + inline("""amet"""), + ), +]) + + +class SessionStoreTestCase(WindowManagerMixin, MarionetteTestCase): + def setUp( + self, + startup_page=1, + include_private=True, + restore_on_demand=False, + no_auto_updates=True, + win_register_restart=False, + test_windows=DEFAULT_WINDOWS, + taskbartabs_enable=False, + ): + super().setUp() + self.marionette.set_context("chrome") + + platform = self.marionette.session_capabilities["platformName"] + self.accelKey = Keys.META if platform == "mac" else Keys.CONTROL + + self.test_windows = test_windows + + self.private_windows = set([ + ( + inline("""consectetur"""), + inline("""ipsum"""), + ), + ( + inline("""adipiscing"""), + inline("""consectetur"""), + ), + ]) + + self.marionette.enforce_gecko_prefs({ + # Set browser restore previous session pref, + # depending on what the test requires. + "browser.startup.page": startup_page, + # Make the content load right away instead of waiting for + # the user to click on the background tabs + "browser.sessionstore.restore_on_demand": restore_on_demand, + # Avoid race conditions by having the content process never + # send us session updates unless the parent has explicitly asked + # for them via the TabStateFlusher. + "browser.sessionstore.debug.no_auto_updates": no_auto_updates, + # Whether to enable the register application restart mechanism. + "toolkit.winRegisterApplicationRestart": win_register_restart, + # Whether to enable taskbar tabs for this test + "browser.taskbarTabs.enabled": taskbartabs_enable, + }) + + self.all_windows = self.test_windows.copy() + self.open_windows(self.test_windows) + + if include_private: + self.all_windows.update(self.private_windows) + self.open_windows(self.private_windows, is_private=True) + + def tearDown(self): + try: + # Create a fresh profile for subsequent tests. + self.marionette.restart(in_app=False, clean=True) + finally: + super().tearDown() + + def open_windows(self, window_sets, is_private=False): + """Open a set of windows with tabs pointing at some URLs. + + @param window_sets (list) + A set of URL tuples. Each tuple within window_sets + represents a window, and each URL in the URL + tuples represents what will be loaded in a tab. + + Note that if is_private is False, then the first + URL tuple will be opened in the current window, and + subequent tuples will be opened in new windows. + + Example: + + set( + (self.marionette.absolute_url('layout/mozilla_1.html'), + self.marionette.absolute_url('layout/mozilla_2.html')), + + (self.marionette.absolute_url('layout/mozilla_3.html'), + self.marionette.absolute_url('layout/mozilla_4.html')), + ) + + This would take the currently open window, and load + mozilla_1.html and mozilla_2.html in new tabs. It would + then open a new, second window, and load tabs at + mozilla_3.html and mozilla_4.html. + @param is_private (boolean, optional) + Whether or not any new windows should be a private browsing + windows. + """ + if is_private: + win = self.open_window(private=True) + self.marionette.switch_to_window(win) + else: + win = self.marionette.current_chrome_window_handle + + for index, urls in enumerate(window_sets): + if index > 0: + win = self.open_window(private=is_private) + self.marionette.switch_to_window(win) + self.open_tabs(win, urls) + + # Open a Firefox web app (taskbar tab) window + def open_taskbartab_window(self): + self.marionette.execute_async_script( + """ + let [resolve] = arguments; + (async () => { + let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag2 + ); + extraOptions.setPropertyAsBool("taskbartab", true); + + let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + args.appendElement(null); + args.appendElement(extraOptions); + args.appendElement(null); + + // Simulate opening a taskbar tab window + let win = Services.ww.openWindow( + null, + AppConstants.BROWSER_CHROME_URL, + "_blank", + "chrome,dialog=no,titlebar,close,toolbar,location,personalbar=no,status,menubar=no,resizable,minimizable,scrollbars", + args + ); + await new Promise(resolve => { + win.addEventListener("load", resolve, { once: true }); + }); + await win.delayedStartupPromise; + })().then(resolve); + """ + ) + + # Helper function for taskbar tabs tests, opens a taskbar tab window, + # closes the regular window, and reopens another regular window. + # Firefox will then be in a "ready to restore" state + def setup_taskbartab_restore_scenario(self): + self.open_taskbartab_window() + taskbar_tab_window_handle = self.marionette.close_chrome_window()[0] + self.marionette.switch_to_window(taskbar_tab_window_handle) + self.marionette.open(type="window") + + def open_tabs(self, win, urls): + """Open a set of URLs inside a window in new tabs. + + @param win (browser window) + The browser window to load the tabs in. + @param urls (tuple) + A tuple of URLs to load in this window. The + first URL will be loaded in the currently selected + browser tab. Subsequent URLs will be loaded in + new tabs. + """ + # If there are any remaining URLs for this window, + # open some new tabs and navigate to them. + with self.marionette.using_context("content"): + if isinstance(urls, str): + self.marionette.navigate(urls) + else: + for index, url in enumerate(urls): + if index > 0: + tab = self.open_tab() + self.marionette.switch_to_window(tab) + self.marionette.navigate(url) + + def wait_for_windows(self, expected_windows, message, timeout=5): + current_windows = None + + def check(_): + nonlocal current_windows + current_windows = self.convert_open_windows_to_set() + return current_windows == expected_windows + + try: + wait = Wait(self.marionette, timeout=timeout, interval=0.1) + wait.until(check, message=message) + except errors.TimeoutException as e: + # Update the message to include the most recent list of windows + message = ( + f"{e.message}. Expected {expected_windows}, got {current_windows}." + ) + raise errors.TimeoutException(message) + + def get_urls_for_window(self, win): + orig_handle = self.marionette.current_chrome_window_handle + + try: + with self.marionette.using_context("chrome"): + self.marionette.switch_to_window(win) + return self.marionette.execute_script( + """ + return gBrowser.tabs.map(tab => { + return tab.linkedBrowser.currentURI.spec; + }); + """ + ) + finally: + self.marionette.switch_to_window(orig_handle) + + def convert_open_windows_to_set(self): + # There's no guarantee that Marionette will return us an + # iterator for the opened windows that will match the + # order within our window list. Instead, we'll convert + # the list of URLs within each open window to a set of + # tuples that will allow us to do a direct comparison + # while allowing the windows to be in any order. + opened_windows = set() + for win in self.marionette.chrome_window_handles: + urls = tuple(self.get_urls_for_window(win)) + opened_windows.add(urls) + + return opened_windows + + def _close_window(self): + """Use as a callback to `marionette.quit` in order to close the + browser window. + + `marionette.close`/`marionette.close_chrome_window` cannot + be used alone because they don't allow closing the last window. + """ + + self.marionette.execute_script("window.close()") + + def close_all_tabs_and_restart(self): + self.close_all_tabs() + self.marionette.quit(callback=self._close_window) + self.marionette.start_session() + + def simulate_os_shutdown(self): + """Simulate an OS shutdown. + + :raises: Exception: if not supported on the current platform + :raises: WindowsError: if a Windows API call failed + """ + if self.marionette.session_capabilities["platformName"] != "windows": + raise Exception("Unsupported platform for simulate_os_shutdown") + + self._shutdown_with_windows_restart_manager(self.marionette.process_id) + + def _shutdown_with_windows_restart_manager(self, pid): + """Shut down a process using the Windows Restart Manager. + + When Windows shuts down, it uses a protocol including the + WM_QUERYENDSESSION and WM_ENDSESSION messages to give + applications a chance to shut down safely. The best way to + simulate this is via the Restart Manager, which allows a process + (such as an installer) to use the same mechanism to shut down + any other processes which are using registered resources. + + This function starts a Restart Manager session, registers the + process as a resource, and shuts down the process. + + :param pid: The process id (int) of the process to shutdown + + :raises: WindowsError: if a Windows API call fails + """ + import ctypes + from ctypes import POINTER, WINFUNCTYPE, Structure, WinError, pointer, windll + from ctypes.wintypes import BOOL, DWORD, HANDLE, LPCWSTR, UINT, ULONG, WCHAR + + # set up Windows SDK types + OpenProcess = windll.kernel32.OpenProcess + OpenProcess.restype = HANDLE + OpenProcess.argtypes = [ + DWORD, # dwDesiredAccess + BOOL, # bInheritHandle + DWORD, + ] # dwProcessId + PROCESS_QUERY_INFORMATION = 0x0400 + + class FILETIME(Structure): + _fields_ = [("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD)] + + LPFILETIME = POINTER(FILETIME) + + GetProcessTimes = windll.kernel32.GetProcessTimes + GetProcessTimes.restype = BOOL + GetProcessTimes.argtypes = [ + HANDLE, # hProcess + LPFILETIME, # lpCreationTime + LPFILETIME, # lpExitTime + LPFILETIME, # lpKernelTime + LPFILETIME, + ] # lpUserTime + + ERROR_SUCCESS = 0 + + class RM_UNIQUE_PROCESS(Structure): + _fields_ = [("dwProcessId", DWORD), ("ProcessStartTime", FILETIME)] + + RmStartSession = windll.rstrtmgr.RmStartSession + RmStartSession.restype = DWORD + RmStartSession.argtypes = [ + POINTER(DWORD), # pSessionHandle + DWORD, # dwSessionFlags + POINTER(WCHAR), + ] # strSessionKey + + class GUID(ctypes.Structure): + _fields_ = [ + ("Data1", ctypes.c_ulong), + ("Data2", ctypes.c_ushort), + ("Data3", ctypes.c_ushort), + ("Data4", ctypes.c_ubyte * 8), + ] + + CCH_RM_SESSION_KEY = ctypes.sizeof(GUID) * 2 + + RmRegisterResources = windll.rstrtmgr.RmRegisterResources + RmRegisterResources.restype = DWORD + RmRegisterResources.argtypes = [ + DWORD, # dwSessionHandle + UINT, # nFiles + POINTER(LPCWSTR), # rgsFilenames + UINT, # nApplications + POINTER(RM_UNIQUE_PROCESS), # rgApplications + UINT, # nServices + POINTER(LPCWSTR), + ] # rgsServiceNames + + RM_WRITE_STATUS_CALLBACK = WINFUNCTYPE(None, UINT) + RmShutdown = windll.rstrtmgr.RmShutdown + RmShutdown.restype = DWORD + RmShutdown.argtypes = [ + DWORD, # dwSessionHandle + ULONG, # lActionFlags + RM_WRITE_STATUS_CALLBACK, + ] # fnStatus + + RmEndSession = windll.rstrtmgr.RmEndSession + RmEndSession.restype = DWORD + RmEndSession.argtypes = [DWORD] # dwSessionHandle + + # Get the info needed to uniquely identify the process + hProc = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid) + if not hProc: + raise WinError() + + creationTime = FILETIME() + exitTime = FILETIME() + kernelTime = FILETIME() + userTime = FILETIME() + if not GetProcessTimes( + hProc, + pointer(creationTime), + pointer(exitTime), + pointer(kernelTime), + pointer(userTime), + ): + raise WinError() + + # Start the Restart Manager Session + dwSessionHandle = DWORD() + sessionKeyType = WCHAR * (CCH_RM_SESSION_KEY + 1) + sessionKey = sessionKeyType() + if RmStartSession(pointer(dwSessionHandle), 0, sessionKey) != ERROR_SUCCESS: + raise WinError() + + try: + UProcs_count = 1 + UProcsArrayType = RM_UNIQUE_PROCESS * UProcs_count + UProcs = UProcsArrayType(RM_UNIQUE_PROCESS(pid, creationTime)) + + # Register the process as a resource + if ( + RmRegisterResources( + dwSessionHandle, 0, None, UProcs_count, UProcs, 0, None + ) + != ERROR_SUCCESS + ): + raise WinError() + + # Shut down all processes using registered resources + if ( + RmShutdown( + dwSessionHandle, 0, ctypes.cast(None, RM_WRITE_STATUS_CALLBACK) + ) + != ERROR_SUCCESS + ): + raise WinError() + + finally: + RmEndSession(dwSessionHandle) + + def windows_shutdown_with_variety(self, restart_by_os, expect_restore): + """Test restoring windows after Windows shutdown. + + Opens a set of windows, both standard and private, with + some number of tabs in them. Once the tabs have loaded, shuts down + the browser with the Windows Restart Manager and restarts the browser. + + This specifically exercises the Windows synchronous shutdown mechanism, + which terminates the process in response to the Restart Manager's + WM_ENDSESSION message. + + If restart_by_os is True, the -os-restarted arg is passed when restarting, + simulating being automatically restarted by the Restart Manager. + + If expect_restore is True, this ensures that the standard tabs have been + restored, and that the private ones have not. Otherwise it ensures that + no tabs and windows have been restored. + """ + current_windows_set = self.convert_open_windows_to_set() + self.assertEqual( + current_windows_set, + self.all_windows, + msg=f"Not all requested windows have been opened. Expected {self.all_windows}, got {current_windows_set}.", + ) + + self.marionette.quit(callback=self.simulate_os_shutdown) + + saved_args = self.marionette.instance.app_args + try: + if restart_by_os: + self.marionette.instance.app_args = ["-os-restarted"] + + self.marionette.start_session() + self.marionette.set_context("chrome") + finally: + self.marionette.instance.app_args = saved_args + + if expect_restore: + self.wait_for_windows( + self.test_windows, + "Non private browsing windows should have been restored", + ) + else: + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Windows from last session shouldn`t have been restored.", + ) + self.assertEqual( + len(self.marionette.window_handles), + 1, + msg="Tabs from last session shouldn`t have been restored.", + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_log_files.py b/src/zen/tests/mochitests/sessionstore/marionette/test_log_files.py new file mode 100644 index 000000000..070fb252f --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_log_files.py @@ -0,0 +1,176 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +from urllib.parse import quote + +from marionette_driver import Wait, errors +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +def inline(doc): + return f"data:text/html;charset=utf-8,{quote(doc)}" + + +class TestSessionRestoreLogging(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super().setUp() + self.marionette.enforce_gecko_prefs({ + "browser.sessionstore.loglevel": "Debug", + "browser.sessionstore.log.appender.file.logOnSuccess": True, + }) + + def tearDown(self): + try: + # Create a fresh profile for subsequent tests. + self.marionette.restart(in_app=False, clean=True) + finally: + super().tearDown() + + def getSessionFilePath(self): + profilePath = self.marionette.instance.profile.profile + assert profilePath is not None + return os.path.join(profilePath, "sessionstore.jsonlz4") + + def getLogDirectoryPath(self): + profilePath = self.marionette.instance.profile.profile + assert profilePath is not None + return os.path.join(profilePath, "sessionstore-logs") + + def cleanupLogDirectory(self): + dirPath = self.getLogDirectoryPath() + for fname in self.getLogFiles(): + fpath = os.path.join(dirPath, fname) + os.remove(fpath) + + def getLogFiles(self, matchstr=""): + dirPath = self.getLogDirectoryPath() + fileNames = [] + if os.path.isdir(dirPath): + fileNames = [entry.name for entry in os.scandir(dirPath) if entry.is_file()] + # Optionally filter by substring + if len(matchstr) > 0 and len(fileNames) > 0: + fileNames = filter(lambda name: matchstr in name, fileNames) + return fileNames + + def getMostRecentLogFile(self, matchstr=""): + dirPath = self.getLogDirectoryPath() + # Build full paths + files = [os.path.join(dirPath, f) for f in self.getLogFiles(matchstr)] + # Get the newest file by creation time + return max(files, key=os.path.getctime) if len(files) > 0 else None + + def openAndCloseSaveworthyWindow(self): + urls = ( + inline("""ipsum"""), + inline("""dolor"""), + ) + self.marionette.set_context("chrome") + origWindow = self.marionette.current_window_handle + win = self.open_window(private=False) + self.marionette.switch_to_window(win) + with self.marionette.using_context("content"): + for index, url in enumerate(urls): + if index > 0: + tab = self.open_tab() + self.marionette.switch_to_window(tab) + self.marionette.navigate(url) + self.marionette.execute_script("window.close()") + self.marionette.switch_to_window(origWindow) + + def getLineCount(self, logFile): + with open(logFile) as f: + return sum(1 for _ in f) + + def test_per_startup_logfile_creation(self): + self.marionette.quit() + self.cleanupLogDirectory() + self.marionette.start_session() + self.marionette.set_context("chrome") + self.marionette.quit() + + # There were no errors so we just expect the success log file to have been created + self.assertEqual( + len(self.getLogFiles()), + 1, + "Exactly one log file was created after a restart and quit", + ) + self.marionette.start_session() + + def test_errors_flush_to_disk(self): + self.marionette.enforce_gecko_prefs({ + "browser.sessionstore.log.appender.file.logOnSuccess": False, + }) + self.marionette.quit() + sessionFile = self.getSessionFilePath() + self.assertTrue( + os.path.isfile(sessionFile), + "The sessionstore.jsonlz4 file exists in the profile directory", + ) + # replace the contents with nonsense so we get a not-readable error on startup + with open(sessionFile, "wb") as f: + f.write(b"\x00\xff\xabgarbageDATA") + + self.marionette.start_session() + self.marionette.set_context("chrome") + + errorLogFile = self.getMostRecentLogFile("error-") + self.assertTrue(errorLogFile, "We logged errors immediately") + + if os.path.isfile(sessionFile): + os.remove(sessionFile) + + def test_periodic_flush_to_disk(self): + # enable debug logging and verify that log messages are batched + # and only flush when the interval has passed + logFile = self.getMostRecentLogFile() + logFileCount = len(self.getLogFiles()) + self.assertTrue(logFile, "A log file already exists") + self.assertEqual(logFileCount, 1, "A single 'success' log file exists") + startLineCount = self.getLineCount(logFile) + + # open and close a window + self.openAndCloseSaveworthyWindow() + + self.assertEqual( + self.getLineCount(logFile), + startLineCount, + "Log file line count unchanged; debug messages should be buffered and not yet flushed to disk", + ) + + # Set the flush interval to 1 second and open+close another window to trigger some + # debug log messages + self.marionette.set_pref("browser.sessionstore.logFlushIntervalSeconds", 1) + origWindow = self.marionette.current_window_handle + self.openAndCloseSaveworthyWindow() + + try: + wait = Wait(self.marionette, timeout=5, interval=0.1) + wait.until( + lambda _: self.getLineCount(logFile) > startLineCount, + message="Waiting for line count in the log file to grow", + ) + newLineCount = self.getLineCount(logFile) + self.assertTrue( + newLineCount > startLineCount, + f"{newLineCount - startLineCount} lines got flushed to the log file", + ) + except errors.TimeoutException as e: + message = ( + f"{e.message}. Line count didn't grow as expected in log file {logFile}" + ) + raise errors.TimeoutException(message) + finally: + self.marionette.switch_to_window(origWindow) + self.marionette.clear_pref("browser.sessionstore.logFlushIntervalSeconds") + + self.assertTrue( + self.getLineCount(logFile) > startLineCount, + "Debug log messages got flushed to disk", + ) + self.assertEqual( + len(self.getLogFiles()), + logFileCount, + "We just appended to the one log file and didn't create any others", + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_new_tab_on_restore.py b/src/zen/tests/mochitests/sessionstore/marionette/test_new_tab_on_restore.py new file mode 100644 index 000000000..81543b746 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_new_tab_on_restore.py @@ -0,0 +1,370 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import copy +import os +import sys + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from marionette_driver import Wait +from session_store_test_case import SessionStoreTestCase + + +def inline(title): + return f"data:text/html;charset=utf-8,{title}" + + +RESTORED_TAB_COUNT = 2 + + +class TestNewTabOnRestore(SessionStoreTestCase): + def setUp(self): + super().setUp( + startup_page=3, + include_private=False, + restore_on_demand=False, + test_windows=set([ + ( + inline("Page 1"), + inline("Page 2"), + ), + ]), + ) + + def _set_new_tab_on_restore_prefs(self, enabled, show_setting=True): + self.marionette.set_prefs({ + "browser.sessionstore.newTabOnRestore": enabled, + "browser.sessionstore.newTabOnRestore.showSetting": show_setting, + }) + + def _quit_and_restore(self, expected_tab_count=None): + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + if expected_tab_count is not None: + Wait(self.marionette, timeout=5, interval=0.1).until( + lambda _: self._get_tab_count() == expected_tab_count, + message=f"Expected {expected_tab_count} tabs after restore", + ) + + def _get_tab_count(self): + return self.marionette.execute_script( + """ + let win = BrowserWindowTracker.getTopWindow(); + return win.gBrowser.tabs.length; + """ + ) + + def _get_selected_tab_url(self): + return self.marionette.execute_script( + """ + let win = BrowserWindowTracker.getTopWindow(); + return win.gBrowser.selectedBrowser.currentURI.spec; + """ + ) + + def _is_selected_tab_hidden(self): + return self.marionette.execute_script( + """ + let win = BrowserWindowTracker.getTopWindow(); + return win.gBrowser.selectedTab.hidden; + """ + ) + + def _get_telemetry_events(self): + return self.marionette.execute_script( + """ + return Glean.sessionRestore.startupSessionAutoRestored.testGetValue(); + """ + ) + + def test_new_tab_opens_when_enabled(self): + self._set_new_tab_on_restore_prefs(enabled=True) + self._quit_and_restore(expected_tab_count=RESTORED_TAB_COUNT + 1) + + selected_url = self._get_selected_tab_url() + self.assertIn( + "newtab", + selected_url.lower().replace("_", ""), + "Selected tab should be a new tab page", + ) + + events = self._get_telemetry_events() + self.assertEqual(len(events), 1, "One telemetry event recorded") + self.assertEqual(events[0]["extra"]["new_tab_action"], "opened") + + def test_no_new_tab_when_disabled(self): + self._set_new_tab_on_restore_prefs(enabled=False) + self._quit_and_restore(expected_tab_count=RESTORED_TAB_COUNT) + + events = self._get_telemetry_events() + self.assertEqual(len(events), 1, "One telemetry event recorded") + self.assertEqual(events[0]["extra"]["new_tab_action"], "disabled") + + def test_no_new_tab_when_show_setting_false(self): + self._set_new_tab_on_restore_prefs(enabled=True, show_setting=False) + self._quit_and_restore(expected_tab_count=RESTORED_TAB_COUNT) + + events = self._get_telemetry_events() + self.assertEqual(len(events), 1, "One telemetry event recorded") + self.assertEqual(events[0]["extra"]["new_tab_action"], "disabled") + + def test_reuses_existing_newtab_rightmost(self): + self._set_new_tab_on_restore_prefs(enabled=True) + + self.marionette.execute_script( + """ + let win = BrowserWindowTracker.getTopWindow(); + let { AboutNewTab } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTab.sys.mjs" + ); + win.gBrowser.addTrustedTab(AboutNewTab.newTabURL, { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + """ + ) + + self._quit_and_restore(expected_tab_count=RESTORED_TAB_COUNT + 1) + + events = self._get_telemetry_events() + self.assertEqual(len(events), 1, "One telemetry event recorded") + self.assertEqual(events[0]["extra"]["new_tab_action"], "reused") + + def test_reuses_selected_newtab(self): + self._set_new_tab_on_restore_prefs(enabled=True) + + self.marionette.execute_script( + """ + let win = BrowserWindowTracker.getTopWindow(); + let { AboutNewTab } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTab.sys.mjs" + ); + let tab = win.gBrowser.addTrustedTab(AboutNewTab.newTabURL, { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + win.gBrowser.selectedTab = tab; + """ + ) + + self._quit_and_restore(expected_tab_count=RESTORED_TAB_COUNT + 1) + + events = self._get_telemetry_events() + self.assertEqual(len(events), 1, "One telemetry event recorded") + self.assertEqual(events[0]["extra"]["new_tab_action"], "reused") + + def test_no_new_tab_when_cmdline_url(self): + self._set_new_tab_on_restore_prefs(enabled=True) + + orig_args = copy.copy(self.marionette.instance.app_args) + try: + self.marionette.quit() + self.marionette.instance.app_args.extend([ + "-url", + "data:text/html,cmdline", + ]) + self.marionette.start_session() + self.marionette.set_context("chrome") + + Wait(self.marionette, timeout=5, interval=0.1).until( + lambda _: self._get_tab_count() >= RESTORED_TAB_COUNT, + message=f"Expected at least {RESTORED_TAB_COUNT} tabs after restore", + ) + + events = self._get_telemetry_events() + self.assertEqual(len(events), 1, "One telemetry event recorded") + self.assertEqual(events[0]["extra"]["new_tab_action"], "preempted") + finally: + self.marionette.instance.app_args = orig_args + + def test_reuse_newtab_selects_visible_tab(self): + self._set_new_tab_on_restore_prefs(enabled=True) + + self.marionette.execute_script( + """ + let win = BrowserWindowTracker.getTopWindow(); + let { AboutNewTab } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTab.sys.mjs" + ); + let visibleTab = win.gBrowser.addTrustedTab(AboutNewTab.newTabURL, { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + let hiddenTab = win.gBrowser.addTrustedTab(AboutNewTab.newTabURL, { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }); + win.gBrowser.hideTab(hiddenTab); + // Select first tab so neither newtab is selected + win.gBrowser.selectedTab = win.gBrowser.tabs[0]; + """ + ) + + self._quit_and_restore(expected_tab_count=RESTORED_TAB_COUNT + 2) + + self.assertFalse( + self._is_selected_tab_hidden(), + "Selected tab should not be hidden", + ) + + events = self._get_telemetry_events() + self.assertEqual(len(events), 1, "One telemetry event recorded") + self.assertEqual(events[0]["extra"]["new_tab_action"], "reused") + + +class TestNewTabOnRestoreNotSettingBased(SessionStoreTestCase): + """ + Test that no new tab or telemetry is recorded when restore is not + triggered by the user's startup.page setting. + """ + + def setUp(self): + super().setUp( + startup_page=1, + include_private=False, + restore_on_demand=False, + test_windows=set([ + (inline("Page 1"),), + ]), + ) + + def test_no_new_tab_on_non_setting_restore(self): + self.marionette.set_prefs({ + "browser.sessionstore.newTabOnRestore": True, + "browser.sessionstore.newTabOnRestore.showSetting": True, + }) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + # Manually restore previous session (simulates non-setting-based restore) + self.marionette.execute_script( + """ + const { SessionStore } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionStore.sys.mjs" + ); + SessionStore.restoreLastSession(); + """ + ) + + events = self.marionette.execute_script( + """ + return Glean.sessionRestore.startupSessionAutoRestored.testGetValue(); + """ + ) + self.assertIsNone( + events, + "No telemetry when restore is not setting-based", + ) + + +class TestNewTabOnRestoreAfterCrash(SessionStoreTestCase): + """ + A crash-triggered restore is not a user-configured restore, so the + new-tab-on-restore feature must not engage even when the setting is on. + Guards the `!ss.previousSessionCrashed` check in SessionStore's initSession. + """ + + def setUp(self): + super().setUp( + startup_page=3, + include_private=False, + restore_on_demand=False, + test_windows=set([ + ( + inline("Page 1"), + inline("Page 2"), + ), + ]), + ) + self.marionette.set_prefs({ + "browser.sessionstore.newTabOnRestore": True, + "browser.sessionstore.newTabOnRestore.showSetting": True, + }) + + def test_no_new_tab_after_crash(self): + # Persist prefs and the session to disk before the crash. + self.marionette.execute_async_script( + """ + let [resolve] = arguments; + Services.prefs.savePrefFile(Services.dirsvc.get("PrefF", Ci.nsIFile)); + const { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" + ); + const { SessionSaver } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionSaver.sys.mjs" + ); + (async () => { + for (let win of Services.wm.getEnumerator("navigator:browser")) { + await TabStateFlusher.flushWindow(win); + } + await SessionSaver.run(); + })().then(resolve); + """ + ) + + # Simulate a crash: kill the process without a clean shutdown so the + # "final-state-write-complete" checkpoint is never recorded. + self.marionette.quit(in_app=False) + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.marionette.execute_async_script( + """ + let [resolve] = arguments; + const { SessionStore } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionStore.sys.mjs" + ); + SessionStore.promiseAllWindowsRestored.then(resolve); + """ + ) + + # Validity check: the prefs must have actually persisted across the + # crash. + prefs = self.marionette.execute_script( + """ + return { + enabled: Services.prefs.getBoolPref( + "browser.sessionstore.newTabOnRestore", false + ), + showSetting: Services.prefs.getBoolPref( + "browser.sessionstore.newTabOnRestore.showSetting", false + ), + }; + """ + ) + self.assertTrue( + prefs["enabled"] and prefs["showSetting"], + "newTabOnRestore prefs should persist across the crash so the " + "feature is enabled; otherwise this test is not exercising the " + "previousSessionCrashed path", + ) + + # No new tab should be added: only the restored tabs are present. + tab_count = self.marionette.execute_script( + """ + let win = BrowserWindowTracker.getTopWindow(); + return win.gBrowser.tabs.length; + """ + ) + self.assertEqual( + tab_count, + RESTORED_TAB_COUNT, + "No new tab should be added after a crash restore", + ) + + events = self.marionette.execute_script( + """ + return Glean.sessionRestore.startupSessionAutoRestored.testGetValue(); + """ + ) + self.assertIsNone( + events, + "The new-tab-on-restore feature must not engage after a crash " + "restore (previousSessionCrashed)", + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_persist_closed_tabs_restore_manually.py b/src/zen/tests/mochitests/sessionstore/marionette/test_persist_closed_tabs_restore_manually.py new file mode 100644 index 000000000..1a58a92da --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_persist_closed_tabs_restore_manually.py @@ -0,0 +1,221 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from marionette_driver import Wait, errors +from session_store_test_case import SessionStoreTestCase + + +def inline(title): + return f"data:text/html;charset=utf-8,{title}" + + +class TestSessionRestoreClosedTabs(SessionStoreTestCase): + """ + Test that closed tabs persist between sessions and + that any previously open tabs are added to the recently + closed tab list. When a previous session is restored, + an open tab is restored and removed from the closed tabs list. + + If additional tabs are opened (and closed) before a previous + session is restored, those should be merged with the restored open + and closed tabs, preserving their state. + """ + + def setUp(self): + super().setUp( + startup_page=1, + include_private=False, + restore_on_demand=True, + test_windows=set([ + # Window 1 + ( + inline("lorem ipsom"), + inline("dolor"), + ), + ]), + ) + + def test_restore(self): + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.sessionstore.persist_closed_tabs_between_sessions", true); + """ + ) + + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + # Close the second tab leaving the first tab open + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + let tab = gBrowser.tabs[1]; + gBrowser.removeTab(tab); + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flush(tab).then(resolve); + """ + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script( + """ + let { SessionStore } = ChromeUtils.importESModule("resource:///modules/sessionstore/SessionStore.sys.mjs"); + let state = JSON.parse(SessionStore.getBrowserState()); + return state.windows[0]._closedTabs.length; + """ + ), + 2, + msg="Should have 2 closed tabs after restart.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let { SessionStore } = ChromeUtils.importESModule("resource:///modules/sessionstore/SessionStore.sys.mjs"); + let state = JSON.parse(SessionStore.getBrowserState()); + return state.windows[0]._closedTabs[0].removeAfterRestore; + """ + ), + True, + msg="Previously open tab that was added to closedTabs should have removeAfterRestore property.", + ) + + # open two new tabs, the second one will be closed + win = self.marionette.current_chrome_window_handle + self.open_tabs(win, (inline("sit"), inline("amet"))) + + self.assertEqual( + self.marionette.execute_script( + """ + return gBrowser.tabs[0].label + """ + ), + "sit", + msg="First open tab should now be sit", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + return gBrowser.tabs[1].label + """ + ), + "amet", + msg="Second open tab should be amet", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + return gBrowser.tabs.length + """ + ), + 2, + msg="should have 2 tabs open", + ) + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + let tab = gBrowser.tabs[1]; + gBrowser.removeTab(tab); + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flush(tab).then(resolve); + """ + ) + + self.wait_for_tabcount(1, "Waiting for 1 tabs") + + # restore the previous session + self.assertEqual( + self.marionette.execute_script( + """ + const lazy = {}; + ChromeUtils.defineESModuleGetters(lazy, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + }); + function observeClosedTabsChange() { + return new Promise(resolve => { + function observe(subject, topic, data) { + if (topic == "sessionstore-closed-objects-changed") { + Services.obs.removeObserver(this, "sessionstore-closed-objects-changed"); + resolve('observed closed objects changed'); + }; + } + Services.obs.addObserver(observe, "sessionstore-closed-objects-changed"); + }); + }; + + async function checkForClosedTabs() { + let closedTabsObserver = observeClosedTabsChange(); + lazy.SessionStore.restoreLastSession(); + await closedTabsObserver; + let state = JSON.parse(lazy.SessionStore.getBrowserState()); + return state.windows[0]._closedTabs.length; + } + return checkForClosedTabs(); + """ + ), + 2, + msg="Should have 2 closed tab after restoring session.", + ) + + self.wait_for_tabcount(2, "Waiting for 2 tabs") + + self.assertEqual( + self.marionette.execute_script( + """ + return gBrowser.tabs[0].label + """ + ), + "sit", + msg="Newly opened tab should still exist", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + return gBrowser.tabs[1].label + """ + ), + "lorem ipsom", + msg="The open tab from the previous session should be restored", + ) + + # temporary until we remove this pref + self.marionette.execute_script( + """ + Services.prefs.clearUserPref("browser.sessionstore.persist_closed_tabs_between_sessions"); + """ + ) + + def wait_for_tabcount(self, expected_tabcount, message, timeout=5): + current_tabcount = None + + def check(_): + nonlocal current_tabcount + current_tabcount = self.marionette.execute_script( + "return gBrowser.tabs.length;" + ) + return current_tabcount == expected_tabcount + + try: + wait = Wait(self.marionette, timeout=timeout, interval=0.1) + wait.until(check, message=message) + except errors.TimeoutException as e: + # Update the message to include the most recent list of windows + message = ( + f"{e.message}. Expected {expected_tabcount}, got {current_tabcount}." + ) + raise errors.TimeoutException(message) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_aiwindow.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_aiwindow.py new file mode 100644 index 000000000..530bfdecb --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_aiwindow.py @@ -0,0 +1,387 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + + +def inline(title): + return f"data:text/html;charset=utf-8,{title}" + + +class AIWindowTestMixin: + def setUp(self, startup_page=1): + super().setUp( + startup_page=startup_page, + include_private=False, + restore_on_demand=True, + test_windows=set([ + ( + inline("Tab 1"), + inline("Tab 2"), + inline("Tab 3"), + ), + ]), + ) + self.marionette.set_context("chrome") + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.smartwindow.enabled", true); + """ + ) + + def is_ai_window(self): + return self.marionette.execute_script( + """ + return window.document.documentElement.hasAttribute("ai-window"); + """ + ) + + def get_tab_count(self): + return self.marionette.execute_script( + """ + return gBrowser.tabs.length; + """ + ) + + def toggle_ai_window(self, enabled): + self.marionette.execute_script( + """ + const { AIWindow } = ChromeUtils.importESModule( + "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs" + ); + AIWindow.toggleAIWindow(window, arguments[0]); + """, + script_args=[enabled], + ) + + def restore_last_session(self): + self.marionette.execute_script( + """ + const lazy = {}; + ChromeUtils.defineESModuleGetters(lazy, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + }); + function observeClosedObjectsChange() { + return new Promise(resolve => { + function observe(subject, topic, data) { + if (topic == "sessionstore-closed-objects-changed") { + Services.obs.removeObserver(observe, "sessionstore-closed-objects-changed"); + resolve(); + } + } + Services.obs.addObserver(observe, "sessionstore-closed-objects-changed"); + }); + } + + async function restoreSession() { + let closedWindowsObserver = observeClosedObjectsChange(); + lazy.SessionStore.restoreLastSession(); + await closedWindowsObserver; + } + return restoreSession(); + """ + ) + + +class SmartWindowDefaultMixin(AIWindowTestMixin): + """Sets the prefs so the *next* startup opens a Smart Window. + + Setting them after super().setUp() (which opened the Classic test windows) + means only the post-restart startup window is affected. The prefs flush to + prefs.js on the clean quit, so they apply at the restart startup. + """ + + def setUp(self, startup_page=1): + super().setUp(startup_page=startup_page) + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.smartwindow.isDefaultWindow", true); + Services.prefs.setIntPref("browser.smartwindow.tos.consentTime", 1); + // Keep the Smart Window sign-in flow from navigating to the real + // accounts.firefox.com (non-local address crashes the test sandbox). + Services.prefs.setCharPref( + "identity.fxaccounts.remote.root", + "http://127.0.0.1/" + ); + """ + ) + + def can_restore_last_session(self): + return self.marionette.execute_script( + """ + const { SessionStore } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionStore.sys.mjs" + ); + return SessionStore.canRestoreLastSession; + """ + ) + + +class TestAIWindowSessionRestore(AIWindowTestMixin, SessionStoreTestCase): + """ + Test that AI Window state persists correctly across session restarts. + """ + + def test_window_mode_persists_across_restart(self): + """Test that both Classic and AI Window states persist across session restarts.""" + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.sessionstore.persist_closed_tabs_between_sessions", true); + """ + ) + + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.assertFalse( + self.is_ai_window(), msg="Window should start as Classic Window" + ) + + tab_count = self.get_tab_count() + self.assertEqual(tab_count, 3, msg="Should have 3 tabs") + + self.toggle_ai_window(True) + self.assertTrue( + self.is_ai_window(), msg="Window should be AI Window after toggle" + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.restore_last_session() + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 2, + msg="AI Window opened in new window due to type mismatch with startup window.", + ) + + # Switch to the AI window (the second window) + self.marionette.switch_to_window(self.marionette.chrome_window_handles[1]) + + self.assertTrue( + self.is_ai_window(), + msg="AI Window state should persist after restart", + ) + + self.assertEqual( + self.get_tab_count(), + tab_count, + msg="Tab count should be preserved after restart", + ) + + def test_aiwindow_not_restored_when_pref_disabled(self): + """Test that AI Windows revert to Classic when pref is disabled after restart.""" + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.sessionstore.persist_closed_tabs_between_sessions", true); + """ + ) + + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.toggle_ai_window(True) + self.assertTrue(self.is_ai_window(), msg="Window should be AI before restart") + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.smartwindow.enabled", false); + """ + ) + + self.restore_last_session() + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Window from last session has been restored.", + ) + + self.assertFalse( + self.is_ai_window(), + msg="AI Window should revert to Classic when pref is disabled", + ) + + +class TestAIWindowAutomaticRestore(AIWindowTestMixin, SessionStoreTestCase): + """Test AI Window persistence with automatic session restore.""" + + def setUp(self): + super().setUp(startup_page=3) + + def test_single_window_stays_in_smart_window_on_automatic_restart(self): + + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.assertFalse( + self.is_ai_window(), msg="Window should start as Classic Window" + ) + + tab_count = self.get_tab_count() + self.assertEqual(tab_count, 3, msg="Should have 3 tabs") + + self.toggle_ai_window(True) + self.assertTrue( + self.is_ai_window(), msg="Window should be AI Window after toggle" + ) + + # Restart with automatic session restore + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Should have exactly one window after automatic restore", + ) + + self.assertTrue( + self.is_ai_window(), + msg="Window should stay in Smart Window mode after restart", + ) + + self.assertEqual( + self.get_tab_count(), + tab_count, + msg="Tab count should be preserved after restart", + ) + + +class TestSmartWindowDefaultRestore(SmartWindowDefaultMixin, SessionStoreTestCase): + """ + Regression test: "Restore previous session" must stay available when the + user's default is a Smart Window. + + When Smart Window is the default, the first window opens as Smart at startup. + That startup path used to read the session restore state too early — before + the saved session had been loaded from disk — so the browser concluded there + was no session and disabled "Restore previous session". This test starts with + Smart Window as the default, restarts, and verifies the previous session is + still restorable and that the window stays Smart. + """ + + def test_restore_previous_session_available_with_smart_default(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + self.assertFalse( + self.is_ai_window(), + msg="Window opened during setUp should still be Classic", + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + # Startup window opens as Smart because Smart Window is the default, and + # it must stay Smart (no flash back to Classic). + self.assertTrue( + self.is_ai_window(), + msg="Startup window should open and stay Smart when it is the default", + ) + + # The actual regression: the previous session must not be discarded. + self.assertTrue( + self.can_restore_last_session(), + msg="Previous session should be restorable when Smart Window is default", + ) + + +class TestSmartWindowDefaultClassicAutomaticRestore( + SmartWindowDefaultMixin, SessionStoreTestCase +): + """With Smart Window as default AND automatic restore (page=3), a window the + user kept as Classic must come back Classic — a full restore respects the + saved window type, even though new windows default to Smart.""" + + def setUp(self): + super().setUp(startup_page=3) + + def test_classic_window_restored_as_classic_on_automatic_restore(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + self.assertFalse(self.is_ai_window(), msg="Window should start as Classic") + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertFalse( + self.is_ai_window(), + msg="Classic window must restore as Classic on automatic restore, " + "even when Smart Window is the default", + ) + + +class TestSmartWindowDefaultManualRestore( + SmartWindowDefaultMixin, SessionStoreTestCase +): + """With Smart Window as default (startup.page=1), restoring a previously + Classic session via "Restore previous session" brings it back as Classic. + Smart-by-default only applies to the new startup window, not to windows + restored from a saved session.""" + + def test_classic_session_restored_as_classic_from_history_menu(self): + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.sessionstore.persist_closed_tabs_between_sessions", true); + """ + ) + + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + self.assertFalse( + self.is_ai_window(), msg="Window opened during setUp should be Classic" + ) + tab_count = self.get_tab_count() + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + # The new startup window opens Smart because Smart Window is the default. + self.assertTrue( + self.is_ai_window(), + msg="Startup window should open Smart when Smart Window is the default", + ) + + self.restore_last_session() + + # The saved session was Classic, so restoring it opens its own window + # (its type doesn't match the Smart startup window). + self.assertEqual( + len(self.marionette.chrome_window_handles), + 2, + msg="Restored Classic session should open in a separate window", + ) + + self.marionette.switch_to_window(self.marionette.chrome_window_handles[1]) + self.assertFalse( + self.is_ai_window(), + msg="Restored window must stay Classic (restore respects the saved type)", + ) + self.assertEqual( + self.get_tab_count(), + tab_count, + msg="Tab count should be preserved after restore", + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_loading_tab.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_loading_tab.py new file mode 100644 index 000000000..a7544184a --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_loading_tab.py @@ -0,0 +1,69 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from urllib.parse import quote + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +def inline(doc): + return f"data:text/html;charset=utf-8,{quote(doc)}" + + +class TestRestoreLoadingPage(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super().setUp() + self.delayed_page = self.marionette.absolute_url("slow") + + def do_test(self, html, is_restoring_expected): + self.marionette.navigate(inline(html.format(self.delayed_page))) + link = self.marionette.find_element("id", "link") + link.click() + + self.marionette.restart(in_app=True) + + with self.marionette.using_context("chrome"): + urls = self.marionette.execute_script( + "return gBrowser.tabs.map(t => t.linkedBrowser.currentURI.spec);" + ) + + if is_restoring_expected: + self.assertEqual( + len(urls), + 2, + msg="The tab opened should be restored", + ) + self.assertEqual( + urls[1], + self.delayed_page, + msg="The tab restored is correct", + ) + else: + self.assertEqual( + len(urls), + 1, + msg="The tab opened should not be restored", + ) + + self.close_all_tabs() + + def test_target_blank(self): + self.do_test("click", True) + + def test_target_other(self): + self.do_test("click", False) + + def test_by_script(self): + self.do_test( + """ + click + + """, + False, + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually.py new file mode 100644 index 000000000..0f3291f2e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually.py @@ -0,0 +1,140 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + + +def inline(title): + return f"data:text/html;charset=utf-8,{title}" + + +class TestSessionRestoreManually(SessionStoreTestCase): + """ + Test that window attributes for each window are restored + correctly (with manual session restore) in a new session. + """ + + def setUp(self): + super().setUp( + startup_page=1, + include_private=False, + restore_on_demand=True, + test_windows=set([ + # Window 1 + ( + inline("lorem ipsom"), + inline("dolor"), + ), + # Window 2 + (inline("sit"),), + ]), + ) + + def test_restore(self): + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.sessionstore.persist_closed_tabs_between_sessions", true); + """ + ) + + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 2, + msg="Should have 3 windows open.", + ) + self.marionette.execute_async_script( + """ + function getAllBrowserWindows() { + return Array.from(Services.wm.getEnumerator("navigator:browser")); + } + function promiseResize(value, win) { + let deferred = Promise.withResolvers(); + let id; + function listener() { + win.clearTimeout(id); + if (win.innerWidth <= value) { + id = win.setTimeout(() => { + win.removeEventListener("resize", listener); + deferred.resolve() + }, 100); + } + } + if (win.innerWidth > value) { + win.addEventListener("resize", listener); + win.resizeTo(value, value); + } else { + deferred.resolve() + } + return deferred.promise; + } + + let resolve = arguments[0]; + let windows = getAllBrowserWindows(); + let value = 500; + promiseResize(value, windows[1]).then(resolve); + """ + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + # restore the previous session + self.marionette.execute_script( + """ + const lazy = {}; + ChromeUtils.defineESModuleGetters(lazy, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + }); + function observeClosedObjectsChange() { + return new Promise(resolve => { + function observe(subject, topic, data) { + if (topic == "sessionstore-closed-objects-changed") { + Services.obs.removeObserver(this, "sessionstore-closed-objects-changed"); + resolve('observed closed objects changed'); + }; + } + Services.obs.addObserver(observe, "sessionstore-closed-objects-changed"); + }); + }; + + async function checkForWindowHeight() { + let closedWindowsObserver = observeClosedObjectsChange(); + lazy.SessionStore.restoreLastSession(); + await closedWindowsObserver; + } + checkForWindowHeight(); + """ + ) + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 2, + msg="Windows from last session have been restored.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + const lazy = {}; + ChromeUtils.defineESModuleGetters(lazy, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + }); + let state = SessionStore.getCurrentState() + return state.windows[1]["height"] + """ + ), + 500, + "Second window has been restored to the correct height.", + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually_with_pinned_tabs.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually_with_pinned_tabs.py new file mode 100644 index 000000000..5e92dce2b --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually_with_pinned_tabs.py @@ -0,0 +1,106 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +from urllib.parse import quote + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from marionette_driver import Wait, errors +from session_store_test_case import SessionStoreTestCase + + +def inline(doc): + return f"data:text/html;charset=utf-8,{quote(doc)}" + + +class TestSessionRestoreWithPinnedTabs(SessionStoreTestCase): + def setUp(self): + super().setUp( + startup_page=1, + include_private=False, + restore_on_demand=True, + test_windows=set([ + # Window 1 + ( + inline("""ipsum"""), + inline("""dolor"""), + inline("""amet"""), + ), + ]), + ) + + def test_no_restore_with_quit(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + # add pinned tab in first window. + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + gBrowser.pinTab(gBrowser.tabs[0]); + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flush(gBrowser.tabs[0]).then(resolve); + """ + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script("return gBrowser.tabs.length"), + 2, + msg="Should have 2 tabs.", + ) + + self.assertEqual( + self.marionette.execute_script( + "return gBrowser.tabs.filter(t => t.pinned).length" + ), + 1, + msg="Pinned tab should have been restored.", + ) + + self.marionette.execute_script( + """ + SessionStore.restoreLastSession(); + """ + ) + self.wait_for_tabcount(3, "Waiting for 3 tabs") + + self.assertEqual( + self.marionette.execute_script("return gBrowser.tabs.length"), + 3, + msg="Should have 2 tabs.", + ) + self.assertEqual( + self.marionette.execute_script( + "return gBrowser.tabs.filter(t => t.pinned).length" + ), + 1, + msg="Should still have 1 pinned tab", + ) + + def wait_for_tabcount(self, expected_tabcount, message, timeout=5): + current_tabcount = None + + def check(_): + nonlocal current_tabcount + current_tabcount = self.marionette.execute_script( + "return gBrowser.tabs.length;" + ) + return current_tabcount == expected_tabcount + + try: + wait = Wait(self.marionette, timeout=timeout, interval=0.1) + wait.until(check, message=message) + except errors.TimeoutException as e: + # Update the message to include the most recent list of windows + message = ( + f"{e.message}. Expected {expected_tabcount}, got {current_tabcount}." + ) + raise errors.TimeoutException(message) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually_with_tab_groups.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually_with_tab_groups.py new file mode 100644 index 000000000..b3f727822 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_manually_with_tab_groups.py @@ -0,0 +1,325 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +from urllib.parse import quote + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from marionette_driver import Wait, errors +from session_store_test_case import SessionStoreTestCase + + +def inline(doc): + return f"data:text/html;charset=utf-8,{quote(doc)}" + + +class TestSessionRestoreWithTabGroups(SessionStoreTestCase): + def setUp(self): + super().setUp( + startup_page=1, + include_private=False, + restore_on_demand=True, + test_windows=set([ + ( + inline("""lorem"""), + inline("""ipsum"""), + inline("""dolor"""), + inline("""sit"""), + inline("""amet"""), + ), + ]), + ) + + def test_no_restore_with_quit(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + gBrowser.addTabGroup([gBrowser.tabs[3], gBrowser.tabs[4]], { id: "group-left-open-1", label: "open-1" }); + gBrowser.addTabGroup([gBrowser.tabs[2]], { id: "group-left-open-2", label: "open-2" }); + let closedGroup = gBrowser.addTabGroup([gBrowser.tabs[1]], { id:"group-closed", label: "closed" }); + gBrowser.removeTabGroup(closedGroup); + + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flushWindow(gBrowser.documentGlobal).then(resolve); + """ + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + self.assertEqual( + self.marionette.execute_script( + """ + let closedGroupIds = SessionStore.getWindowState(gBrowser.documentGlobal).windows[0].closedGroups.map(g => g.id); + return !(closedGroupIds.includes("group-left-open-1") && closedGroupIds.includes("group-left-open-2")); + """ + ), + True, + "Groups that were left open are NOT in closedGroups", + ) + + self.assertEqual( + self.marionette.execute_script( + "return SessionStore.getWindowState(gBrowser.documentGlobal).windows[0].closedGroups.length" + ), + 1, + msg="There is one closed group in the window", + ) + + self.assertEqual( + self.marionette.execute_script( + "return SessionStore.getWindowState(gBrowser.documentGlobal).windows[0].closedGroups[0].id" + ), + "group-closed", + msg="Correct group appears in closedGroups", + ) + + self.assertEqual( + self.marionette.execute_script("return SessionStore.savedGroups.length"), + 2, + msg="There are two saved groups", + ) + + self.assertEqual( + self.marionette.execute_script( + "return SessionStore.savedGroups.map(g => g.id)" + ), + ["group-left-open-1", "group-left-open-2"], + msg="The open groups from last session are now saved", + ) + + self.assertEqual( + self.marionette.execute_script( + "return SessionStore.getWindowState(gBrowser.documentGlobal).windows[0].closedGroups[0].tabs.length" + ), + 1, + msg="Closed group has 1 tab", + ) + + self.assertEqual( + self.marionette.execute_script( + "return SessionStore.savedGroups.map(g => g.tabs.length)" + ), + [2, 1], + msg="Saved groups have 1 and 2 tabs respectively", + ) + + self.marionette.execute_script( + """ + SessionStore.restoreLastSession(); + """ + ) + self.wait_for_tabcount(4, "Waiting for 4 tabs") + + self.assertEqual( + self.marionette.execute_script("return gBrowser.tabs.length"), + 4, + msg="4 tabs was restored", + ) + self.assertEqual( + self.marionette.execute_script("return gBrowser.tabGroups.length"), + 2, + msg="2 tab groups were restored", + ) + + self.assertEqual( + self.marionette.execute_script("return SessionStore.savedGroups?.length"), + 0, + msg="We have no saved tab groups", + ) + + def test_no_redundant_saves(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.execute_script( + """ + let group = gBrowser.addTabGroup([...gBrowser.tabs], { id: "group-to-save", label: "to-save" }); + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flushWindow(gBrowser.documentGlobal).then(() => { + group.saveAndClose(); + }); + """ + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + self.assertEqual( + self.marionette.execute_script( + """ + return SessionStore.savedGroups.length; + """ + ), + 1, + "There is one and only one saved group", + ) + + self.marionette.execute_script( + """ + SessionStore.forgetSavedTabGroup("group-to-save"); + """ + ) + + def test_open_groups_become_permanently_saved_after_two_quits_bug1954488(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + gBrowser.addTabGroup([gBrowser.tabs[3], gBrowser.tabs[4]], { id: "save-through-restore-1", label: "open-1" }); + gBrowser.addTabGroup([gBrowser.tabs[1], gBrowser.tabs[2]], { id: "save-through-restore-2", label: "open-2" }); + + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flushWindow(gBrowser.documentGlobal).then(resolve); + """ + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script( + """ + return SessionStore.savedGroups.length; + """ + ), + 2, + "There are two saved groups", + ) + + win = self.marionette.current_chrome_window_handle + self.open_tabs(win, (inline("sit"), inline("amet"))) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script( + """ + return SessionStore.savedGroups.length; + """ + ), + 2, + "There are two saved groups", + ) + + self.marionette.execute_script( + """ + SessionStore.restoreLastSession(); + """ + ) + self.wait_for_tabcount(2, "Waiting for 2 tabs") + + self.assertEqual( + self.marionette.execute_script( + """ + return SessionStore.savedGroups.length; + """ + ), + 2, + "There are still two saved groups after the second quit", + ) + + self.marionette.execute_script( + """ + SessionStore.forgetSavedTabGroup("save-through-restore-1"); + SessionStore.forgetSavedTabGroup("save-through-restore-2"); + """ + ) + + def test_saved_groups_no_longer_saved_if_restored_to_tab_strip_bug1956254(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + gBrowser.addTabGroup([gBrowser.tabs[3], gBrowser.tabs[4]], { id: "not-saved-after-restore-1", label: "open-1" }); + gBrowser.addTabGroup([gBrowser.tabs[1], gBrowser.tabs[2]], { id: "not-saved-after-restore-2", label: "open-2" }); + + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flushWindow(gBrowser.documentGlobal).then(resolve); + """ + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script( + """ + return SessionStore.savedGroups.length; + """ + ), + 2, + "There are two saved groups in the default state before restoration", + ) + + self.marionette.execute_script( + """ + SessionStore.restoreLastSession(); + """ + ) + self.wait_for_tabcount(5, "Waiting for 5 tabs") + + self.assertEqual( + self.marionette.execute_script( + """ + return SessionStore.savedGroups.length; + """ + ), + 0, + "The two saved groups should no longer be saved because they were restored to the tab strip", + ) + + tab_group_ids = set( + self.marionette.execute_script( + """ + return gBrowser.getAllTabGroups().map(tabGroup => tabGroup.id); + """ + ) + ) + self.assertTrue( + "not-saved-after-restore-1" in tab_group_ids, + "the first saved tab group should now be in the tab strip", + ) + self.assertTrue( + "not-saved-after-restore-2" in tab_group_ids, + "the second saved tab group should now be in the tab strip", + ) + + def wait_for_tabcount(self, expected_tabcount, message, timeout=5): + current_tabcount = None + + def check(_): + nonlocal current_tabcount + current_tabcount = self.marionette.execute_script( + "return gBrowser.tabs.length;" + ) + return current_tabcount == expected_tabcount + + try: + wait = Wait(self.marionette, timeout=timeout, interval=0.1) + wait.until(check, message=message) + except errors.TimeoutException as e: + # Update the message to include the most recent list of windows + message = ( + f"{e.message}. Expected {expected_tabcount}, got {current_tabcount}." + ) + raise errors.TimeoutException(message) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_sidebar.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_sidebar.py new file mode 100644 index 000000000..fcccf7c13 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_sidebar.py @@ -0,0 +1,304 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 0.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/0.0/. + +import os +import sys + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + + +def inline(title): + return f"data:text/html;charset=utf-8,{title}" + + +class TestSessionRestore(SessionStoreTestCase): + """ + Test that the sidebar and its attributes are restored on reopening of window. + """ + + def setUp(self): + super().setUp( + startup_page=1, + include_private=False, + restore_on_demand=True, + test_windows=set([ + ( + inline("lorem ipsom"), + inline("dolor"), + ), + ]), + ) + + def test_restore_sidebar_open(self): + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Should have 1 window open.", + ) + self.marionette.execute_async_script( + """ + const resolve = arguments[0]; + let window = BrowserWindowTracker.getTopWindow() + window.SidebarController.show("viewHistorySidebar").then(() => { + let sidebarBox = window.document.getElementById("sidebar-box") + sidebarBox.style.width = "100px"; + }).then(resolve); + """ + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + return !window.document.getElementById("sidebar-box").hidden; + """ + ), + True, + "Sidebar is open before window is closed.", + ) + + self.marionette.restart() + self.marionette.set_context("chrome") + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Windows from last session have been restored.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + return !window.document.getElementById("sidebar-box").hidden; + """ + ), + True, + "Sidebar has been restored.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + return window.document.getElementById("sidebar-box").style.width; + """ + ), + "100px", + "Sidebar width been restored.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + const lazy = {}; + ChromeUtils.defineESModuleGetters(lazy, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + }); + let state = SessionStore.getCurrentState(); + return state.windows[0].sidebar.command; + """ + ), + "viewHistorySidebar", + "Correct sidebar category has been restored.", + ) + + def test_restore_sidebar_closed(self): + self.marionette.execute_async_script( + """ + const resolve = arguments[0]; + let window = BrowserWindowTracker.getTopWindow() + window.SidebarController.show("viewHistorySidebar").then(() => { + let sidebarBox = window.document.getElementById("sidebar-box") + sidebarBox.style.width = "100px"; + window.SidebarController.toggle(); + }).then(resolve); + """ + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + return window.document.getElementById("sidebar-box").hidden; + """ + ), + True, + "Sidebar is hidden before window is closed.", + ) + + self.marionette.restart() + self.marionette.set_context("chrome") + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Windows from last session have been restored.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + return window.document.getElementById("sidebar-box").hidden; + """ + ), + True, + "Sidebar is hidden on session restore.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + return window.document.getElementById("sidebar-box").style.width; + """ + ), + "100px", + "Sidebar width has been restored.", + ) + + def test_restore_for_always_show(self): + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("sidebar.revamp", true); + // Always show is only available with vertical tabs + Services.prefs.setBoolPref("sidebar.verticalTabs", true); + Services.prefs.setBoolPref("sidebar.animation.enabled", false); + Services.prefs.setStringPref("sidebar.visibility", "always-show"); + """ + ) + self.marionette.restart() + self.marionette.set_context("chrome") + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Should have 1 window open.", + ) + self.assertTrue( + self.marionette.execute_script( + """ + const window = BrowserWindowTracker.getTopWindow(); + return window.SidebarController.sidebarMain.expanded; + """ + ), + "Sidebar is expanded before window is closed.", + ) + + self.marionette.restart() + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + let { BrowserInitState } = ChromeUtils.importESModule("resource:///modules/BrowserGlue.sys.mjs"); + BrowserInitState.startupIdleTaskPromise.then(resolve); + """ + ) + + self.assertTrue( + self.marionette.execute_script( + """ + const window = BrowserWindowTracker.getTopWindow(); + return window.SidebarController.sidebarMain.expanded; + """ + ), + "Sidebar expanded state has been restored.", + ) + + def test_restore_for_hide_sidebar(self): + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("sidebar.revamp", true); + Services.prefs.setStringPref("sidebar.visibility", "hide-sidebar"); + """ + ) + self.marionette.restart() + self.marionette.set_context("chrome") + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Should have 1 window open.", + ) + self.assertTrue( + self.marionette.execute_script( + """ + const window = BrowserWindowTracker.getTopWindow(); + return window.SidebarController.sidebarContainer.hidden; + """ + ), + "Sidebar is hidden before window is closed.", + ) + + self.marionette.restart() + + self.assertTrue( + self.marionette.execute_script( + """ + const window = BrowserWindowTracker.getTopWindow(); + return window.SidebarController.sidebarContainer.hidden; + """ + ), + "Sidebar visibility state has been restored.", + ) + + def test_restore_sidebar_open_from_backup_pref(self): + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("sidebar.revamp", true); + Services.prefs.setBoolPref("browser.privatebrowsing.autostart", true); + """ + ) + self.marionette.restart() + self.marionette.set_context("chrome") + + # Open the history panel. + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + let window = BrowserWindowTracker.getTopWindow(); + window.SidebarController.show("viewHistorySidebar").then(resolve); + """ + ) + + # Restart the browser. + self.marionette.restart() + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + let { BrowserInitState } = ChromeUtils.importESModule("resource:///modules/BrowserGlue.sys.mjs"); + BrowserInitState.startupIdleTaskPromise.then(resolve); + """ + ) + + # Check to see if the history panel was restored. + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow(); + return window.SidebarController.currentID; + """ + ), + "viewHistorySidebar", + "Correct sidebar category has been restored.", + ) + + def test_upgrade_profile_and_restore_sidebar_from_backup_pref(self): + # Bug 1963549 - Sidebar bookmarks no longer open automatically since the last update. + self.marionette.execute_script( + """ + Services.prefs.setIntPref("browser.migration.version", 154); + Services.xulStore.setValue( + AppConstants.BROWSER_CHROME_URL, + "sidebar-box", + "checked", + "true" + ); + """ + ) + self.test_restore_sidebar_open_from_backup_pref() diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_sidebar_automatic.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_sidebar_automatic.py new file mode 100644 index 000000000..19c64844c --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_sidebar_automatic.py @@ -0,0 +1,106 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 0.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/0.0/. + +import os +import sys + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + + +def inline(title): + return f"data:text/html;charset=utf-8,{title}" + + +class TestSessionRestore(SessionStoreTestCase): + """ + Test that the sidebar and its attributes are restored on reopening of window. + """ + + def setUp(self): + super().setUp( + startup_page=3, + include_private=False, + restore_on_demand=False, + test_windows=set([ + ( + inline("lorem ipsom"), + inline("dolor"), + ), + ]), + ) + + def test_restore(self): + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Should have 1 window open.", + ) + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + window.SidebarController.show("viewHistorySidebar"); + let sidebarBox = window.document.getElementById("sidebar-box") + sidebarBox.style.width = "100px"; + """ + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + return !window.document.getElementById("sidebar-box").hidden; + """ + ), + True, + "Sidebar is open before window is closed.", + ) + + self.marionette.restart() + self.marionette.set_context("chrome") + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Windows from last session have been restored.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + return !window.document.getElementById("sidebar-box").hidden; + """ + ), + True, + "Sidebar has been restored.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + let window = BrowserWindowTracker.getTopWindow() + return window.document.getElementById("sidebar-box").style.width; + """ + ), + "100px", + "Sidebar width been restored.", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + const lazy = {}; + ChromeUtils.defineESModuleGetters(lazy, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + }); + let state = SessionStore.getCurrentState(); + return state.windows[0].sidebar.command; + """ + ), + "viewHistorySidebar", + "Correct sidebar category has been restored.", + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_close_last_tabs.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_close_last_tabs.py new file mode 100644 index 000000000..48bc5447e --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_close_last_tabs.py @@ -0,0 +1,57 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# add this directory to the path +import os +import sys + +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + + +class TestSessionStoreEnabledAllWindows(SessionStoreTestCase): + def setUp(self, include_private=True): + """Setup for the test, enabling session restore. + + :param include_private: Whether to open private windows. + """ + super().setUp(include_private=include_private, startup_page=3) + + def test_with_variety(self): + """Test opening and restoring both standard and private windows. + + Opens a set of windows, both standard and private, with + some number of tabs in them. Once the tabs have loaded, restarts + the browser, and then ensures that the standard tabs have been + restored, and that the private ones have not. + """ + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.quit() + self.marionette.start_session() + + self.wait_for_windows( + self.test_windows, "Non private browsing windows should have been restored" + ) + + def test_close_tabs(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.close_all_tabs_and_restart() + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Windows from last session shouldn`t have been restored.", + ) + self.assertEqual( + len(self.marionette.window_handles), + 1, + msg="Tabs from last session shouldn`t have been restored.", + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_restart_and_quit.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_restart_and_quit.py new file mode 100644 index 000000000..2498a5f8c --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_restart_and_quit.py @@ -0,0 +1,82 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# add this directory to the path +import os +import sys + +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + + +def inline(title): + return f"data:text/html;charset=utf-8,{title}" + + +class TestSessionStoreEnabledAllWindows(SessionStoreTestCase): + def setUp(self, include_private=True): + """Setup for the test, enabling session restore. + + :param include_private: Whether to open private windows. + """ + super().setUp(include_private=include_private, startup_page=3) + + def test_with_variety(self): + """Test opening and restoring both standard and private windows. + + Opens a set of windows, both standard and private, with + some number of tabs in them. Once the tabs have loaded, restarts + the browser, and then ensures that the standard tabs have been + restored, and that the private ones have not. + """ + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.wait_for_windows( + self.test_windows, "Non private browsing windows should have been restored" + ) + + +class TestSessionStoreEnabledNoPrivateWindows(TestSessionStoreEnabledAllWindows): + def setUp(self): + super().setUp(include_private=False) + + +class TestSessionStoreDisabled(SessionStoreTestCase): + def test_no_restore_with_quit(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Windows from last session shouldn`t have been restored.", + ) + self.assertEqual( + len(self.marionette.window_handles), + 1, + msg="Tabs from last session shouldn`t have been restored.", + ) + + def test_restore_with_restart(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.restart(in_app=True) + + self.wait_for_windows( + self.test_windows, "Non private browsing windows should have been restored" + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_windows_shutdown.py b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_windows_shutdown.py new file mode 100644 index 000000000..cc7ac1673 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_restore_windows_after_windows_shutdown.py @@ -0,0 +1,62 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# add this directory to the path +import os +import sys + +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + +# We test the following combinations with simulated Windows shutdown: +# - Start page = restore session (expect restore in all cases) +# - RAR (toolkit.winRegisterApplicationRestart) disabled +# - RAR enabled, restarted manually +# +# - Start page = home +# - RAR disabled (no restore) +# - RAR enabled: +# - restarted by OS (restore) +# - restarted manually (no restore) + + +class TestWindowsShutdown(SessionStoreTestCase): + def setUp(self): + super().setUp(startup_page=3, no_auto_updates=False) + + def test_with_variety(self): + """Test session restore selected by user.""" + self.windows_shutdown_with_variety(restart_by_os=False, expect_restore=True) + + +class TestWindowsShutdownRegisterRestart(SessionStoreTestCase): + def setUp(self): + super().setUp(startup_page=3, no_auto_updates=False, win_register_restart=True) + + def test_manual_restart(self): + """Test that restore tabs works in case of register restart failure.""" + self.windows_shutdown_with_variety(restart_by_os=False, expect_restore=True) + + +class TestWindowsShutdownNormal(SessionStoreTestCase): + def setUp(self): + super().setUp(no_auto_updates=False) + + def test_with_variety(self): + """Test that windows are not restored on a normal restart.""" + self.windows_shutdown_with_variety(restart_by_os=False, expect_restore=False) + + +class TestWindowsShutdownForcedSessionRestore(SessionStoreTestCase): + def setUp(self): + super().setUp(no_auto_updates=False, win_register_restart=True) + + def test_os_restart(self): + """Test that register application restart restores the session.""" + self.windows_shutdown_with_variety(restart_by_os=True, expect_restore=True) + + def test_manual_restart(self): + """Test that OS shutdown is ignored on manual start.""" + self.windows_shutdown_with_variety(restart_by_os=False, expect_restore=False) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_splitview_restore.py b/src/zen/tests/mochitests/sessionstore/marionette/test_splitview_restore.py new file mode 100644 index 000000000..81239d34d --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_splitview_restore.py @@ -0,0 +1,183 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# add this directory to the path +import os +import sys +import unittest +from urllib.parse import quote + +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + + +def inline(doc): + return f"data:text/html;charset=utf-8,{quote(doc)}" + + +# Each list element represents a window of tabs loaded at +# some testing URL +DEFAULT_WINDOWS = set([ + # Window 1. Note the comma after the inline call - + # this is Python's way of declaring a 1 item tuple. + ( + inline("""Lorem"""), + inline("""Ipsum"""), + inline("""Dolor"""), + inline("""sit"""), + inline("""amet"""), + ), +]) + + +class TestAutoRestoreWithSplitView(SessionStoreTestCase): + def setUp(self): + super().setUp( + startup_page=3, + include_private=False, + restore_on_demand=True, + test_windows=DEFAULT_WINDOWS, + ) + self.marionette.set_context("chrome") + + def test_splitview_restored_after_quit(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + gBrowser.addTabSplitView([gBrowser.tabs[0], gBrowser.tabs[1]], { + insertBefore: gBrowser.tabs[0], + }); + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flushWindow(gBrowser.documentGlobal).then(resolve); + """ + ) + + self.assertEqual( + self.marionette.execute_script( + "return gBrowser.tabs[1].splitview.tabs.length" + ), + 2, + "There is a splitview with two tabs", + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script( + "return gBrowser.tabs[1].splitview.tabs.length" + ), + 2, + "Splitview with two tabs restored", + ) + + self.assertIsNone( + self.marionette.execute_script("return gBrowser.activeSplitView"), + "Split view is not activated on restore when the selected tab is not in the split view", + ) + + def test_splitview_restored_multiple_after_quit(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + gBrowser.addTabSplitView([gBrowser.tabs[0], gBrowser.tabs[1]], { + insertBefore: gBrowser.tabs[0], + }); + gBrowser.addTabSplitView([gBrowser.tabs[2], gBrowser.tabs[3]], { + insertBefore: gBrowser.tabs[2], + }); + gBrowser.selectedTab = gBrowser.tabs[2]; + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flushWindow(gBrowser.documentGlobal).then(resolve); + """ + ) + + self.assertEqual( + self.marionette.execute_script( + "return gBrowser.tabs[2].splitview.tabs.length" + ), + 2, + "There is a second splitview with two tabs", + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertTrue( + self.marionette.execute_script( + "return gBrowser.tabs[1].splitview.tabs.length && gBrowser.tabs[2].splitview.tabs.length" + ), + "Two split views have been restored", + ) + + self.assertTrue( + self.marionette.execute_script( + "return gBrowser.selectedTab == gBrowser.tabs[2]" + ), + "Third tab that's in a splitview is the selected tab", + ) + self.assertTrue( + self.marionette.execute_script( + "return gBrowser.activeSplitView != gBrowser.tabs[0].splitview && gBrowser.activeSplitView == gBrowser.tabs[2].splitview" + ), + "Split view is not activated on restore when the selected tab does not belong to that split view", + ) + + @unittest.skipIf( + sys.platform.startswith("darwin"), + "macOS does not close Firefox when the last window closes", + ) + def test_splitview_restored_after_closing_last_window(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + gBrowser.addTabSplitView([gBrowser.tabs[0], gBrowser.tabs[1]], { + insertBefore: gBrowser.tabs[0], + }); + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flushWindow(gBrowser.documentGlobal).then(resolve); + """ + ) + + self.assertEqual( + self.marionette.execute_script( + """ + return gBrowser.tabs[0].splitview.tabs.length + """ + ), + 2, + "There is a splitview with two tabs", + ) + + self.marionette.quit(callback=self._close_window) + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script( + "return gBrowser.tabs[0].splitview.tabs.length" + ), + 2, + "Splitview with two tabs restored", + ) + + self.assertIsNone( + self.marionette.execute_script("return gBrowser.activeSplitView"), + "Split view is not activated on restore when the selected tab is not in the split view", + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_tabgroups_restore.py b/src/zen/tests/mochitests/sessionstore/marionette/test_tabgroups_restore.py new file mode 100644 index 000000000..c4c13ea6a --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_tabgroups_restore.py @@ -0,0 +1,185 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# add this directory to the path +import os +import sys +import unittest +from urllib.parse import quote + +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + + +def inline(doc): + return f"data:text/html;charset=utf-8,{quote(doc)}" + + +# Each list element represents a window of tabs loaded at +# some testing URL +DEFAULT_WINDOWS = set([ + # Window 1. Note the comma after the inline call - + # this is Python's way of declaring a 1 item tuple. + (inline("""Lorem"""), inline("""Ipsum""")), +]) + + +class TestAutoRestoreWithTabGroups(SessionStoreTestCase): + def setUp(self): + super().setUp( + startup_page=3, + include_private=False, + restore_on_demand=True, + test_windows=DEFAULT_WINDOWS, + ) + self.marionette.set_context("chrome") + + def test_saved_groups_restored_after_quit(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + let group = gBrowser.addTabGroup([gBrowser.tabs[0]], { id: "test-group", label: "test-group" }); + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flushWindow(gBrowser.documentGlobal).then(resolve); + """ + ) + + self.assertEqual( + self.marionette.execute_script("return gBrowser.getAllTabGroups().length"), + 1, + "There is one open group", + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script("return gBrowser.getAllTabGroups().length"), + 1, + "There is one open group", + ) + + self.assertEqual( + self.marionette.execute_script("return SessionStore.savedGroups.length"), + 0, + "The group was not saved because it was automatically restored", + ) + + self.marionette.execute_script( + """ + let group = gBrowser.getTabGroupById("test-group"); + group.documentGlobal.SessionStore.addSavedTabGroup(group); + group.documentGlobal.gBrowser.removeTabGroup(group, { animate: false }); + """ + ) + + self.assertEqual( + self.marionette.execute_script("return SessionStore.savedGroups.length"), + 1, + "The group is now saved", + ) + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script("return gBrowser.getAllTabGroups().length"), + 0, + "The group was not automatically restored because it was manually saved", + ) + + self.assertEqual( + self.marionette.execute_script("return SessionStore.savedGroups.length"), + 1, + "The saved group persists after a second restart", + ) + + self.marionette.execute_script( + """ + SessionStore.forgetSavedTabGroup("test-group"); + """ + ) + + @unittest.skipIf( + sys.platform.startswith("darwin"), + "macOS does not close Firefox when the last window closes", + ) + def test_saved_groups_restored_after_closing_last_window(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.execute_async_script( + """ + let resolve = arguments[0]; + let group = gBrowser.addTabGroup([gBrowser.tabs[0]], { id: "test-group", label: "test-group" }); + let { TabStateFlusher } = ChromeUtils.importESModule("resource:///modules/sessionstore/TabStateFlusher.sys.mjs"); + TabStateFlusher.flushWindow(gBrowser.documentGlobal).then(resolve); + """ + ) + + self.assertEqual( + self.marionette.execute_script("return gBrowser.getAllTabGroups().length"), + 1, + "There is one open group", + ) + + self.marionette.quit(callback=self._close_window) + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script("return gBrowser.getAllTabGroups().length"), + 1, + "There is one open group", + ) + + self.assertEqual( + self.marionette.execute_script("return SessionStore.savedGroups.length"), + 0, + "The group was not saved because it was automatically restored", + ) + + self.marionette.execute_script( + """ + let group = gBrowser.getTabGroupById("test-group"); + group.documentGlobal.SessionStore.addSavedTabGroup(group); + group.documentGlobal.gBrowser.removeTabGroup(group, { animate: false }); + """ + ) + + self.assertEqual( + self.marionette.execute_script("return SessionStore.savedGroups.length"), + 1, + "The group is now saved", + ) + + self.marionette.quit(callback=self._close_window) + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + self.marionette.execute_script("return gBrowser.getAllTabGroups().length"), + 0, + "The group was not automatically restored because it was manually saved", + ) + + self.assertEqual( + self.marionette.execute_script("return SessionStore.savedGroups.length"), + 1, + "The saved group persists after a second restart", + ) + + self.marionette.execute_script( + """ + SessionStore.forgetSavedTabGroup("test-group"); + """ + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_tabnotes_canonicalurl_restore.py b/src/zen/tests/mochitests/sessionstore/marionette/test_tabnotes_canonicalurl_restore.py new file mode 100644 index 000000000..bfa41cc4d --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_tabnotes_canonicalurl_restore.py @@ -0,0 +1,195 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +from urllib.parse import quote + +sys.path.append(os.path.dirname(__file__)) + +from marionette_driver import Wait +from session_store_test_case import SessionStoreTestCase + + +def inline(doc): + return f"data:text/html;charset=utf-8,{quote(doc)}" + + +CANONICAL_URL = "https://example.com/canonical" + + +class TestCanonicalUrlRestore(SessionStoreTestCase): + """ + Test that canonicalUrl and hasTabNote are properly restored on pending + tabs after browser restart with session restore enabled. + + Uses two tabs with restore_on_demand=True: + - tabs[0]: pending tab (not selected, stays pending) + - tabs[1]: selected tab (the last one opened) + """ + + def setUp(self): + super().setUp( + startup_page=3, # Restore previous session + include_private=False, + restore_on_demand=True, # Keep non-selected tabs pending + test_windows=set([ + # Two tabs - the last one opened will be selected + ( + inline("
Tab 1
"), + inline("
Tab 2
"), + ), + ]), + ) + self.marionette.set_context("chrome") + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.tabs.notes.enabled", true); + """ + ) + + def test_hasTabNote_set_after_restart(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + tab_count = self.marionette.execute_script("return gBrowser.tabs.length;") + self.assertEqual(tab_count, 2, "Should have 2 tabs") + + # Set canonicalUrl and note on tabs[0] (will be pending after restart) + self.marionette.execute_async_script( + f""" + let resolve = arguments[0]; + (async () => {{ + const {{ TabNotes }} = ChromeUtils.importESModule( + "moz-src:///browser/components/tabnotes/TabNotes.sys.mjs" + ); + + await TabNotes.init(); + + let pendingTab = gBrowser.tabs[0]; + pendingTab.canonicalUrl = "{CANONICAL_URL}"; + await TabNotes.set(pendingTab, "Note for pending tab"); + + let {{ TabStateFlusher }} = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" + ); + await TabStateFlusher.flushWindow(gBrowser.documentGlobal); + }})().then(resolve); + """ + ) + + # Verify state before restart + result_before = self.marionette.execute_script( + """ + return { + canonicalUrl: gBrowser.tabs[0].canonicalUrl, + hasTabNote: gBrowser.tabs[0].hasTabNote + }; + """ + ) + self.assertEqual( + result_before["canonicalUrl"], + CANONICAL_URL, + "canonicalUrl should be set before restart", + ) + self.assertTrue( + result_before["hasTabNote"], + "hasTabNote should be true before restart", + ) + + # Restart browser + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + # Wait for hasTabNote to be set on the pending tab (async due to database check) + Wait(self.marionette, timeout=10, interval=0.2).until( + lambda _: self.marionette.execute_script( + """ + return gBrowser.tabs[0].hasTabNote === true; + """ + ), + message="hasTabNote should be set on pending tab after restart", + ) + + # Verify state after restart + result = self.marionette.execute_script( + """ + let pendingTab = gBrowser.tabs[0]; + return { + isPending: pendingTab.hasAttribute("pending"), + canonicalUrl: pendingTab.canonicalUrl, + hasTabNote: pendingTab.hasTabNote + }; + """ + ) + + self.assertTrue(result["isPending"], "Tab should be pending after restore") + self.assertEqual( + result["canonicalUrl"], + CANONICAL_URL, + "canonicalUrl should be restored on pending tab", + ) + self.assertTrue( + result["hasTabNote"], + "hasTabNote should be true for pending tab with existing note", + ) + + def test_canonicalUrl_cleared_when_feature_disabled(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + # Set canonicalUrl on a tab + self.marionette.execute_async_script( + f""" + let resolve = arguments[0]; + (async () => {{ + let tab = gBrowser.tabs[0]; + tab.canonicalUrl = "{CANONICAL_URL}"; + + let {{ TabStateFlusher }} = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" + ); + await TabStateFlusher.flushWindow(gBrowser.documentGlobal); + }})().then(resolve); + """ + ) + + canonicalUrl = self.marionette.execute_script( + "return gBrowser.tabs[0].canonicalUrl;" + ) + self.assertEqual( + canonicalUrl, CANONICAL_URL, "canonicalUrl should be set before restart" + ) + + # Restart browser with tab notes disabled + self.marionette.execute_script( + """ + Services.prefs.setBoolPref("browser.tabs.notes.enabled", false); + """ + ) + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + result = self.marionette.execute_script( + """ + let tab = gBrowser.tabs[0]; + return { + canonicalUrl: tab.canonicalUrl, + isPending: tab.hasAttribute("pending") + }; + """ + ) + + self.assertTrue(result["isPending"], "Tab should be pending after restore") + self.assertIsNone( + result["canonicalUrl"], + "canonicalUrl should be cleared when feature is disabled", + ) + + def test_hasTabNote_false_when_no_note(self): + self.skipTest("To be added in Bug 2014766 pending TabNotesController updates") diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_taskbartab_restore.py b/src/zen/tests/mochitests/sessionstore/marionette/test_taskbartab_restore.py new file mode 100644 index 000000000..e332fef87 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_taskbartab_restore.py @@ -0,0 +1,132 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import os +import sys + +from marionette_driver import Wait + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) +from session_store_test_case import SessionStoreTestCase + + +def inline(title): + return f"data:text/html;charset=utf-8,{title}" + + +class TestManualRestoreWithTaskbarTabs(SessionStoreTestCase): + def setUp(self): + super().setUp( + startup_page=1, + include_private=False, + restore_on_demand=False, + taskbartabs_enable=True, + test_windows=set([ + # Window 1 + ( + inline("lorem ipsom"), + inline("dolor"), + ), + ]), + ) + + """ + Close all regular windows except for a taskbar tab window. The + session should be over at this point. Opening another regular Firefox + window will open "restore previous session" in the hamburger menu. + And clicking it will restore the correct session. + """ + + def test_restore_without_closing_taskbartab(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + # See session_store_test_case.py + self.setup_taskbartab_restore_scenario() + + # Verify that "restore previous session" button + # is visible in the hamburger menu + self.assertEqual( + self.marionette.execute_script( + """ + let newWindow = BrowserWindowTracker.getTopWindow({ allowTaskbarTabs: false }); + return PanelMultiView.getViewNode( + newWindow.document, + "appMenu-restoreSession" + ).hasAttribute("disabled"); + """ + ), + False, + "The restore last session button should be visible", + ) + + # Simulate clicking "restore previous session" + self.marionette.execute_script( + """ + SessionStore.restoreLastSession(); + """ + ) + + # Wait for the restore to be completed, + # meaning the window we opened should have + # two tabs again. + Wait(self.marionette).until( + lambda mn: ( + mn.execute_script( + """ + let newWindow = BrowserWindowTracker.getTopWindow({ allowTaskbarTabs: false }); + return newWindow.gBrowser.tabs.length; + """ + ) + == 2 + ) + ) + + +class TestAutoRestoreWithTaskbarTabs(SessionStoreTestCase): + def setUp(self): + super().setUp( + startup_page=3, + include_private=False, + restore_on_demand=False, + taskbartabs_enable=True, + test_windows=set([ + # Window 1 + ( + inline("lorem ipsom"), + inline("dolor"), + ), + ]), + ) + + """ + Close all regular windows except for a taskbar tab window. The + session should be over at this point. Opening another regular Firefox + window will open automatically restore the correct session + """ + + def test_restore_without_closing_taskbartab(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.setup_taskbartab_restore_scenario() + + # Wait for the auto restore to be completed, + # meaning the window we opened should have + # the original two tabs plus the home page tab. + Wait(self.marionette).until( + lambda mn: ( + mn.execute_script( + """ + let newWindow = BrowserWindowTracker.getTopWindow({ allowTaskbarTabs: false }); + return newWindow.gBrowser.tabs.length; + """ + ) + == 3 + ) + ) diff --git a/src/zen/tests/mochitests/sessionstore/marionette/test_taskbartab_sessionstate.py b/src/zen/tests/mochitests/sessionstore/marionette/test_taskbartab_sessionstate.py new file mode 100644 index 000000000..7f75f27df --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/marionette/test_taskbartab_sessionstate.py @@ -0,0 +1,87 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import os +import sys + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) +from session_store_test_case import SessionStoreTestCase + + +def inline(title): + return f"data:text/html;charset=utf-8,{title}" + + +class TestTaskbarTabSessionState(SessionStoreTestCase): + def setUp(self): + super().setUp( + startup_page=1, + include_private=False, + restore_on_demand=False, + taskbartabs_enable=True, + test_windows=set([ + # Window 1 + ( + inline("lorem ipsom"), + inline("dolor"), + ), + ]), + ) + + """ + Close all Firefox windows with the web app being closed last, + the session store state should include the last regular window + that's closed, but not the web app + """ + + def test_taskbartab_session_state(self): + self.wait_for_windows( + self.all_windows, "Not all requested windows have been opened" + ) + + self.marionette.enforce_gecko_prefs({"browser.taskbarTabs.enabled": True}) + + self.open_taskbartab_window() + + # Close the original regular Firefox window + taskbar_tab_window_handle = self.marionette.close_chrome_window()[0] + self.marionette.switch_to_window(taskbar_tab_window_handle) + + self.marionette.set_context("content") + dummy_html = self.marionette.absolute_url("empty.html") + self.marionette.navigate(dummy_html) + self.marionette.set_context("chrome") + + self.marionette.quit() + self.marionette.start_session() + self.marionette.set_context("chrome") + + # check for the session store state, it should have only two tabs and one window + self.assertEqual( + self.marionette.execute_script( + """ + const { _LastSession } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionStore.sys.mjs" + ); + return _LastSession.getState().windows.length + """ + ), + 1, + "One window should be in the session state", + ) + + self.assertEqual( + self.marionette.execute_script( + """ + const { _LastSession } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionStore.sys.mjs" + ); + return _LastSession.getState().windows[0].tabs.length + """ + ), + 2, + "Two tabs should be in the session state", + ) diff --git a/src/zen/tests/mochitests/sessionstore/restore_redirect_http.html b/src/zen/tests/mochitests/sessionstore/restore_redirect_http.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/zen/tests/mochitests/sessionstore/restore_redirect_http.html^headers^ b/src/zen/tests/mochitests/sessionstore/restore_redirect_http.html^headers^ new file mode 100644 index 000000000..533bda36f --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/restore_redirect_http.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Moved Temporarily +Location: restore_redirect_target.html diff --git a/src/zen/tests/mochitests/sessionstore/restore_redirect_js.html b/src/zen/tests/mochitests/sessionstore/restore_redirect_js.html new file mode 100644 index 000000000..f0130847b --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/restore_redirect_js.html @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/zen/tests/mochitests/sessionstore/restore_redirect_target.html b/src/zen/tests/mochitests/sessionstore/restore_redirect_target.html new file mode 100644 index 000000000..813af0550 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/restore_redirect_target.html @@ -0,0 +1,8 @@ + + + + +Test page + +Test page + diff --git a/src/zen/tests/mochitests/sessionstore/unit/data/sessionCheckpoints_all.json b/src/zen/tests/mochitests/sessionstore/unit/data/sessionCheckpoints_all.json new file mode 100644 index 000000000..e02c421c3 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/data/sessionCheckpoints_all.json @@ -0,0 +1,11 @@ +{ + "profile-after-change": true, + "final-ui-startup": true, + "sessionstore-windows-restored": true, + "quit-application-granted": true, + "quit-application": true, + "sessionstore-final-state-write-complete": true, + "profile-change-net-teardown": true, + "profile-change-teardown": true, + "profile-before-change": true +} diff --git a/src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_complete.json b/src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_complete.json new file mode 100644 index 000000000..92cb150ee --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_complete.json @@ -0,0 +1,907 @@ +{ + "version": ["sessionrestore", 1], + "windows": [ + { + "tabs": [ + { + "entries": [ + { + "url": "about:home", + "title": "New Tab", + "cacheKey": 0, + "ID": 889527166, + "docshellUUID": "{1c51a53b-d2b5-4484-8deb-aef7bd44abc4}", + "resultPrincipalURI": null, + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{6ec4bec4-5873-4b33-b269-be6653e318d8}\"}}", + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 12, + "transient": false, + "navigationKey": "{d0185d51-bd9f-4f90-afb2-f40547ac08a8}", + "navigationId": "{08e9abae-27f4-4cbb-8eb6-1524776e7327}" + }, + { + "url": "about:mozilla", + "title": "The Book of Mozilla, 6:27", + "cacheKey": 0, + "ID": 889527167, + "docshellUUID": "{1c51a53b-d2b5-4484-8deb-aef7bd44abc4}", + "resultPrincipalURI": null, + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{b5d58330-101d-4601-9291-6435ebcb854e}\"}}", + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 13, + "transient": false, + "navigationKey": "{e546bd8d-4813-4a8a-8e90-0cdde6b5ab72}", + "navigationId": "{7fd8f06e-2863-4f78-85f4-a4684c635086}" + } + ], + "lastAccessed": 1770416063211, + "hidden": false, + "groupId": "1770415115529-1", + "searchMode": null, + "userContextId": 0, + "attributes": {}, + "index": 2, + "requestedIndex": 0, + "image": null + }, + { + "entries": [ + { + "url": "about:newtab", + "title": "New Tab", + "cacheKey": 0, + "ID": 889527170, + "docshellUUID": "{739fd540-1ff5-4d46-8e55-896b64d6cfb3}", + "resultPrincipalURI": null, + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 14, + "transient": false, + "navigationKey": "{c3718cd7-fac1-4ca7-b728-73b028e8e26a}", + "navigationId": "{b0b9cd99-3b46-4b98-b032-2d9ab1d649c0}" + }, + { + "url": "about:support", + "title": "Troubleshooting Information", + "cacheKey": 0, + "ID": 889527171, + "docshellUUID": "{739fd540-1ff5-4d46-8e55-896b64d6cfb3}", + "resultPrincipalURI": null, + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{675de4b7-6fd5-47d4-a538-50bbcdca4c6c}\"}}", + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 15, + "transient": false, + "navigationKey": "{f8decd0b-ac89-4e55-892e-3e8ed627ba0e}", + "navigationId": "{ad8b07d9-29bf-4281-806a-c3534fc58d84}" + }, + { + "url": "about:preferences", + "title": "Settings", + "cacheKey": 0, + "ID": 889527172, + "docshellUUID": "{739fd540-1ff5-4d46-8e55-896b64d6cfb3}", + "originalURI": "about:preferences", + "resultPrincipalURI": null, + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{a6850bcd-5678-4594-9311-0affec22d009}\"}}", + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 16, + "structuredCloneState": "AgAAAAAA8f8LAACABAD//3BhbmVHZW5lcmFsAAAAAAA=", + "structuredCloneVersion": 8, + "children": [ + { + "url": "about:blank", + "title": "about:blank", + "cacheKey": 0, + "ID": 889527173, + "docshellUUID": "{5cdf1a82-9508-4a80-8884-312cb35c4b5a}", + "resultPrincipalURI": null, + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "policyContainer": "ydqGXsPXSqGicQ9XHwE8MAAAAAAAAAAAwAAAAAAAAEYAAAABAQnZ7Rrl1EAEv+Anzrkj2awdYyAIbJdIrqUcFuLaoPT2AeDaHXAvexHTjNAAYLD8FKOSBzpUbXhPMJE6uHGBMgjGAAAAAAVhYm91dAAAAAtwcmVmZXJlbmNlcwAAAAAACHsiMyI6e319AAAAAQAAAI8AZABlAGYAYQB1AGwAdAAtAHMAcgBjACAAYwBoAHIAbwBtAGUAOgA7ACAAaQBtAGcALQBzAHIAYwAgAGMAaAByAG8AbQBlADoAIABtAG8AegAtAGkAYwBvAG4AOgAgAG0AbwB6AC0AcgBlAG0AbwB0AGUALQBpAG0AYQBnAGUAOgAgAGgAdAB0AHAAcwA6ACAAYgBsAG8AYgA6ACAAZABhAHQAYQA6ADsAIABzAHQAeQBsAGUALQBzAHIAYwAgAGMAaAByAG8AbQBlADoAIABkAGEAdABhADoAIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwA7ACAAbwBiAGoAZQBjAHQALQBzAHIAYwAgACcAbgBvAG4AZQAnAAEAAUgS/zIb/0I7l/xApWltrSJIEv8yG/9CO5f8QKVpba0iAAAAAQAA", + "docIdentifier": 17, + "transient": true, + "navigationKey": "{d292a0e0-a212-4147-a523-7196c1d82c09}", + "navigationId": "{72db572a-c072-4aa9-b282-e22fdb01150d}" + }, + { + "url": "about:blank", + "title": "about:blank", + "cacheKey": 0, + "ID": 20, + "docshellUUID": "{99483b7c-4429-4610-970d-8e43b680cda6}", + "resultPrincipalURI": null, + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "policyContainer": "ydqGXsPXSqGicQ9XHwE8MAAAAAAAAAAAwAAAAAAAAEYAAAABAQnZ7Rrl1EAEv+Anzrkj2awdYyAIbJdIrqUcFuLaoPT2AeDaHXAvexHTjNAAYLD8FKOSBzpUbXhPMJE6uHGBMgjGAAAAAAVhYm91dAAAAAtwcmVmZXJlbmNlcwAAAAAACHsiMyI6e319AAAAAQAAAI8AZABlAGYAYQB1AGwAdAAtAHMAcgBjACAAYwBoAHIAbwBtAGUAOgA7ACAAaQBtAGcALQBzAHIAYwAgAGMAaAByAG8AbQBlADoAIABtAG8AegAtAGkAYwBvAG4AOgAgAG0AbwB6AC0AcgBlAG0AbwB0AGUALQBpAG0AYQBnAGUAOgAgAGgAdAB0AHAAcwA6ACAAYgBsAG8AYgA6ACAAZABhAHQAYQA6ADsAIABzAHQAeQBsAGUALQBzAHIAYwAgAGMAaAByAG8AbQBlADoAIABkAGEAdABhADoAIAAnAHUAbgBzAGEAZgBlAC0AaQBuAGwAaQBuAGUAJwA7ACAAbwBiAGoAZQBjAHQALQBzAHIAYwAgACcAbgBvAG4AZQAnAAEAAUgS/zIb/0I7l/xApWltrSJIEv8yG/9CO5f8QKVpba0iAAAAAQAA", + "docIdentifier": 22, + "transient": true, + "navigationKey": "{6dfc5997-9767-471e-a092-3d3cb72d5c18}", + "navigationId": "{949e9fde-2b4d-4588-8bfe-807a46c38f02}" + } + ], + "transient": false, + "navigationKey": "{f8241c63-5204-446f-82f4-9a200b7a57eb}", + "navigationId": "{86153ed4-da40-4d2a-8017-8268fc40158f}" + } + ], + "lastAccessed": 1770416070546, + "hidden": false, + "splitViewId": 1, + "searchMode": null, + "userContextId": 0, + "attributes": {}, + "index": 3, + "requestedIndex": 0, + "formdata": { + "url": "about:preferences", + "id": { + "searchInput": { + "state": "", + "value": "" + }, + "browserRestoreSession": { + "state": "on", + "value": "on" + }, + "windowsLaunchOnLogin": { + "state": "on", + "value": "on" + }, + "alwaysCheckDefault": { + "state": "on", + "value": "on" + }, + "linkTargeting": { + "state": "on", + "value": "on" + }, + "switchToNewTabs": { + "state": "on", + "value": "on" + }, + "openAppLinksNextToActiveTab": { + "state": "on", + "value": "on" + }, + "warnOpenMany": { + "state": "on", + "value": "on" + }, + "ctrlTabRecentlyUsedOrder": { + "state": "on", + "value": "on" + }, + "tabPreviewShowThumbnails": { + "state": "on", + "value": "on" + }, + "tabGroupSuggestions": { + "state": "on", + "value": "on" + }, + "showTabsInTaskbar": { + "state": "on", + "value": "on" + }, + "browserContainersCheckbox": { + "state": "on", + "value": "on" + }, + "warnCloseMultiple": { + "state": "on", + "value": "on" + }, + "warnOnQuitKey": { + "state": "on", + "value": "on" + }, + "browserLayoutRadioGroup": { + "state": "false", + "value": "false" + }, + "browserLayoutShowSidebar": { + "state": "on", + "value": "on" + }, + "web-appearance-chooser": { + "state": "auto", + "value": "auto" + }, + "contrastControlSettings": { + "state": "1", + "value": "1" + }, + "contrastSettingsAuto": { + "state": "0", + "value": "0" + }, + "contrastSettingsOff": { + "state": "1", + "value": "1" + }, + "contrastSettingsOn": { + "state": "2", + "value": "2" + }, + "defaultZoom": { + "state": "100", + "value": "100" + }, + "zoomText": { + "state": "on", + "value": "on" + }, + "chooseFolder": { + "state": "/home/sfoster/Downloads", + "value": "/home/sfoster/Downloads" + }, + "alwaysAsk": { + "state": "on", + "value": "on" + }, + "deletePrivate": { + "state": "on", + "value": "on" + }, + "filter": { + "state": "", + "value": "" + }, + "playDRMContent": { + "state": "on", + "value": "on" + }, + "useRecommendedPerformanceSettings": { + "state": "on", + "value": "on" + }, + "allowHWAccel": { + "state": "on", + "value": "on" + }, + "useAutoScroll": { + "state": "on", + "value": "on" + }, + "useSmoothScrolling": { + "state": "on", + "value": "on" + }, + "useOverlayScrollbars": { + "state": "on", + "value": "on" + }, + "useOnScreenKeyboard": { + "state": "on", + "value": "on" + }, + "useCursorNavigation": { + "state": "on", + "value": "on" + }, + "useFullKeyboardNavigation": { + "state": "on", + "value": "on" + }, + "alwaysUnderlineLinks": { + "state": "on", + "value": "on" + }, + "searchStartTyping": { + "state": "on", + "value": "on" + }, + "pictureInPictureToggleEnabled": { + "state": "on", + "value": "on" + }, + "pictureInPictureEnableWhenSwitchingTabs": { + "state": "on", + "value": "on" + }, + "mediaControlToggleEnabled": { + "state": "on", + "value": "on" + }, + "cfrRecommendations": { + "state": "on", + "value": "on" + }, + "cfrRecommendations-features": { + "state": "on", + "value": "on" + }, + "linkPreviewEnabled": { + "state": "on", + "value": "on" + }, + "linkPreviewKeyPoints": { + "state": "on", + "value": "on" + }, + "linkPreviewLongPress": { + "state": "on", + "value": "on" + }, + "defaultEngineNormal": { + "state": "google", + "value": "google" + }, + "searchShowSearchTermCheckbox": { + "state": "on", + "value": "on" + }, + "browserSeparateDefaultEngine": { + "state": "on", + "value": "on" + }, + "defaultPrivateEngine": { + "state": "google", + "value": "google" + }, + "suggestionsInSearchFieldsCheckbox": { + "state": "on", + "value": "on" + }, + "urlBarSuggestionCheckbox": { + "state": "on", + "value": "on" + }, + "showSearchSuggestionsFirstCheckbox": { + "state": "on", + "value": "on" + }, + "showSearchSuggestionsPrivateWindowsCheckbox": { + "state": "on", + "value": "on" + }, + "showTrendingSuggestionsCheckbox": { + "state": "on", + "value": "on" + }, + "historySuggestion": { + "state": "on", + "value": "on" + }, + "bookmarkSuggestion": { + "state": "on", + "value": "on" + }, + "clipboardSuggestion": { + "state": "on", + "value": "on" + }, + "openpageSuggestion": { + "state": "on", + "value": "on" + }, + "topSitesSuggestion": { + "state": "on", + "value": "on" + }, + "enableRecentSearches": { + "state": "on", + "value": "on" + }, + "enginesSuggestion": { + "state": "on", + "value": "on" + }, + "enableQuickActions": { + "state": "on", + "value": "on" + }, + "firefoxSuggestAll": { + "state": "on", + "value": "on" + }, + "firefoxSuggestSponsored": { + "state": "on", + "value": "on" + }, + "firefoxSuggestOnlineEnabledToggle": { + "state": "on", + "value": "on" + }, + "contentBlockingBaselineExceptionsStrict": { + "state": "on", + "value": "on" + }, + "contentBlockingConvenienceExceptionsStrict": { + "state": "on", + "value": "on" + }, + "contentBlockingBaselineExceptionsCustom": { + "state": "on", + "value": "on" + }, + "contentBlockingConvenienceExceptionsCustom": { + "state": "on", + "value": "on" + }, + "gpcEnabled": { + "state": "on", + "value": "on" + }, + "ipProtectionAutoStartCheckbox": { + "state": "on", + "value": "on" + }, + "ipProtectionAutoStartPrivateCheckbox": { + "state": "on", + "value": "on" + }, + "deleteOnClose": { + "state": "on", + "value": "on" + }, + "saveAndFillPayments": { + "state": "on", + "value": "on" + }, + "requireOSAuthForPayments": { + "state": "on", + "value": "on" + }, + "saveAndFillAddresses": { + "state": "on", + "value": "on" + }, + "historyMode": { + "state": "remember", + "value": "remember" + }, + "privateBrowsingAutoStart": { + "state": "on", + "value": "on" + }, + "rememberHistory": { + "state": "on", + "value": "on" + }, + "rememberForms": { + "state": "on", + "value": "on" + }, + "alwaysClear": { + "state": "on", + "value": "on" + }, + "popupPolicy": { + "state": "on", + "value": "on" + }, + "warnAddonInstall": { + "state": "on", + "value": "on" + }, + "notificationsDoNotDisturb": { + "state": "on", + "value": "on" + }, + "enableSafeBrowsing": { + "state": "on", + "value": "on" + }, + "blockDownloads": { + "state": "on", + "value": "on" + }, + "blockUncommonUnwanted": { + "state": "on", + "value": "on" + }, + "certEnableThirdPartyToggle": { + "state": "on", + "value": "on" + }, + "httpsOnlyRadioGroup": { + "state": "disabled", + "value": "disabled" + }, + "httpsOnlyRadioEnabled": { + "state": "enabled", + "value": "enabled" + }, + "httpsOnlyRadioEnabledPBM": { + "state": "privateOnly", + "value": "privateOnly" + }, + "httpsOnlyRadioDisabled": { + "state": "disabled", + "value": "disabled" + }, + "firefox-labs-urlbar-ime-search": { + "state": "on", + "value": "on" + }, + "copyProfileSelect": { + "state": "", + "value": "" + }, + "aiControlTranslationsSelect": { + "state": "available", + "value": "available" + }, + "aiControlPdfjsAltTextSelect": { + "state": "available", + "value": "available" + }, + "aiControlSmartTabGroupsSelect": { + "state": "available", + "value": "available" + }, + "aiControlLinkPreviewKeyPointsSelect": { + "state": "available", + "value": "available" + }, + "aiControlSidebarChatbotSelect": { + "state": "available", + "value": "available" + }, + "learnFromChatActivity": { + "state": "on", + "value": "on" + }, + "learnFromBrowsingActivity": { + "state": "on", + "value": "on" + } + } + }, + "image": "chrome://global/skin/icons/settings.svg" + }, + { + "entries": [ + { + "url": "about:newtab", + "title": "New Tab", + "cacheKey": 0, + "ID": 889527170, + "docshellUUID": "{b164ef01-b988-4cf7-96c3-1caaa1dd02c3}", + "resultPrincipalURI": null, + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 18, + "transient": false, + "navigationKey": "{5cb78b82-cd07-4aa0-8a5d-0c63b1e58ba1}", + "navigationId": "{a5ff9eb9-724a-4c8a-8dcf-9f2655731aba}" + }, + { + "url": "about:robots", + "title": "Gort! Klaatu barada nikto!", + "cacheKey": 0, + "ID": 889527171, + "docshellUUID": "{b164ef01-b988-4cf7-96c3-1caaa1dd02c3}", + "resultPrincipalURI": null, + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{eccd56de-0975-43ff-bda3-8740fea45548}\"}}", + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 19, + "transient": false, + "navigationKey": "{39e9956c-07d7-4ed5-81f9-375f71a24981}", + "navigationId": "{4116d9c7-01a3-45d7-ac56-c59caef156c6}" + } + ], + "lastAccessed": 1770415081903, + "hidden": false, + "splitViewId": 1, + "searchMode": null, + "userContextId": 0, + "attributes": {}, + "index": 2, + "requestedIndex": 0, + "image": "chrome://browser/content/robot.ico" + } + ], + "groups": [ + { + "id": "1770415115529-1", + "name": "Abouts", + "color": "blue", + "collapsed": false, + "saveOnWindowClose": true, + "saved": true, + "closedAt": 1770416070547, + "windowClosedId": 7, + "tabs": [ + { + "state": { + "entries": [ + { + "url": "about:home", + "title": "New Tab", + "cacheKey": 0, + "ID": 889527166, + "docshellUUID": "{1c51a53b-d2b5-4484-8deb-aef7bd44abc4}", + "resultPrincipalURI": null, + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{6ec4bec4-5873-4b33-b269-be6653e318d8}\"}}", + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 12, + "transient": false, + "navigationKey": "{d0185d51-bd9f-4f90-afb2-f40547ac08a8}", + "navigationId": "{08e9abae-27f4-4cbb-8eb6-1524776e7327}" + }, + { + "url": "about:mozilla", + "title": "The Book of Mozilla, 6:27", + "cacheKey": 0, + "ID": 889527167, + "docshellUUID": "{1c51a53b-d2b5-4484-8deb-aef7bd44abc4}", + "resultPrincipalURI": null, + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{b5d58330-101d-4601-9291-6435ebcb854e}\"}}", + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 13, + "transient": false, + "navigationKey": "{e546bd8d-4813-4a8a-8e90-0cdde6b5ab72}", + "navigationId": "{7fd8f06e-2863-4f78-85f4-a4684c635086}" + } + ], + "lastAccessed": 1770416063211, + "hidden": false, + "groupId": "1770415115529-1", + "searchMode": null, + "userContextId": 0, + "attributes": {}, + "index": 2, + "requestedIndex": 0, + "image": null + }, + "title": "The Book of Mozilla, 6:27", + "image": null, + "closedAt": 1770416070547, + "closedId": 8 + } + ], + "splitViews": [] + } + ], + "closedGroups": [], + "selected": 2, + "_closedTabs": [ + { + "state": { + "entries": [ + { + "url": "https://www.mozilla.org/en-US/privacy/firefox/", + "title": "Firefox Privacy Notice \u2014 Mozilla", + "cacheKey": 0, + "ID": 9, + "docshellUUID": "{1f248ec8-bd6a-422d-b6b7-29a954883cf5}", + "originalURI": "https://www.mozilla.org/privacy/firefox/", + "resultPrincipalURI": "https://www.mozilla.org/en-US/privacy/firefox/", + "loadReplace": true, + "loadReplace2": true, + "contentType": "text/html", + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{273498ac-3366-4117-aadb-8d6cf137c161}?https://www.mozilla.org\"}}", + "hasUserInteraction": false, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 13, + "transient": false, + "navigationKey": "{866447d8-1c36-4f56-a99d-9fc7ae3d47a7}", + "navigationId": "{e95f717f-9e82-4278-8985-816681a20a95}" + } + ], + "lastAccessed": 1770415063435, + "hidden": false, + "searchMode": null, + "userContextId": 0, + "attributes": {}, + "index": 1, + "requestedIndex": 0, + "storage": { + "https://www.mozilla.org": { + "tcms": "1" + } + }, + "image": "data:image/x-icon;base64,AAABAAMAAAAAAAEAIACPAAAANgAAAAAAAAABACAAHgEAAMUAAAAAAAAAAQAgACsCAADjAQAAiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAVklEQVQ4T2NUV1H9z0ABYBx8Blis0mQ4EXYdzkYHMDkYwGkAskEwgE0MpwHIAOYSdHEQIGgAOh8dEDQABChyATLAJofTAJit6IBoA4gFg9AAUsHAGwAAReJNAc+NKZcAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAADlSURBVFhHY1RXUf3PMICAcdQBow4YdQAhB1is0gTTJ8KuQ0UgACZOCKDrQwdDzwHofGyAVYOdwbhJCczGpw4EqOYARgEmBj4LXgYxMz4GYR0euDi6OnRAsgOQAYcRFwO/NheDhDk/A6cYG1z899e/DC9PfmR4vu0dw99Hv+Hi2ABFDoDJwQDY0v0fGH6c+wYXIwSo6oC3V74wPNv/nuHb5W8M/z/8g4vjAxQ5AFcUgAAoNF6d+sTw9fAXuBg2QJEDkMGAJEJcgC7ZkFgw/BxAbTDqgFEHjDqAoANoDUYdMOqAAXcAAIu1rwGq2kmfAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAB8klEQVR4Xu3ZO0/CYBTG8bdcowW8JcawKDI7qzOuxq/iqrORD2FcdTauMKuzsxoHGZiI0ADlojkmPaltofSWvqHPf+pbkXB+9qYq1d29H5HgFAAAAAAA4FUCAwAAAAAAAPAqgQEgCMDb4zpvG5Uve2Lldcxra/2DjGhdF3gdRtXTDm97DQAAAEC0AK36/2EnqiLU5xGvvdat5cR4O8VrSmoAp9f4/cB09LTPV5MFQMN2zvJCO8raBjfy8n7WpASYqoronuTEdy0n9Eqa98/K7f3mJQ0ADU0/Ze04K7TDLO+fV+5jIkpNXaw9DHmf1yIHoPPWmvXrdGGj4QnBLWNo9WkkMu0p7/db5ADz+rwtzTyvzdGgxaYuSg09lKHNxQrg9P1GNCjdLmnw/PuE94edtAAb9wOxeTfgdVRJC0CFfb47FSsAPdR4vfgVG7pIab4/sq1YASg/tz/1ZfR3VNA1IihG7ADm6I5AEIs+AFGEsXOl8dprUgGYG+6n+fnA7Vbp9GC1aNICmHN7WFp6ACMa/qtesJ0eUgNY/x4QtGElbTsKpAZwek3YAQAAEgOEfQ1wqnzR422vRQ4gewAAAAAAAABeeQwAAAAAbxsBAAAAAEBiAJx+1d266Uf6v7ywCwSwDAEAAAAAAAB4lcAAAAAAJBvgF12b0JDX5aCIAAAAAElFTkSuQmCC" + }, + "title": "Firefox Privacy Notice \u2014 Mozilla", + "image": "data:image/x-icon;base64,AAABAAMAAAAAAAEAIACPAAAANgAAAAAAAAABACAAHgEAAMUAAAAAAAAAAQAgACsCAADjAQAAiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAVklEQVQ4T2NUV1H9z0ABYBx8Blis0mQ4EXYdzkYHMDkYwGkAskEwgE0MpwHIAOYSdHEQIGgAOh8dEDQABChyATLAJofTAJit6IBoA4gFg9AAUsHAGwAAReJNAc+NKZcAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAADlSURBVFhHY1RXUf3PMICAcdQBow4YdQAhB1is0gTTJ8KuQ0UgACZOCKDrQwdDzwHofGyAVYOdwbhJCczGpw4EqOYARgEmBj4LXgYxMz4GYR0euDi6OnRAsgOQAYcRFwO/NheDhDk/A6cYG1z899e/DC9PfmR4vu0dw99Hv+Hi2ABFDoDJwQDY0v0fGH6c+wYXIwSo6oC3V74wPNv/nuHb5W8M/z/8g4vjAxQ5AFcUgAAoNF6d+sTw9fAXuBg2QJEDkMGAJEJcgC7ZkFgw/BxAbTDqgFEHjDqAoANoDUYdMOqAAXcAAIu1rwGq2kmfAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAB8klEQVR4Xu3ZO0/CYBTG8bdcowW8JcawKDI7qzOuxq/iqrORD2FcdTauMKuzsxoHGZiI0ADlojkmPaltofSWvqHPf+pbkXB+9qYq1d29H5HgFAAAAAAA4FUCAwAAAAAAAPAqgQEgCMDb4zpvG5Uve2Lldcxra/2DjGhdF3gdRtXTDm97DQAAAEC0AK36/2EnqiLU5xGvvdat5cR4O8VrSmoAp9f4/cB09LTPV5MFQMN2zvJCO8raBjfy8n7WpASYqoronuTEdy0n9Eqa98/K7f3mJQ0ADU0/Ze04K7TDLO+fV+5jIkpNXaw9DHmf1yIHoPPWmvXrdGGj4QnBLWNo9WkkMu0p7/db5ADz+rwtzTyvzdGgxaYuSg09lKHNxQrg9P1GNCjdLmnw/PuE94edtAAb9wOxeTfgdVRJC0CFfb47FSsAPdR4vfgVG7pIab4/sq1YASg/tz/1ZfR3VNA1IihG7ADm6I5AEIs+AFGEsXOl8dprUgGYG+6n+fnA7Vbp9GC1aNICmHN7WFp6ACMa/qtesJ0eUgNY/x4QtGElbTsKpAZwek3YAQAAEgOEfQ1wqnzR422vRQ4gewAAAAAAAABeeQwAAAAAbxsBAAAAAEBiAJx+1d266Uf6v7ywCwSwDAEAAAAAAAB4lcAAAAAAJBvgF12b0JDX5aCIAAAAAElFTkSuQmCC", + "pos": 1, + "closedAt": 1770415063436, + "closedInTabGroupId": null, + "sourceWindowId": "window0", + "closedId": 6, + "_originalStateIndex": 0 + } + ], + "_lastClosedTabGroupCount": -1, + "lastClosedTabGroupId": null, + "chromeFlags": 2150633470, + "sizemodeBeforeMinimized": "normal", + "splitViews": [ + { + "id": 1, + "numberOfTabs": 2 + } + ], + "width": 1332, + "height": 1024, + "screenX": 24, + "screenY": 27, + "sizemode": "normal", + "sidebar": { + "command": "", + "panelOpen": false, + "launcherWidth": 0, + "launcherExpanded": false, + "launcherVisible": false + }, + "isAIWindow": false, + "zIndex": 1, + "title": "Settings", + "_shouldRestore": true, + "closedAt": 1770416070547, + "closedId": 7 + } + ], + "selectedWindow": 0, + "_closedWindows": [], + "savedGroups": [ + { + "id": "1770415115529-1", + "name": "Abouts", + "color": "blue", + "collapsed": false, + "saveOnWindowClose": true, + "saved": true, + "closedAt": 1770416070547, + "windowClosedId": 7, + "tabs": [ + { + "state": { + "entries": [ + { + "url": "about:home", + "title": "New Tab", + "cacheKey": 0, + "ID": 889527166, + "docshellUUID": "{1c51a53b-d2b5-4484-8deb-aef7bd44abc4}", + "resultPrincipalURI": null, + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{6ec4bec4-5873-4b33-b269-be6653e318d8}\"}}", + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 12, + "transient": false, + "navigationKey": "{d0185d51-bd9f-4f90-afb2-f40547ac08a8}", + "navigationId": "{08e9abae-27f4-4cbb-8eb6-1524776e7327}" + }, + { + "url": "about:mozilla", + "title": "The Book of Mozilla, 6:27", + "cacheKey": 0, + "ID": 889527167, + "docshellUUID": "{1c51a53b-d2b5-4484-8deb-aef7bd44abc4}", + "resultPrincipalURI": null, + "principalToInherit_base64": "{\"0\":{\"0\":\"moz-nullprincipal:{b5d58330-101d-4601-9291-6435ebcb854e}\"}}", + "hasUserInteraction": true, + "triggeringPrincipal_base64": "{\"3\":{}}", + "docIdentifier": 13, + "transient": false, + "navigationKey": "{e546bd8d-4813-4a8a-8e90-0cdde6b5ab72}", + "navigationId": "{7fd8f06e-2863-4f78-85f4-a4684c635086}" + } + ], + "lastAccessed": 1770416063211, + "hidden": false, + "groupId": "1770415115529-1", + "searchMode": null, + "userContextId": 0, + "attributes": {}, + "index": 2, + "requestedIndex": 0, + "image": null + }, + "title": "The Book of Mozilla, 6:27", + "image": null, + "closedAt": 1770416070547, + "closedId": 8 + } + ], + "splitViews": [] + } + ], + "maxSplitViewId": 1, + "session": { + "lastUpdate": 1770416070757, + "startTime": 1770415040600, + "recentCrashes": 0 + }, + "global": {}, + "cookies": [ + { + "host": "addons.mozilla.org", + "value": "ef51d7121d46a4fd86e874d8a850d58e7ef917a047014268bf1d3be112b72b77", + "path": "/", + "name": "taarId", + "secure": true, + "httponly": true, + "originAttributes": { + "firstPartyDomain": "", + "geckoViewSessionContextId": "", + "partitionKey": "", + "privateBrowsingId": 0, + "userContextId": 3 + }, + "sameSite": 1, + "schemeMap": 2 + }, + { + "host": "addons.mozilla.org", + "value": "ef51d7121d46a4fd86e874d8a850d58e7ef917a047014268bf1d3be112b72b77", + "path": "/", + "name": "taarId", + "secure": true, + "httponly": true, + "originAttributes": { + "firstPartyDomain": "", + "geckoViewSessionContextId": "", + "partitionKey": "", + "privateBrowsingId": 0, + "userContextId": 4 + }, + "sameSite": 1, + "schemeMap": 2 + }, + { + "host": "addons.mozilla.org", + "value": "ef51d7121d46a4fd86e874d8a850d58e7ef917a047014268bf1d3be112b72b77", + "path": "/", + "name": "taarId", + "secure": true, + "httponly": true, + "originAttributes": { + "firstPartyDomain": "", + "geckoViewSessionContextId": "", + "partitionKey": "", + "privateBrowsingId": 0, + "userContextId": 0 + }, + "sameSite": 1, + "schemeMap": 2 + }, + { + "host": "addons.mozilla.org", + "value": "ef51d7121d46a4fd86e874d8a850d58e7ef917a047014268bf1d3be112b72b77", + "path": "/", + "name": "taarId", + "secure": true, + "httponly": true, + "originAttributes": { + "firstPartyDomain": "", + "geckoViewSessionContextId": "", + "partitionKey": "", + "privateBrowsingId": 0, + "userContextId": 1 + }, + "sameSite": 1, + "schemeMap": 2 + }, + { + "host": "addons.mozilla.org", + "value": "ef51d7121d46a4fd86e874d8a850d58e7ef917a047014268bf1d3be112b72b77", + "path": "/", + "name": "taarId", + "secure": true, + "httponly": true, + "originAttributes": { + "firstPartyDomain": "", + "geckoViewSessionContextId": "", + "partitionKey": "", + "privateBrowsingId": 0, + "userContextId": 2 + }, + "sameSite": 1, + "schemeMap": 2 + } + ], + "browserConsole": false, + "browserToolbox": false +} diff --git a/src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_invalid.js b/src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_invalid.js new file mode 100644 index 000000000..a8c3ff2ff --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_invalid.js @@ -0,0 +1,3 @@ +{ + "windows": // invalid json +} diff --git a/src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_valid.js b/src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_valid.js new file mode 100644 index 000000000..f9511f29f --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/data/sessionstore_valid.js @@ -0,0 +1,3 @@ +{ + "windows": [] +} \ No newline at end of file diff --git a/src/zen/tests/mochitests/sessionstore/unit/head.js b/src/zen/tests/mochitests/sessionstore/unit/head.js new file mode 100644 index 000000000..03f7e8239 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/head.js @@ -0,0 +1,37 @@ +ChromeUtils.defineESModuleGetters(this, { + SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", +}); + +// Call a function once initialization of SessionStartup is complete +function afterSessionStartupInitialization(cb) { + info("Waiting for session startup initialization"); + let observer = function () { + try { + info("Session startup initialization observed"); + Services.obs.removeObserver(observer, "sessionstore-state-finalized"); + cb(); + } catch (ex) { + do_throw(ex); + } + }; + Services.obs.addObserver(observer, "sessionstore-state-finalized"); + + // We need the Crash Monitor initialized for sessionstartup to run + // successfully. + const { CrashMonitor } = ChromeUtils.importESModule( + "resource://gre/modules/CrashMonitor.sys.mjs" + ); + CrashMonitor.init(); + + // Start sessionstartup initialization. + SessionStartup.init(); +} + +// Compress the source file using lz4 and put the result to destination file. +// After that, source file is deleted. +async function writeCompressedFile(source, destination) { + let s = await IOUtils.read(source); + await IOUtils.write(destination, s, { compress: true }); + await IOUtils.remove(source); +} diff --git a/src/zen/tests/mochitests/sessionstore/unit/test_backup_once.js b/src/zen/tests/mochitests/sessionstore/unit/test_backup_once.js new file mode 100644 index 000000000..db566491d --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/test_backup_once.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SessionWriter } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionWriter.sys.mjs" +); + +// Make sure that we have a profile before initializing SessionFile. +const profd = do_get_profile(); +const { SessionFile } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionFile.sys.mjs" +); +const Paths = SessionFile.Paths; + +// We need a XULAppInfo to initialize SessionFile +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +updateAppInfo({ + name: "SessionRestoreTest", + ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}", + version: "1", + platformVersion: "", +}); + +add_setup(async function () { + let source = do_get_file("data/sessionstore_valid.js"); + source.copyTo(profd, "sessionstore.js"); + await writeCompressedFile(Paths.clean.replace("jsonlz4", "js"), Paths.clean); + + // Finish initialization of SessionFile + await SessionFile.read(); +}); + +function promise_check_exist(path, shouldExist) { + return (async function () { + info( + "Ensuring that " + path + (shouldExist ? " exists" : " does not exist") + ); + if ((await IOUtils.exists(path)) != shouldExist) { + throw new Error( + "File" + path + " should " + (shouldExist ? "exist" : "not exist") + ); + } + })(); +} + +function promise_check_contents(path, expect) { + return (async function () { + info("Checking whether " + path + " has the right contents"); + let actual = await IOUtils.readJSON(path, { + decompress: true, + }); + Assert.deepEqual( + actual, + expect, + `File ${path} contains the expected data.` + ); + })(); +} + +function generateFileContents(id) { + let url = `http://example.com/test_backup_once#${id}_${Math.random()}`; + return { windows: [{ tabs: [{ entries: [{ url }], index: 1 }] }] }; +} + +// Write to the store, and check that it creates: +// - $Path.recovery with the new data +// - $Path.nextUpgradeBackup with the old data +add_task(async function test_first_write_backup() { + let initial_content = generateFileContents("initial"); + let new_content = generateFileContents("test_1"); + + info("Before the first write, none of the files should exist"); + await promise_check_exist(Paths.backups, false); + + await IOUtils.makeDirectory(Paths.backups); + await IOUtils.writeJSON(Paths.clean, initial_content, { + compress: true, + }); + await SessionFile.write(new_content); + + info("After first write, a few files should have been created"); + await promise_check_exist(Paths.backups, true); + await promise_check_exist(Paths.clean, false); + await promise_check_exist(Paths.cleanBackup, true); + await promise_check_exist(Paths.recovery, true); + await promise_check_exist(Paths.recoveryBackup, false); + await promise_check_exist(Paths.nextUpgradeBackup, true); + + await promise_check_contents(Paths.recovery, new_content); + await promise_check_contents(Paths.nextUpgradeBackup, initial_content); +}); + +// Write to the store again, and check that +// - $Path.clean is not written +// - $Path.recovery contains the new data +// - $Path.recoveryBackup contains the previous data +add_task(async function test_second_write_no_backup() { + let new_content = generateFileContents("test_2"); + let previous_backup_content = await IOUtils.readJSON(Paths.recovery, { + decompress: true, + }); + + await IOUtils.remove(Paths.cleanBackup); + + await SessionFile.write(new_content); + + await promise_check_exist(Paths.backups, true); + await promise_check_exist(Paths.clean, false); + await promise_check_exist(Paths.cleanBackup, false); + await promise_check_exist(Paths.recovery, true); + await promise_check_exist(Paths.nextUpgradeBackup, true); + + await promise_check_contents(Paths.recovery, new_content); + await promise_check_contents(Paths.recoveryBackup, previous_backup_content); +}); + +// Make sure that we create $Paths.clean and remove $Paths.recovery* +// upon shutdown +add_task(async function test_shutdown() { + let output = generateFileContents("test_3"); + + await IOUtils.writeUTF8(Paths.recovery, "I should disappear"); + await IOUtils.writeUTF8(Paths.recoveryBackup, "I should also disappear"); + + await SessionWriter.write(output, { + isFinalWrite: true, + performShutdownCleanup: true, + }); + + Assert.ok(!(await IOUtils.exists(Paths.recovery))); + Assert.ok(!(await IOUtils.exists(Paths.recoveryBackup))); + await promise_check_contents(Paths.clean, output); +}); diff --git a/src/zen/tests/mochitests/sessionstore/unit/test_final_write_cleanup.js b/src/zen/tests/mochitests/sessionstore/unit/test_final_write_cleanup.js new file mode 100644 index 000000000..b3fbd6b20 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/test_final_write_cleanup.js @@ -0,0 +1,116 @@ +"use strict"; + +/** + * This test ensures that we correctly clean up the session state when + * writing with isFinalWrite, which is used on shutdown. It tests that each + * tab's shistory is capped to a maximum number of preceding and succeeding + * entries. + */ + +const { SessionWriter } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionWriter.sys.mjs" +); + +// Make sure that we have a profile before initializing SessionFile. +do_get_profile(); +const { + SessionFile: { Paths }, +} = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionFile.sys.mjs" +); + +const MAX_ENTRIES = 9; +const URL = "http://example.com/#"; + +async function prepareWithLimit(back, fwd) { + SessionWriter.init("empty", false, Paths, { + maxSerializeBack: back, + maxSerializeForward: fwd, + maxUpgradeBackups: 3, + }); + await SessionWriter.wipe(); +} + +add_setup(async function () { + registerCleanupFunction(() => SessionWriter.wipe()); +}); + +function createSessionState(index) { + // Generate the tab state entries and set the one-based + // tab-state index to the middle session history entry. + let tabState = { entries: [], index }; + for (let i = 0; i < MAX_ENTRIES; i++) { + tabState.entries.push({ url: URL + i }); + } + + return { windows: [{ tabs: [tabState] }] }; +} + +async function writeAndParse(state, path, options = {}) { + // We clone here because `write` can change the data passed. + let data = structuredClone(state); + await SessionWriter.write(data, options); + return IOUtils.readJSON(path, { decompress: true }); +} + +add_task(async function test_shistory_cap_none() { + let state = createSessionState(5); + + // Don't limit the number of shistory entries. + await prepareWithLimit(-1, -1); + + // Check that no caps are applied. + let diskState = await writeAndParse(state, Paths.clean, { + isFinalWrite: true, + }); + Assert.deepEqual(state, diskState, "no cap applied"); +}); + +add_task(async function test_shistory_cap_middle() { + let state = createSessionState(5); + await prepareWithLimit(2, 3); + + // Cap is only applied on clean shutdown. + let diskState = await writeAndParse(state, Paths.recovery); + Assert.deepEqual(state, diskState, "no cap applied"); + + // Check that the right number of shistory entries was discarded + // and the shistory index updated accordingly. + diskState = await writeAndParse(state, Paths.clean, { isFinalWrite: true }); + let tabState = state.windows[0].tabs[0]; + tabState.entries = tabState.entries.slice(2, 8); + tabState.index = 3; + Assert.deepEqual(state, diskState, "cap applied"); +}); + +add_task(async function test_shistory_cap_lower_bound() { + let state = createSessionState(1); + await prepareWithLimit(5, 5); + + // Cap is only applied on clean shutdown. + let diskState = await writeAndParse(state, Paths.recovery); + Assert.deepEqual(state, diskState, "no cap applied"); + + // Check that the right number of shistory entries was discarded. + diskState = await writeAndParse(state, Paths.clean, { isFinalWrite: true }); + let tabState = state.windows[0].tabs[0]; + tabState.entries = tabState.entries.slice(0, 6); + Assert.deepEqual(state, diskState, "cap applied"); +}); + +add_task(async function test_shistory_cap_upper_bound() { + let state = createSessionState(MAX_ENTRIES); + await prepareWithLimit(5, 5); + + // Cap is only applied on clean shutdown. + let diskState = await writeAndParse(state, Paths.recovery); + Assert.deepEqual(state, diskState, "no cap applied"); + + // Check that the right number of shistory entries was discarded + // and the shistory index updated accordingly. + diskState = await writeAndParse(state, Paths.clean, { isFinalWrite: true }); + let tabState = state.windows[0].tabs[0]; + tabState.entries = tabState.entries.slice(3); + tabState.index = 6; + Assert.deepEqual(state, diskState, "cap applied"); +}); diff --git a/src/zen/tests/mochitests/sessionstore/unit/test_histogram_corrupt_files.js b/src/zen/tests/mochitests/sessionstore/unit/test_histogram_corrupt_files.js new file mode 100644 index 000000000..2c469ed3b --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/test_histogram_corrupt_files.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * The primary purpose of this test is to ensure that + * the sessionstore component records information about + * corrupted backup files into a histogram. + */ + +"use strict"; + +const Telemetry = Services.telemetry; +const HistogramId = "FX_SESSION_RESTORE_ALL_FILES_CORRUPT"; + +// Prepare the session file. +do_get_profile(); +const { SessionFile } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionFile.sys.mjs" +); + +/** + * A utility function for resetting the histogram and the contents + * of the backup directory. This will also compress the file using lz4 compression. + */ +function promise_reset_session(backups = {}) { + return (async function () { + // Reset the histogram. + Telemetry.getHistogramById(HistogramId).clear(); + + // Reset the contents of the backups directory + await IOUtils.makeDirectory(SessionFile.Paths.backups); + let basePath = do_get_cwd().path; + for (let key of SessionFile.Paths.loadOrder) { + if (backups.hasOwnProperty(key)) { + let path = backups[key]; + const fullPath = PathUtils.join(basePath, ...path); + let s = await IOUtils.read(fullPath); + await IOUtils.write(SessionFile.Paths[key], s, { + compress: true, + }); + } else { + await IOUtils.remove(SessionFile.Paths[key]); + } + } + })(); +} + +/** + * In order to use FX_SESSION_RESTORE_ALL_FILES_CORRUPT histogram + * it has to be registered in "toolkit/components/telemetry/Histograms.json". + * This test ensures that the histogram is registered and empty. + */ +add_task(async function test_ensure_histogram_exists_and_empty() { + let s = Telemetry.getHistogramById(HistogramId).snapshot(); + Assert.equal(s.sum, 0, "Initially, the sum of probes is 0"); +}); + +/** + * Makes sure that the histogram is negatively updated when no + * backup files are present. + */ +add_task(async function test_no_files_exist() { + // No session files are available to SessionFile. + await promise_reset_session(); + + await SessionFile.read(); + // Checking if the histogram is updated negatively + let h = Telemetry.getHistogramById(HistogramId); + let s = h.snapshot(); + Assert.equal(s.values[0], 1, "One probe for the 'false' bucket."); + Assert.equal(s.values[1], 0, "No probes in the 'true' bucket."); +}); + +/** + * Makes sure that the histogram is negatively updated when at least one + * backup file is not corrupted. + */ +add_task(async function test_one_file_valid() { + // Corrupting some backup files. + let invalidSession = ["data", "sessionstore_invalid.js"]; + let validSession = ["data", "sessionstore_valid.js"]; + await promise_reset_session({ + clean: invalidSession, + cleanBackup: validSession, + recovery: invalidSession, + recoveryBackup: invalidSession, + }); + + await SessionFile.read(); + // Checking if the histogram is updated negatively. + let h = Telemetry.getHistogramById(HistogramId); + let s = h.snapshot(); + Assert.equal(s.values[0], 1, "One probe for the 'false' bucket."); + Assert.equal(s.values[1], 0, "No probes in the 'true' bucket."); +}); + +/** + * Makes sure that the histogram is positively updated when all + * backup files are corrupted. + */ +add_task(async function test_all_files_corrupt() { + // Corrupting all backup files. + let invalidSession = ["data", "sessionstore_invalid.js"]; + await promise_reset_session({ + clean: invalidSession, + cleanBackup: invalidSession, + recovery: invalidSession, + recoveryBackup: invalidSession, + }); + + await SessionFile.read(); + // Checking if the histogram is positively updated. + let h = Telemetry.getHistogramById(HistogramId); + let s = h.snapshot(); + Assert.equal(s.values[1], 1, "One probe for the 'true' bucket."); + Assert.equal(s.values[0], 0, "No probes in the 'false' bucket."); +}); diff --git a/src/zen/tests/mochitests/sessionstore/unit/test_migration_lz4compression.js b/src/zen/tests/mochitests/sessionstore/unit/test_migration_lz4compression.js new file mode 100644 index 000000000..4d9b700d8 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/test_migration_lz4compression.js @@ -0,0 +1,151 @@ +"use strict"; + +const { SessionWriter } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionWriter.sys.mjs" +); + +// Make sure that we have a profile before initializing SessionFile. +const profd = do_get_profile(); +const { SessionFile } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionFile.sys.mjs" +); +const Paths = SessionFile.Paths; + +// We need a XULAppInfo to initialize SessionFile +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +updateAppInfo({ + name: "SessionRestoreTest", + ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}", + version: "1", + platformVersion: "", +}); + +function promise_check_exist(path, shouldExist) { + return (async function () { + info( + "Ensuring that " + path + (shouldExist ? " exists" : " does not exist") + ); + if ((await IOUtils.exists(path)) != shouldExist) { + throw new Error( + "File " + path + " should " + (shouldExist ? "exist" : "not exist") + ); + } + })(); +} + +function promise_check_contents(path, expect) { + return (async function () { + info("Checking whether " + path + " has the right contents"); + let actual = await IOUtils.readJSON(path, { + decompress: true, + }); + Assert.deepEqual( + actual, + expect, + `File ${path} contains the expected data.` + ); + })(); +} + +// Check whether the migration from .js to .jslz4 is correct. +add_task(async function test_migration() { + let source = do_get_file("data/sessionstore_valid.js"); + source.copyTo(profd, "sessionstore.js"); + + // Read the content of the session store file. + let parsed = await IOUtils.readJSON(Paths.clean.replace("jsonlz4", "js")); + + // Read the session file with .js extension. + let result = await SessionFile.read(); + + // Check whether the result is what we wanted. + equal(result.origin, "clean"); + equal(result.useOldExtension, true); + Assert.deepEqual( + result.parsed, + parsed, + "result.parsed contains expected data" + ); + + // Initiate a write to ensure we write the compressed version. + await SessionFile.write(parsed); + await promise_check_exist(Paths.backups, true); + await promise_check_exist(Paths.clean, false); + await promise_check_exist(Paths.cleanBackup, true); + await promise_check_exist(Paths.recovery, true); + await promise_check_exist(Paths.recoveryBackup, false); + await promise_check_exist(Paths.nextUpgradeBackup, true); + // The deprecated $Path.clean should exist. + await promise_check_exist(Paths.clean.replace("jsonlz4", "js"), true); + + await promise_check_contents(Paths.recovery, parsed); +}); + +add_task(async function test_startup_with_compressed_clean() { + let state = { windows: [] }; + + // Mare sure we have an empty profile dir. + await SessionFile.wipe(); + + // Populate session files to profile dir. + await IOUtils.writeJSON(Paths.clean, state, { + compress: true, + }); + await IOUtils.makeDirectory(Paths.backups); + await IOUtils.writeJSON(Paths.cleanBackup, state, { + compress: true, + }); + + // Initiate a read. + let result = await SessionFile.read(); + + // Make sure we read correct session file and its content. + equal(result.origin, "clean"); + equal(result.useOldExtension, false); + Assert.deepEqual( + state, + result.parsed, + "result.parsed contains expected data" + ); +}); + +add_task(async function test_empty_profile_dir() { + // Make sure that we have empty profile dir. + await SessionFile.wipe(); + await promise_check_exist(Paths.backups, false); + await promise_check_exist(Paths.clean, false); + await promise_check_exist(Paths.cleanBackup, false); + await promise_check_exist(Paths.recovery, false); + await promise_check_exist(Paths.recoveryBackup, false); + await promise_check_exist(Paths.nextUpgradeBackup, false); + await promise_check_exist(Paths.backups.replace("jsonlz4", "js"), false); + await promise_check_exist(Paths.clean.replace("jsonlz4", "js"), false); + await promise_check_exist(Paths.cleanBackup.replace("lz4", ""), false); + await promise_check_exist(Paths.recovery.replace("jsonlz4", "js"), false); + await promise_check_exist( + Paths.recoveryBackup.replace("jsonlz4", "js"), + false + ); + await promise_check_exist( + Paths.nextUpgradeBackup.replace("jsonlz4", "js"), + false + ); + + // Initiate a read and make sure that we are in empty state. + let result = await SessionFile.read(); + equal(result.origin, "empty"); + equal(result.noFilesFound, true); + + // Create a state to store. + let state = { windows: [] }; + await SessionWriter.write(state, { isFinalWrite: true }); + + // Check session files are created, but not deprecated ones. + await promise_check_exist(Paths.clean, true); + await promise_check_exist(Paths.clean.replace("jsonlz4", "js"), false); + + // Check session file' content is correct. + await promise_check_contents(Paths.clean, state); +}); diff --git a/src/zen/tests/mochitests/sessionstore/unit/test_session_schema.js b/src/zen/tests/mochitests/sessionstore/unit/test_session_schema.js new file mode 100644 index 000000000..f14674037 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/test_session_schema.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_schema_smoke_test() { + let sessionData = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "data", "sessionstore_complete.json") + ); + let result; + + Assert.ok( + sessionData.version[0], + "Looks like we loaded up the session data ok" + ); + + try { + result = await SessionStore.validateState(sessionData); + } catch (ex) { + info("Exception validating session data:" + ex.message); + } + + if (!result.valid) { + info(`Failed validation: ${JSON.stringify(result.errors)}`); + } + Assert.ok(result, "Got a validation result object"); + + Assert.ok(result.valid, "the session file was validated against the schema"); +}); diff --git a/src/zen/tests/mochitests/sessionstore/unit/test_startup_invalid_session.js b/src/zen/tests/mochitests/sessionstore/unit/test_startup_invalid_session.js new file mode 100644 index 000000000..50960b1d4 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/test_startup_invalid_session.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + let profd = do_get_profile(); + var SessionFile = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionFile.sys.mjs" + ).SessionFile; + + let sourceSession = do_get_file("data/sessionstore_invalid.js"); + sourceSession.copyTo(profd, "sessionstore.js"); + + let sourceCheckpoints = do_get_file("data/sessionCheckpoints_all.json"); + sourceCheckpoints.copyTo(profd, "sessionCheckpoints.json"); + + // Compress sessionstore.js to sessionstore.jsonlz4 + // and remove sessionstore.js + let oldExtSessionFile = SessionFile.Paths.clean.replace("jsonlz4", "js"); + writeCompressedFile(oldExtSessionFile, SessionFile.Paths.clean).then(() => { + afterSessionStartupInitialization(function cb() { + Assert.equal(SessionStartup.sessionType, SessionStartup.NO_SESSION); + do_test_finished(); + }); + }); + + do_test_pending(); +} diff --git a/src/zen/tests/mochitests/sessionstore/unit/test_startup_nosession_async.js b/src/zen/tests/mochitests/sessionstore/unit/test_startup_nosession_async.js new file mode 100644 index 000000000..259c393e6 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/test_startup_nosession_async.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test SessionStartup.sessionType in the following scenario: +// - no sessionstore.js; +// - the session store has been loaded, so no need to go +// through the synchronous fallback + +function run_test() { + // Initialize the profile (the session startup uses it) + do_get_profile(); + + do_test_pending(); + + afterSessionStartupInitialization(function cb() { + Assert.equal(SessionStartup.sessionType, SessionStartup.NO_SESSION); + do_test_finished(); + }); +} diff --git a/src/zen/tests/mochitests/sessionstore/unit/test_startup_session_async.js b/src/zen/tests/mochitests/sessionstore/unit/test_startup_session_async.js new file mode 100644 index 000000000..a61c9fe42 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/test_startup_session_async.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test SessionStartup.sessionType in the following scenario: +// - valid sessionstore.js; +// - valid sessionCheckpoints.json with all checkpoints; +// - the session store has been loaded + +function run_test() { + let profd = do_get_profile(); + var SessionFile = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionFile.sys.mjs" + ).SessionFile; + + let sourceSession = do_get_file("data/sessionstore_valid.js"); + sourceSession.copyTo(profd, "sessionstore.js"); + + let sourceCheckpoints = do_get_file("data/sessionCheckpoints_all.json"); + sourceCheckpoints.copyTo(profd, "sessionCheckpoints.json"); + + // Compress sessionstore.js to sessionstore.jsonlz4 + // and remove sessionstore.js + let oldExtSessionFile = SessionFile.Paths.clean.replace("jsonlz4", "js"); + writeCompressedFile(oldExtSessionFile, SessionFile.Paths.clean).then(() => { + afterSessionStartupInitialization(function cb() { + Assert.equal(SessionStartup.sessionType, SessionStartup.DEFER_SESSION); + do_test_finished(); + }); + }); + + do_test_pending(); +} diff --git a/src/zen/tests/mochitests/sessionstore/unit/test_write_json_length_hint.js b/src/zen/tests/mochitests/sessionstore/unit/test_write_json_length_hint.js new file mode 100644 index 000000000..b0e479c5d --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/test_write_json_length_hint.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SessionWriter } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionWriter.sys.mjs" +); + +const profd = do_get_profile(); +const { SessionFile } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionFile.sys.mjs" +); + +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +updateAppInfo({ + name: "SessionRestoreTest", + ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}", + version: "1", + platformVersion: "", +}); + +add_setup(async function () { + let source = do_get_file("data/sessionstore_valid.js"); + source.copyTo(profd, "sessionstore.js"); + await writeCompressedFile( + SessionFile.Paths.clean.replace("jsonlz4", "js"), + SessionFile.Paths.clean + ); + await SessionFile.read(); +}); + +add_task(async function test_length_hint_updates_after_write() { + Assert.equal(SessionWriter._jsonLengthHint, 0, "Length hint starts at 0"); + + await SessionFile.write({}); + + let hintAfterSmall = SessionWriter._jsonLengthHint; + Assert.equal( + hintAfterSmall, + JSON.stringify({}).length, + "Hint matches the uncompressed JSON byte length" + ); + + let largerState = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, "data", "sessionstore_complete.json") + ); + await SessionFile.write(largerState); + + Assert.greater( + SessionWriter._jsonLengthHint, + hintAfterSmall, + "Hint grows after writing a larger state" + ); +}); + +add_task(async function test_length_hint_resets_on_wipe() { + await SessionFile.write({ windows: [{ tabs: [{ entries: [] }] }] }); + Assert.greater(SessionWriter._jsonLengthHint, 0, "Hint is nonzero"); + + await SessionFile.wipe(); + Assert.equal(SessionWriter._jsonLengthHint, 0, "Hint resets to 0 after wipe"); +}); diff --git a/src/zen/tests/mochitests/sessionstore/unit/xpcshell.toml b/src/zen/tests/mochitests/sessionstore/unit/xpcshell.toml new file mode 100644 index 000000000..01b0fe1f2 --- /dev/null +++ b/src/zen/tests/mochitests/sessionstore/unit/xpcshell.toml @@ -0,0 +1,48 @@ +[DEFAULT] +run-if = [ + "os != 'android'", +] +head = "head.js" +tags = "condprof" +firefox-appdir = "browser" +support-files = [ + "data/sessionCheckpoints_all.json", + "data/sessionstore_invalid.js", + "data/sessionstore_valid.js", +] + +["test_backup_once.js"] +skip-if = [ + "condprof", # Bug 1769154 +] + +["test_final_write_cleanup.js"] + +["test_histogram_corrupt_files.js"] + +["test_migration_lz4compression.js"] +skip-if = [ + "condprof", # Bug 1769154 +] + +["test_session_schema.js"] +support-files = [ + "data/sessionstore_complete.json", +] + +["test_startup_invalid_session.js"] +skip-if = [ + "condprof", # Bug 1769154 +] + +["test_startup_nosession_async.js"] +skip-if = [ + "condprof", # Bug 1769154 +] + +["test_startup_session_async.js"] + +["test_write_json_length_hint.js"] +support-files = [ + "data/sessionstore_complete.json", +] diff --git a/src/zen/tests/mochitests/zoom/browser.toml b/src/zen/tests/mochitests/zoom/browser.toml new file mode 100644 index 000000000..bac083464 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser.toml @@ -0,0 +1,63 @@ +[DEFAULT] +support-files = [ + "head.js", + "../../../../browser/base/content/test/general/moz.png", + "zoom_test.html", +] + +["browser_background_link_zoom_reset.js"] +https_first_disabled = true + +["browser_background_zoom.js"] +https_first_disabled = true + +["browser_default_zoom.js"] + +["browser_default_zoom_fission.js"] + +["browser_default_zoom_multitab.js"] +https_first_disabled = true + +["browser_default_zoom_sitespecific.js"] + +["browser_image_zoom_tabswitch.js"] +https_first_disabled = true +skip-if = [ + "os == 'mac' && os_version == '10.15' && arch == 'x86_64'", # Bug 1526628 + "os == 'mac' && os_version == '14.70' && arch == 'x86_64'", # Bug 1526628 + "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 1526628 +] + +["browser_keyboard_mousewheel_zoom_consistency.js"] +skip-if = [ + "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 2019466 +] + +["browser_mousewheel_zoom.js"] +https_first_disabled = true + +["browser_sitespecific_background_pref.js"] +https_first_disabled = true + +["browser_sitespecific_image_zoom.js"] + +["browser_sitespecific_video_zoom.js"] +https_first_disabled = true +support-files = ["../../../../browser/base/content/test/general/video.webm"] +skip-if = [ + "os == 'win' && debug", # Bug 1315042 +] + +["browser_subframe_textzoom.js"] + +["browser_tabswitch_zoom_flicker.js"] +https_first_disabled = true +skip-if = [ + "os == 'linux' && os_version == '22.04' && arch == 'x86_64' && display == 'wayland' && debug", # Bug 1652383 + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && debug", # Bug 1652383 + "os == 'win' && opt", # Bug 1652383 +] + +["browser_tooltip_zoom.js"] + +["browser_zoom_commands.js"] diff --git a/src/zen/tests/mochitests/zoom/browser_background_link_zoom_reset.js b/src/zen/tests/mochitests/zoom/browser_background_link_zoom_reset.js new file mode 100644 index 000000000..ac224cc4a --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_background_link_zoom_reset.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +const TEST_PAGE = "/browser/browser/base/content/test/zoom/zoom_test.html"; +var gTestTab, gBgTab, gTestZoom; + +function testBackgroundLoad() { + (async function () { + is( + ZoomManager.zoom, + gTestZoom, + "opening a background tab should not change foreground zoom" + ); + + await FullZoomHelper.removeTabAndWaitForLocationChange(gBgTab); + + await FullZoom.reset(); + await FullZoomHelper.removeTabAndWaitForLocationChange(gTestTab); + finish(); + })(); +} + +function testInitialZoom() { + (async function () { + is(ZoomManager.zoom, 1, "initial zoom level should be 1"); + FullZoom.enlarge(); + + gTestZoom = ZoomManager.zoom; + isnot(gTestZoom, 1, "zoom level should have changed"); + + gBgTab = BrowserTestUtils.addTab(gBrowser); + await FullZoomHelper.load(gBgTab, "http://mochi.test:8888" + TEST_PAGE); + })().then(testBackgroundLoad, FullZoomHelper.failAndContinue(finish)); +} + +function test() { + waitForExplicitFinish(); + + (async function () { + gTestTab = BrowserTestUtils.addTab(gBrowser); + await FullZoomHelper.selectTabAndWaitForLocationChange(gTestTab); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await FullZoomHelper.load(gTestTab, "http://example.org" + TEST_PAGE); + })().then(testInitialZoom, FullZoomHelper.failAndContinue(finish)); +} diff --git a/src/zen/tests/mochitests/zoom/browser_background_zoom.js b/src/zen/tests/mochitests/zoom/browser_background_zoom.js new file mode 100644 index 000000000..d757d8e7b --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_background_zoom.js @@ -0,0 +1,115 @@ +var gTestPage = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html"; +var gTestImage = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/browser/base/content/test/general/moz.png"; +var gTab1, gTab2, gTab3; +var gLevel; + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(async () => { + await new Promise(resolve => { + ContentPrefService2.removeByName(FullZoom.name, Cu.createLoadContext(), { + handleCompletion: resolve, + }); + }); + }); + + (async function () { + gTab1 = BrowserTestUtils.addTab(gBrowser, gTestPage); + gTab2 = BrowserTestUtils.addTab(gBrowser); + gTab3 = BrowserTestUtils.addTab(gBrowser); + + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + await FullZoomHelper.load(gTab1, gTestPage); + await FullZoomHelper.load(gTab2, gTestPage); + })().then(secondPageLoaded, FullZoomHelper.failAndContinue(finish)); +} + +function secondPageLoaded() { + (async function () { + FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1"); + FullZoomHelper.zoomTest(gTab3, 1, "Initial zoom of tab 3 should be 1"); + + // Now have three tabs, two with the test page, one blank. Tab 1 is selected + // Zoom tab 1 + FullZoom.enlarge(); + gLevel = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1)); + + Assert.greater(gLevel, 1, "New zoom for tab 1 should be greater than 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2"); + FullZoomHelper.zoomTest(gTab3, 1, "Zooming tab 1 should not affect tab 3"); + + await FullZoomHelper.load(gTab3, gTestPage); + })().then(thirdPageLoaded, FullZoomHelper.failAndContinue(finish)); +} + +function thirdPageLoaded() { + (async function () { + FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed"); + FullZoomHelper.zoomTest(gTab2, 1, "Tab 2 should still not be affected"); + FullZoomHelper.zoomTest( + gTab3, + gLevel, + "Tab 3 should have zoomed as it was loading in the background" + ); + + // Switching to tab 2 should update its zoom setting. + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed"); + FullZoomHelper.zoomTest(gTab2, gLevel, "Tab 2 should be zoomed now"); + FullZoomHelper.zoomTest(gTab3, gLevel, "Tab 3 should still be zoomed"); + + await FullZoomHelper.load(gTab1, gTestImage); + })().then(imageLoaded, FullZoomHelper.failAndContinue(finish)); +} + +function imageLoaded() { + (async function () { + FullZoomHelper.zoomTest( + gTab1, + 1, + "Zoom should be 1 when image was loaded in the background" + ); + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + FullZoomHelper.zoomTest( + gTab1, + 1, + "Zoom should still be 1 when tab with image is selected" + ); + })().then(imageZoomSwitch, FullZoomHelper.failAndContinue(finish)); +} + +function imageZoomSwitch() { + (async function () { + await FullZoomHelper.navigate(FullZoomHelper.BACK); + await FullZoomHelper.navigate(FullZoomHelper.FORWARD); + FullZoomHelper.zoomTest( + gTab1, + 1, + "Tab 1 should not be zoomed when an image loads" + ); + + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoomHelper.zoomTest( + gTab1, + 1, + "Tab 1 should still not be zoomed when deselected" + ); + })().then(finishTest, FullZoomHelper.failAndContinue(finish)); +} + +var finishTestStarted = false; +function finishTest() { + (async function () { + ok(!finishTestStarted, "finishTest called more than once"); + finishTestStarted = true; + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + await FullZoomHelper.removeTabAndWaitForLocationChange(gTab1); + await FullZoomHelper.removeTabAndWaitForLocationChange(gTab2); + await FullZoomHelper.removeTabAndWaitForLocationChange(gTab3); + })().then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/src/zen/tests/mochitests/zoom/browser_default_zoom.js b/src/zen/tests/mochitests/zoom/browser_default_zoom.js new file mode 100644 index 000000000..d6e8e2575 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_default_zoom.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_init_default_zoom() { + const TEST_PAGE_URL = + "data:text/html;charset=utf-8,test_init_default_zoom"; + + // Prepare the test tab + info("Creating tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL); + let tabBrowser = gBrowser.getBrowserForTab(tab); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab); + + // 100% global zoom + info("Getting default zoom"); + let defaultZoom = await FullZoomHelper.getGlobalValue(); + is(defaultZoom, 1, "Global zoom is init at 100%"); + // 100% tab zoom + is( + ZoomManager.getZoomForBrowser(tabBrowser), + 1, + "Current zoom is init at 100%" + ); + info("Removing tab"); + await FullZoomHelper.removeTabAndWaitForLocationChange(); +}); + +add_task(async function test_set_default_zoom() { + const TEST_PAGE_URL = + "data:text/html;charset=utf-8,test_set_default_zoom"; + + // Prepare the test tab + info("Creating tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL); + let tabBrowser = gBrowser.getBrowserForTab(tab); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab); + + // 120% global zoom + info("Changing default zoom"); + await FullZoomHelper.changeDefaultZoom(120); + let defaultZoom = await FullZoomHelper.getGlobalValue(); + is(defaultZoom, 1.2, "Global zoom is at 120%"); + + // 120% tab zoom + await TestUtils.waitForCondition(() => { + info("Current zoom is: " + ZoomManager.getZoomForBrowser(tabBrowser)); + return ZoomManager.getZoomForBrowser(tabBrowser) == 1.2; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser), + 1.2, + "Current zoom matches changed default zoom" + ); + info("Removing tab"); + await FullZoomHelper.removeTabAndWaitForLocationChange(); + await FullZoomHelper.changeDefaultZoom(100); +}); + +add_task(async function test_enlarge_reduce_reset_local_zoom() { + const TEST_PAGE_URL = + "data:text/html;charset=utf-8,test_enlarge_reduce_reset_local_zoom"; + + // Prepare the test tab + info("Creating tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL); + let tabBrowser = gBrowser.getBrowserForTab(tab); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab); + + // 120% global zoom + info("Changing default zoom"); + await FullZoomHelper.changeDefaultZoom(120); + let defaultZoom = await FullZoomHelper.getGlobalValue(); + is(defaultZoom, 1.2, "Global zoom is at 120%"); + + await TestUtils.waitForCondition(() => { + info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser)); + return ZoomManager.getZoomForBrowser(tabBrowser) == 1.2; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser), + 1.2, + "Current tab zoom matches default zoom" + ); + + await FullZoom.enlarge(); + info("Enlarged!"); + defaultZoom = await FullZoomHelper.getGlobalValue(); + info("Current global zoom is " + defaultZoom); + + // 133% tab zoom + await TestUtils.waitForCondition(() => { + info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser)); + return ZoomManager.getZoomForBrowser(tabBrowser) == 1.3; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser), + 1.3, + "Increasing zoom changes zoom of current tab." + ); + defaultZoom = await FullZoomHelper.getGlobalValue(); + // 120% global zoom + is( + defaultZoom, + 1.2, + "Increasing zoom of current tab doesn't change default zoom." + ); + info("Reducing..."); + info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser)); + await FullZoom.reduce(); // 120% tab zoom + info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser)); + await FullZoom.reduce(); // 110% tab zoom + info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser)); + await FullZoom.reduce(); // 100% tab zoom + info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser)); + + await TestUtils.waitForCondition(() => { + info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser)); + return ZoomManager.getZoomForBrowser(tabBrowser) == 1; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser), + 1, + "Decreasing zoom changes zoom of current tab." + ); + defaultZoom = await FullZoomHelper.getGlobalValue(); + // 120% global zoom + is( + defaultZoom, + 1.2, + "Decreasing zoom of current tab doesn't change default zoom." + ); + info("Resetting..."); + FullZoom.reset(); // 120% tab zoom + await TestUtils.waitForCondition(() => { + info("Current tab zoom is ", ZoomManager.getZoomForBrowser(tabBrowser)); + return ZoomManager.getZoomForBrowser(tabBrowser) == 1.2; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser), + 1.2, + "Reseting zoom causes current tab to zoom to default zoom." + ); + + // no reset necessary, it was performed as part of the test + info("Removing tab"); + await FullZoomHelper.removeTabAndWaitForLocationChange(); +}); diff --git a/src/zen/tests/mochitests/zoom/browser_default_zoom_fission.js b/src/zen/tests/mochitests/zoom/browser_default_zoom_fission.js new file mode 100644 index 000000000..4d3d8b389 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_default_zoom_fission.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_sitespecific_iframe_global_zoom() { + const TEST_PAGE_URL = + 'data:text/html;charset=utf-8,test_sitespecific_iframe_global_zoom'; + const TEST_IFRAME_URL = "https://example.com/"; + + // Prepare the test tab + console.log("Creating tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL); + let tabBrowser = gBrowser.getBrowserForTab(tab); + console.log("Loading tab"); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab); + + // 67% global zoom + console.log("Changing default zoom"); + await FullZoomHelper.changeDefaultZoom(67); + let defaultZoom = await FullZoomHelper.getGlobalValue(); + is(defaultZoom, 0.67, "Global zoom is at 67%"); + await TestUtils.waitForCondition(() => { + console.log("Current zoom is: ", ZoomManager.getZoomForBrowser(tabBrowser)); + return ZoomManager.getZoomForBrowser(tabBrowser) == 0.67; + }); + + let zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser).toFixed(2); + is(zoomLevel, "0.67", "tab zoom has been set to 67%"); + + let frameLoadedPromise = BrowserTestUtils.browserLoaded( + tabBrowser, + true, + TEST_IFRAME_URL + ); + console.log("Spawinging iframe"); + SpecialPowers.spawn(tabBrowser, [TEST_IFRAME_URL], url => { + content.document.querySelector("iframe").src = url; + }); + console.log("Awaiting frame promise"); + let loadedURL = await frameLoadedPromise; + is(loadedURL, TEST_IFRAME_URL, "got the load event for the iframe"); + + let frameZoom = await SpecialPowers.spawn( + gBrowser.selectedBrowser.browsingContext.children[0], + [], + async () => { + await ContentTaskUtils.waitForCondition(() => { + return content.docShell.browsingContext.fullZoom.toFixed(2) == 0.67; + }); + return content.docShell.browsingContext.fullZoom.toFixed(2); + } + ); + + is(frameZoom, zoomLevel, "global zoom is reflected in iframe"); + console.log("Removing tab"); + await FullZoomHelper.removeTabAndWaitForLocationChange(); +}); + +add_task(async function test_sitespecific_global_zoom_enlarge() { + const TEST_PAGE_URL = + 'data:text/html;charset=utf-8,test_sitespecific_global_zoom_enlarge'; + const TEST_IFRAME_URL = "https://example.org/"; + + // Prepare the test tab + console.log("Adding tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL); + let tabBrowser = gBrowser.getBrowserForTab(tab); + console.log("Awaiting tab load"); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab); + + // 67% global zoom persists from previous test + + let frameLoadedPromise = BrowserTestUtils.browserLoaded( + tabBrowser, + true, + TEST_IFRAME_URL + ); + console.log("Spawning iframe"); + SpecialPowers.spawn(tabBrowser, [TEST_IFRAME_URL], url => { + content.document.querySelector("iframe").src = url; + }); + console.log("Awaiting iframe load"); + let loadedURL = await frameLoadedPromise; + is(loadedURL, TEST_IFRAME_URL, "got the load event for the iframe"); + console.log("Enlarging tab"); + await FullZoom.enlarge(); + // 80% local zoom + await TestUtils.waitForCondition(() => { + console.log("Current zoom is: ", ZoomManager.getZoomForBrowser(tabBrowser)); + return ZoomManager.getZoomForBrowser(tabBrowser) == 0.8; + }); + + is( + ZoomManager.getZoomForBrowser(tabBrowser).toFixed(2), + "0.80", + "Local zoom is increased" + ); + + let frameZoom = await SpecialPowers.spawn( + gBrowser.selectedBrowser.browsingContext.children[0], + [], + async () => { + await ContentTaskUtils.waitForCondition(() => { + return content.docShell.browsingContext.fullZoom.toFixed(2) == 0.8; + }); + return content.docShell.browsingContext.fullZoom.toFixed(2); + } + ); + + is(frameZoom, "0.80", "(without fission) iframe zoom matches page zoom"); + console.log("Removing tab"); + await FullZoomHelper.removeTabAndWaitForLocationChange(); +}); diff --git a/src/zen/tests/mochitests/zoom/browser_default_zoom_multitab.js b/src/zen/tests/mochitests/zoom/browser_default_zoom_multitab.js new file mode 100644 index 000000000..d20402088 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_default_zoom_multitab.js @@ -0,0 +1,190 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_multidomain_global_zoom() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const TEST_PAGE_URL_1 = "http://example.com/"; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const TEST_PAGE_URL_2 = "http://example.org/"; + + // Prepare the test tabs + console.log("Creating tab 1"); + let tab1 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL_1); + let tabBrowser1 = gBrowser.getBrowserForTab(tab1); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + + console.log("Creating tab 2"); + let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL_2); + let tabBrowser2 = gBrowser.getBrowserForTab(tab2); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + + // 67% global zoom + console.log("Changing default zoom"); + await FullZoomHelper.changeDefaultZoom(67); + let defaultZoom = await FullZoomHelper.getGlobalValue(); + is(defaultZoom, 0.67, "Global zoom is set to 67%"); + + // 67% local zoom tab 1 + console.log("Selecting tab 1"); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + await TestUtils.waitForCondition(() => { + console.log( + "Currnet zoom is: ", + ZoomManager.getZoomForBrowser(tabBrowser1) + ); + return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.67; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser1), + 0.67, + "Setting default zoom causes tab 1 (background) to zoom to default zoom." + ); + + // 67% local zoom tab 2 + console.log("Selecting tab 2"); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + await TestUtils.waitForCondition(() => { + console.log( + "Current zoom is: ", + ZoomManager.getZoomForBrowser(tabBrowser2) + ); + return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.67; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser2), + 0.67, + "Setting default zoom causes tab 2 (foreground) to zoom to default zoom." + ); + console.log("Enlarging tab"); + await FullZoom.enlarge(); + // 80% local zoom tab 2 + await TestUtils.waitForCondition(() => { + console.log( + "Current zoom is: ", + ZoomManager.getZoomForBrowser(tabBrowser2) + ); + return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.8; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser2), + 0.8, + "Enlarging local zoom of tab 2." + ); + + // 67% local zoom tab 1 + console.log("Selecting tab 1"); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + await TestUtils.waitForCondition(() => { + console.log( + "Current zoom is: ", + ZoomManager.getZoomForBrowser(tabBrowser1) + ); + return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.67; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser1), + 0.67, + "Tab 1 is unchanged by tab 2's enlarge call." + ); + console.log("Removing tab"); + await FullZoomHelper.removeTabAndWaitForLocationChange(); + console.log("Removing tab"); + await FullZoomHelper.removeTabAndWaitForLocationChange(); +}); + +add_task(async function test_site_specific_global_zoom() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const TEST_PAGE_URL_1 = "http://example.net/"; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const TEST_PAGE_URL_2 = "http://example.net/"; + + // Prepare the test tabs + console.log("Adding tab 1"); + let tab1 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL_1); + console.log("Getting tab 1 browser"); + let tabBrowser1 = gBrowser.getBrowserForTab(tab1); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + + console.log("Adding tab 2"); + let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL_2); + console.log("Getting tab 2 browser"); + let tabBrowser2 = gBrowser.getBrowserForTab(tab2); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + + console.log("checking global zoom"); + // 67% global zoom persists from previous test + let defaultZoom = await FullZoomHelper.getGlobalValue(); + is(defaultZoom, 0.67, "Default zoom is 67%"); + + // 67% local zoom tab 1 + console.log("Selecting tab 1"); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + console.log("Awaiting condition"); + await TestUtils.waitForCondition(() => { + console.log( + "Current tab 1 zoom is: ", + ZoomManager.getZoomForBrowser(tabBrowser1) + ); + return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.67; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser1), + 0.67, + "Setting default zoom causes tab 1 (background) to zoom to default zoom." + ); + + // 67% local zoom tab 2 + console.log("Selecting tab 2"); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + console.log("Awaiting condition"); + await TestUtils.waitForCondition(() => { + console.log( + "Current tab 2 zoom is: ", + ZoomManager.getZoomForBrowser(tabBrowser2) + ); + return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.67; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser2), + 0.67, + "Setting default zoom causes tab 2 (foreground) to zoom to default zoom." + ); + + // 80% site specific zoom + console.log("Selecting tab 1"); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + console.log("Enlarging"); + await FullZoom.enlarge(); + await TestUtils.waitForCondition(() => { + console.log( + "Current tab 1 zoom is: ", + ZoomManager.getZoomForBrowser(tabBrowser1) + ); + return ZoomManager.getZoomForBrowser(tabBrowser1) == 0.8; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser1), + 0.8, + "Changed local zoom in tab one." + ); + console.log("Selecting tab 2"); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + await TestUtils.waitForCondition(() => { + console.log( + "Current tab 2 zoom is: ", + ZoomManager.getZoomForBrowser(tabBrowser2) + ); + return ZoomManager.getZoomForBrowser(tabBrowser2) == 0.8; + }); + is( + ZoomManager.getZoomForBrowser(tabBrowser2), + 0.8, + "Second tab respects site specific zoom." + ); + console.log("Removing tab"); + await FullZoomHelper.removeTabAndWaitForLocationChange(); + console.log("Removing tab"); + await FullZoomHelper.removeTabAndWaitForLocationChange(); +}); diff --git a/src/zen/tests/mochitests/zoom/browser_default_zoom_sitespecific.js b/src/zen/tests/mochitests/zoom/browser_default_zoom_sitespecific.js new file mode 100644 index 000000000..212df6579 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_default_zoom_sitespecific.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_disabled_ss_multi() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.zoom.siteSpecific", false]], + }); + const TEST_PAGE_URL = "https://example.org/"; + + // Prepare the test tabs + let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL); + let tabBrowser2 = gBrowser.getBrowserForTab(tab2); + let isLoaded = BrowserTestUtils.browserLoaded( + tabBrowser2, + false, + TEST_PAGE_URL + ); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + await isLoaded; + + let zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser2); + is(zoomLevel, 1, "tab 2 zoom has been set to 100%"); + + let tab1 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL); + let tabBrowser1 = gBrowser.getBrowserForTab(tab1); + isLoaded = BrowserTestUtils.browserLoaded(tabBrowser1, false, TEST_PAGE_URL); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + await isLoaded; + + zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1); + is(zoomLevel, 1, "tab 1 zoom has been set to 100%"); + + // 70% global zoom + await FullZoomHelper.changeDefaultZoom(70); + let defaultZoom = await FullZoomHelper.getGlobalValue(); + is(defaultZoom, 0.7, "Global zoom is at 70%"); + + await TestUtils.waitForCondition( + () => ZoomManager.getZoomForBrowser(tabBrowser1) == 0.7 + ); + zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1); + is(zoomLevel, 0.7, "tab 1 zoom has been set to 70%"); + + await FullZoom.enlarge(); + + zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1); + is(zoomLevel, 0.8, "tab 1 zoom has been set to 80%"); + + await FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser2); + is(zoomLevel, 1, "tab 2 zoom remains 100%"); + + let tab3 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL); + let tabBrowser3 = gBrowser.getBrowserForTab(tab3); + isLoaded = BrowserTestUtils.browserLoaded(tabBrowser3, false, TEST_PAGE_URL); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab3); + await isLoaded; + + zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser3); + is(zoomLevel, 0.7, "tab 3 zoom has been set to 70%"); + + await FullZoomHelper.removeTabAndWaitForLocationChange(); + await FullZoomHelper.removeTabAndWaitForLocationChange(); + await FullZoomHelper.removeTabAndWaitForLocationChange(); +}); + +add_task(async function test_disabled_ss_custom() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.zoom.siteSpecific", false]], + }); + const TEST_PAGE_URL = "https://example.org/"; + + // 150% global zoom + await FullZoomHelper.changeDefaultZoom(150); + let defaultZoom = await FullZoomHelper.getGlobalValue(); + is(defaultZoom, 1.5, "Global zoom is at 150%"); + + // Prepare test tab + let tab1 = BrowserTestUtils.addTab(gBrowser, TEST_PAGE_URL); + let tabBrowser1 = gBrowser.getBrowserForTab(tab1); + let isLoaded = BrowserTestUtils.browserLoaded( + tabBrowser1, + false, + TEST_PAGE_URL + ); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + await isLoaded; + + await TestUtils.waitForCondition( + () => ZoomManager.getZoomForBrowser(tabBrowser1) == 1.5 + ); + let zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1); + is(zoomLevel, 1.5, "tab 1 zoom has been set to 150%"); + + await FullZoom.enlarge(); + + zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1); + is(zoomLevel, 1.6, "tab 1 zoom has been set to 160%"); + + await BrowserTestUtils.reloadTab(tab1); + + zoomLevel = ZoomManager.getZoomForBrowser(tabBrowser1); + is(zoomLevel, 1.6, "tab 1 zoom remains 160%"); + + await FullZoomHelper.removeTabAndWaitForLocationChange(); +}); diff --git a/src/zen/tests/mochitests/zoom/browser_image_zoom_tabswitch.js b/src/zen/tests/mochitests/zoom/browser_image_zoom_tabswitch.js new file mode 100644 index 000000000..b2357d813 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_image_zoom_tabswitch.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +function test() { + let tab1, tab2; + const TEST_IMAGE = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/browser/base/content/test/general/moz.png"; + + waitForExplicitFinish(); + + (async function () { + tab1 = BrowserTestUtils.addTab(gBrowser); + tab2 = BrowserTestUtils.addTab(gBrowser); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + await FullZoomHelper.load(tab1, TEST_IMAGE); + + is(ZoomManager.zoom, 1, "initial zoom level for first should be 1"); + + await FullZoom.enlarge(tab1.linkedBrowser); + let zoom = ZoomManager.zoom; + isnot(zoom, 1, "zoom level should have changed"); + + await FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + is(ZoomManager.zoom, 1, "initial zoom level for second tab should be 1"); + + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + is( + ZoomManager.zoom, + zoom, + "zoom level for first tab should not have changed" + ); + + await FullZoom.reset(tab1.linkedBrowser); + + await FullZoomHelper.removeTabAndWaitForLocationChange(tab1); + await FullZoomHelper.removeTabAndWaitForLocationChange(tab2); + })().then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/src/zen/tests/mochitests/zoom/browser_keyboard_mousewheel_zoom_consistency.js b/src/zen/tests/mochitests/zoom/browser_keyboard_mousewheel_zoom_consistency.js new file mode 100644 index 000000000..bf278dc66 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_keyboard_mousewheel_zoom_consistency.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PAGE_URL = + "data:text/html;charset=utf-8,test_keyboard_mousewheel_consistency"; + +const WHEEL_ZOOM_IN = -1; +const WHEEL_ZOOM_OUT = 1; + +/** + * Tests that keyboard zoom (Ctrl+/- keys) and mouse wheel zoom (Ctrl+wheel) + * step through the same predefined zoom levels consistently, ensuring both + * methods can reach all standard zoom levels including 100%. + */ + +async function synthesizeMouseWheelZoom(browser, direction, expectedZoom) { + await BrowserTestUtils.synthesizeMouse( + null, + 10, + 10, + { + wheel: true, + ctrlKey: true, + deltaY: direction, + deltaMode: WheelEvent.DOM_DELTA_LINE, + }, + browser + ); + + await TestUtils.waitForCondition( + () => { + return ZoomManager.getZoomForBrowser(browser) == expectedZoom; + }, + `Waiting for mousewheel zoom to ${expectedZoom * 100}%` + ); +} + +add_task(async function test_keyboard_mousewheel_zoom_consistency() { + // One some platforms, Ctrl-Wheel doesn't zoom by default, so we'll enable + // that for this test. + await SpecialPowers.pushPrefEnv({ + set: [["mousewheel.with_control.action", 3]], + }); + + await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async browser => { + let currentZoom = ZoomManager.getZoomForBrowser(browser); + Assert.equal(currentZoom, 1, "Initial zoom should be 100%"); + + await FullZoom.enlarge(); + Assert.equal( + ZoomManager.getZoomForBrowser(browser), + 1.1, + "First enlarge should go to 110%" + ); + + await FullZoom.enlarge(); + Assert.equal( + ZoomManager.getZoomForBrowser(browser), + 1.2, + "Second enlarge should go to 120%" + ); + + await FullZoom.enlarge(); + Assert.equal( + ZoomManager.getZoomForBrowser(browser), + 1.3, + "Third enlarge should go to 130%" + ); + + await synthesizeMouseWheelZoom(browser, WHEEL_ZOOM_OUT, 1.2); + await synthesizeMouseWheelZoom(browser, WHEEL_ZOOM_OUT, 1.1); + await synthesizeMouseWheelZoom(browser, WHEEL_ZOOM_OUT, 1); + + Assert.equal( + ZoomManager.getZoomForBrowser(browser), + 1, + "Should return to 100% after zooming out from 133%" + ); + + await synthesizeMouseWheelZoom(browser, WHEEL_ZOOM_OUT, 0.9); + + Assert.equal( + ZoomManager.getZoomForBrowser(browser), + 0.9, + "Should be at 90% after one more zoom out" + ); + + await synthesizeMouseWheelZoom(browser, WHEEL_ZOOM_IN, 1); + await synthesizeMouseWheelZoom(browser, WHEEL_ZOOM_IN, 1.1); + await synthesizeMouseWheelZoom(browser, WHEEL_ZOOM_IN, 1.2); + + Assert.equal( + ZoomManager.getZoomForBrowser(browser), + 1.2, + "Should be at 120% after zooming back in with wheel" + ); + + await FullZoom.reduce(); + Assert.equal( + ZoomManager.getZoomForBrowser(browser), + 1.1, + "First keyboard reduce should go to 110%" + ); + + await FullZoom.reduce(); + Assert.equal( + ZoomManager.getZoomForBrowser(browser), + 1, + "Second keyboard reduce should go to 100%" + ); + + await FullZoom.reduce(); + Assert.equal( + ZoomManager.getZoomForBrowser(browser), + 0.9, + "Third keyboard reduce should go to 90%" + ); + + await FullZoom.reset(); + }); +}); diff --git a/src/zen/tests/mochitests/zoom/browser_mousewheel_zoom.js b/src/zen/tests/mochitests/zoom/browser_mousewheel_zoom.js new file mode 100644 index 000000000..a814f1dba --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_mousewheel_zoom.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const TEST_PAGE = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html"; + +var gTab1, gTab2, gLevel1; + +function test() { + waitForExplicitFinish(); + + // Scroll on Ctrl + mousewheel + SpecialPowers.pushPrefEnv({ set: [["mousewheel.with_control.action", 3]] }); + + (async function () { + gTab1 = BrowserTestUtils.addTab(gBrowser); + gTab2 = BrowserTestUtils.addTab(gBrowser); + + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + await FullZoomHelper.load(gTab1, TEST_PAGE); + await FullZoomHelper.load(gTab2, TEST_PAGE); + })().then(zoomTab1, FullZoomHelper.failAndContinue(finish)); +} + +function zoomTab1() { + (async function () { + is(gBrowser.selectedTab, gTab1, "Tab 1 is selected"); + FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1"); + + let browser1 = gBrowser.getBrowserForTab(gTab1); + await BrowserTestUtils.synthesizeMouse( + null, + 10, + 10, + { + wheel: true, + ctrlKey: true, + deltaY: -1, + deltaMode: WheelEvent.DOM_DELTA_LINE, + }, + browser1 + ); + + info("Waiting for tab 1 to be zoomed"); + await TestUtils.waitForCondition(() => { + gLevel1 = ZoomManager.getZoomForBrowser(browser1); + return gLevel1 > 1; + }); + + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoomHelper.zoomTest( + gTab2, + gLevel1, + "Tab 2 should have zoomed along with tab 1" + ); + })().then(finishTest, FullZoomHelper.failAndContinue(finish)); +} + +function finishTest() { + (async function () { + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + await FullZoom.reset(); + await FullZoomHelper.removeTabAndWaitForLocationChange(gTab1); + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + await FullZoom.reset(); + await FullZoomHelper.removeTabAndWaitForLocationChange(gTab2); + })().then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/src/zen/tests/mochitests/zoom/browser_sitespecific_background_pref.js b/src/zen/tests/mochitests/zoom/browser_sitespecific_background_pref.js new file mode 100644 index 000000000..5756c4d8d --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_sitespecific_background_pref.js @@ -0,0 +1,35 @@ +function test() { + waitForExplicitFinish(); + + (async function () { + let testPage = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html"; + let tab1 = BrowserTestUtils.addTab(gBrowser); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + await FullZoomHelper.load(tab1, testPage); + + let tab2 = BrowserTestUtils.addTab(gBrowser); + await FullZoomHelper.load(tab2, testPage); + + await FullZoom.enlarge(); + let tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser); + + await FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + let tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser); + is(tab2Zoom, tab1Zoom, "Zoom should affect background tabs"); + + Services.prefs.setBoolPref("browser.zoom.updateBackgroundTabs", false); + await FullZoom.reset(); + gBrowser.selectedTab = tab1; + tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser); + tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser); + isnot(tab1Zoom, tab2Zoom, "Zoom should not affect background tabs"); + + if (Services.prefs.prefHasUserValue("browser.zoom.updateBackgroundTabs")) { + Services.prefs.clearUserPref("browser.zoom.updateBackgroundTabs"); + } + await FullZoomHelper.removeTabAndWaitForLocationChange(tab1); + await FullZoomHelper.removeTabAndWaitForLocationChange(tab2); + })().then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/src/zen/tests/mochitests/zoom/browser_sitespecific_image_zoom.js b/src/zen/tests/mochitests/zoom/browser_sitespecific_image_zoom.js new file mode 100644 index 000000000..fae454fd0 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_sitespecific_image_zoom.js @@ -0,0 +1,52 @@ +var tabElm, zoomLevel; +function start_test_prefNotSet() { + (async function () { + is(ZoomManager.zoom, 1, "initial zoom level should be 1"); + FullZoom.enlarge(); + + // capture the zoom level to test later + zoomLevel = ZoomManager.zoom; + isnot(zoomLevel, 1, "zoom level should have changed"); + + await FullZoomHelper.load( + gBrowser.selectedTab, + "http://mochi.test:8888/browser/browser/base/content/test/general/moz.png" + ); + })().then(continue_test_prefNotSet, FullZoomHelper.failAndContinue(finish)); +} + +function continue_test_prefNotSet() { + (async function () { + is(ZoomManager.zoom, 1, "zoom level pref should not apply to an image"); + await FullZoom.reset(); + + await FullZoomHelper.load( + gBrowser.selectedTab, + "http://mochi.test:8888/browser/browser/base/content/test/zoom/zoom_test.html" + ); + })().then(end_test_prefNotSet, FullZoomHelper.failAndContinue(finish)); +} + +function end_test_prefNotSet() { + (async function () { + is(ZoomManager.zoom, zoomLevel, "the zoom level should have persisted"); + + // Reset the zoom so that other tests have a fresh zoom level + await FullZoom.reset(); + await FullZoomHelper.removeTabAndWaitForLocationChange(); + finish(); + })(); +} + +function test() { + waitForExplicitFinish(); + + (async function () { + tabElm = BrowserTestUtils.addTab(gBrowser); + await FullZoomHelper.selectTabAndWaitForLocationChange(tabElm); + await FullZoomHelper.load( + tabElm, + "http://mochi.test:8888/browser/browser/base/content/test/zoom/zoom_test.html" + ); + })().then(start_test_prefNotSet, FullZoomHelper.failAndContinue(finish)); +} diff --git a/src/zen/tests/mochitests/zoom/browser_sitespecific_video_zoom.js b/src/zen/tests/mochitests/zoom/browser_sitespecific_video_zoom.js new file mode 100644 index 000000000..94fd0dee5 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_sitespecific_video_zoom.js @@ -0,0 +1,128 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const TEST_PAGE = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html"; +const TEST_VIDEO = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/browser/base/content/test/general/video.webm"; + +var gTab1, gTab2, gLevel1; + +function test() { + waitForExplicitFinish(); + + (async function () { + gTab1 = BrowserTestUtils.addTab(gBrowser); + gTab2 = BrowserTestUtils.addTab(gBrowser); + + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + await FullZoomHelper.load(gTab1, TEST_PAGE); + await FullZoomHelper.load(gTab2, TEST_VIDEO); + })().then(zoomTab1, FullZoomHelper.failAndContinue(finish)); +} + +function zoomTab1() { + (async function () { + is(gBrowser.selectedTab, gTab1, "Tab 1 is selected"); + + // Reset zoom level if we run this test > 1 time in same browser session. + var level1 = ZoomManager.getZoomForBrowser( + gBrowser.getBrowserForTab(gTab1) + ); + if (level1 > 1) { + FullZoom.reduce(); + } + + FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1"); + + FullZoom.enlarge(); + gLevel1 = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1)); + + Assert.greater(gLevel1, 1, "New zoom for tab 1 should be greater than 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2"); + + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoomHelper.zoomTest( + gTab2, + 1, + "Tab 2 is still unzoomed after it is selected" + ); + FullZoomHelper.zoomTest(gTab1, gLevel1, "Tab 1 is still zoomed"); + })().then(zoomTab2, FullZoomHelper.failAndContinue(finish)); +} + +function zoomTab2() { + (async function () { + is(gBrowser.selectedTab, gTab2, "Tab 2 is selected"); + + FullZoom.reduce(); + let level2 = ZoomManager.getZoomForBrowser( + gBrowser.getBrowserForTab(gTab2) + ); + + Assert.less(level2, 1, "New zoom for tab 2 should be less than 1"); + FullZoomHelper.zoomTest( + gTab1, + gLevel1, + "Zooming tab 2 should not affect tab 1" + ); + + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + FullZoomHelper.zoomTest( + gTab1, + gLevel1, + "Tab 1 should have the same zoom after it's selected" + ); + })().then(testNavigation, FullZoomHelper.failAndContinue(finish)); +} + +function testNavigation() { + (async function () { + await FullZoomHelper.load(gTab1, TEST_VIDEO); + FullZoomHelper.zoomTest( + gTab1, + 1, + "Zoom should be 1 when a video was loaded" + ); + await waitForNextTurn(); // trying to fix orange bug 806046 + await FullZoomHelper.navigate(FullZoomHelper.BACK); + FullZoomHelper.zoomTest( + gTab1, + gLevel1, + "Zoom should be restored when a page is loaded" + ); + await waitForNextTurn(); // trying to fix orange bug 806046 + await FullZoomHelper.navigate(FullZoomHelper.FORWARD); + FullZoomHelper.zoomTest( + gTab1, + 1, + "Zoom should be 1 again when navigating back to a video" + ); + })().then(finishTest, FullZoomHelper.failAndContinue(finish)); +} + +function waitForNextTurn() { + return new Promise(resolve => { + setTimeout(() => resolve(), 0); + }); +} + +var finishTestStarted = false; +function finishTest() { + (async function () { + ok(!finishTestStarted, "finishTest called more than once"); + finishTestStarted = true; + + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + await FullZoom.reset(); + await FullZoomHelper.removeTabAndWaitForLocationChange(gTab1); + await FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + await FullZoom.reset(); + await FullZoomHelper.removeTabAndWaitForLocationChange(gTab2); + })().then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/src/zen/tests/mochitests/zoom/browser_subframe_textzoom.js b/src/zen/tests/mochitests/zoom/browser_subframe_textzoom.js new file mode 100644 index 000000000..e5d40cf58 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_subframe_textzoom.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test the fix for bug 441778 to ensure site-specific page zoom doesn't get + * modified by sub-document loads of content from a different domain. + */ + +function test() { + waitForExplicitFinish(); + + const TEST_PAGE_URL = 'data:text/html,'; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const TEST_IFRAME_URL = "http://test2.example.org/"; + + (async function () { + // Prepare the test tab + let tab = BrowserTestUtils.addTab(gBrowser); + await FullZoomHelper.selectTabAndWaitForLocationChange(tab); + + let testBrowser = tab.linkedBrowser; + + await FullZoomHelper.load(tab, TEST_PAGE_URL); + + // Change the zoom level and then save it so we can compare it to the level + // after loading the sub-document. + FullZoom.enlarge(); + var zoomLevel = ZoomManager.zoom; + + // Start the sub-document load. + await new Promise(resolve => { + executeSoon(function () { + BrowserTestUtils.browserLoaded(testBrowser, true).then(url => { + is(url, TEST_IFRAME_URL, "got the load event for the iframe"); + is( + ZoomManager.zoom, + zoomLevel, + "zoom is retained after sub-document load" + ); + + FullZoomHelper.removeTabAndWaitForLocationChange().then(() => + resolve() + ); + }); + SpecialPowers.spawn(testBrowser, [TEST_IFRAME_URL], url => { + content.document.querySelector("iframe").src = url; + }); + }); + }); + })().then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/src/zen/tests/mochitests/zoom/browser_tabswitch_zoom_flicker.js b/src/zen/tests/mochitests/zoom/browser_tabswitch_zoom_flicker.js new file mode 100644 index 000000000..df1c3816a --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_tabswitch_zoom_flicker.js @@ -0,0 +1,45 @@ +var tab; + +function test() { + // ---------- + // Test setup + + waitForExplicitFinish(); + + Services.prefs.setBoolPref("browser.zoom.updateBackgroundTabs", true); + Services.prefs.setBoolPref("browser.zoom.siteSpecific", true); + + let uri = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html"; + + (async function () { + tab = BrowserTestUtils.addTab(gBrowser); + await FullZoomHelper.load(tab, uri); + + // ------------------------------------------------------------------- + // Test - Trigger a tab switch that should update the zoom level + await FullZoomHelper.selectTabAndWaitForLocationChange(tab); + ok(true, "applyPrefToSetting was called"); + })().then(endTest, FullZoomHelper.failAndContinue(endTest)); +} + +// ------------- +// Test clean-up +function endTest() { + (async function () { + await FullZoomHelper.removeTabAndWaitForLocationChange(tab); + + tab = null; + + if (Services.prefs.prefHasUserValue("browser.zoom.updateBackgroundTabs")) { + Services.prefs.clearUserPref("browser.zoom.updateBackgroundTabs"); + } + + if (Services.prefs.prefHasUserValue("browser.zoom.siteSpecific")) { + Services.prefs.clearUserPref("browser.zoom.siteSpecific"); + } + + finish(); + })(); +} diff --git a/src/zen/tests/mochitests/zoom/browser_tooltip_zoom.js b/src/zen/tests/mochitests/zoom/browser_tooltip_zoom.js new file mode 100644 index 000000000..f7627b174 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_tooltip_zoom.js @@ -0,0 +1,41 @@ +add_task(async function test_zoom_tooltip() { + const TEST_PAGE_URL = 'data:text/html,'; + await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async function (browser) { + FullZoom.setZoom(2.0, browser); + + const tooltip = document.getElementById("remoteBrowserTooltip"); + const popupShown = new Promise(resolve => { + tooltip.addEventListener("popupshown", resolve, { once: true }); + }); + + // Fire a mousemove to trigger the tooltip. + // Margin from the anchor and stuff depends on the platform, but these + // should be big enough so that all platforms pass, but not big enough so + // that it'd pass even when messing up the coordinates would. + const DISTANCE = 300; + const EPSILON = 25; + + EventUtils.synthesizeMouse(browser, DISTANCE, DISTANCE, { + type: "mousemove", + }); + + await popupShown; + ok( + true, + `popup should be shown (coords: ${tooltip.screenX}, ${tooltip.screenY})` + ); + + isfuzzy( + tooltip.screenX, + browser.screenX + DISTANCE, + EPSILON, + "Should be at the right x position, more or less" + ); + isfuzzy( + tooltip.screenY, + browser.screenY + DISTANCE, + EPSILON, + "Should be at the right y position, more or less" + ); + }); +}); diff --git a/src/zen/tests/mochitests/zoom/browser_zoom_commands.js b/src/zen/tests/mochitests/zoom/browser_zoom_commands.js new file mode 100644 index 000000000..e8992bf37 --- /dev/null +++ b/src/zen/tests/mochitests/zoom/browser_zoom_commands.js @@ -0,0 +1,202 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PAGE_URL = + "data:text/html;charset=utf-8,test_zoom_levels"; + +/** + * Waits for the zoom commands in the window to have the expected enabled + * state. + * + * @param {object} expectedState + * An object where each key represents one of the zoom commands, + * and the value is a boolean that is true if the command should + * be enabled, and false if it should be disabled. + * + * The keys are "enlarge", "reduce" and "reset" for readability, + * and internally this function maps those keys to the appropriate + * commands. + * @returns {Promise} + */ +async function waitForCommandEnabledState(expectedState) { + const COMMAND_MAP = { + enlarge: "cmd_fullZoomEnlarge", + reduce: "cmd_fullZoomReduce", + reset: "cmd_fullZoomReset", + }; + + await TestUtils.waitForCondition(() => { + for (let commandKey in expectedState) { + let commandID = COMMAND_MAP[commandKey]; + let command = document.getElementById(commandID); + let expectedEnabled = expectedState[commandKey]; + + if (command.hasAttribute("disabled") == expectedEnabled) { + return false; + } + } + Assert.ok("Commands finally reached the expected state."); + return true; + }, "Waiting for commands to reach the right state."); +} + +/** + * Tests that the "Zoom Text Only" command is in the right checked + * state. + * + * @param {boolean} isChecked + * True if the command should have its "checked" attribute set to + * "true". Otherwise, ensures that the attribute is set to "false". + */ +function assertTextZoomCommandCheckedState(isChecked) { + let command = document.getElementById("cmd_fullZoomToggle"); + Assert.equal( + command.hasAttribute("checked"), + isChecked, + "Text zoom command has expected checked attribute" + ); +} + +/** + * Tests that zoom commands are properly updated when changing + * zoom levels and/or preferences on an individual browser. + */ +add_task(async function test_update_browser_zoom() { + await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async () => { + let currentZoom = await FullZoomHelper.getGlobalValue(); + Assert.equal( + currentZoom, + 1, + "We expect to start at the default zoom level." + ); + + await waitForCommandEnabledState({ + enlarge: true, + reduce: true, + reset: false, + }); + assertTextZoomCommandCheckedState(false); + + // We'll run two variations of this test - one with text zoom enabled, + // and the other without. + for (let textZoom of [true, false]) { + info(`Running variation with textZoom set to ${textZoom}`); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.zoom.full", !textZoom]], + }); + + // 120% global zoom + info("Changing default zoom by a single level"); + ZoomManager.zoom = 1.2; + + await waitForCommandEnabledState({ + enlarge: true, + reduce: true, + reset: true, + }); + await assertTextZoomCommandCheckedState(textZoom); + + // Now max out the zoom level. + ZoomManager.zoom = ZoomManager.MAX; + + await waitForCommandEnabledState({ + enlarge: false, + reduce: true, + reset: true, + }); + await assertTextZoomCommandCheckedState(textZoom); + + // Now min out the zoom level. + ZoomManager.zoom = ZoomManager.MIN; + await waitForCommandEnabledState({ + enlarge: true, + reduce: false, + reset: true, + }); + await assertTextZoomCommandCheckedState(textZoom); + + // Now reset back to the default zoom level + ZoomManager.zoom = 1; + await waitForCommandEnabledState({ + enlarge: true, + reduce: true, + reset: false, + }); + await assertTextZoomCommandCheckedState(textZoom); + } + }); +}); + +/** + * Tests that zoom commands are properly updated when changing + * zoom levels when the default zoom is not at 1.0. + */ +add_task(async function test_update_browser_zoom() { + await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async () => { + let currentZoom = await FullZoomHelper.getGlobalValue(); + Assert.equal( + currentZoom, + 1, + "We expect to start at the default zoom level." + ); + + // Now change the default zoom to 200%, which is what we'll switch + // back to when choosing to reset the zoom level. + // + // It's a bit maddening that changeDefaultZoom takes values in integer + // units from 30 to 500, whereas ZoomManager.zoom takes things in float + // units from 0.3 to 5.0, but c'est la vie for now. + await FullZoomHelper.changeDefaultZoom(200); + registerCleanupFunction(async () => { + await FullZoomHelper.changeDefaultZoom(100); + }); + + await waitForCommandEnabledState({ + enlarge: true, + reduce: true, + reset: false, + }); + + // 120% global zoom + info("Changing default zoom by a single level"); + ZoomManager.zoom = 2.2; + + await waitForCommandEnabledState({ + enlarge: true, + reduce: true, + reset: true, + }); + await assertTextZoomCommandCheckedState(false); + + // Now max out the zoom level. + ZoomManager.zoom = ZoomManager.MAX; + + await waitForCommandEnabledState({ + enlarge: false, + reduce: true, + reset: true, + }); + await assertTextZoomCommandCheckedState(false); + + // Now min out the zoom level. + ZoomManager.zoom = ZoomManager.MIN; + await waitForCommandEnabledState({ + enlarge: true, + reduce: false, + reset: true, + }); + await assertTextZoomCommandCheckedState(false); + + // Now reset back to the default zoom level + ZoomManager.zoom = 2; + await waitForCommandEnabledState({ + enlarge: true, + reduce: true, + reset: false, + }); + await assertTextZoomCommandCheckedState(false); + }); +}); diff --git a/src/zen/tests/mochitests/zoom/head.js b/src/zen/tests/mochitests/zoom/head.js new file mode 100644 index 000000000..b8f9f18cb --- /dev/null +++ b/src/zen/tests/mochitests/zoom/head.js @@ -0,0 +1,190 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +let gContentPrefs = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 +); + +let gLoadContext = Cu.createLoadContext(); + +registerCleanupFunction(async function () { + await new Promise(resolve => { + gContentPrefs.removeByName(window.FullZoom.name, gLoadContext, { + handleResult() {}, + handleCompletion() { + resolve(); + }, + }); + }); +}); + +var FullZoomHelper = { + async changeDefaultZoom(newZoom) { + let nonPrivateLoadContext = Cu.createLoadContext(); + /* Because our setGlobal function takes in a browsing context, and + * because we want to keep this property consistent across both private + * and non-private contexts, we crate a non-private context and use that + * to set the property, regardless of our actual context. + */ + + let parsedZoomValue = parseFloat((parseInt(newZoom) / 100).toFixed(2)); + await new Promise(resolve => { + gContentPrefs.setGlobal( + FullZoom.name, + parsedZoomValue, + nonPrivateLoadContext, + { + handleCompletion() { + resolve(); + }, + } + ); + }); + // The zoom level is used to update the commands associated with + // increasing, decreasing or resetting the Zoom levels. There are + // a series of async things we need to wait for (writing the content + // pref to the database, and then reading that content pref back out + // again and reacting to it), so waiting for the zoom level to reach + // the expected level is actually simplest to make sure we're okay to + // proceed. + await TestUtils.waitForCondition(() => { + return ZoomManager.zoom == parsedZoomValue; + }); + }, + + async getGlobalValue() { + return new Promise(resolve => { + let cachedVal = parseFloat( + gContentPrefs.getCachedGlobal(FullZoom.name, gLoadContext) + ); + if (cachedVal) { + // We've got cached information, though it may be we've cached + // an undefined value, or the cached info is invalid. To ensure + // a valid return, we opt to return the default 1.0 in the + // undefined and invalid cases. + resolve(parseFloat(cachedVal.value) || 1.0); + return; + } + let value = 1.0; + gContentPrefs.getGlobal(FullZoom.name, gLoadContext, { + handleResult(pref) { + if (pref.value) { + value = parseFloat(pref.value); + } + }, + handleCompletion() { + resolve(value); + }, + handleError(error) { + console.error(error); + }, + }); + }); + }, + + waitForLocationChange: function waitForLocationChange() { + return new Promise(resolve => { + Services.obs.addObserver(function obs(subj, topic) { + Services.obs.removeObserver(obs, topic); + resolve(); + }, "browser-fullZoom:location-change"); + }); + }, + + selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange( + tab + ) { + if (!tab) { + throw new Error("tab must be given."); + } + if (gBrowser.selectedTab == tab) { + return Promise.resolve(); + } + + return Promise.all([ + BrowserTestUtils.switchTab(gBrowser, tab), + this.waitForLocationChange(), + ]); + }, + + removeTabAndWaitForLocationChange: function removeTabAndWaitForLocationChange( + tab + ) { + tab = tab || gBrowser.selectedTab; + let selected = gBrowser.selectedTab == tab; + gBrowser.removeTab(tab); + if (selected) { + return this.waitForLocationChange(); + } + return Promise.resolve(); + }, + + load: function load(tab, url) { + return new Promise(resolve => { + let didLoad = false; + let didZoom = false; + + BrowserTestUtils.loadURIString({ + browser: tab.linkedBrowser, + uriString: url, + }).then(() => { + didLoad = true; + if (didZoom) { + resolve(); + } + }, true); + + this.waitForLocationChange().then(function () { + didZoom = true; + if (didLoad) { + resolve(); + } + }); + }); + }, + + zoomTest: function zoomTest(tab, val, msg) { + is(ZoomManager.getZoomForBrowser(tab.linkedBrowser), val, msg); + }, + + BACK: 0, + FORWARD: 1, + navigate: function navigate(direction) { + return new Promise(resolve => { + let didPs = false; + let didZoom = false; + + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "pageshow", + true + ).then(() => { + didPs = true; + if (didZoom) { + resolve(); + } + }); + + if (direction == this.BACK) { + gBrowser.goBack(false); + } else if (direction == this.FORWARD) { + gBrowser.goForward(); + } + + this.waitForLocationChange().then(function () { + didZoom = true; + if (didPs) { + resolve(); + } + }); + }); + }, + + failAndContinue: function failAndContinue(func) { + return function (err) { + console.error(err); + ok(false, err); + func(); + }; + }, +}; diff --git a/src/zen/tests/mochitests/zoom/zoom_test.html b/src/zen/tests/mochitests/zoom/zoom_test.html new file mode 100644 index 000000000..bf80490ca --- /dev/null +++ b/src/zen/tests/mochitests/zoom/zoom_test.html @@ -0,0 +1,14 @@ + + + + + Test for zoom setting + + + + Bug 416661 +

Site specific zoom settings should not apply to image documents.

+ +