From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../test/SessionStoreTestUtils.sys.mjs | 186 ++++++ browser/components/sessionstore/test/browser.toml | 577 +++++++++++++++++ .../sessionstore/test/browser_1234021.js | 22 + .../sessionstore/test/browser_1234021_page.html | 6 + .../test/browser_1284886_suspend_tab.html | 12 + .../test/browser_1284886_suspend_tab.js | 95 +++ .../test/browser_1284886_suspend_tab_2.html | 11 + .../test/browser_1446343-windowsize.js | 39 ++ .../test/browser_248970_b_perwindowpb.js | 199 ++++++ .../sessionstore/test/browser_248970_b_sample.html | 37 ++ .../components/sessionstore/test/browser_339445.js | 39 ++ .../sessionstore/test/browser_339445_sample.html | 18 + .../components/sessionstore/test/browser_345898.js | 69 +++ .../components/sessionstore/test/browser_350525.js | 136 ++++ .../test/browser_354894_perwindowpb.js | 489 +++++++++++++++ .../components/sessionstore/test/browser_367052.js | 52 ++ .../components/sessionstore/test/browser_393716.js | 103 +++ .../sessionstore/test/browser_394759_basic.js | 123 ++++ .../sessionstore/test/browser_394759_behavior.js | 91 +++ .../test/browser_394759_perwindowpb.js | 61 ++ .../sessionstore/test/browser_394759_purge.js | 247 ++++++++ .../components/sessionstore/test/browser_423132.js | 52 ++ .../sessionstore/test/browser_423132_sample.html | 14 + .../components/sessionstore/test/browser_447951.js | 84 +++ .../sessionstore/test/browser_447951_sample.html | 5 + .../components/sessionstore/test/browser_454908.js | 65 ++ .../sessionstore/test/browser_454908_sample.html | 8 + .../components/sessionstore/test/browser_456342.js | 90 +++ .../sessionstore/test/browser_456342_sample.xhtml | 46 ++ .../components/sessionstore/test/browser_459906.js | 78 +++ .../sessionstore/test/browser_459906_empty.html | 3 + .../sessionstore/test/browser_459906_sample.html | 41 ++ .../components/sessionstore/test/browser_461634.js | 133 ++++ .../components/sessionstore/test/browser_461743.js | 53 ++ .../sessionstore/test/browser_461743_sample.html | 56 ++ .../components/sessionstore/test/browser_463205.js | 40 ++ .../sessionstore/test/browser_463205_sample.html | 7 + .../components/sessionstore/test/browser_463206.js | 120 ++++ .../sessionstore/test/browser_463206_sample.html | 11 + .../components/sessionstore/test/browser_464199.js | 176 ++++++ .../sessionstore/test/browser_464620_a.html | 54 ++ .../sessionstore/test/browser_464620_a.js | 64 ++ .../sessionstore/test/browser_464620_b.html | 57 ++ .../sessionstore/test/browser_464620_b.js | 64 ++ .../sessionstore/test/browser_464620_xd.html | 5 + .../components/sessionstore/test/browser_465215.js | 36 ++ .../components/sessionstore/test/browser_465223.js | 51 ++ .../components/sessionstore/test/browser_466937.js | 51 ++ .../sessionstore/test/browser_466937_sample.html | 20 + .../test/browser_467409-backslashplosion.js | 88 +++ .../components/sessionstore/test/browser_477657.js | 80 +++ .../components/sessionstore/test/browser_480893.js | 45 ++ .../components/sessionstore/test/browser_485482.js | 76 +++ .../sessionstore/test/browser_485482_sample.html | 12 + .../components/sessionstore/test/browser_485563.js | 33 + .../components/sessionstore/test/browser_490040.js | 105 ++++ .../components/sessionstore/test/browser_491168.js | 113 ++++ .../components/sessionstore/test/browser_491577.js | 212 +++++++ .../components/sessionstore/test/browser_495495.js | 47 ++ .../components/sessionstore/test/browser_500328.js | 132 ++++ .../components/sessionstore/test/browser_506482.js | 78 +++ .../components/sessionstore/test/browser_514751.js | 41 ++ .../components/sessionstore/test/browser_522375.js | 25 + .../components/sessionstore/test/browser_522545.js | 443 +++++++++++++ .../components/sessionstore/test/browser_524745.js | 55 ++ .../components/sessionstore/test/browser_526613.js | 86 +++ .../components/sessionstore/test/browser_528776.js | 27 + .../components/sessionstore/test/browser_579868.js | 31 + .../components/sessionstore/test/browser_579879.js | 31 + .../components/sessionstore/test/browser_580512.js | 117 ++++ .../components/sessionstore/test/browser_581937.js | 22 + .../sessionstore/test/browser_586068-apptabs.js | 109 ++++ .../test/browser_586068-apptabs_ondemand.js | 105 ++++ .../browser_586068-browser_state_interrupted.js | 212 +++++++ .../sessionstore/test/browser_586068-cascade.js | 107 ++++ .../test/browser_586068-multi_window.js | 115 ++++ .../sessionstore/test/browser_586068-reload.js | 118 ++++ .../sessionstore/test/browser_586068-select.js | 128 ++++ .../test/browser_586068-window_state.js | 120 ++++ .../test/browser_586068-window_state_override.js | 118 ++++ .../components/sessionstore/test/browser_586147.js | 52 ++ .../components/sessionstore/test/browser_588426.js | 62 ++ .../components/sessionstore/test/browser_589246.js | 286 +++++++++ .../components/sessionstore/test/browser_590268.js | 159 +++++ .../components/sessionstore/test/browser_590563.js | 120 ++++ .../test/browser_595601-restore_hidden.js | 165 +++++ .../components/sessionstore/test/browser_597071.js | 36 ++ .../components/sessionstore/test/browser_600545.js | 123 ++++ .../components/sessionstore/test/browser_601955.js | 54 ++ .../components/sessionstore/test/browser_607016.js | 155 +++++ ...ser_615394-SSWindowState_events_duplicateTab.js | 69 +++ ..._615394-SSWindowState_events_setBrowserState.js | 152 +++++ ...wser_615394-SSWindowState_events_setTabState.js | 61 ++ ...r_615394-SSWindowState_events_setWindowState.js | 65 ++ ...ser_615394-SSWindowState_events_undoCloseTab.js | 65 ++ ..._615394-SSWindowState_events_undoCloseWindow.js | 146 +++++ .../components/sessionstore/test/browser_618151.js | 67 ++ .../components/sessionstore/test/browser_623779.js | 13 + .../components/sessionstore/test/browser_624727.js | 33 + .../components/sessionstore/test/browser_625016.js | 103 +++ .../components/sessionstore/test/browser_628270.js | 43 ++ .../components/sessionstore/test/browser_635418.js | 58 ++ .../components/sessionstore/test/browser_636279.js | 144 +++++ .../components/sessionstore/test/browser_637020.js | 74 +++ .../sessionstore/test/browser_637020_slow.sjs | 22 + .../components/sessionstore/test/browser_645428.js | 22 + .../components/sessionstore/test/browser_659591.js | 39 ++ .../components/sessionstore/test/browser_662743.js | 138 +++++ .../sessionstore/test/browser_662743_sample.html | 15 + .../components/sessionstore/test/browser_662812.js | 43 ++ .../test/browser_665702-state_session.js | 26 + .../components/sessionstore/test/browser_682507.js | 22 + .../components/sessionstore/test/browser_687710.js | 58 ++ .../sessionstore/test/browser_687710_2.js | 100 +++ .../components/sessionstore/test/browser_694378.js | 41 ++ .../components/sessionstore/test/browser_701377.js | 56 ++ .../components/sessionstore/test/browser_705597.js | 85 +++ .../components/sessionstore/test/browser_707862.js | 95 +++ .../components/sessionstore/test/browser_739531.js | 55 ++ .../sessionstore/test/browser_739531_frame.html | 1 + .../sessionstore/test/browser_739531_sample.html | 23 + .../components/sessionstore/test/browser_739805.js | 49 ++ .../test/browser_819510_perwindowpb.js | 152 +++++ .../sessionstore/test/browser_906076_lazy_tabs.js | 163 +++++ .../components/sessionstore/test/browser_911547.js | 82 +++ .../sessionstore/test/browser_911547_sample.html | 18 + .../test/browser_911547_sample.html^headers^ | 1 + .../test/browser_aboutPrivateBrowsing.js | 24 + .../test/browser_aboutSessionRestore.js | 67 ++ .../test/browser_async_duplicate_tab.js | 87 +++ .../sessionstore/test/browser_async_flushes.js | 131 ++++ .../sessionstore/test/browser_async_remove_tab.js | 209 +++++++ .../test/browser_async_window_flushing.js | 208 +++++++ .../sessionstore/test/browser_attributes.js | 83 +++ .../test/browser_background_tab_crash.js | 262 ++++++++ .../sessionstore/test/browser_backup_recovery.js | 308 +++++++++ .../sessionstore/test/browser_bfcache_telemetry.js | 45 ++ .../sessionstore/test/browser_broadcast.js | 162 +++++ .../sessionstore/test/browser_capabilities.js | 90 +++ .../sessionstore/test/browser_cleaner.js | 214 +++++++ .../sessionstore/test/browser_closedId.js | 109 ++++ ...er_closed_objects_changed_notifications_tabs.js | 138 +++++ ...closed_objects_changed_notifications_windows.js | 131 ++++ .../test/browser_closed_tabs_closed_windows.js | 319 ++++++++++ .../test/browser_closed_tabs_windows.js | 337 ++++++++++ .../sessionstore/test/browser_cookies.js | 81 +++ .../sessionstore/test/browser_cookies_legacy.js | 76 +++ .../sessionstore/test/browser_cookies_privacy.js | 125 ++++ .../sessionstore/test/browser_cookies_sameSite.js | 89 +++ .../sessionstore/test/browser_crashedTabs.js | 503 +++++++++++++++ .../test/browser_docshell_uuid_consistency.js | 104 ++++ .../sessionstore/test/browser_duplicate_history.js | 30 + .../test/browser_duplicate_tab_in_new_window.js | 37 ++ .../sessionstore/test/browser_dying_cache.js | 84 +++ .../sessionstore/test/browser_dynamic_frames.js | 105 ++++ .../test/browser_firefoxView_restore.js | 39 ++ .../test/browser_firefoxView_selected_restore.js | 88 +++ .../test/browser_focus_after_restore.js | 34 + .../test/browser_forget_async_closings.js | 163 +++++ .../test/browser_forget_closed_tab_window_byId.js | 148 +++++ .../sessionstore/test/browser_formdata.js | 227 +++++++ .../sessionstore/test/browser_formdata_cc.js | 107 ++++ .../sessionstore/test/browser_formdata_face.js | 168 +++++ .../sessionstore/test/browser_formdata_format.js | 170 +++++ .../test/browser_formdata_format_sample.html | 7 + .../sessionstore/test/browser_formdata_max_size.js | 131 ++++ .../sessionstore/test/browser_formdata_password.js | 69 +++ .../sessionstore/test/browser_formdata_sample.html | 20 + .../sessionstore/test/browser_formdata_xpath.js | 242 ++++++++ .../test/browser_formdata_xpath_sample.html | 37 ++ .../sessionstore/test/browser_frame_history.js | 230 +++++++ .../sessionstore/test/browser_frame_history_a.html | 5 + .../sessionstore/test/browser_frame_history_b.html | 10 + .../sessionstore/test/browser_frame_history_c.html | 5 + .../test/browser_frame_history_c1.html | 5 + .../test/browser_frame_history_c2.html | 5 + .../test/browser_frame_history_index.html | 9 + .../test/browser_frame_history_index2.html | 3 + .../test/browser_frame_history_index_blank.html | 4 + .../sessionstore/test/browser_frametree.js | 134 ++++ .../test/browser_frametree_sample.html | 8 + .../test/browser_frametree_sample_frameset.html | 11 + .../test/browser_frametree_sample_iframes.html | 9 + .../sessionstore/test/browser_global_store.js | 53 ++ .../sessionstore/test/browser_history_persist.js | 163 +++++ .../test/browser_ignore_updates_crashed_tabs.js | 108 ++++ .../sessionstore/test/browser_label_and_icon.js | 53 ++ .../test/browser_movePendingTabToNewWindow.js | 124 ++++ .../test/browser_multiple_navigateAndRestore.js | 45 ++ .../test/browser_multiple_select_after_load.js | 54 ++ .../test/browser_newtab_userTypedValue.js | 92 +++ .../test/browser_not_collect_when_idle.js | 118 ++++ .../sessionstore/test/browser_old_favicon.js | 57 ++ .../sessionstore/test/browser_page_title.js | 54 ++ .../test/browser_parentProcessRestoreHash.js | 115 ++++ .../sessionstore/test/browser_pending_tabs.js | 38 ++ .../sessionstore/test/browser_pinned_tabs.js | 324 ++++++++++ .../sessionstore/test/browser_privatetabs.js | 60 ++ .../sessionstore/test/browser_purge_shistory.js | 65 ++ .../test/browser_remoteness_flip_on_restore.js | 310 +++++++++ .../test/browser_reopen_all_windows.js | 146 +++++ .../sessionstore/test/browser_replace_load.js | 56 ++ .../test/browser_restoreLastActionCorrectOrder.js | 101 +++ ...rowser_restoreLastClosedTabOrWindowOrSession.js | 284 +++++++++ .../test/browser_restoreTabContainer.js | 81 +++ .../test/browser_restore_container_tabs_oa.js | 249 ++++++++ .../browser_restore_cookies_noOriginAttributes.js | 191 ++++++ .../test/browser_restore_pageProxyState.js | 77 +++ .../test/browser_restore_private_tab_os.js | 59 ++ .../sessionstore/test/browser_restore_redirect.js | 72 +++ .../test/browser_restore_reversed_z_order.js | 128 ++++ .../sessionstore/test/browser_restore_srcdoc.js | 44 ++ .../test/browser_restore_tabless_window.js | 56 ++ .../test/browser_restored_window_features.js | 167 +++++ .../test/browser_revive_crashed_bg_tabs.js | 59 ++ .../sessionstore/test/browser_scrollPositions.js | 258 ++++++++ .../test/browser_scrollPositionsReaderMode.js | 76 +++ .../browser_scrollPositions_readerModeArticle.html | 26 + .../test/browser_scrollPositions_sample.html | 8 + .../test/browser_scrollPositions_sample2.html | 8 + .../browser_scrollPositions_sample_frameset.html | 11 + .../test/browser_send_async_message_oom.js | 75 +++ .../sessionstore/test/browser_sessionHistory.js | 331 ++++++++++ .../test/browser_sessionHistory_slow.sjs | 22 + .../sessionstore/test/browser_sessionStorage.html | 28 + .../sessionstore/test/browser_sessionStorage.js | 298 +++++++++ .../test/browser_sessionStorage_size.js | 34 + .../test/browser_sessionStoreContainer.js | 166 +++++ .../test/browser_should_restore_tab.js | 139 +++++ .../test/browser_sizemodeBeforeMinimized.js | 44 ++ .../test/browser_speculative_connect.html | 8 + .../test/browser_speculative_connect.js | 145 +++++ .../sessionstore/test/browser_swapDocShells.js | 40 ++ .../sessionstore/test/browser_switch_remoteness.js | 57 ++ .../test/browser_tab_label_during_restore.js | 182 ++++++ .../test/browser_tabicon_after_bg_tab_crash.js | 52 ++ .../sessionstore/test/browser_tabs_in_urlbar.js | 151 +++++ .../sessionstore/test/browser_undoCloseById.js | 185 ++++++ .../test/browser_undoCloseById_targetWindow.js | 93 +++ .../test/browser_unrestored_crashedTabs.js | 73 +++ .../sessionstore/test/browser_upgrade_backup.js | 158 +++++ .../sessionstore/test/browser_urlbarSearchMode.js | 57 ++ .../browser_userTyped_restored_after_discard.js | 47 ++ .../test/browser_windowRestore_perwindowpb.js | 32 + .../test/browser_windowStateContainer.js | 176 ++++++ .../sessionstore/test/coopHeaderCommon.sjs | 32 + .../components/sessionstore/test/coop_coep.html | 6 + .../sessionstore/test/coop_coep.html^headers^ | 2 + browser/components/sessionstore/test/empty.html | 6 + .../test/file_async_duplicate_tab.html | 1 + .../sessionstore/test/file_async_flushes.html | 1 + .../sessionstore/test/file_formdata_password.html | 17 + .../test/file_sessionHistory_hashchange.html | 1 + browser/components/sessionstore/test/head.js | 690 +++++++++++++++++++++ .../sessionstore/test/marionette/manifest.toml | 19 + .../test/marionette/session_store_test_case.py | 432 +++++++++++++ .../test_persist_closed_tabs_restore_manually.py | 225 +++++++ .../test/marionette/test_restore_loading_tab.py | 69 +++ .../test/marionette/test_restore_manually.py | 144 +++++ .../test_restore_manually_with_pinned_tabs.py | 108 ++++ .../test_restore_windows_after_close_last_tabs.py | 59 ++ .../test_restore_windows_after_restart_and_quit.py | 88 +++ .../test_restore_windows_after_windows_shutdown.py | 66 ++ .../sessionstore/test/restore_redirect_http.html | 0 .../test/restore_redirect_http.html^headers^ | 2 + .../sessionstore/test/restore_redirect_js.html | 10 + .../sessionstore/test/restore_redirect_target.html | 8 + .../test/unit/data/sessionCheckpoints_all.json | 11 + .../test/unit/data/sessionstore_invalid.js | 3 + .../test/unit/data/sessionstore_valid.js | 3 + browser/components/sessionstore/test/unit/head.js | 36 ++ .../sessionstore/test/unit/test_backup_once.js | 137 ++++ .../test/unit/test_final_write_cleanup.js | 116 ++++ .../test/unit/test_histogram_corrupt_files.js | 117 ++++ .../test/unit/test_migration_lz4compression.js | 151 +++++ .../test/unit/test_startup_invalid_session.js | 27 + .../test/unit/test_startup_nosession_async.js | 19 + .../test/unit/test_startup_session_async.js | 32 + .../sessionstore/test/unit/xpcshell.toml | 28 + 279 files changed, 26233 insertions(+) create mode 100644 browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs create mode 100644 browser/components/sessionstore/test/browser.toml create mode 100644 browser/components/sessionstore/test/browser_1234021.js create mode 100644 browser/components/sessionstore/test/browser_1234021_page.html create mode 100644 browser/components/sessionstore/test/browser_1284886_suspend_tab.html create mode 100644 browser/components/sessionstore/test/browser_1284886_suspend_tab.js create mode 100644 browser/components/sessionstore/test/browser_1284886_suspend_tab_2.html create mode 100644 browser/components/sessionstore/test/browser_1446343-windowsize.js create mode 100644 browser/components/sessionstore/test/browser_248970_b_perwindowpb.js create mode 100644 browser/components/sessionstore/test/browser_248970_b_sample.html create mode 100644 browser/components/sessionstore/test/browser_339445.js create mode 100644 browser/components/sessionstore/test/browser_339445_sample.html create mode 100644 browser/components/sessionstore/test/browser_345898.js create mode 100644 browser/components/sessionstore/test/browser_350525.js create mode 100644 browser/components/sessionstore/test/browser_354894_perwindowpb.js create mode 100644 browser/components/sessionstore/test/browser_367052.js create mode 100644 browser/components/sessionstore/test/browser_393716.js create mode 100644 browser/components/sessionstore/test/browser_394759_basic.js create mode 100644 browser/components/sessionstore/test/browser_394759_behavior.js create mode 100644 browser/components/sessionstore/test/browser_394759_perwindowpb.js create mode 100644 browser/components/sessionstore/test/browser_394759_purge.js create mode 100644 browser/components/sessionstore/test/browser_423132.js create mode 100644 browser/components/sessionstore/test/browser_423132_sample.html create mode 100644 browser/components/sessionstore/test/browser_447951.js create mode 100644 browser/components/sessionstore/test/browser_447951_sample.html create mode 100644 browser/components/sessionstore/test/browser_454908.js create mode 100644 browser/components/sessionstore/test/browser_454908_sample.html create mode 100644 browser/components/sessionstore/test/browser_456342.js create mode 100644 browser/components/sessionstore/test/browser_456342_sample.xhtml create mode 100644 browser/components/sessionstore/test/browser_459906.js create mode 100644 browser/components/sessionstore/test/browser_459906_empty.html create mode 100644 browser/components/sessionstore/test/browser_459906_sample.html create mode 100644 browser/components/sessionstore/test/browser_461634.js create mode 100644 browser/components/sessionstore/test/browser_461743.js create mode 100644 browser/components/sessionstore/test/browser_461743_sample.html create mode 100644 browser/components/sessionstore/test/browser_463205.js create mode 100644 browser/components/sessionstore/test/browser_463205_sample.html create mode 100644 browser/components/sessionstore/test/browser_463206.js create mode 100644 browser/components/sessionstore/test/browser_463206_sample.html create mode 100644 browser/components/sessionstore/test/browser_464199.js create mode 100644 browser/components/sessionstore/test/browser_464620_a.html create mode 100644 browser/components/sessionstore/test/browser_464620_a.js create mode 100644 browser/components/sessionstore/test/browser_464620_b.html create mode 100644 browser/components/sessionstore/test/browser_464620_b.js create mode 100644 browser/components/sessionstore/test/browser_464620_xd.html create mode 100644 browser/components/sessionstore/test/browser_465215.js create mode 100644 browser/components/sessionstore/test/browser_465223.js create mode 100644 browser/components/sessionstore/test/browser_466937.js create mode 100644 browser/components/sessionstore/test/browser_466937_sample.html create mode 100644 browser/components/sessionstore/test/browser_467409-backslashplosion.js create mode 100644 browser/components/sessionstore/test/browser_477657.js create mode 100644 browser/components/sessionstore/test/browser_480893.js create mode 100644 browser/components/sessionstore/test/browser_485482.js create mode 100644 browser/components/sessionstore/test/browser_485482_sample.html create mode 100644 browser/components/sessionstore/test/browser_485563.js create mode 100644 browser/components/sessionstore/test/browser_490040.js create mode 100644 browser/components/sessionstore/test/browser_491168.js create mode 100644 browser/components/sessionstore/test/browser_491577.js create mode 100644 browser/components/sessionstore/test/browser_495495.js create mode 100644 browser/components/sessionstore/test/browser_500328.js create mode 100644 browser/components/sessionstore/test/browser_506482.js create mode 100644 browser/components/sessionstore/test/browser_514751.js create mode 100644 browser/components/sessionstore/test/browser_522375.js create mode 100644 browser/components/sessionstore/test/browser_522545.js create mode 100644 browser/components/sessionstore/test/browser_524745.js create mode 100644 browser/components/sessionstore/test/browser_526613.js create mode 100644 browser/components/sessionstore/test/browser_528776.js create mode 100644 browser/components/sessionstore/test/browser_579868.js create mode 100644 browser/components/sessionstore/test/browser_579879.js create mode 100644 browser/components/sessionstore/test/browser_580512.js create mode 100644 browser/components/sessionstore/test/browser_581937.js create mode 100644 browser/components/sessionstore/test/browser_586068-apptabs.js create mode 100644 browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js create mode 100644 browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js create mode 100644 browser/components/sessionstore/test/browser_586068-cascade.js create mode 100644 browser/components/sessionstore/test/browser_586068-multi_window.js create mode 100644 browser/components/sessionstore/test/browser_586068-reload.js create mode 100644 browser/components/sessionstore/test/browser_586068-select.js create mode 100644 browser/components/sessionstore/test/browser_586068-window_state.js create mode 100644 browser/components/sessionstore/test/browser_586068-window_state_override.js create mode 100644 browser/components/sessionstore/test/browser_586147.js create mode 100644 browser/components/sessionstore/test/browser_588426.js create mode 100644 browser/components/sessionstore/test/browser_589246.js create mode 100644 browser/components/sessionstore/test/browser_590268.js create mode 100644 browser/components/sessionstore/test/browser_590563.js create mode 100644 browser/components/sessionstore/test/browser_595601-restore_hidden.js create mode 100644 browser/components/sessionstore/test/browser_597071.js create mode 100644 browser/components/sessionstore/test/browser_600545.js create mode 100644 browser/components/sessionstore/test/browser_601955.js create mode 100644 browser/components/sessionstore/test/browser_607016.js create mode 100644 browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js create mode 100644 browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js create mode 100644 browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js create mode 100644 browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js create mode 100644 browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js create mode 100644 browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js create mode 100644 browser/components/sessionstore/test/browser_618151.js create mode 100644 browser/components/sessionstore/test/browser_623779.js create mode 100644 browser/components/sessionstore/test/browser_624727.js create mode 100644 browser/components/sessionstore/test/browser_625016.js create mode 100644 browser/components/sessionstore/test/browser_628270.js create mode 100644 browser/components/sessionstore/test/browser_635418.js create mode 100644 browser/components/sessionstore/test/browser_636279.js create mode 100644 browser/components/sessionstore/test/browser_637020.js create mode 100644 browser/components/sessionstore/test/browser_637020_slow.sjs create mode 100644 browser/components/sessionstore/test/browser_645428.js create mode 100644 browser/components/sessionstore/test/browser_659591.js create mode 100644 browser/components/sessionstore/test/browser_662743.js create mode 100644 browser/components/sessionstore/test/browser_662743_sample.html create mode 100644 browser/components/sessionstore/test/browser_662812.js create mode 100644 browser/components/sessionstore/test/browser_665702-state_session.js create mode 100644 browser/components/sessionstore/test/browser_682507.js create mode 100644 browser/components/sessionstore/test/browser_687710.js create mode 100644 browser/components/sessionstore/test/browser_687710_2.js create mode 100644 browser/components/sessionstore/test/browser_694378.js create mode 100644 browser/components/sessionstore/test/browser_701377.js create mode 100644 browser/components/sessionstore/test/browser_705597.js create mode 100644 browser/components/sessionstore/test/browser_707862.js create mode 100644 browser/components/sessionstore/test/browser_739531.js create mode 100644 browser/components/sessionstore/test/browser_739531_frame.html create mode 100644 browser/components/sessionstore/test/browser_739531_sample.html create mode 100644 browser/components/sessionstore/test/browser_739805.js create mode 100644 browser/components/sessionstore/test/browser_819510_perwindowpb.js create mode 100644 browser/components/sessionstore/test/browser_906076_lazy_tabs.js create mode 100644 browser/components/sessionstore/test/browser_911547.js create mode 100644 browser/components/sessionstore/test/browser_911547_sample.html create mode 100644 browser/components/sessionstore/test/browser_911547_sample.html^headers^ create mode 100644 browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js create mode 100644 browser/components/sessionstore/test/browser_aboutSessionRestore.js create mode 100644 browser/components/sessionstore/test/browser_async_duplicate_tab.js create mode 100644 browser/components/sessionstore/test/browser_async_flushes.js create mode 100644 browser/components/sessionstore/test/browser_async_remove_tab.js create mode 100644 browser/components/sessionstore/test/browser_async_window_flushing.js create mode 100644 browser/components/sessionstore/test/browser_attributes.js create mode 100644 browser/components/sessionstore/test/browser_background_tab_crash.js create mode 100644 browser/components/sessionstore/test/browser_backup_recovery.js create mode 100644 browser/components/sessionstore/test/browser_bfcache_telemetry.js create mode 100644 browser/components/sessionstore/test/browser_broadcast.js create mode 100644 browser/components/sessionstore/test/browser_capabilities.js create mode 100644 browser/components/sessionstore/test/browser_cleaner.js create mode 100644 browser/components/sessionstore/test/browser_closedId.js create mode 100644 browser/components/sessionstore/test/browser_closed_objects_changed_notifications_tabs.js create mode 100644 browser/components/sessionstore/test/browser_closed_objects_changed_notifications_windows.js create mode 100644 browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js create mode 100644 browser/components/sessionstore/test/browser_closed_tabs_windows.js create mode 100644 browser/components/sessionstore/test/browser_cookies.js create mode 100644 browser/components/sessionstore/test/browser_cookies_legacy.js create mode 100644 browser/components/sessionstore/test/browser_cookies_privacy.js create mode 100644 browser/components/sessionstore/test/browser_cookies_sameSite.js create mode 100644 browser/components/sessionstore/test/browser_crashedTabs.js create mode 100644 browser/components/sessionstore/test/browser_docshell_uuid_consistency.js create mode 100644 browser/components/sessionstore/test/browser_duplicate_history.js create mode 100644 browser/components/sessionstore/test/browser_duplicate_tab_in_new_window.js create mode 100644 browser/components/sessionstore/test/browser_dying_cache.js create mode 100644 browser/components/sessionstore/test/browser_dynamic_frames.js create mode 100644 browser/components/sessionstore/test/browser_firefoxView_restore.js create mode 100644 browser/components/sessionstore/test/browser_firefoxView_selected_restore.js create mode 100644 browser/components/sessionstore/test/browser_focus_after_restore.js create mode 100644 browser/components/sessionstore/test/browser_forget_async_closings.js create mode 100644 browser/components/sessionstore/test/browser_forget_closed_tab_window_byId.js create mode 100644 browser/components/sessionstore/test/browser_formdata.js create mode 100644 browser/components/sessionstore/test/browser_formdata_cc.js create mode 100644 browser/components/sessionstore/test/browser_formdata_face.js create mode 100644 browser/components/sessionstore/test/browser_formdata_format.js create mode 100644 browser/components/sessionstore/test/browser_formdata_format_sample.html create mode 100644 browser/components/sessionstore/test/browser_formdata_max_size.js create mode 100644 browser/components/sessionstore/test/browser_formdata_password.js create mode 100644 browser/components/sessionstore/test/browser_formdata_sample.html create mode 100644 browser/components/sessionstore/test/browser_formdata_xpath.js create mode 100644 browser/components/sessionstore/test/browser_formdata_xpath_sample.html create mode 100644 browser/components/sessionstore/test/browser_frame_history.js create mode 100644 browser/components/sessionstore/test/browser_frame_history_a.html create mode 100644 browser/components/sessionstore/test/browser_frame_history_b.html create mode 100644 browser/components/sessionstore/test/browser_frame_history_c.html create mode 100644 browser/components/sessionstore/test/browser_frame_history_c1.html create mode 100644 browser/components/sessionstore/test/browser_frame_history_c2.html create mode 100644 browser/components/sessionstore/test/browser_frame_history_index.html create mode 100644 browser/components/sessionstore/test/browser_frame_history_index2.html create mode 100644 browser/components/sessionstore/test/browser_frame_history_index_blank.html create mode 100644 browser/components/sessionstore/test/browser_frametree.js create mode 100644 browser/components/sessionstore/test/browser_frametree_sample.html create mode 100644 browser/components/sessionstore/test/browser_frametree_sample_frameset.html create mode 100644 browser/components/sessionstore/test/browser_frametree_sample_iframes.html create mode 100644 browser/components/sessionstore/test/browser_global_store.js create mode 100644 browser/components/sessionstore/test/browser_history_persist.js create mode 100644 browser/components/sessionstore/test/browser_ignore_updates_crashed_tabs.js create mode 100644 browser/components/sessionstore/test/browser_label_and_icon.js create mode 100644 browser/components/sessionstore/test/browser_movePendingTabToNewWindow.js create mode 100644 browser/components/sessionstore/test/browser_multiple_navigateAndRestore.js create mode 100644 browser/components/sessionstore/test/browser_multiple_select_after_load.js create mode 100644 browser/components/sessionstore/test/browser_newtab_userTypedValue.js create mode 100644 browser/components/sessionstore/test/browser_not_collect_when_idle.js create mode 100644 browser/components/sessionstore/test/browser_old_favicon.js create mode 100644 browser/components/sessionstore/test/browser_page_title.js create mode 100644 browser/components/sessionstore/test/browser_parentProcessRestoreHash.js create mode 100644 browser/components/sessionstore/test/browser_pending_tabs.js create mode 100644 browser/components/sessionstore/test/browser_pinned_tabs.js create mode 100644 browser/components/sessionstore/test/browser_privatetabs.js create mode 100644 browser/components/sessionstore/test/browser_purge_shistory.js create mode 100644 browser/components/sessionstore/test/browser_remoteness_flip_on_restore.js create mode 100644 browser/components/sessionstore/test/browser_reopen_all_windows.js create mode 100644 browser/components/sessionstore/test/browser_replace_load.js create mode 100644 browser/components/sessionstore/test/browser_restoreLastActionCorrectOrder.js create mode 100644 browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js create mode 100644 browser/components/sessionstore/test/browser_restoreTabContainer.js create mode 100644 browser/components/sessionstore/test/browser_restore_container_tabs_oa.js create mode 100644 browser/components/sessionstore/test/browser_restore_cookies_noOriginAttributes.js create mode 100644 browser/components/sessionstore/test/browser_restore_pageProxyState.js create mode 100644 browser/components/sessionstore/test/browser_restore_private_tab_os.js create mode 100644 browser/components/sessionstore/test/browser_restore_redirect.js create mode 100644 browser/components/sessionstore/test/browser_restore_reversed_z_order.js create mode 100644 browser/components/sessionstore/test/browser_restore_srcdoc.js create mode 100644 browser/components/sessionstore/test/browser_restore_tabless_window.js create mode 100644 browser/components/sessionstore/test/browser_restored_window_features.js create mode 100644 browser/components/sessionstore/test/browser_revive_crashed_bg_tabs.js create mode 100644 browser/components/sessionstore/test/browser_scrollPositions.js create mode 100644 browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js create mode 100644 browser/components/sessionstore/test/browser_scrollPositions_readerModeArticle.html create mode 100644 browser/components/sessionstore/test/browser_scrollPositions_sample.html create mode 100644 browser/components/sessionstore/test/browser_scrollPositions_sample2.html create mode 100644 browser/components/sessionstore/test/browser_scrollPositions_sample_frameset.html create mode 100644 browser/components/sessionstore/test/browser_send_async_message_oom.js create mode 100644 browser/components/sessionstore/test/browser_sessionHistory.js create mode 100644 browser/components/sessionstore/test/browser_sessionHistory_slow.sjs create mode 100644 browser/components/sessionstore/test/browser_sessionStorage.html create mode 100644 browser/components/sessionstore/test/browser_sessionStorage.js create mode 100644 browser/components/sessionstore/test/browser_sessionStorage_size.js create mode 100644 browser/components/sessionstore/test/browser_sessionStoreContainer.js create mode 100644 browser/components/sessionstore/test/browser_should_restore_tab.js create mode 100644 browser/components/sessionstore/test/browser_sizemodeBeforeMinimized.js create mode 100644 browser/components/sessionstore/test/browser_speculative_connect.html create mode 100644 browser/components/sessionstore/test/browser_speculative_connect.js create mode 100644 browser/components/sessionstore/test/browser_swapDocShells.js create mode 100644 browser/components/sessionstore/test/browser_switch_remoteness.js create mode 100644 browser/components/sessionstore/test/browser_tab_label_during_restore.js create mode 100644 browser/components/sessionstore/test/browser_tabicon_after_bg_tab_crash.js create mode 100644 browser/components/sessionstore/test/browser_tabs_in_urlbar.js create mode 100644 browser/components/sessionstore/test/browser_undoCloseById.js create mode 100644 browser/components/sessionstore/test/browser_undoCloseById_targetWindow.js create mode 100644 browser/components/sessionstore/test/browser_unrestored_crashedTabs.js create mode 100644 browser/components/sessionstore/test/browser_upgrade_backup.js create mode 100644 browser/components/sessionstore/test/browser_urlbarSearchMode.js create mode 100644 browser/components/sessionstore/test/browser_userTyped_restored_after_discard.js create mode 100644 browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js create mode 100644 browser/components/sessionstore/test/browser_windowStateContainer.js create mode 100644 browser/components/sessionstore/test/coopHeaderCommon.sjs create mode 100644 browser/components/sessionstore/test/coop_coep.html create mode 100644 browser/components/sessionstore/test/coop_coep.html^headers^ create mode 100644 browser/components/sessionstore/test/empty.html create mode 100644 browser/components/sessionstore/test/file_async_duplicate_tab.html create mode 100644 browser/components/sessionstore/test/file_async_flushes.html create mode 100644 browser/components/sessionstore/test/file_formdata_password.html create mode 100644 browser/components/sessionstore/test/file_sessionHistory_hashchange.html create mode 100644 browser/components/sessionstore/test/head.js create mode 100644 browser/components/sessionstore/test/marionette/manifest.toml create mode 100644 browser/components/sessionstore/test/marionette/session_store_test_case.py create mode 100644 browser/components/sessionstore/test/marionette/test_persist_closed_tabs_restore_manually.py create mode 100644 browser/components/sessionstore/test/marionette/test_restore_loading_tab.py create mode 100644 browser/components/sessionstore/test/marionette/test_restore_manually.py create mode 100644 browser/components/sessionstore/test/marionette/test_restore_manually_with_pinned_tabs.py create mode 100644 browser/components/sessionstore/test/marionette/test_restore_windows_after_close_last_tabs.py create mode 100644 browser/components/sessionstore/test/marionette/test_restore_windows_after_restart_and_quit.py create mode 100644 browser/components/sessionstore/test/marionette/test_restore_windows_after_windows_shutdown.py create mode 100644 browser/components/sessionstore/test/restore_redirect_http.html create mode 100644 browser/components/sessionstore/test/restore_redirect_http.html^headers^ create mode 100644 browser/components/sessionstore/test/restore_redirect_js.html create mode 100644 browser/components/sessionstore/test/restore_redirect_target.html create mode 100644 browser/components/sessionstore/test/unit/data/sessionCheckpoints_all.json create mode 100644 browser/components/sessionstore/test/unit/data/sessionstore_invalid.js create mode 100644 browser/components/sessionstore/test/unit/data/sessionstore_valid.js create mode 100644 browser/components/sessionstore/test/unit/head.js create mode 100644 browser/components/sessionstore/test/unit/test_backup_once.js create mode 100644 browser/components/sessionstore/test/unit/test_final_write_cleanup.js create mode 100644 browser/components/sessionstore/test/unit/test_histogram_corrupt_files.js create mode 100644 browser/components/sessionstore/test/unit/test_migration_lz4compression.js create mode 100644 browser/components/sessionstore/test/unit/test_startup_invalid_session.js create mode 100644 browser/components/sessionstore/test/unit/test_startup_nosession_async.js create mode 100644 browser/components/sessionstore/test/unit/test_startup_session_async.js create mode 100644 browser/components/sessionstore/test/unit/xpcshell.toml (limited to 'browser/components/sessionstore/test') diff --git a/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs b/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs new file mode 100644 index 0000000000..dd2885cee4 --- /dev/null +++ b/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs @@ -0,0 +1,186 @@ +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} scope 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.info = scope.info; + this.registerCleanupFunction = scope.registerCleanupFunction; + this.windowGlobal = windowGlobal; + }, + + 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(aEvent) { + 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, aData) { + 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/browser/components/sessionstore/test/browser.toml b/browser/components/sessionstore/test/browser.toml new file mode 100644 index 0000000000..26fb4b4550 --- /dev/null +++ b/browser/components/sessionstore/test/browser.toml @@ -0,0 +1,577 @@ +[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", + "browser_911547_sample.html", + "browser_911547_sample.html^headers^", + "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'"] # 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"] + +["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"] + +["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 == '18.04'"] # bug 1610668 for ubuntu 18.04 + +["browser_480893.js"] + +["browser_485482.js"] + +["browser_485563.js"] + +["browser_490040.js"] + +["browser_491168.js"] + +["browser_491577.js"] +skip-if = [ + "verify && debug && os == 'mac'", + "verify && debug && os == 'win'", +] + +["browser_495495.js"] + +["browser_500328.js"] + +["browser_514751.js"] + +["browser_522375.js"] + +["browser_522545.js"] +skip-if = ["true"] # Bug 1380968 + +["browser_524745.js"] +skip-if = [ + "win10_2009 && !ccov", # Bug 1418627 + "os == 'linux'", # Bug 1803187 +] + +["browser_528776.js"] + +["browser_579868.js"] + +["browser_579879.js"] +skip-if = ["os == 'linux' && (debug || asan)"] # Bug 1234404 + +["browser_581937.js"] + +["browser_586068-apptabs.js"] + +["browser_586068-apptabs_ondemand.js"] +skip-if = ["verify && (os == 'mac' || os == 'win')"] + +["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"] + +["browser_595601-restore_hidden.js"] + +["browser_597071.js"] +skip-if = ["true"] # Needs to be rewritten as Marionette test, bug 995916 + +["browser_600545.js"] + +["browser_601955.js"] + +["browser_607016.js"] + +["browser_615394-SSWindowState_events_duplicateTab.js"] + +["browser_615394-SSWindowState_events_setBrowserState.js"] +skip-if = ["verify && debug && os == 'mac'"] + +["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"] +skip-if = [ + "os == 'win' && !debug", # Bug 1572554 + "os == 'linux'", # Bug 1572554 +] + +["browser_618151.js"] + +["browser_623779.js"] + +["browser_624727.js"] + +["browser_625016.js"] +skip-if = [ + "os == 'mac'", # Disabled on OS X: + "os == 'linux'", # linux, Bug 1348583 + "os == 'win' && debug", # Bug 1430977 +] + +["browser_628270.js"] + +["browser_635418.js"] + +["browser_636279.js"] + +["browser_637020.js"] + +["browser_645428.js"] + +["browser_659591.js"] + +["browser_662743.js"] + +["browser_662812.js"] +skip-if = ["verify"] + +["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"] +skip-if = [ + "verify && debug && os == 'win'", + "verify && debug && os == 'mac'", +] + +["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 +skip-if = ["os == 'linux' && os_version == '18.04'"] # bug 1446464 + +["browser_911547.js"] + +["browser_aboutPrivateBrowsing.js"] + +["browser_aboutSessionRestore.js"] +skip-if = [ + "verify && debug && os == 'win'", + "verify && debug && os == 'mac'", +] + +["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 = ["!sessionHistoryInParent"] + +["browser_async_window_flushing.js"] +https_first_disabled = true +skip-if = ["true"] # Bug 1775616 + +["browser_attributes.js"] + +["browser_background_tab_crash.js"] +https_first_disabled = true +run-if = ["crashreporter"] + +["browser_backup_recovery.js"] +https_first_disabled = true +skip-if = ["verify && debug && os == 'linux'"] + +["browser_bfcache_telemetry.js"] + +["browser_broadcast.js"] +https_first_disabled = true + +["browser_capabilities.js"] + +["browser_cleaner.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.js"] + +["browser_cookies_legacy.js"] + +["browser_cookies_privacy.js"] + +["browser_cookies_sameSite.js"] + +["browser_crashedTabs.js"] +https_first_disabled = true +run-if = ["crashreporter"] +skip-if = [ + "verify", + "os == 'mac'", # high frequency intermittent +] + +["browser_docshell_uuid_consistency.js"] + +["browser_duplicate_history.js"] + +["browser_duplicate_tab_in_new_window.js"] + +["browser_dying_cache.js"] +skip-if = ["os == 'win'"] # bug 1331853 + +["browser_dynamic_frames.js"] + +["browser_firefoxView_restore.js"] + +["browser_firefoxView_selected_restore.js"] + +["browser_focus_after_restore.js"] + +["browser_forget_async_closings.js"] + +["browser_forget_closed_tab_window_byId.js"] +https_first_disabled = true + +["browser_formdata.js"] +skip-if = ["verify && debug"] + +["browser_formdata_cc.js"] + +["browser_formdata_face.js"] + +["browser_formdata_format.js"] +skip-if = ["!debug && os == 'linux'"] # Bug 1535645 + +["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", + "os == 'win' && fission && verify", # bug 1709907 + "os == 'mac' && fission", # Bug 1711008; high frequency intermittent +] + +["browser_label_and_icon.js"] +https_first_disabled = true +skip-if = [ + "apple_silicon", # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs + "apple_catalina && !debug", # Bug 1638958 + "os == 'linux' && !debug", # Bug 1638958 + "win11_2009 && !debug", # Bug 1775605 +] + +["browser_movePendingTabToNewWindow.js"] +https_first_disabled = true + +["browser_multiple_navigateAndRestore.js"] +skip-if = ["os == 'linux' && debug"] #Bug 1570468 + +["browser_multiple_select_after_load.js"] + +["browser_newtab_userTypedValue.js"] +skip-if = ["verify && debug"] + +["browser_not_collect_when_idle.js"] + +["browser_old_favicon.js"] +https_first_disabled = true + +["browser_page_title.js"] + +["browser_parentProcessRestoreHash.js"] +https_first_disabled = true +tags = "openUILinkIn" + +["browser_pending_tabs.js"] + +["browser_pinned_tabs.js"] +skip-if = [ + "debug", + "ccov", # Bug 1625525 +] + +["browser_privatetabs.js"] + +["browser_purge_shistory.js"] +skip-if = ["!sessionHistoryInParent"] # Bug 1271024 + +["browser_remoteness_flip_on_restore.js"] + +["browser_reopen_all_windows.js"] +https_first_disabled = true + +["browser_replace_load.js"] +skip-if = ["true"] # Bug 1646894 + +["browser_restoreLastActionCorrectOrder.js"] + +["browser_restoreLastClosedTabOrWindowOrSession.js"] +skip-if = ["os == 'mac' && !debug"] # Bug 1838996 + +["browser_restoreTabContainer.js"] + +["browser_restore_container_tabs_oa.js"] + +["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"] + +["browser_restored_window_features.js"] + +["browser_revive_crashed_bg_tabs.js"] +https_first_disabled = true +skip-if = ["!crashreporter"] + +["browser_scrollPositions.js"] +https_first_disabled = true +skip-if = [ + "!fission", + "os == 'linux'", # Bug 1716445 +] + +["browser_scrollPositionsReaderMode.js"] + +["browser_send_async_message_oom.js"] +skip-if = ["sessionHistoryInParent"] # Tests that the frame script OOMs, which is unused when SHIP is enabled. + +["browser_sessionHistory.js"] +https_first_disabled = true +support-files = ["file_sessionHistory_hashchange.html"] +skip-if = [ + "os == 'linux'", # Bug 1775608 + "os == 'mac' && os_version == '10.15' && debug", # Bug 1775608 +] + +["browser_sessionStorage.js"] + +["browser_sessionStorage_size.js"] + +["browser_sessionStoreContainer.js"] + +["browser_should_restore_tab.js"] + +["browser_sizemodeBeforeMinimized.js"] + +["browser_speculative_connect.js"] + +["browser_swapDocShells.js"] + +["browser_switch_remoteness.js"] + +["browser_tab_label_during_restore.js"] +https_first_disabled = true + +["browser_tabicon_after_bg_tab_crash.js"] +skip-if = ["!crashreporter"] + +["browser_tabs_in_urlbar.js"] +https_first_disabled = true + +["browser_undoCloseById.js"] +skip-if = ["debug"] + +["browser_undoCloseById_targetWindow.js"] + +["browser_unrestored_crashedTabs.js"] +skip-if = ["!crashreporter"] + +["browser_upgrade_backup.js"] +skip-if = [ + "debug", + "asan", + "tsan", + "verify && debug && os == 'mac'", # Bug 1435394 disabled on Linux, OSX and Windows +] + +["browser_urlbarSearchMode.js"] + +["browser_userTyped_restored_after_discard.js"] + +["browser_windowRestore_perwindowpb.js"] + +["browser_windowStateContainer.js"] diff --git a/browser/components/sessionstore/test/browser_1234021.js b/browser/components/sessionstore/test/browser_1234021.js new file mode 100644 index 0000000000..f6a95ad68d --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_1234021_page.html b/browser/components/sessionstore/test/browser_1234021_page.html new file mode 100644 index 0000000000..0c3fca84db --- /dev/null +++ b/browser/components/sessionstore/test/browser_1234021_page.html @@ -0,0 +1,6 @@ + + + + diff --git a/browser/components/sessionstore/test/browser_1284886_suspend_tab.html b/browser/components/sessionstore/test/browser_1284886_suspend_tab.html new file mode 100644 index 0000000000..ec3edbffdc --- /dev/null +++ b/browser/components/sessionstore/test/browser_1284886_suspend_tab.html @@ -0,0 +1,12 @@ + + + + + +TEST PAGE + + diff --git a/browser/components/sessionstore/test/browser_1284886_suspend_tab.js b/browser/components/sessionstore/test/browser_1284886_suspend_tab.js new file mode 100644 index 0000000000..3c6a2c1f2c --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_1284886_suspend_tab_2.html b/browser/components/sessionstore/test/browser_1284886_suspend_tab_2.html new file mode 100644 index 0000000000..5c42913635 --- /dev/null +++ b/browser/components/sessionstore/test/browser_1284886_suspend_tab_2.html @@ -0,0 +1,11 @@ + + + + + +TEST PAGE + + diff --git a/browser/components/sessionstore/test/browser_1446343-windowsize.js b/browser/components/sessionstore/test/browser_1446343-windowsize.js new file mode 100644 index 0000000000..97f664a460 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js b/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js new file mode 100644 index 0000000000..d62a701ea3 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_248970_b_sample.html b/browser/components/sessionstore/test/browser_248970_b_sample.html new file mode 100644 index 0000000000..76c3ae1aa0 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_339445.js b/browser/components/sessionstore/test/browser_339445.js new file mode 100644 index 0000000000..e7c7ffa5cb --- /dev/null +++ b/browser/components/sessionstore/test/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 ContentTask.spawn(tab2.linkedBrowser, null, 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/browser/components/sessionstore/test/browser_339445_sample.html b/browser/components/sessionstore/test/browser_339445_sample.html new file mode 100644 index 0000000000..ff5b4acd9f --- /dev/null +++ b/browser/components/sessionstore/test/browser_339445_sample.html @@ -0,0 +1,18 @@ + + +Test for bug 339445 + +storageTestItem = FAIL + + + + diff --git a/browser/components/sessionstore/test/browser_345898.js b/browser/components/sessionstore/test/browser_345898.js new file mode 100644 index 0000000000..7e08702222 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_350525.js b/browser/components/sessionstore/test/browser_350525.js new file mode 100644 index 0000000000..ca577bf093 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_354894_perwindowpb.js b/browser/components/sessionstore/test/browser_354894_perwindowpb.js new file mode 100644 index 0000000000..90368536dc --- /dev/null +++ b/browser/components/sessionstore/test/browser_354894_perwindowpb.js @@ -0,0 +1,489 @@ +/* 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 + * BrowserTryToCloseWindow() 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, aData) { + // 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 BrowserTryToCloseWindow 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.BrowserTryToCloseWindow(); + 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, obs) { + 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 = 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/browser/components/sessionstore/test/browser_367052.js b/browser/components/sessionstore/test/browser_367052.js new file mode 100644 index 0000000000..4137945729 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_393716.js b/browser/components/sessionstore/test/browser_393716.js new file mode 100644 index 0000000000..383c25e385 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_394759_basic.js b/browser/components/sessionstore/test/browser_394759_basic.js new file mode 100644 index 0000000000..62d5c40e17 --- /dev/null +++ b/browser/components/sessionstore/test/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(aEvent) { + ++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/browser/components/sessionstore/test/browser_394759_behavior.js b/browser/components/sessionstore/test/browser_394759_behavior.js new file mode 100644 index 0000000000..ee4b121e84 --- /dev/null +++ b/browser/components/sessionstore/test/browser_394759_behavior.js @@ -0,0 +1,91 @@ +/** + * 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, i, arr) { + 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 repoen" + ); + })(); +} + +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: false }, + { isPopup: true }, + { isPopup: true }, + { isPopup: true }, + ]; + let expectedResults = { + mac: { popup: 3, normal: 0 }, + other: { popup: 3, normal: 1 }, + }; + + await testWindows(windowsToOpen, expectedResults); + + let windowsToOpen2 = [ + { isPopup: false }, + { isPopup: false }, + { isPopup: false }, + { isPopup: false }, + { isPopup: false }, + ]; + let expectedResults2 = { + mac: { popup: 0, normal: 3 }, + other: { popup: 0, normal: 3 }, + }; + + await testWindows(windowsToOpen2, expectedResults2); +}); diff --git a/browser/components/sessionstore/test/browser_394759_perwindowpb.js b/browser/components/sessionstore/test/browser_394759_perwindowpb.js new file mode 100644 index 0000000000..b79527d4c7 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_394759_purge.js b/browser/components/sessionstore/test/browser_394759_purge.js new file mode 100644 index 0000000000..e5218c9936 --- /dev/null +++ b/browser/components/sessionstore/test/browser_394759_purge.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/. */ + +let { ForgetAboutSite } = ChromeUtils.importESModule( + "resource://gre/modules/ForgetAboutSite.sys.mjs" +); + +function promiseClearHistory() { + return new Promise(resolve => { + let observer = { + observe(aSubject, aTopic, aData) { + 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: [], + }, + // _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: [], + }, + // _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, + }, + ], + }, + ], + }; + + // 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.removeDataFromDomain("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/browser/components/sessionstore/test/browser_423132.js b/browser/components/sessionstore/test/browser_423132.js new file mode 100644 index 0000000000..3a0113d4d0 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_423132_sample.html b/browser/components/sessionstore/test/browser_423132_sample.html new file mode 100644 index 0000000000..6ff7e7aa3e --- /dev/null +++ b/browser/components/sessionstore/test/browser_423132_sample.html @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/browser/components/sessionstore/test/browser_447951.js b/browser/components/sessionstore/test/browser_447951.js new file mode 100644 index 0000000000..aa08f59bbe --- /dev/null +++ b/browser/components/sessionstore/test/browser_447951.js @@ -0,0 +1,84 @@ +/* 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); + promiseBrowserLoaded(tab.linkedBrowser).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/browser/components/sessionstore/test/browser_447951_sample.html b/browser/components/sessionstore/test/browser_447951_sample.html new file mode 100644 index 0000000000..00282f25ef --- /dev/null +++ b/browser/components/sessionstore/test/browser_447951_sample.html @@ -0,0 +1,5 @@ + + +Testcase for bug 447951 + +click me diff --git a/browser/components/sessionstore/test/browser_454908.js b/browser/components/sessionstore/test/browser_454908.js new file mode 100644 index 0000000000..415930c32d --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_454908_sample.html b/browser/components/sessionstore/test/browser_454908_sample.html new file mode 100644 index 0000000000..02f40bf20b --- /dev/null +++ b/browser/components/sessionstore/test/browser_454908_sample.html @@ -0,0 +1,8 @@ + +Test for bug 454908 + +

Dummy Login

+
+

Username: +

Password: +

diff --git a/browser/components/sessionstore/test/browser_456342.js b/browser/components/sessionstore/test/browser_456342.js new file mode 100644 index 0000000000..e7f1c96e34 --- /dev/null +++ b/browser/components/sessionstore/test/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.ownerGlobal, 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/browser/components/sessionstore/test/browser_456342_sample.xhtml b/browser/components/sessionstore/test/browser_456342_sample.xhtml new file mode 100644 index 0000000000..ea8704d17d --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_frame_history.js b/browser/components/sessionstore/test/browser_frame_history.js new file mode 100644 index 0000000000..1db32e74ab --- /dev/null +++ b/browser/components/sessionstore/test/browser_frame_history.js @@ -0,0 +1,230 @@ +/* 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]], + }); + + 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" + ); + } + 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(aEvent) { + 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/browser/components/sessionstore/test/browser_frame_history_a.html b/browser/components/sessionstore/test/browser_frame_history_a.html new file mode 100644 index 0000000000..8e7b35d7a1 --- /dev/null +++ b/browser/components/sessionstore/test/browser_frame_history_a.html @@ -0,0 +1,5 @@ + + + I'm A! + + diff --git a/browser/components/sessionstore/test/browser_frame_history_b.html b/browser/components/sessionstore/test/browser_frame_history_b.html new file mode 100644 index 0000000000..38b43da211 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_frame_history_c.html b/browser/components/sessionstore/test/browser_frame_history_c.html new file mode 100644 index 0000000000..0efd7d9026 --- /dev/null +++ b/browser/components/sessionstore/test/browser_frame_history_c.html @@ -0,0 +1,5 @@ + + + I'm C! + + diff --git a/browser/components/sessionstore/test/browser_frame_history_c1.html b/browser/components/sessionstore/test/browser_frame_history_c1.html new file mode 100644 index 0000000000..b55c1d45a9 --- /dev/null +++ b/browser/components/sessionstore/test/browser_frame_history_c1.html @@ -0,0 +1,5 @@ + + + I'm C1! + + diff --git a/browser/components/sessionstore/test/browser_frame_history_c2.html b/browser/components/sessionstore/test/browser_frame_history_c2.html new file mode 100644 index 0000000000..aec504141b --- /dev/null +++ b/browser/components/sessionstore/test/browser_frame_history_c2.html @@ -0,0 +1,5 @@ + + + I'm C2! + + diff --git a/browser/components/sessionstore/test/browser_frame_history_index.html b/browser/components/sessionstore/test/browser_frame_history_index.html new file mode 100644 index 0000000000..04a44555ab --- /dev/null +++ b/browser/components/sessionstore/test/browser_frame_history_index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/browser/components/sessionstore/test/browser_frame_history_index2.html b/browser/components/sessionstore/test/browser_frame_history_index2.html new file mode 100644 index 0000000000..d465abef62 --- /dev/null +++ b/browser/components/sessionstore/test/browser_frame_history_index2.html @@ -0,0 +1,3 @@ + + + + diff --git a/browser/components/sessionstore/test/browser_global_store.js b/browser/components/sessionstore/test/browser_global_store.js new file mode 100644 index 0000000000..99aa672180 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_history_persist.js b/browser/components/sessionstore/test/browser_history_persist.js new file mode 100644 index 0000000000..f6749b02e3 --- /dev/null +++ b/browser/components/sessionstore/test/browser_history_persist.js @@ -0,0 +1,163 @@ +/* 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 promiseBrowserLoaded(browser); + + // Retrieve the tab state. + await TabStateFlusher.flush(browser); + let state = JSON.parse(ss.getTabState(tab)); + ok(!state.entries[0].persist, "Should have collected the persistence state"); + BrowserTestUtils.removeTab(tab); + browser = null; + + // Open a new tab to restore into. + tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + browser = tab.linkedBrowser; + await promiseTabState(tab, state); + + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + await SpecialPowers.spawn(browser, [], function () { + let sessionHistory = + docShell.browsingContext.childSessionHistory.legacySHistory; + + is(sessionHistory.count, 1, "Should be a single history entry"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:blank", + "Should be the right URL" + ); + }); + } else { + 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 promiseBrowserLoaded(browser, false, "about:robots"); + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + await SpecialPowers.spawn(browser, [], function () { + let sessionHistory = + docShell.browsingContext.childSessionHistory.legacySHistory; + + is(sessionHistory.count, 1, "Should be a single history entry"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:robots", + "Should be the right URL" + ); + }); + } else { + 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 promiseBrowserLoaded(browser); + + // Retrieve the tab state. + await TabStateFlusher.flush(browser); + let state = JSON.parse(ss.getTabState(tab)); + delete state.entries[0].persist; + BrowserTestUtils.removeTab(tab); + browser = null; + + // Open a new tab to restore into. + tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + browser = tab.linkedBrowser; + await promiseTabState(tab, state); + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + await SpecialPowers.spawn(browser, [], function () { + let sessionHistory = + docShell.browsingContext.childSessionHistory.legacySHistory; + + is(sessionHistory.count, 1, "Should be a single history entry"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:blank", + "Should be the right URL" + ); + }); + } else { + 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 promiseBrowserLoaded(browser, false, "about:robots"); + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + await SpecialPowers.spawn(browser, [], function () { + let sessionHistory = + docShell.browsingContext.childSessionHistory.legacySHistory; + + 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" + ); + }); + } else { + 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/browser/components/sessionstore/test/browser_ignore_updates_crashed_tabs.js b/browser/components/sessionstore/test/browser_ignore_updates_crashed_tabs.js new file mode 100644 index 0000000000..b70a3aa392 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_label_and_icon.js b/browser/components/sessionstore/test/browser_label_and_icon.js new file mode 100644 index 0000000000..6938c3b01e --- /dev/null +++ b/browser/components/sessionstore/test/browser_label_and_icon.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * 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 BrowserTestUtils.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/browser/components/sessionstore/test/browser_movePendingTabToNewWindow.js b/browser/components/sessionstore/test/browser_movePendingTabToNewWindow.js new file mode 100644 index 0000000000..3a013132be --- /dev/null +++ b/browser/components/sessionstore/test/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.ownerGlobal, "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/browser/components/sessionstore/test/browser_multiple_navigateAndRestore.js b/browser/components/sessionstore/test/browser_multiple_navigateAndRestore.js new file mode 100644 index 0000000000..e2ee1b9e39 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_multiple_select_after_load.js b/browser/components/sessionstore/test/browser_multiple_select_after_load.js new file mode 100644 index 0000000000..dcb896e435 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_newtab_userTypedValue.js b/browser/components/sessionstore/test/browser_newtab_userTypedValue.js new file mode 100644 index 0000000000..755a1f2859 --- /dev/null +++ b/browser/components/sessionstore/test/browser_newtab_userTypedValue.js @@ -0,0 +1,92 @@ +"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, "about:logo"); + let tabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + win.gBrowser, + () => {} + ); + + // This opens about:newtab: + win.BrowserOpenTab(); + 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) { + continue; // We tested about:newtab using BrowserOpenTab() 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); + 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/browser/components/sessionstore/test/browser_not_collect_when_idle.js b/browser/components/sessionstore/test/browser_not_collect_when_idle.js new file mode 100644 index 0000000000..c4a49ab7b7 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_old_favicon.js b/browser/components/sessionstore/test/browser_old_favicon.js new file mode 100644 index 0000000000..fc416e81f6 --- /dev/null +++ b/browser/components/sessionstore/test/browser_old_favicon.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Ensure that we can restore old style favicon and principals. + */ +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 override the icon. + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionstore.restore_on_demand", true]], + }); + + // Create a new tab. + let tab = BrowserTestUtils.addTab( + gBrowser, + "http://www.example.com/browser/browser/components/sessionstore/test/empty.html" + ); + let browser = tab.linkedBrowser; + await promiseBrowserLoaded(browser); + + let contentPrincipal = browser.contentPrincipal; + let serializedPrincipal = E10SUtils.serializePrincipal(contentPrincipal); + + // Retrieve the tab state. + await TabStateFlusher.flush(browser); + let state = JSON.parse(ss.getTabState(tab)); + state.image = "http://www.example.com/favicon.ico"; + state.iconLoadingPrincipal = serializedPrincipal; + + BrowserTestUtils.removeTab(tab); + + // 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), + "http://www.example.com/favicon.ico", + "icon is set" + ); + is( + tab.getAttribute("image"), + "http://www.example.com/favicon.ico", + "tab image is set" + ); + is( + tab.getAttribute("iconloadingprincipal"), + serializedPrincipal, + "tab image loading principal is set" + ); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/sessionstore/test/browser_page_title.js b/browser/components/sessionstore/test/browser_page_title.js new file mode 100644 index 0000000000..9f84e67a94 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js b/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js new file mode 100644 index 0000000000..442914d580 --- /dev/null +++ b/browser/components/sessionstore/test/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(aURI) { + // 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/browser/components/sessionstore/test/browser_pending_tabs.js b/browser/components/sessionstore/test/browser_pending_tabs.js new file mode 100644 index 0000000000..279d2efcf9 --- /dev/null +++ b/browser/components/sessionstore/test/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 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(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/browser/components/sessionstore/test/browser_pinned_tabs.js b/browser/components/sessionstore/test/browser_pinned_tabs.js new file mode 100644 index 0000000000..7a51da7ccc --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_privatetabs.js b/browser/components/sessionstore/test/browser_privatetabs.js new file mode 100644 index 0000000000..237d9ff036 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_purge_shistory.js b/browser/components/sessionstore/test/browser_purge_shistory.js new file mode 100644 index 0000000000..2078bb46ed --- /dev/null +++ b/browser/components/sessionstore/test/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 promiseBrowserLoaded(browser); + await promiseTabState(tab, TAB_STATE); + + // Create another new tab. + let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser2 = tab2.linkedBrowser; + await promiseBrowserLoaded(browser2); + + // 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/browser/components/sessionstore/test/browser_remoteness_flip_on_restore.js b/browser/components/sessionstore/test/browser_remoteness_flip_on_restore.js new file mode 100644 index 0000000000..8674664ede --- /dev/null +++ b/browser/components/sessionstore/test/browser_remoteness_flip_on_restore.js @@ -0,0 +1,310 @@ +"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.ok( + 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/browser/components/sessionstore/test/browser_reopen_all_windows.js b/browser/components/sessionstore/test/browser_reopen_all_windows.js new file mode 100644 index 0000000000..532e689f50 --- /dev/null +++ b/browser/components/sessionstore/test/browser_reopen_all_windows.js @@ -0,0 +1,146 @@ +"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(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]); + + 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/browser/components/sessionstore/test/browser_replace_load.js b/browser/components/sessionstore/test/browser_replace_load.js new file mode 100644 index 0000000000..21bec044a9 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_restoreLastActionCorrectOrder.js b/browser/components/sessionstore/test/browser_restoreLastActionCorrectOrder.js new file mode 100644 index 0000000000..967dd7d7e7 --- /dev/null +++ b/browser/components/sessionstore/test/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 */); + restoreLastClosedTabOrWindowOrSession(); + 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"); + restoreLastClosedTabOrWindowOrSession(); + Assert.equal(window.gBrowser.tabs.length, 2, "Window now has two open tabs"); + + await SpecialPowers.popPrefEnv(); + gBrowser.removeAllTabsBut(gBrowser.tabs[0]); +}); diff --git a/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js b/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js new file mode 100644 index 0000000000..cc340c4617 --- /dev/null +++ b/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js @@ -0,0 +1,284 @@ +/* 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 */); + restoreLastClosedTabOrWindowOrSession(); + 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/", + }); + restoreLastClosedTabOrWindowOrSession(); + 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); + restoreLastClosedTabOrWindowOrSession(); + + // close one tab and reopen it + BrowserTestUtils.removeTab(window.gBrowser.tabs[2]); + Assert.equal(window.gBrowser.tabs.length, 2, "Window has two open tabs"); + restoreLastClosedTabOrWindowOrSession(); + 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 + restoreLastClosedTabOrWindowOrSession(); + 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); + restoreLastClosedTabOrWindowOrSession(); + Assert.equal( + window.gBrowser.tabs.length, + 2, + "Window still has two open tabs" + ); + + gBrowser.removeAllTabsBut(gBrowser.tabs[0]); +}); + +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 browser => { + 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); + restoreLastClosedTabOrWindowOrSession(); + 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]); + 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 */); + restoreLastClosedTabOrWindowOrSession(); + await sessionRestored; + Assert.equal(gBrowser.tabs.length, 4, "Got the expected number of tabs"); + gBrowser.removeAllTabsBut(gBrowser.tabs[0]); +}); diff --git a/browser/components/sessionstore/test/browser_restoreTabContainer.js b/browser/components/sessionstore/test/browser_restoreTabContainer.js new file mode 100644 index 0000000000..a38dca386e --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_restore_container_tabs_oa.js b/browser/components/sessionstore/test/browser_restore_container_tabs_oa.js new file mode 100644 index 0000000000..74edcef6e7 --- /dev/null +++ b/browser/components/sessionstore/test/browser_restore_container_tabs_oa.js @@ -0,0 +1,249 @@ +/* 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( + "resource:///modules/UrlbarProviderOpenTabs.sys.mjs" +); + +const PATH = "browser/browser/components/sessionstore/test/empty.html"; + +/* import-globals-from ../../../base/content/test/tabs/helper_origin_attrs_testing.js */ +loadTestSubscript( + "../../../base/content/test/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.ownerGlobal) { + 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(7); +}); + +function setupRemoteTypes() { + if (gFissionBrowser) { + 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 = Array( + NUM_DIFF_TAB_MODES * 2 /** 2 is the number of non parent uris */ + ).fill("web"); + } + 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 + */ +add_task(async function testRestore() { + setupRemoteTypes(); + 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 = BrowserTestUtils.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.ownerGlobal.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.ownerGlobal.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" + ); +}); diff --git a/browser/components/sessionstore/test/browser_restore_cookies_noOriginAttributes.js b/browser/components/sessionstore/test/browser_restore_cookies_noOriginAttributes.js new file mode 100644 index 0000000000..bdfdf7fbe3 --- /dev/null +++ b/browser/components/sessionstore/test/browser_restore_cookies_noOriginAttributes.js @@ -0,0 +1,191 @@ +/* + * 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/", +}; +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", + }, + ], + }, + ], + 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", + 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/browser/components/sessionstore/test/browser_restore_pageProxyState.js b/browser/components/sessionstore/test/browser_restore_pageProxyState.js new file mode 100644 index 0000000000..f98237c7e8 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_restore_private_tab_os.js b/browser/components/sessionstore/test/browser_restore_private_tab_os.js new file mode 100644 index 0000000000..3041fe6f39 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_restore_redirect.js b/browser/components/sessionstore/test/browser_restore_redirect.js new file mode 100644 index 0000000000..206b783191 --- /dev/null +++ b/browser/components/sessionstore/test/browser_restore_redirect.js @@ -0,0 +1,72 @@ +"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; + 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; + 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/browser/components/sessionstore/test/browser_restore_reversed_z_order.js b/browser/components/sessionstore/test/browser_restore_reversed_z_order.js new file mode 100644 index 0000000000..69745148a4 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_restore_srcdoc.js b/browser/components/sessionstore/test/browser_restore_srcdoc.js new file mode 100644 index 0000000000..9e670100ea --- /dev/null +++ b/browser/components/sessionstore/test/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.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 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, "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); +}); + +/** + * Ensure that document wireframes can be persisted when they're enabled. + */ +add_task(async function test_wireframes() { + // Wireframes only works when Fission and SHIP are enabled. + if ( + !Services.appinfo.fissionAutostart || + !Services.appinfo.sessionHistoryInParent + ) { + ok(true, "Skipping test_wireframes when Fission or SHIP is not enabled."); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [["browser.history.collectWireframes", true]], + }); + + let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com"); + 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 shistory entry"); + + // Check for the wireframe + ok(entries[0].wireframe, "A wireframe was captured and serialized."); + ok( + entries[0].wireframe.rects.length, + "Several wireframe rects were captured." + ); + + // Cleanup. + gBrowser.removeTab(tab); +}); diff --git a/browser/components/sessionstore/test/browser_sessionHistory_slow.sjs b/browser/components/sessionstore/test/browser_sessionHistory_slow.sjs new file mode 100644 index 0000000000..abb1dee829 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_sessionStorage.html b/browser/components/sessionstore/test/browser_sessionStorage.html new file mode 100644 index 0000000000..f3664ebc9b --- /dev/null +++ b/browser/components/sessionstore/test/browser_sessionStorage.html @@ -0,0 +1,28 @@ + + + + + browser_sessionStorage.html + + + + + + + diff --git a/browser/components/sessionstore/test/browser_sessionStorage.js b/browser/components/sessionstore/test/browser_sessionStorage.js new file mode 100644 index 0000000000..c1d6f898da --- /dev/null +++ b/browser/components/sessionstore/test/browser_sessionStorage.js @@ -0,0 +1,298 @@ +/* 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_REJECT_TRACKER_AND_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; + await promiseTabRestored(tab2); + + // 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); + await promiseTabRestored(tab2); + 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_SESSION_HISTORY, + resolve + ); + }); +} diff --git a/browser/components/sessionstore/test/browser_sessionStorage_size.js b/browser/components/sessionstore/test/browser_sessionStorage_size.js new file mode 100644 index 0000000000..1045482817 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_sessionStoreContainer.js b/browser/components/sessionstore/test/browser_sessionStoreContainer.js new file mode 100644 index 0000000000..86833dea82 --- /dev/null +++ b/browser/components/sessionstore/test/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.ownerGlobal.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/browser/components/sessionstore/test/browser_should_restore_tab.js b/browser/components/sessionstore/test/browser_should_restore_tab.js new file mode 100644 index 0000000000..ab9513083a --- /dev/null +++ b/browser/components/sessionstore/test/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.ownerGlobal; + let initialTabCount = SessionStore.getClosedTabCountForWindow(win); + + let tabClosed = BrowserTestUtils.waitForTabClosing(openedTab); + let notified = false; + function topicObserver(_, topic) { + 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.BrowserOpenTab(); + 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/browser/components/sessionstore/test/browser_sizemodeBeforeMinimized.js b/browser/components/sessionstore/test/browser_sizemodeBeforeMinimized.js new file mode 100644 index 0000000000..a832e71bcf --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_speculative_connect.html b/browser/components/sessionstore/test/browser_speculative_connect.html new file mode 100644 index 0000000000..a0fb88e0a6 --- /dev/null +++ b/browser/components/sessionstore/test/browser_speculative_connect.html @@ -0,0 +1,8 @@ + +
+ Dummy html page to test speculative connect +
+ + Hello Speculative Connect + + diff --git a/browser/components/sessionstore/test/browser_speculative_connect.js b/browser/components/sessionstore/test/browser_speculative_connect.js new file mode 100644 index 0000000000..bece5e7baa --- /dev/null +++ b/browser/components/sessionstore/test/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 = 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 = 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/browser/components/sessionstore/test/browser_swapDocShells.js b/browser/components/sessionstore/test/browser_swapDocShells.js new file mode 100644 index 0000000000..047a36c510 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_switch_remoteness.js b/browser/components/sessionstore/test/browser_switch_remoteness.js new file mode 100644 index 0000000000..927162d830 --- /dev/null +++ b/browser/components/sessionstore/test/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 promiseBrowserLoaded(browser); + 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/browser/components/sessionstore/test/browser_tab_label_during_restore.js b/browser/components/sessionstore/test/browser_tab_label_during_restore.js new file mode 100644 index 0000000000..7108990818 --- /dev/null +++ b/browser/components/sessionstore/test/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 BrowserTestUtils.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/browser/components/sessionstore/test/browser_tabicon_after_bg_tab_crash.js b/browser/components/sessionstore/test/browser_tabicon_after_bg_tab_crash.js new file mode 100644 index 0000000000..8a26806985 --- /dev/null +++ b/browser/components/sessionstore/test/browser_tabicon_after_bg_tab_crash.js @@ -0,0 +1,52 @@ +"use strict"; + +const FAVICON = + "data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw=="; +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 BrowserTestUtils.waitForCondition( + () => { + return gBrowser.getIcon() != null; + }, + "wait for favicon load to finish", + 100, + 5 + ); + Assert.equal(browser.mIconURL, FAVICON, "Favicon is correctly set."); + await BrowserTestUtils.switchTab(gBrowser, originalTab); + await BrowserTestUtils.crashFrame( + browser, + false /* shouldShowTabCrashPage */ + ); + Assert.equal( + browser.mIconURL, + FAVICON, + "Favicon is still set after crash." + ); + } + ); +}); diff --git a/browser/components/sessionstore/test/browser_tabs_in_urlbar.js b/browser/components/sessionstore/test/browser_tabs_in_urlbar.js new file mode 100644 index 0000000000..b82ba24a2a --- /dev/null +++ b/browser/components/sessionstore/test/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( + "resource:///modules/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/browser/components/sessionstore/test/browser_undoCloseById.js b/browser/components/sessionstore/test/browser_undoCloseById.js new file mode 100644 index 0000000000..f1c49ae22c --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_undoCloseById_targetWindow.js b/browser/components/sessionstore/test/browser_undoCloseById_targetWindow.js new file mode 100644 index 0000000000..62e3da89ea --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_unrestored_crashedTabs.js b/browser/components/sessionstore/test/browser_unrestored_crashedTabs.js new file mode 100644 index 0000000000..51e54af12a --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_upgrade_backup.js b/browser/components/sessionstore/test/browser_upgrade_backup.js new file mode 100644 index 0000000000..fa0e34d421 --- /dev/null +++ b/browser/components/sessionstore/test/browser_upgrade_backup.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +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 () { + // 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 = Preferences.get(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/browser/components/sessionstore/test/browser_urlbarSearchMode.js b/browser/components/sessionstore/test/browser_urlbarSearchMode.js new file mode 100644 index 0000000000..052fcf355c --- /dev/null +++ b/browser/components/sessionstore/test/browser_urlbarSearchMode.js @@ -0,0 +1,57 @@ +/* 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, { + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.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() { + // Open the urlbar view and enter search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.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: UrlbarUtils.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/browser/components/sessionstore/test/browser_userTyped_restored_after_discard.js b/browser/components/sessionstore/test/browser_userTyped_restored_after_discard.js new file mode 100644 index 0000000000..171197a743 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js b/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js new file mode 100644 index 0000000000..73568cb348 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/browser_windowStateContainer.js b/browser/components/sessionstore/test/browser_windowStateContainer.js new file mode 100644 index 0000000000..f0d6f42d39 --- /dev/null +++ b/browser/components/sessionstore/test/browser_windowStateContainer.js @@ -0,0 +1,176 @@ +"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(event) { + 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.moveTabTo(win.gBrowser.tabs[0], win.gBrowser.tabs.length - 1); + + 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 ContentTask.spawn( + browser, + { expectedId: i + 1 }, + async function (args) { + Assert.equal( + docShell.getOriginAttributes().userContextId, + args.expectedId, + "The docShell has the correct userContextId" + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.userContextId, + args.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, + [{ expectedId: 0 }], + async function (args) { + Assert.equal( + docShell.getOriginAttributes().userContextId, + args.expectedId, + "The docShell has the correct userContextId" + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.userContextId, + args.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.moveTabTo(win2.gBrowser.tabs[0], win2.gBrowser.tabs.length - 1); + 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 ContentTask.spawn(browser, { expectedId: i }, async function (args) { + Assert.equal( + docShell.getOriginAttributes().userContextId, + args.expectedId, + "The docShell has the correct userContextId" + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.userContextId, + args.expectedId, + "The document has the correct userContextId" + ); + }); + } + + await BrowserTestUtils.closeWindow(win); + await BrowserTestUtils.closeWindow(win2); +}); diff --git a/browser/components/sessionstore/test/coopHeaderCommon.sjs b/browser/components/sessionstore/test/coopHeaderCommon.sjs new file mode 100644 index 0000000000..be4607b2c2 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/coop_coep.html b/browser/components/sessionstore/test/coop_coep.html new file mode 100644 index 0000000000..9fe6f7a03e --- /dev/null +++ b/browser/components/sessionstore/test/coop_coep.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/browser/components/sessionstore/test/coop_coep.html^headers^ b/browser/components/sessionstore/test/coop_coep.html^headers^ new file mode 100644 index 0000000000..5f8621ef83 --- /dev/null +++ b/browser/components/sessionstore/test/coop_coep.html^headers^ @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Opener-Policy: same-origin diff --git a/browser/components/sessionstore/test/empty.html b/browser/components/sessionstore/test/empty.html new file mode 100644 index 0000000000..ba0056bc32 --- /dev/null +++ b/browser/components/sessionstore/test/empty.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/browser/components/sessionstore/test/file_async_duplicate_tab.html b/browser/components/sessionstore/test/file_async_duplicate_tab.html new file mode 100644 index 0000000000..17f1c080ea --- /dev/null +++ b/browser/components/sessionstore/test/file_async_duplicate_tab.html @@ -0,0 +1 @@ +clickme diff --git a/browser/components/sessionstore/test/file_async_flushes.html b/browser/components/sessionstore/test/file_async_flushes.html new file mode 100644 index 0000000000..17f1c080ea --- /dev/null +++ b/browser/components/sessionstore/test/file_async_flushes.html @@ -0,0 +1 @@ +clickme diff --git a/browser/components/sessionstore/test/file_formdata_password.html b/browser/components/sessionstore/test/file_formdata_password.html new file mode 100644 index 0000000000..0f072c31e1 --- /dev/null +++ b/browser/components/sessionstore/test/file_formdata_password.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/browser/components/sessionstore/test/file_sessionHistory_hashchange.html b/browser/components/sessionstore/test/file_sessionHistory_hashchange.html new file mode 100644 index 0000000000..4b64fc180a --- /dev/null +++ b/browser/components/sessionstore/test/file_sessionHistory_hashchange.html @@ -0,0 +1 @@ +clickme diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js new file mode 100644 index 0000000000..d475fa86a1 --- /dev/null +++ b/browser/components/sessionstore/test/head.js @@ -0,0 +1,690 @@ +/* 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 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(subject, topic, data) { + 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, topic, data) { + 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) { + while (ss.getClosedTabCountForWindow(win) > 0) { + ss.forgetClosedTab(win, 0); + } +} + +/** + * 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, options]) { + 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.ownerGlobal.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.ownerGlobal, 0); + node.dispatchEvent(event); + } + ); +} + +function promiseOnHistoryReplaceEntry(browser) { + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + return new Promise(resolve => { + let sessionHistory = browser.browsingContext?.sessionHistory; + if (sessionHistory) { + var historyListener = { + OnHistoryNewEntry() {}, + OnHistoryGotoIndex() {}, + OnHistoryPurge() {}, + OnHistoryReload() { + return true; + }, + + OnHistoryReplaceEntry() { + resolve(); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + + sessionHistory.addSHistoryListener(historyListener); + } + }); + } + + return SpecialPowers.spawn(browser, [], () => { + return new Promise(resolve => { + var historyListener = { + OnHistoryNewEntry() {}, + OnHistoryGotoIndex() {}, + OnHistoryPurge() {}, + OnHistoryReload() { + return true; + }, + + OnHistoryReplaceEntry() { + resolve(); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + + var { sessionHistory } = this.docShell.QueryInterface( + Ci.nsIWebNavigation + ); + if (sessionHistory) { + sessionHistory.legacySHistory.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.ownerGlobal + ); + await tabMenuShown; + + return tabMenu; +} diff --git a/browser/components/sessionstore/test/marionette/manifest.toml b/browser/components/sessionstore/test/marionette/manifest.toml new file mode 100644 index 0000000000..6b62bea84e --- /dev/null +++ b/browser/components/sessionstore/test/marionette/manifest.toml @@ -0,0 +1,19 @@ +[DEFAULT] +tags = "local" + +["test_persist_closed_tabs_restore_manually.py"] + +["test_restore_loading_tab.py"] + +["test_restore_manually.py"] + +["test_restore_manually_with_pinned_tabs.py"] + +["test_restore_windows_after_close_last_tabs.py"] +skip-if = ["os == 'mac'"] + +["test_restore_windows_after_restart_and_quit.py"] + +["test_restore_windows_after_windows_shutdown.py"] +run-if = ["os == 'win'"] +skip-if = ["win11_2009"] # Bug 1727691 diff --git a/browser/components/sessionstore/test/marionette/session_store_test_case.py b/browser/components/sessionstore/test/marionette/session_store_test_case.py new file mode 100644 index 0000000000..3bcbcd3f56 --- /dev/null +++ b/browser/components/sessionstore/test/marionette/session_store_test_case.py @@ -0,0 +1,432 @@ +# 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 "data:text/html;charset=utf-8,{}".format(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, + ): + super(SessionStoreTestCase, self).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, + } + ) + + 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(SessionStoreTestCase, self).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) + + 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_tab_shortcut(self): + self.marionette.actions.sequence("key", "keyboard_id").key_down( + self.accelKey + ).key_down("w").key_up("w").key_up(self.accelKey).perform() + + def close_all_tabs_and_restart(self): + self.close_all_tabs() + self.marionette.quit(callback=self._close_tab_shortcut) + 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="Not all requested windows have been opened. Expected {}, got {}.".format( + self.all_windows, current_windows_set + ), + ) + + self.marionette.quit(callback=lambda: 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/browser/components/sessionstore/test/marionette/test_persist_closed_tabs_restore_manually.py b/browser/components/sessionstore/test/marionette/test_persist_closed_tabs_restore_manually.py new file mode 100644 index 0000000000..9aa2a3871f --- /dev/null +++ b/browser/components/sessionstore/test/marionette/test_persist_closed_tabs_restore_manually.py @@ -0,0 +1,225 @@ +# 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 "data:text/html;charset=utf-8,{}".format( + 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(TestSessionRestoreClosedTabs, self).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/browser/components/sessionstore/test/marionette/test_restore_loading_tab.py b/browser/components/sessionstore/test/marionette/test_restore_loading_tab.py new file mode 100644 index 0000000000..f053081b02 --- /dev/null +++ b/browser/components/sessionstore/test/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 "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestRestoreLoadingPage(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestRestoreLoadingPage, self).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/browser/components/sessionstore/test/marionette/test_restore_manually.py b/browser/components/sessionstore/test/marionette/test_restore_manually.py new file mode 100644 index 0000000000..e3c0a83607 --- /dev/null +++ b/browser/components/sessionstore/test/marionette/test_restore_manually.py @@ -0,0 +1,144 @@ +# 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 "data:text/html;charset=utf-8,{}".format( + 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(TestSessionRestoreManually, self).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/browser/components/sessionstore/test/marionette/test_restore_manually_with_pinned_tabs.py b/browser/components/sessionstore/test/marionette/test_restore_manually_with_pinned_tabs.py new file mode 100644 index 0000000000..fa00c25a4c --- /dev/null +++ b/browser/components/sessionstore/test/marionette/test_restore_manually_with_pinned_tabs.py @@ -0,0 +1,108 @@ +# 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 "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestSessionRestoreWithPinnedTabs(SessionStoreTestCase): + def setUp(self): + super(TestSessionRestoreWithPinnedTabs, self).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/browser/components/sessionstore/test/marionette/test_restore_windows_after_close_last_tabs.py b/browser/components/sessionstore/test/marionette/test_restore_windows_after_close_last_tabs.py new file mode 100644 index 0000000000..2022d8fb87 --- /dev/null +++ b/browser/components/sessionstore/test/marionette/test_restore_windows_after_close_last_tabs.py @@ -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/. + +# 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(TestSessionStoreEnabledAllWindows, self).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/browser/components/sessionstore/test/marionette/test_restore_windows_after_restart_and_quit.py b/browser/components/sessionstore/test/marionette/test_restore_windows_after_restart_and_quit.py new file mode 100644 index 0000000000..3dd9dc1bf3 --- /dev/null +++ b/browser/components/sessionstore/test/marionette/test_restore_windows_after_restart_and_quit.py @@ -0,0 +1,88 @@ +# 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 "data:text/html;charset=utf-8,{}".format( + 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(TestSessionStoreEnabledAllWindows, self).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(TestSessionStoreEnabledNoPrivateWindows, self).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/browser/components/sessionstore/test/marionette/test_restore_windows_after_windows_shutdown.py b/browser/components/sessionstore/test/marionette/test_restore_windows_after_windows_shutdown.py new file mode 100644 index 0000000000..21eec455bb --- /dev/null +++ b/browser/components/sessionstore/test/marionette/test_restore_windows_after_windows_shutdown.py @@ -0,0 +1,66 @@ +# 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(TestWindowsShutdown, self).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(TestWindowsShutdownRegisterRestart, self).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(TestWindowsShutdownNormal, self).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(TestWindowsShutdownForcedSessionRestore, self).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/browser/components/sessionstore/test/restore_redirect_http.html b/browser/components/sessionstore/test/restore_redirect_http.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/browser/components/sessionstore/test/restore_redirect_http.html^headers^ b/browser/components/sessionstore/test/restore_redirect_http.html^headers^ new file mode 100644 index 0000000000..533bda36f3 --- /dev/null +++ b/browser/components/sessionstore/test/restore_redirect_http.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Moved Temporarily +Location: restore_redirect_target.html diff --git a/browser/components/sessionstore/test/restore_redirect_js.html b/browser/components/sessionstore/test/restore_redirect_js.html new file mode 100644 index 0000000000..f0130847b6 --- /dev/null +++ b/browser/components/sessionstore/test/restore_redirect_js.html @@ -0,0 +1,10 @@ + + + + + + + diff --git a/browser/components/sessionstore/test/restore_redirect_target.html b/browser/components/sessionstore/test/restore_redirect_target.html new file mode 100644 index 0000000000..813af05508 --- /dev/null +++ b/browser/components/sessionstore/test/restore_redirect_target.html @@ -0,0 +1,8 @@ + + + + +Test page + +Test page + diff --git a/browser/components/sessionstore/test/unit/data/sessionCheckpoints_all.json b/browser/components/sessionstore/test/unit/data/sessionCheckpoints_all.json new file mode 100644 index 0000000000..e02c421c3b --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/unit/data/sessionstore_invalid.js b/browser/components/sessionstore/test/unit/data/sessionstore_invalid.js new file mode 100644 index 0000000000..a8c3ff2ff9 --- /dev/null +++ b/browser/components/sessionstore/test/unit/data/sessionstore_invalid.js @@ -0,0 +1,3 @@ +{ + "windows": // invalid json +} diff --git a/browser/components/sessionstore/test/unit/data/sessionstore_valid.js b/browser/components/sessionstore/test/unit/data/sessionstore_valid.js new file mode 100644 index 0000000000..f9511f29f6 --- /dev/null +++ b/browser/components/sessionstore/test/unit/data/sessionstore_valid.js @@ -0,0 +1,3 @@ +{ + "windows": [] +} \ No newline at end of file diff --git a/browser/components/sessionstore/test/unit/head.js b/browser/components/sessionstore/test/unit/head.js new file mode 100644 index 0000000000..b342a886fb --- /dev/null +++ b/browser/components/sessionstore/test/unit/head.js @@ -0,0 +1,36 @@ +ChromeUtils.defineESModuleGetters(this, { + SessionStartup: "resource:///modules/sessionstore/SessionStartup.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/browser/components/sessionstore/test/unit/test_backup_once.js b/browser/components/sessionstore/test/unit/test_backup_once.js new file mode 100644 index 0000000000..db566491d5 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/unit/test_final_write_cleanup.js b/browser/components/sessionstore/test/unit/test_final_write_cleanup.js new file mode 100644 index 0000000000..b3fbd6b206 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/unit/test_histogram_corrupt_files.js b/browser/components/sessionstore/test/unit/test_histogram_corrupt_files.js new file mode 100644 index 0000000000..2c469ed3b4 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/unit/test_migration_lz4compression.js b/browser/components/sessionstore/test/unit/test_migration_lz4compression.js new file mode 100644 index 0000000000..4d9b700d8b --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/unit/test_startup_invalid_session.js b/browser/components/sessionstore/test/unit/test_startup_invalid_session.js new file mode 100644 index 0000000000..50960b1d43 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/unit/test_startup_nosession_async.js b/browser/components/sessionstore/test/unit/test_startup_nosession_async.js new file mode 100644 index 0000000000..259c393e63 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/unit/test_startup_session_async.js b/browser/components/sessionstore/test/unit/test_startup_session_async.js new file mode 100644 index 0000000000..a61c9fe422 --- /dev/null +++ b/browser/components/sessionstore/test/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/browser/components/sessionstore/test/unit/xpcshell.toml b/browser/components/sessionstore/test/unit/xpcshell.toml new file mode 100644 index 0000000000..6a53151b2b --- /dev/null +++ b/browser/components/sessionstore/test/unit/xpcshell.toml @@ -0,0 +1,28 @@ +[DEFAULT] +head = "head.js" +tags = "condprof" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] # bug 1730213 +support-files = [ + "data/sessionCheckpoints_all.json", + "data/sessionstore_invalid.js", + "data/sessionstore_valid.js", +] + +["test_backup_once.js"] +skip-if = ["condprof"] # 1769154 + +["test_final_write_cleanup.js"] + +["test_histogram_corrupt_files.js"] + +["test_migration_lz4compression.js"] +skip-if = ["condprof"] # 1769154 + +["test_startup_invalid_session.js"] +skip-if = ["condprof"] # 1769154 + +["test_startup_nosession_async.js"] +skip-if = ["condprof"] # 1769154 + +["test_startup_session_async.js"] -- cgit v1.2.3