summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/test
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/sessionstore/test')
-rw-r--r--browser/components/sessionstore/test/browser.ini390
-rw-r--r--browser/components/sessionstore/test/browser_1234021.js22
-rw-r--r--browser/components/sessionstore/test/browser_1234021_page.html6
-rw-r--r--browser/components/sessionstore/test/browser_1284886_suspend_tab.html12
-rw-r--r--browser/components/sessionstore/test/browser_1284886_suspend_tab.js95
-rw-r--r--browser/components/sessionstore/test/browser_1284886_suspend_tab_2.html11
-rw-r--r--browser/components/sessionstore/test/browser_1446343-windowsize.js39
-rw-r--r--browser/components/sessionstore/test/browser_248970_b_perwindowpb.js198
-rw-r--r--browser/components/sessionstore/test/browser_248970_b_sample.html37
-rw-r--r--browser/components/sessionstore/test/browser_339445.js39
-rw-r--r--browser/components/sessionstore/test/browser_339445_sample.html18
-rw-r--r--browser/components/sessionstore/test/browser_345898.js69
-rw-r--r--browser/components/sessionstore/test/browser_350525.js135
-rw-r--r--browser/components/sessionstore/test/browser_354894_perwindowpb.js489
-rw-r--r--browser/components/sessionstore/test/browser_367052.js48
-rw-r--r--browser/components/sessionstore/test/browser_393716.js103
-rw-r--r--browser/components/sessionstore/test/browser_394759_basic.js123
-rw-r--r--browser/components/sessionstore/test/browser_394759_behavior.js91
-rw-r--r--browser/components/sessionstore/test/browser_394759_perwindowpb.js57
-rw-r--r--browser/components/sessionstore/test/browser_394759_purge.js247
-rw-r--r--browser/components/sessionstore/test/browser_423132.js52
-rw-r--r--browser/components/sessionstore/test/browser_423132_sample.html14
-rw-r--r--browser/components/sessionstore/test/browser_447951.js84
-rw-r--r--browser/components/sessionstore/test/browser_447951_sample.html5
-rw-r--r--browser/components/sessionstore/test/browser_454908.js65
-rw-r--r--browser/components/sessionstore/test/browser_454908_sample.html8
-rw-r--r--browser/components/sessionstore/test/browser_456342.js90
-rw-r--r--browser/components/sessionstore/test/browser_456342_sample.xhtml46
-rw-r--r--browser/components/sessionstore/test/browser_459906.js79
-rw-r--r--browser/components/sessionstore/test/browser_459906_empty.html3
-rw-r--r--browser/components/sessionstore/test/browser_459906_sample.html41
-rw-r--r--browser/components/sessionstore/test/browser_461634.js133
-rw-r--r--browser/components/sessionstore/test/browser_461743.js53
-rw-r--r--browser/components/sessionstore/test/browser_461743_sample.html56
-rw-r--r--browser/components/sessionstore/test/browser_463205.js40
-rw-r--r--browser/components/sessionstore/test/browser_463205_sample.html7
-rw-r--r--browser/components/sessionstore/test/browser_463206.js120
-rw-r--r--browser/components/sessionstore/test/browser_463206_sample.html11
-rw-r--r--browser/components/sessionstore/test/browser_464199.js176
-rw-r--r--browser/components/sessionstore/test/browser_464620_a.html54
-rw-r--r--browser/components/sessionstore/test/browser_464620_a.js64
-rw-r--r--browser/components/sessionstore/test/browser_464620_b.html57
-rw-r--r--browser/components/sessionstore/test/browser_464620_b.js64
-rw-r--r--browser/components/sessionstore/test/browser_464620_xd.html5
-rw-r--r--browser/components/sessionstore/test/browser_465215.js36
-rw-r--r--browser/components/sessionstore/test/browser_465223.js51
-rw-r--r--browser/components/sessionstore/test/browser_466937.js51
-rw-r--r--browser/components/sessionstore/test/browser_466937_sample.html20
-rw-r--r--browser/components/sessionstore/test/browser_467409-backslashplosion.js88
-rw-r--r--browser/components/sessionstore/test/browser_477657.js80
-rw-r--r--browser/components/sessionstore/test/browser_480893.js45
-rw-r--r--browser/components/sessionstore/test/browser_485482.js76
-rw-r--r--browser/components/sessionstore/test/browser_485482_sample.html12
-rw-r--r--browser/components/sessionstore/test/browser_485563.js33
-rw-r--r--browser/components/sessionstore/test/browser_490040.js105
-rw-r--r--browser/components/sessionstore/test/browser_491168.js112
-rw-r--r--browser/components/sessionstore/test/browser_491577.js212
-rw-r--r--browser/components/sessionstore/test/browser_495495.js47
-rw-r--r--browser/components/sessionstore/test/browser_500328.js132
-rw-r--r--browser/components/sessionstore/test/browser_506482.js78
-rw-r--r--browser/components/sessionstore/test/browser_514751.js41
-rw-r--r--browser/components/sessionstore/test/browser_522375.js22
-rw-r--r--browser/components/sessionstore/test/browser_522545.js443
-rw-r--r--browser/components/sessionstore/test/browser_524745.js55
-rw-r--r--browser/components/sessionstore/test/browser_526613.js86
-rw-r--r--browser/components/sessionstore/test/browser_528776.js27
-rw-r--r--browser/components/sessionstore/test/browser_579868.js31
-rw-r--r--browser/components/sessionstore/test/browser_579879.js31
-rw-r--r--browser/components/sessionstore/test/browser_580512.js117
-rw-r--r--browser/components/sessionstore/test/browser_581937.js22
-rw-r--r--browser/components/sessionstore/test/browser_586068-apptabs.js109
-rw-r--r--browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js105
-rw-r--r--browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js212
-rw-r--r--browser/components/sessionstore/test/browser_586068-cascade.js107
-rw-r--r--browser/components/sessionstore/test/browser_586068-multi_window.js115
-rw-r--r--browser/components/sessionstore/test/browser_586068-reload.js118
-rw-r--r--browser/components/sessionstore/test/browser_586068-select.js128
-rw-r--r--browser/components/sessionstore/test/browser_586068-window_state.js120
-rw-r--r--browser/components/sessionstore/test/browser_586068-window_state_override.js118
-rw-r--r--browser/components/sessionstore/test/browser_586147.js52
-rw-r--r--browser/components/sessionstore/test/browser_588426.js62
-rw-r--r--browser/components/sessionstore/test/browser_589246.js286
-rw-r--r--browser/components/sessionstore/test/browser_590268.js155
-rw-r--r--browser/components/sessionstore/test/browser_590563.js120
-rw-r--r--browser/components/sessionstore/test/browser_595601-restore_hidden.js165
-rw-r--r--browser/components/sessionstore/test/browser_597071.js36
-rw-r--r--browser/components/sessionstore/test/browser_600545.js123
-rw-r--r--browser/components/sessionstore/test/browser_601955.js54
-rw-r--r--browser/components/sessionstore/test/browser_607016.js155
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js69
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js152
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js61
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js65
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js65
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js146
-rw-r--r--browser/components/sessionstore/test/browser_618151.js67
-rw-r--r--browser/components/sessionstore/test/browser_623779.js13
-rw-r--r--browser/components/sessionstore/test/browser_624727.js33
-rw-r--r--browser/components/sessionstore/test/browser_625016.js103
-rw-r--r--browser/components/sessionstore/test/browser_628270.js43
-rw-r--r--browser/components/sessionstore/test/browser_635418.js58
-rw-r--r--browser/components/sessionstore/test/browser_636279.js140
-rw-r--r--browser/components/sessionstore/test/browser_637020.js74
-rw-r--r--browser/components/sessionstore/test/browser_637020_slow.sjs22
-rw-r--r--browser/components/sessionstore/test/browser_645428.js22
-rw-r--r--browser/components/sessionstore/test/browser_659591.js39
-rw-r--r--browser/components/sessionstore/test/browser_662743.js138
-rw-r--r--browser/components/sessionstore/test/browser_662743_sample.html15
-rw-r--r--browser/components/sessionstore/test/browser_662812.js43
-rw-r--r--browser/components/sessionstore/test/browser_665702-state_session.js26
-rw-r--r--browser/components/sessionstore/test/browser_682507.js22
-rw-r--r--browser/components/sessionstore/test/browser_687710.js58
-rw-r--r--browser/components/sessionstore/test/browser_687710_2.js100
-rw-r--r--browser/components/sessionstore/test/browser_694378.js41
-rw-r--r--browser/components/sessionstore/test/browser_701377.js56
-rw-r--r--browser/components/sessionstore/test/browser_705597.js85
-rw-r--r--browser/components/sessionstore/test/browser_707862.js95
-rw-r--r--browser/components/sessionstore/test/browser_739531.js55
-rw-r--r--browser/components/sessionstore/test/browser_739531_frame.html1
-rw-r--r--browser/components/sessionstore/test/browser_739531_sample.html23
-rw-r--r--browser/components/sessionstore/test/browser_739805.js49
-rw-r--r--browser/components/sessionstore/test/browser_819510_perwindowpb.js152
-rw-r--r--browser/components/sessionstore/test/browser_906076_lazy_tabs.js163
-rw-r--r--browser/components/sessionstore/test/browser_911547.js82
-rw-r--r--browser/components/sessionstore/test/browser_911547_sample.html18
-rw-r--r--browser/components/sessionstore/test/browser_911547_sample.html^headers^1
-rw-r--r--browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js24
-rw-r--r--browser/components/sessionstore/test/browser_aboutSessionRestore.js67
-rw-r--r--browser/components/sessionstore/test/browser_async_duplicate_tab.js87
-rw-r--r--browser/components/sessionstore/test/browser_async_flushes.js131
-rw-r--r--browser/components/sessionstore/test/browser_async_remove_tab.js209
-rw-r--r--browser/components/sessionstore/test/browser_async_window_flushing.js208
-rw-r--r--browser/components/sessionstore/test/browser_attributes.js83
-rw-r--r--browser/components/sessionstore/test/browser_background_tab_crash.js262
-rw-r--r--browser/components/sessionstore/test/browser_backup_recovery.js307
-rw-r--r--browser/components/sessionstore/test/browser_bfcache_telemetry.js45
-rw-r--r--browser/components/sessionstore/test/browser_broadcast.js162
-rw-r--r--browser/components/sessionstore/test/browser_capabilities.js90
-rw-r--r--browser/components/sessionstore/test/browser_cleaner.js214
-rw-r--r--browser/components/sessionstore/test/browser_closedId.js109
-rw-r--r--browser/components/sessionstore/test/browser_closed_objects_changed_notifications_tabs.js136
-rw-r--r--browser/components/sessionstore/test/browser_closed_objects_changed_notifications_windows.js129
-rw-r--r--browser/components/sessionstore/test/browser_closed_tabs_windows.js191
-rw-r--r--browser/components/sessionstore/test/browser_cookies.js81
-rw-r--r--browser/components/sessionstore/test/browser_cookies_legacy.js75
-rw-r--r--browser/components/sessionstore/test/browser_cookies_privacy.js125
-rw-r--r--browser/components/sessionstore/test/browser_cookies_sameSite.js89
-rw-r--r--browser/components/sessionstore/test/browser_crashedTabs.js501
-rw-r--r--browser/components/sessionstore/test/browser_docshell_uuid_consistency.js104
-rw-r--r--browser/components/sessionstore/test/browser_duplicate_history.js30
-rw-r--r--browser/components/sessionstore/test/browser_duplicate_tab_in_new_window.js37
-rw-r--r--browser/components/sessionstore/test/browser_dying_cache.js80
-rw-r--r--browser/components/sessionstore/test/browser_dynamic_frames.js105
-rw-r--r--browser/components/sessionstore/test/browser_firefoxView_restore.js39
-rw-r--r--browser/components/sessionstore/test/browser_firefoxView_selected_restore.js88
-rw-r--r--browser/components/sessionstore/test/browser_focus_after_restore.js34
-rw-r--r--browser/components/sessionstore/test/browser_forget_async_closings.js163
-rw-r--r--browser/components/sessionstore/test/browser_formdata.js227
-rw-r--r--browser/components/sessionstore/test/browser_formdata_cc.js107
-rw-r--r--browser/components/sessionstore/test/browser_formdata_format.js170
-rw-r--r--browser/components/sessionstore/test/browser_formdata_format_sample.html7
-rw-r--r--browser/components/sessionstore/test/browser_formdata_max_size.js131
-rw-r--r--browser/components/sessionstore/test/browser_formdata_password.js69
-rw-r--r--browser/components/sessionstore/test/browser_formdata_sample.html20
-rw-r--r--browser/components/sessionstore/test/browser_formdata_xpath.js242
-rw-r--r--browser/components/sessionstore/test/browser_formdata_xpath_sample.html37
-rw-r--r--browser/components/sessionstore/test/browser_frame_history.js230
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_a.html5
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_b.html10
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_c.html5
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_c1.html5
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_c2.html5
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_index.html9
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_index2.html3
-rw-r--r--browser/components/sessionstore/test/browser_frame_history_index_blank.html4
-rw-r--r--browser/components/sessionstore/test/browser_frametree.js134
-rw-r--r--browser/components/sessionstore/test/browser_frametree_sample.html8
-rw-r--r--browser/components/sessionstore/test/browser_frametree_sample_frameset.html11
-rw-r--r--browser/components/sessionstore/test/browser_frametree_sample_iframes.html9
-rw-r--r--browser/components/sessionstore/test/browser_global_store.js53
-rw-r--r--browser/components/sessionstore/test/browser_history_persist.js163
-rw-r--r--browser/components/sessionstore/test/browser_ignore_updates_crashed_tabs.js108
-rw-r--r--browser/components/sessionstore/test/browser_label_and_icon.js53
-rw-r--r--browser/components/sessionstore/test/browser_merge_closed_tabs.js116
-rw-r--r--browser/components/sessionstore/test/browser_movePendingTabToNewWindow.js124
-rw-r--r--browser/components/sessionstore/test/browser_multiple_navigateAndRestore.js45
-rw-r--r--browser/components/sessionstore/test/browser_multiple_select_after_load.js54
-rw-r--r--browser/components/sessionstore/test/browser_newtab_userTypedValue.js92
-rw-r--r--browser/components/sessionstore/test/browser_not_collect_when_idle.js118
-rw-r--r--browser/components/sessionstore/test/browser_old_favicon.js57
-rw-r--r--browser/components/sessionstore/test/browser_page_title.js54
-rw-r--r--browser/components/sessionstore/test/browser_parentProcessRestoreHash.js115
-rw-r--r--browser/components/sessionstore/test/browser_pending_tabs.js38
-rw-r--r--browser/components/sessionstore/test/browser_pinned_tabs.js324
-rw-r--r--browser/components/sessionstore/test/browser_privatetabs.js33
-rw-r--r--browser/components/sessionstore/test/browser_purge_shistory.js65
-rw-r--r--browser/components/sessionstore/test/browser_remoteness_flip_on_restore.js310
-rw-r--r--browser/components/sessionstore/test/browser_reopen_all_windows.js146
-rw-r--r--browser/components/sessionstore/test/browser_replace_load.js56
-rw-r--r--browser/components/sessionstore/test/browser_restoreTabContainer.js81
-rw-r--r--browser/components/sessionstore/test/browser_restore_container_tabs_oa.js231
-rw-r--r--browser/components/sessionstore/test/browser_restore_cookies_noOriginAttributes.js191
-rw-r--r--browser/components/sessionstore/test/browser_restore_pageProxyState.js77
-rw-r--r--browser/components/sessionstore/test/browser_restore_private_tab_os.js59
-rw-r--r--browser/components/sessionstore/test/browser_restore_redirect.js72
-rw-r--r--browser/components/sessionstore/test/browser_restore_reversed_z_order.js125
-rw-r--r--browser/components/sessionstore/test/browser_restore_session_in_undoCloseTab.js56
-rw-r--r--browser/components/sessionstore/test/browser_restore_srcdoc.js44
-rw-r--r--browser/components/sessionstore/test/browser_restore_tabless_window.js56
-rw-r--r--browser/components/sessionstore/test/browser_restored_window_features.js164
-rw-r--r--browser/components/sessionstore/test/browser_revive_crashed_bg_tabs.js59
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositions.js258
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js76
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositions_readerModeArticle.html26
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositions_sample.html8
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositions_sample2.html8
-rw-r--r--browser/components/sessionstore/test/browser_scrollPositions_sample_frameset.html11
-rw-r--r--browser/components/sessionstore/test/browser_send_async_message_oom.js75
-rw-r--r--browser/components/sessionstore/test/browser_sessionHistory.js331
-rw-r--r--browser/components/sessionstore/test/browser_sessionHistory_slow.sjs22
-rw-r--r--browser/components/sessionstore/test/browser_sessionStorage.html28
-rw-r--r--browser/components/sessionstore/test/browser_sessionStorage.js298
-rw-r--r--browser/components/sessionstore/test/browser_sessionStorage_size.js34
-rw-r--r--browser/components/sessionstore/test/browser_sessionStoreContainer.js165
-rw-r--r--browser/components/sessionstore/test/browser_sizemodeBeforeMinimized.js44
-rw-r--r--browser/components/sessionstore/test/browser_speculative_connect.html8
-rw-r--r--browser/components/sessionstore/test/browser_speculative_connect.js145
-rw-r--r--browser/components/sessionstore/test/browser_swapDocShells.js40
-rw-r--r--browser/components/sessionstore/test/browser_switch_remoteness.js53
-rw-r--r--browser/components/sessionstore/test/browser_tab_label_during_restore.js182
-rw-r--r--browser/components/sessionstore/test/browser_tabicon_after_bg_tab_crash.js52
-rw-r--r--browser/components/sessionstore/test/browser_tabs_in_urlbar.js151
-rw-r--r--browser/components/sessionstore/test/browser_undoCloseById.js174
-rw-r--r--browser/components/sessionstore/test/browser_unrestored_crashedTabs.js73
-rw-r--r--browser/components/sessionstore/test/browser_upgrade_backup.js158
-rw-r--r--browser/components/sessionstore/test/browser_urlbarSearchMode.js57
-rw-r--r--browser/components/sessionstore/test/browser_userTyped_restored_after_discard.js47
-rw-r--r--browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js32
-rw-r--r--browser/components/sessionstore/test/browser_windowStateContainer.js176
-rw-r--r--browser/components/sessionstore/test/coopHeaderCommon.sjs31
-rw-r--r--browser/components/sessionstore/test/coop_coep.html6
-rw-r--r--browser/components/sessionstore/test/coop_coep.html^headers^2
-rw-r--r--browser/components/sessionstore/test/empty.html6
-rw-r--r--browser/components/sessionstore/test/file_async_duplicate_tab.html1
-rw-r--r--browser/components/sessionstore/test/file_async_flushes.html1
-rw-r--r--browser/components/sessionstore/test/file_formdata_password.html17
-rw-r--r--browser/components/sessionstore/test/file_sessionHistory_hashchange.html1
-rw-r--r--browser/components/sessionstore/test/head.js782
-rw-r--r--browser/components/sessionstore/test/marionette/manifest.ini14
-rw-r--r--browser/components/sessionstore/test/marionette/session_store_test_case.py432
-rw-r--r--browser/components/sessionstore/test/marionette/test_restore_loading_tab.py69
-rw-r--r--browser/components/sessionstore/test/marionette/test_restore_manually_with_pinned_tabs.py108
-rw-r--r--browser/components/sessionstore/test/marionette/test_restore_windows_after_close_last_tabs.py59
-rw-r--r--browser/components/sessionstore/test/marionette/test_restore_windows_after_restart_and_quit.py82
-rw-r--r--browser/components/sessionstore/test/marionette/test_restore_windows_after_windows_shutdown.py66
-rw-r--r--browser/components/sessionstore/test/restore_redirect_http.html0
-rw-r--r--browser/components/sessionstore/test/restore_redirect_http.html^headers^2
-rw-r--r--browser/components/sessionstore/test/restore_redirect_js.html10
-rw-r--r--browser/components/sessionstore/test/restore_redirect_target.html8
-rw-r--r--browser/components/sessionstore/test/unit/data/sessionCheckpoints_all.json11
-rw-r--r--browser/components/sessionstore/test/unit/data/sessionstore_invalid.js3
-rw-r--r--browser/components/sessionstore/test/unit/data/sessionstore_valid.js3
-rw-r--r--browser/components/sessionstore/test/unit/head.js36
-rw-r--r--browser/components/sessionstore/test/unit/test_backup_once.js137
-rw-r--r--browser/components/sessionstore/test/unit/test_final_write_cleanup.js118
-rw-r--r--browser/components/sessionstore/test/unit/test_histogram_corrupt_files.js117
-rw-r--r--browser/components/sessionstore/test/unit/test_migration_lz4compression.js151
-rw-r--r--browser/components/sessionstore/test/unit/test_startup_invalid_session.js27
-rw-r--r--browser/components/sessionstore/test/unit/test_startup_nosession_async.js19
-rw-r--r--browser/components/sessionstore/test/unit/test_startup_session_async.js32
-rw-r--r--browser/components/sessionstore/test/unit/xpcshell.ini21
271 files changed, 24240 insertions, 0 deletions
diff --git a/browser/components/sessionstore/test/browser.ini b/browser/components/sessionstore/test/browser.ini
new file mode 100644
index 0000000000..85e0c40448
--- /dev/null
+++ b/browser/components/sessionstore/test/browser.ini
@@ -0,0 +1,390 @@
+# 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/.
+
+# browser_506482.js is disabled because of frequent failures (bug 538672)
+# browser_526613.js is disabled because of frequent failures (bug 534489)
+# browser_589246.js is disabled for leaking browser windows (bug 752467)
+# browser_580512.js is disabled for leaking browser windows (bug 752467)
+
+[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
+
+#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 =
+ (os == "win" && os_version == "10.0" && !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 =
+ os == "linux" # Bug 1775616
+ os == "mac" # Bug 1775616
+ os == "win" && !debug # Bug 1775616
+[browser_attributes.js]
+[browser_background_tab_crash.js]
+https_first_disabled = true
+run-if = crashreporter
+# Disabled on debug for frequent intermittent failures:
+[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_windows.js]
+[browser_cookies.js]
+[browser_cookies_legacy.js]
+[browser_cookies_privacy.js]
+[browser_cookies_sameSite.js]
+[browser_crashedTabs.js]
+https_first_disabled = true
+skip-if =
+ !crashreporter
+ verify
+ win10_2004 # high frequency intermittent, Bug 1684120 - timed out
+ 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]
+https_first_disabled = true
+[browser_formdata.js]
+skip-if =
+ verify && debug
+[browser_formdata_cc.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
+ win10_2004 && bits == 64 && !debug # Bug 1638958
+ os == "linux" && !debug # Bug 1638958
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_merge_closed_tabs.js]
+[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
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[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_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_session_in_undoCloseTab.js]
+[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]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_sessionStorage_size.js]
+[browser_sessionStoreContainer.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_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 @@
+<!doctype html>
+<html>
+ <script>
+ sessionStorage;
+ </script>
+</html>
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 @@
+<html>
+<head>
+ <script>
+ window.onbeforeunload = function() {
+ return true;
+ };
+ </script>
+</head>
+<body>
+TEST PAGE
+</body>
+</html>
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 @@
+<html>
+<head>
+ <script>
+ window.onbeforeunload = function() {
+ };
+ </script>
+</head>
+<body>
+TEST PAGE
+</body>
+</html>
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..5dfbc9bc9a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
@@ -0,0 +1,198 @@
+/* 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
+ ok(
+ 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 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<meta charset="utf-8">
+<title>Test for bug 248970</title>
+
+<h3>Text Fields</h3>
+<input type="text" name="input">
+<input type="text" name="spaced 1">
+<input>
+
+<h3>Checkboxes and Radio buttons</h3>
+<input type="checkbox" name="check"> Check 1
+<input type="checkbox" name="uncheck" checked> Check 2
+<p>
+<input type="radio" name="group" value="1"> Radio 1
+<input type="radio" name="group" value="some"> Radio 2
+<input type="radio" name="group" checked> Radio 3
+
+<h3>Selects</h3>
+<select name="any">
+ <option value="1"> Select 1
+ <option value="some"> Select 2
+ <option>Select 3
+</select>
+<select multiple="multiple">
+ <option value=1> Multi-select 1
+ <option value=2> Multi-select 2
+ <option value=3> Multi-select 3
+ <option value=4> Multi-select 4
+</select>
+
+<h3>Text Areas</h3>
+<textarea name="testarea"></textarea>
+<textarea name="sized one" rows="5" cols="25"></textarea>
+<textarea></textarea>
+
+<h3>File Selector</h3>
+<input type="file">
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 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<meta charset="utf-8">
+<title>Test for bug 339445</title>
+
+storageTestItem = <span id="storageTestItem">FAIL</span>
+
+<!--
+ storageTestItem's textContent will be one of the following:
+ * FAIL : sessionStorage wasn't available
+ * PENDING : the test value has been initialized on first load
+ * SUCCESS : the test value was correctly retrieved
+-->
+
+<script type="application/javascript">
+ document.getElementById("storageTestItem").textContent =
+ sessionStorage.storageTestItem || "PENDING";
+ sessionStorage.storageTestItem = "SUCCESS";
+</script>
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..a954cbba3a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_350525.js
@@ -0,0 +1,135 @@
+"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);
+ ok(
+ 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..67623bd3ab
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_367052.js
@@ -0,0 +1,48 @@
+/* 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);
+ ok(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,<input%20id=txt>" +
+ "<input%20type=checkbox%20id=chk>";
+
+/**
+ * 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..b4998f2ed8
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_394759_perwindowpb.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"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.loadURIString(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"
+ );
+ ok(
+ 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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script type="text/javascript">
+ // generate an enormous random number...
+ var r = Math.floor(Math.random() * Math.pow(2, 62)).toString();
+
+ // ... and use it to set a randomly named cookie
+ document.cookie = r + "=value; path=/ohai";
+ </script>
+<body>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Testcase for bug 447951</title>
+
+<a href="#end">click me</a>
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 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<title>Test for bug 454908</title>
+
+<h3>Dummy Login</h3>
+<form>
+<p>Username: <input type="text" id="username">
+<p>Password: <input type="password" id="passwd">
+</form>
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 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head><title>Test for bug 456342</title></head>
+
+<body>
+<form>
+<h3>Non-standard &lt;input&gt;s</h3>
+<p>Search <input type="search" id="searchTerm"/></p>
+<p>Image Search: <input type="image search" /></p>
+<p>Autocomplete: <input type="autocomplete" name="fill-in"/></p>
+<p>Mistyped: <input type="txet" name="mistyped"/></p>
+<p>Invalid attr: <textarea type="password" name="textarea_pass"/></p>
+
+<h3>Ignored types</h3>
+<input type="hidden" name="hideme"/>
+<input type="HIDDEN" name="hideme2"/>
+<input type="submit" name="submit"/>
+<input type="reset" name="reset"/>
+<input type="image" name="image"/>
+<input type="button" name="button"/>
+<input type="password" name="password"/>
+<input type="PassWord" name="password2"/>
+<input type="PASSWORD" name="password3"/>
+<input autocomplete="off" name="auto1"/>
+<input type="text" autocomplete="OFF" name="auto2"/>
+<input type="text" autocomplete=" OFF " name="auto5"/>
+<input autocomplete=" off " name="auto6"/>
+<input autocomplete=" cc-CSC " name="auto7"/>
+<input autocomplete=" NEW-password " name="auto8"/>
+<textarea autocomplete="off" name="auto3"/>
+<select autocomplete="off" name="auto4">
+ <option value="1" selected="true"/>
+ <option value="2"/>
+ <option value="3"/>
+</select>
+<select autocomplete="cc-CSC" name="CSC">
+ <option value="123" selected="true"/>
+ <option value="234"/>
+ <option value="345"/>
+</select>
+</form>
+
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_459906.js b/browser/components/sessionstore/test/browser_459906.js
new file mode 100644
index 0000000000..e232b59f13
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_459906.js
@@ -0,0 +1,79 @@
+/* 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 */
+
+function test() {
+ /** Test for Bug 459906 **/
+
+ waitForExplicitFinish();
+
+ let testURL =
+ "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_459906_sample.html";
+ let uniqueValue = "<b>Unique:</b> " + Date.now();
+
+ var frameCount = 0;
+ let tab = BrowserTestUtils.addTab(gBrowser, testURL);
+ tab.linkedBrowser.addEventListener(
+ "load",
+ function listener(aEvent) {
+ // wait for all frames to load completely
+ if (frameCount++ < 2) {
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", listener, true);
+
+ let iframes = tab.linkedBrowser.contentWindow.frames;
+ // eslint-disable-next-line no-unsanitized/property
+ iframes[1].document.body.innerHTML = uniqueValue;
+
+ frameCount = 0;
+ let tab2 = gBrowser.duplicateTab(tab);
+ tab2.linkedBrowser.addEventListener(
+ "load",
+ function loadListener(eventTab2) {
+ // wait for all frames to load (and reload!) completely
+ if (frameCount++ < 2) {
+ return;
+ }
+ tab2.linkedBrowser.removeEventListener("load", loadListener, true);
+
+ executeSoon(function innerHTMLPoller() {
+ let iframesTab2 = tab2.linkedBrowser.contentWindow.frames;
+ if (iframesTab2[1].document.body.innerHTML !== uniqueValue) {
+ // Poll again the value, since we can't ensure to run
+ // after SessionStore has injected innerHTML value.
+ // See bug 521802.
+ info("Polling for innerHTML value");
+ setTimeout(innerHTMLPoller, 100);
+ return;
+ }
+
+ is(
+ iframesTab2[1].document.body.innerHTML,
+ uniqueValue,
+ "rich textarea's content correctly duplicated"
+ );
+
+ let innerDomain = null;
+ try {
+ innerDomain = iframesTab2[0].document.domain;
+ } catch (ex) {
+ /* throws for chrome: documents */
+ }
+ is(innerDomain, "mochi.test", "XSS exploit prevented!");
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ });
+ },
+ true
+ );
+ },
+ true
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_459906_empty.html b/browser/components/sessionstore/test/browser_459906_empty.html
new file mode 100644
index 0000000000..e01aaa3394
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_459906_empty.html
@@ -0,0 +1,3 @@
+<title>Cross Domain File for bug 459906</title>
+
+cheers from localhost
diff --git a/browser/components/sessionstore/test/browser_459906_sample.html b/browser/components/sessionstore/test/browser_459906_sample.html
new file mode 100644
index 0000000000..6f1c6a52ef
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_459906_sample.html
@@ -0,0 +1,41 @@
+<!-- Testcase originally by David Bloom <bloom@google.com> -->
+
+<!DOCTYPE html>
+<title>Test for bug 459906</title>
+
+<body>
+<iframe src="data:text/html;charset=utf-8,not_on_localhost"></iframe>
+<iframe></iframe>
+
+<script type="application/javascript">
+ var loadCount = 0;
+ frames[0].addEventListener("DOMContentLoaded", handleLoad);
+ frames[1].addEventListener("DOMContentLoaded", handleLoad);
+ function handleLoad() {
+ if (++loadCount < 2)
+ return;
+ frames[0].removeEventListener("DOMContentLoaded", handleLoad);
+ frames[1].removeEventListener("DOMContentLoaded", handleLoad);
+ frames[0].document.designMode = "on";
+ frames[0].document.__defineGetter__("designMode", function() {
+ // inject a cross domain file ...
+ var documentInjected = false;
+ document.getElementsByTagName("iframe")[0].onload =
+ function() { documentInjected = true; };
+ frames[0].location = "browser_459906_empty.html";
+
+ // ... and ensure that it has time to load
+ for (var c = 0; !documentInjected && c < 20; c++) {
+ var r = new XMLHttpRequest();
+ r.open("GET", location.href, false);
+ r.overrideMimeType("text/plain");
+ r.send(null);
+ }
+
+ return "on";
+ });
+
+ frames[1].document.designMode = "on";
+ }
+</script>
+</body>
diff --git a/browser/components/sessionstore/test/browser_461634.js b/browser/components/sessionstore/test/browser_461634.js
new file mode 100644
index 0000000000..2db2fd453a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_461634.js
@@ -0,0 +1,133 @@
+/* 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 testClosedTabData() {
+ /** Test for Bug 461634 **/
+
+ const REMEMBER = Date.now(),
+ FORGET = Math.random();
+ let test_state = {
+ windows: [
+ {
+ tabs: [{ entries: [] }],
+ _closedTabs: [
+ {
+ state: { entries: [{ url: "http://www.example.net/" }] },
+ title: FORGET,
+ },
+ {
+ state: { entries: [{ url: "http://www.example.net/" }] },
+ title: REMEMBER,
+ },
+ {
+ state: { entries: [{ url: "http://www.example.net/" }] },
+ title: FORGET,
+ },
+ {
+ state: { entries: [{ url: "http://www.example.net/" }] },
+ title: REMEMBER,
+ },
+ ],
+ },
+ ],
+ };
+ let remember_count = 2;
+
+ function countByTitle(aClosedTabList, aTitle) {
+ return aClosedTabList.filter(aData => aData.title == aTitle).length;
+ }
+
+ function testForError(aFunction) {
+ try {
+ aFunction();
+ return false;
+ } catch (ex) {
+ return ex.name == "NS_ERROR_ILLEGAL_VALUE";
+ }
+ }
+
+ // Open a window and add the above closed tab list.
+ let newWin = openDialog(location, "", "chrome,all,dialog=no");
+ await promiseWindowLoaded(newWin);
+
+ Services.prefs.setIntPref(
+ "browser.sessionstore.max_tabs_undo",
+ test_state.windows[0]._closedTabs.length
+ );
+ await setWindowState(newWin, test_state);
+
+ let closedTabs = SessionStore.getClosedTabDataForWindow(newWin);
+
+ // Verify that non JSON serialized data is the same as JSON serialized data.
+ is(
+ JSON.stringify(closedTabs),
+ JSON.stringify(SessionStore.getClosedTabDataForWindow(newWin)),
+ "Non-serialized data is the same as serialized data"
+ );
+
+ is(
+ closedTabs.length,
+ test_state.windows[0]._closedTabs.length,
+ "Closed tab list has the expected length"
+ );
+ is(
+ countByTitle(closedTabs, FORGET),
+ test_state.windows[0]._closedTabs.length - remember_count,
+ "The correct amout of tabs are to be forgotten"
+ );
+ is(
+ countByTitle(closedTabs, REMEMBER),
+ remember_count,
+ "Everything is set up"
+ );
+
+ // All of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE.
+ ok(
+ testForError(() => ss.forgetClosedTab({}, 0)),
+ "Invalid window for forgetClosedTab throws"
+ );
+ ok(
+ testForError(() => ss.forgetClosedTab(newWin, -1)),
+ "Invalid tab for forgetClosedTab throws"
+ );
+ ok(
+ testForError(() =>
+ ss.forgetClosedTab(newWin, test_state.windows[0]._closedTabs.length + 1)
+ ),
+ "Invalid tab for forgetClosedTab throws"
+ );
+
+ // Remove third tab, then first tab.
+ ss.forgetClosedTab(newWin, 2);
+ ss.forgetClosedTab(newWin, null);
+
+ closedTabs = SessionStore.getClosedTabDataForWindow(newWin);
+
+ // Verify that non JSON serialized data is the same as JSON serialized data.
+ is(
+ JSON.stringify(closedTabs),
+ JSON.stringify(SessionStore.getClosedTabDataForWindow(newWin)),
+ "Non-serialized data is the same as serialized data"
+ );
+
+ is(
+ closedTabs.length,
+ remember_count,
+ "The correct amout of tabs was removed"
+ );
+ is(
+ countByTitle(closedTabs, FORGET),
+ 0,
+ "All tabs specifically forgotten were indeed removed"
+ );
+ is(
+ countByTitle(closedTabs, REMEMBER),
+ remember_count,
+ "... and tabs not specifically forgetten weren't"
+ );
+
+ // Clean up.
+ Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo");
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/sessionstore/test/browser_461743.js b/browser/components/sessionstore/test/browser_461743.js
new file mode 100644
index 0000000000..fd4501b5ac
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_461743.js
@@ -0,0 +1,53 @@
+/* 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 461743 **/
+
+ waitForExplicitFinish();
+
+ let testURL =
+ "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_461743_sample.html";
+
+ let frameCount = 0;
+ let tab = BrowserTestUtils.addTab(gBrowser, testURL);
+ tab.linkedBrowser.addEventListener(
+ "load",
+ function loadListener(aEvent) {
+ // Wait for all frames to load completely.
+ if (frameCount++ < 2) {
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", loadListener, true);
+ let tab2 = gBrowser.duplicateTab(tab);
+ tab2.linkedBrowser.addEventListener(
+ "461743",
+ function listener(eventTab2) {
+ tab2.linkedBrowser.removeEventListener("461743", listener, true);
+ is(aEvent.data, "done", "XSS injection was attempted");
+
+ executeSoon(function () {
+ let iframes = tab2.linkedBrowser.contentWindow.frames;
+ let innerHTML = iframes[1].document.body.innerHTML;
+ isnot(
+ innerHTML,
+ Cu.reportError.toString(),
+ "chrome access denied!"
+ );
+
+ // Clean up.
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ });
+ },
+ true,
+ true
+ );
+ },
+ true
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_461743_sample.html b/browser/components/sessionstore/test/browser_461743_sample.html
new file mode 100644
index 0000000000..a933ec5dc9
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_461743_sample.html
@@ -0,0 +1,56 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<!DOCTYPE html>
+<title>Test for bug 461743</title>
+
+<body>
+<iframe src="data:text/html;charset=utf-8,empty"></iframe>
+<iframe></iframe>
+
+<script type="application/javascript">
+ var chromeUrl = "chrome://global/content/mozilla.html";
+ var exploitUrl = "javascript:try { document.body.innerHTML = Components.utils.reportError; } catch (ex) { }";
+
+ var loadCount = 0;
+ frames[0].addEventListener("DOMContentLoaded", handleLoad);
+ frames[1].addEventListener("DOMContentLoaded", handleLoad);
+ function handleLoad() {
+ if (++loadCount < 2)
+ return;
+ frames[0].removeEventListener("DOMContentLoaded", handleLoad);
+ frames[1].removeEventListener("DOMContentLoaded", handleLoad);
+
+ var flip = 0;
+ MutationEvent.prototype.toString = function() {
+ return flip++ == 0 ? chromeUrl : exploitUrl;
+ };
+
+ var href = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(frames[1].location), "href").get;
+ var loadChrome = { handleEvent: href };
+ var loadExploit = { handleEvent: href };
+
+ function delay() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", location.href, false);
+ xhr.send(null);
+ }
+ function done() {
+ var event = new MessageEvent("461743", { bubbles: true, cancelable: false,
+ data: "done", origin: location.href,
+ source: window });
+ document.dispatchEvent(event);
+ frames[0].document.removeEventListener("DOMNodeInserted", loadChrome, true);
+ frames[0].document.removeEventListener("DOMNodeInserted", delay, true);
+ frames[0].document.removeEventListener("DOMNodeInserted", loadExploit, true);
+ frames[0].document.removeEventListener("DOMNodeInserted", done, true);
+ }
+
+ frames[0].document.addEventListener("DOMNodeInserted", loadChrome, true);
+ frames[0].document.addEventListener("DOMNodeInserted", delay, true);
+ frames[0].document.addEventListener("DOMNodeInserted", loadExploit, true);
+ frames[0].document.addEventListener("DOMNodeInserted", done, true);
+
+ frames[0].document.designMode = "on";
+ }
+</script>
+</body>
diff --git a/browser/components/sessionstore/test/browser_463205.js b/browser/components/sessionstore/test/browser_463205.js
new file mode 100644
index 0000000000..797425ab05
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_463205.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_463205_sample.html";
+
+/**
+ * Bug 463205 - Check URLs before restoring form data to make sure a malicious
+ * website can't modify frame URLs and make us inject form data into the wrong
+ * web pages.
+ */
+add_task(async function test_check_urls_before_restoring() {
+ // Add a blank tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Restore form data with a valid URL.
+ await promiseTabState(tab, getState(URL));
+
+ let value = await getPropertyOfFormField(browser, "#text", "value");
+ is(value, "foobar", "value was restored");
+
+ // Restore form data with an invalid URL.
+ await promiseTabState(tab, getState("http://example.com/"));
+
+ value = await getPropertyOfFormField(browser, "#text", "value");
+ is(value, "", "value was not restored");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+function getState(url) {
+ return JSON.stringify({
+ entries: [{ url: URL, triggeringPrincipal_base64 }],
+ formdata: { url, id: { text: "foobar" } },
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_463205_sample.html b/browser/components/sessionstore/test/browser_463205_sample.html
new file mode 100644
index 0000000000..6591401b69
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_463205_sample.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>bug 463205</title>
+
+<body>
+ <input type="text" id="text" />
+</body>
diff --git a/browser/components/sessionstore/test/browser_463206.js b/browser/components/sessionstore/test/browser_463206.js
new file mode 100644
index 0000000000..b760f1c762
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_463206.js
@@ -0,0 +1,120 @@
+/* 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 MOCHI_ROOT = ROOT.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+if (gFissionBrowser) {
+ addCoopTask(
+ "browser_463206_sample.html",
+ test_restore_text_data_subframes,
+ HTTPSROOT
+ );
+}
+addNonCoopTask(
+ "browser_463206_sample.html",
+ test_restore_text_data_subframes,
+ HTTPSROOT
+);
+addNonCoopTask(
+ "browser_463206_sample.html",
+ test_restore_text_data_subframes,
+ HTTPROOT
+);
+addNonCoopTask(
+ "browser_463206_sample.html",
+ test_restore_text_data_subframes,
+ MOCHI_ROOT
+);
+
+async function test_restore_text_data_subframes(aURL) {
+ // Add a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, aURL);
+
+ await setPropertyOfFormField(
+ tab.linkedBrowser,
+ "#out1",
+ "value",
+ Date.now().toString(16)
+ );
+
+ await setPropertyOfFormField(
+ tab.linkedBrowser,
+ "input[name='1|#out2']",
+ "value",
+ Math.random()
+ );
+
+ await setPropertyOfFormField(
+ tab.linkedBrowser.browsingContext.children[0].children[1],
+ "#in1",
+ "value",
+ new Date()
+ );
+
+ // Duplicate the tab.
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ await promiseTabRestored(tab2);
+
+ isnot(
+ await getPropertyOfFormField(browser2, "#out1", "value"),
+ await getPropertyOfFormField(
+ browser2.browsingContext.children[1],
+ "#out1",
+ "value"
+ ),
+ "text isn't reused for frames"
+ );
+
+ isnot(
+ await getPropertyOfFormField(browser2, "input[name='1|#out2']", "value"),
+ "",
+ "text containing | and # is correctly restored"
+ );
+
+ is(
+ await getPropertyOfFormField(
+ browser2.browsingContext.children[1],
+ "#out2",
+ "value"
+ ),
+ "",
+ "id prefixes can't be faked"
+ );
+
+ // Query a few values from the top and its child frames.
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], async function () {
+ // Bug 588077
+ // XXX(farre): disabling this, because it started passing more heavily on Windows.
+ /*
+ let in1ValFrame0_1 = await SpecialPowers.spawn(
+ content.frames[0],
+ [],
+ async function() {
+ return SpecialPowers.spawn(content.frames[1], [], async function() {
+ return content.document.getElementById("in1").value;
+ });
+ }
+ );
+ todo_is(in1ValFrame0_1, "", "id prefixes aren't mixed up");
+ */
+ });
+
+ is(
+ await getPropertyOfFormField(
+ browser2.browsingContext.children[1].children[0],
+ "#in1",
+ "value"
+ ),
+ "",
+ "id prefixes aren't mixed up"
+ );
+ // Cleanup.
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+}
diff --git a/browser/components/sessionstore/test/browser_463206_sample.html b/browser/components/sessionstore/test/browser_463206_sample.html
new file mode 100644
index 0000000000..0d31f29066
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_463206_sample.html
@@ -0,0 +1,11 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for bug 463206</title>
+
+<iframe src="data:text/html;charset=utf-8,<iframe></iframe><iframe%20src='data:text/html;charset=utf-8,<input%2520id=%2522in1%2522>'></iframe>"></iframe>
+<iframe src="data:text/html;charset=utf-8,<input%20id='out1'><input%20id='out2'><iframe%20src='data:text/html;charset=utf-8,<input%2520id=%2522in1%2522>'>"></iframe>
+
+<input id="out1">
+<input name="1|#out2">
diff --git a/browser/components/sessionstore/test/browser_464199.js b/browser/components/sessionstore/test/browser_464199.js
new file mode 100644
index 0000000000..4ac8fba1a5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464199.js
@@ -0,0 +1,176 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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 () {
+ /** Test for Bug 464199 **/
+
+ const REMEMBER = Date.now(),
+ FORGET = Math.random();
+ let test_state = {
+ windows: [
+ {
+ tabs: [{ entries: [] }],
+ _closedTabs: [
+ {
+ state: { entries: [{ url: "http://www.example.net/" }] },
+ title: FORGET,
+ },
+ {
+ state: { entries: [{ url: "http://www.example.org/" }] },
+ title: REMEMBER,
+ },
+ {
+ state: {
+ entries: [
+ { url: "http://www.example.net/" },
+ { url: "http://www.example.org/" },
+ ],
+ },
+ title: FORGET,
+ },
+ {
+ state: { entries: [{ url: "http://example.net/" }] },
+ title: FORGET,
+ },
+ {
+ state: { entries: [{ url: "http://sub.example.net/" }] },
+ title: FORGET,
+ },
+ {
+ state: { entries: [{ url: "http://www.example.net:8080/" }] },
+ title: FORGET,
+ },
+ { state: { entries: [{ url: "about:license" }] }, title: REMEMBER },
+ {
+ state: {
+ entries: [
+ {
+ url: "http://www.example.org/frameset",
+ children: [
+ { url: "http://www.example.org/frame" },
+ { url: "http://www.example.org:8080/frame2" },
+ ],
+ },
+ ],
+ },
+ title: REMEMBER,
+ },
+ {
+ state: {
+ entries: [
+ {
+ url: "http://www.example.org/frameset",
+ children: [
+ { url: "http://www.example.org/frame" },
+ { url: "http://www.example.net/frame" },
+ ],
+ },
+ ],
+ },
+ title: FORGET,
+ },
+ {
+ state: {
+ entries: [
+ {
+ url: "http://www.example.org/form",
+ formdata: { id: { url: "http://www.example.net/" } },
+ },
+ ],
+ },
+ title: REMEMBER,
+ },
+ {
+ state: {
+ entries: [{ url: "http://www.example.org/form" }],
+ extData: { setCustomTabValue: "http://example.net:80" },
+ },
+ title: REMEMBER,
+ },
+ ],
+ },
+ ],
+ };
+ let remember_count = 5;
+
+ function countByTitle(aClosedTabList, aTitle) {
+ return aClosedTabList.filter(aData => aData.title == aTitle).length;
+ }
+
+ // open a window and add the above closed tab list
+ let newWin = openDialog(location, "", "chrome,all,dialog=no");
+ await promiseWindowLoaded(newWin);
+ Services.prefs.setIntPref(
+ "browser.sessionstore.max_tabs_undo",
+ test_state.windows[0]._closedTabs.length
+ );
+
+ let restoring = promiseWindowRestoring(newWin);
+ let restored = promiseWindowRestored(newWin);
+ ss.setWindowState(newWin, JSON.stringify(test_state), true);
+ await restoring;
+ await restored;
+
+ let closedTabs = ss.getClosedTabDataForWindow(newWin);
+ is(
+ closedTabs.length,
+ test_state.windows[0]._closedTabs.length,
+ "Closed tab list has the expected length"
+ );
+ is(
+ countByTitle(closedTabs, FORGET),
+ test_state.windows[0]._closedTabs.length - remember_count,
+ "The correct amout of tabs are to be forgotten"
+ );
+ is(
+ countByTitle(closedTabs, REMEMBER),
+ remember_count,
+ "Everything is set up."
+ );
+
+ let promise = promiseClearHistory();
+ await ForgetAboutSite.removeDataFromDomain("example.net");
+ await promise;
+ closedTabs = ss.getClosedTabDataForWindow(newWin);
+ is(
+ closedTabs.length,
+ remember_count,
+ "The correct amout of tabs was removed"
+ );
+ is(
+ countByTitle(closedTabs, FORGET),
+ 0,
+ "All tabs to be forgotten were indeed removed"
+ );
+ is(
+ countByTitle(closedTabs, REMEMBER),
+ remember_count,
+ "... and tabs to be remembered weren't."
+ );
+ // clean up
+ Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo");
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/sessionstore/test/browser_464620_a.html b/browser/components/sessionstore/test/browser_464620_a.html
new file mode 100644
index 0000000000..5edb7e1a46
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_a.html
@@ -0,0 +1,54 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<title>Test for bug 464620 (injection on input)</title>
+
+<iframe></iframe>
+<iframe onload="setup()"></iframe>
+
+<script>
+ var targetUrl = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_464620_xd.html";
+ var firstPass;
+
+ function setup() {
+ if (firstPass !== undefined)
+ return;
+ firstPass = frames[1].location.href == "about:blank";
+ if (firstPass) {
+ frames[0].location = 'data:text/html;charset=utf-8,<body onload="if (parent.firstPass) parent.step();"><input id="x" oninput="parent.xss()">XXX</body>';
+ }
+ frames[1].location = targetUrl;
+ }
+
+ function step() {
+ var x = frames[0].document.getElementById("x");
+ if (x.value == "")
+ x.value = "ready";
+ x.style.display = "none";
+ frames[0].document.designMode = "on";
+ }
+
+ function xss() {
+ step();
+
+ var documentInjected = false;
+ document.getElementsByTagName("iframe")[0].onload =
+ function() { documentInjected = true; };
+ frames[0].location = targetUrl;
+
+ for (var c = 0; !documentInjected && c < 20; c++) {
+ var r = new XMLHttpRequest();
+ r.open("GET", location.href, false);
+ r.overrideMimeType("text/plain");
+ r.send(null);
+ }
+ document.getElementById("state").textContent = "done";
+
+ var event = new MessageEvent("464620_a", { bubbles: true, cancelable: false,
+ data: "done", origin: location.href,
+ source: window });
+ document.dispatchEvent(event);
+ }
+</script>
+
+<p id="state">pending</p>
diff --git a/browser/components/sessionstore/test/browser_464620_a.js b/browser/components/sessionstore/test/browser_464620_a.js
new file mode 100644
index 0000000000..9052d7bec0
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_a.js
@@ -0,0 +1,64 @@
+/* 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 464620 (injection on input) **/
+
+ waitForExplicitFinish();
+
+ let testURL =
+ "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_464620_a.html";
+
+ var frameCount = 0;
+ let tab = BrowserTestUtils.addTab(gBrowser, testURL);
+ tab.linkedBrowser.addEventListener(
+ "load",
+ function loadListener(aEvent) {
+ // wait for all frames to load completely
+ if (frameCount++ < 4) {
+ return;
+ }
+ this.removeEventListener("load", loadListener, true);
+
+ executeSoon(function () {
+ frameCount = 0;
+ let tab2 = gBrowser.duplicateTab(tab);
+ tab2.linkedBrowser.addEventListener(
+ "464620_a",
+ function listener(eventTab2) {
+ tab2.linkedBrowser.removeEventListener("464620_a", listener, true);
+ is(aEvent.data, "done", "XSS injection was attempted");
+
+ // let form restoration complete and take into account the
+ // setTimeout(..., 0) in sss_restoreDocument_proxy
+ executeSoon(function () {
+ setTimeout(function () {
+ let win = tab2.linkedBrowser.contentWindow;
+ isnot(
+ win.frames[0].document.location,
+ testURL,
+ "cross domain document was loaded"
+ );
+ ok(
+ !/XXX/.test(win.frames[0].document.body.innerHTML),
+ "no content was injected"
+ );
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ }, 0);
+ });
+ },
+ true,
+ true
+ );
+ });
+ },
+ true
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_464620_b.html b/browser/components/sessionstore/test/browser_464620_b.html
new file mode 100644
index 0000000000..e7fde55c2b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_b.html
@@ -0,0 +1,57 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<title>Test for bug 464620 (injection on DOM node insertion)</title>
+
+<iframe></iframe>
+<iframe></iframe>
+<iframe onload="setup()"></iframe>
+
+<script>
+ var targetUrl = "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_464620_xd.html";
+ var firstPass;
+
+ function setup() {
+ if (firstPass !== undefined)
+ return;
+ firstPass = frames[2].location.href == "about:blank";
+ if (firstPass) {
+ frames[0].location = 'data:text/html;charset=utf-8,<body onload="parent.step()">a</body>';
+ frames[1].location = 'data:text/html;charset=utf-8,<body onload="document.designMode=\'on\';">XXX</body>';
+ }
+ frames[2].location = targetUrl;
+ }
+
+ function step() {
+ frames[0].document.designMode = "on";
+ if (firstPass)
+ return;
+
+ var body = frames[0].document.body;
+ body.addEventListener("DOMNodeInserted", function() {
+ xss();
+ }, {capture: true, once: true});
+ }
+
+ function xss() {
+ var documentInjected = false;
+ document.getElementsByTagName("iframe")[1].onload =
+ function() { documentInjected = true; };
+ frames[1].location = targetUrl;
+
+ for (var c = 0; !documentInjected && c < 20; c++) {
+ var r = new XMLHttpRequest();
+ r.open("GET", location.href, false);
+ r.overrideMimeType("text/plain");
+ r.send(null);
+ }
+ document.getElementById("state").textContent = "done";
+
+ var event = new MessageEvent("464620_b", { bubbles: true, cancelable: false,
+ data: "done", origin: location.href,
+ source: window });
+ document.dispatchEvent(event);
+ }
+</script>
+
+<p id="state">pending</p>
diff --git a/browser/components/sessionstore/test/browser_464620_b.js b/browser/components/sessionstore/test/browser_464620_b.js
new file mode 100644
index 0000000000..005bb4cc27
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_b.js
@@ -0,0 +1,64 @@
+/* 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 464620 (injection on DOM node insertion) **/
+
+ waitForExplicitFinish();
+
+ let testURL =
+ "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_464620_b.html";
+
+ var frameCount = 0;
+ let tab = BrowserTestUtils.addTab(gBrowser, testURL);
+ tab.linkedBrowser.addEventListener(
+ "load",
+ function loadListener(aEvent) {
+ // wait for all frames to load completely
+ if (frameCount++ < 6) {
+ return;
+ }
+ this.removeEventListener("load", loadListener, true);
+
+ executeSoon(function () {
+ frameCount = 0;
+ let tab2 = gBrowser.duplicateTab(tab);
+ tab2.linkedBrowser.addEventListener(
+ "464620_b",
+ function listener(eventTab2) {
+ tab2.linkedBrowser.removeEventListener("464620_b", listener, true);
+ is(aEvent.data, "done", "XSS injection was attempted");
+
+ // let form restoration complete and take into account the
+ // setTimeout(..., 0) in sss_restoreDocument_proxy
+ executeSoon(function () {
+ setTimeout(function () {
+ let win = tab2.linkedBrowser.contentWindow;
+ isnot(
+ win.frames[1].document.location,
+ testURL,
+ "cross domain document was loaded"
+ );
+ ok(
+ !/XXX/.test(win.frames[1].document.body.innerHTML),
+ "no content was injected"
+ );
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+
+ finish();
+ }, 0);
+ });
+ },
+ true,
+ true
+ );
+ });
+ },
+ true
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_464620_xd.html b/browser/components/sessionstore/test/browser_464620_xd.html
new file mode 100644
index 0000000000..9ec51c4c7b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_464620_xd.html
@@ -0,0 +1,5 @@
+<title>Cross Document File for bug 464620</title>
+
+<body onload="document.designMode='on';" bgcolor="red">
+ This document is editable.
+</body>
diff --git a/browser/components/sessionstore/test/browser_465215.js b/browser/components/sessionstore/test/browser_465215.js
new file mode 100644
index 0000000000..d57d58509f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_465215.js
@@ -0,0 +1,36 @@
+"use strict";
+
+var uniqueName = "bug 465215";
+var uniqueValue1 = "as good as unique: " + Date.now();
+var uniqueValue2 = "as good as unique: " + Math.random();
+
+add_task(async function () {
+ // set a unique value on a new, blank tab
+ let tab1 = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ await promiseBrowserLoaded(tab1.linkedBrowser);
+ ss.setCustomTabValue(tab1, uniqueName, uniqueValue1);
+
+ // duplicate the tab with that value
+ let tab2 = ss.duplicateTab(window, tab1);
+ await promiseTabRestored(tab2);
+ is(
+ ss.getCustomTabValue(tab2, uniqueName),
+ uniqueValue1,
+ "tab value was duplicated"
+ );
+
+ ss.setCustomTabValue(tab2, uniqueName, uniqueValue2);
+ isnot(
+ ss.getCustomTabValue(tab1, uniqueName),
+ uniqueValue2,
+ "tab values aren't sync'd"
+ );
+
+ // overwrite the tab with the value which should remove it
+ await promiseTabState(tab1, { entries: [] });
+ is(ss.getCustomTabValue(tab1, uniqueName), "", "tab value was cleared");
+
+ // clean up
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab1);
+});
diff --git a/browser/components/sessionstore/test/browser_465223.js b/browser/components/sessionstore/test/browser_465223.js
new file mode 100644
index 0000000000..b6863e2634
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_465223.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function test_clearWindowValues() {
+ /** Test for Bug 465223 **/
+
+ let uniqueKey1 = "bug 465223.1";
+ let uniqueKey2 = "bug 465223.2";
+ let uniqueValue1 = "unik" + Date.now();
+ let uniqueValue2 = "pi != " + Math.random();
+
+ // open a window and set a value on it
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
+ await promiseWindowLoaded(newWin);
+ ss.setCustomWindowValue(newWin, uniqueKey1, uniqueValue1);
+
+ let newState = { windows: [{ tabs: [{ entries: [] }], extData: {} }] };
+ newState.windows[0].extData[uniqueKey2] = uniqueValue2;
+ await setWindowState(newWin, newState);
+
+ is(newWin.gBrowser.tabs.length, 2, "original tab wasn't overwritten");
+ is(
+ ss.getCustomWindowValue(newWin, uniqueKey1),
+ uniqueValue1,
+ "window value wasn't overwritten when the tabs weren't"
+ );
+ is(
+ ss.getCustomWindowValue(newWin, uniqueKey2),
+ uniqueValue2,
+ "new window value was correctly added"
+ );
+
+ newState.windows[0].extData[uniqueKey2] = uniqueValue1;
+ await setWindowState(newWin, newState, true);
+
+ is(newWin.gBrowser.tabs.length, 1, "original tabs were overwritten");
+ is(
+ ss.getCustomWindowValue(newWin, uniqueKey1),
+ "",
+ "window value was cleared"
+ );
+ is(
+ ss.getCustomWindowValue(newWin, uniqueKey2),
+ uniqueValue1,
+ "window value was correctly overwritten"
+ );
+
+ // clean up
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/sessionstore/test/browser_466937.js b/browser/components/sessionstore/test/browser_466937.js
new file mode 100644
index 0000000000..cca45fec3a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_466937.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_466937_sample.html";
+
+/**
+ * Bug 466937 - Prevent file stealing with sessionstore.
+ */
+add_task(async function test_prevent_file_stealing() {
+ // Add a tab with some file input fields.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Generate a path to a 'secret' file.
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append("466937_test.file");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ let testPath = file.path;
+
+ // Fill in form values.
+ await setPropertyOfFormField(
+ browser,
+ "#reverse_thief",
+ "value",
+ "/home/user/secret2"
+ );
+ await setPropertyOfFormField(browser, "#bystander", "value", testPath);
+
+ // Duplicate and check form values.
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ await promiseTabRestored(tab2);
+
+ let thief = await getPropertyOfFormField(browser2, "#thief", "value");
+ is(thief, "", "file path wasn't set to text field value");
+ let reverse_thief = await getPropertyOfFormField(
+ browser2,
+ "#reverse_thief",
+ "value"
+ );
+ is(reverse_thief, "", "text field value wasn't set to full file path");
+ let bystander = await getPropertyOfFormField(browser2, "#bystander", "value");
+ is(bystander, testPath, "normal case: file path was correctly preserved");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(tab2);
+});
diff --git a/browser/components/sessionstore/test/browser_466937_sample.html b/browser/components/sessionstore/test/browser_466937_sample.html
new file mode 100644
index 0000000000..a05defa122
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_466937_sample.html
@@ -0,0 +1,20 @@
+<!-- Testcase originally by <moz_bug_r_a4@yahoo.com> -->
+
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for bug 466937</title>
+
+<input id="thief" value="/home/user/secret">
+<input type="file" id="reverse_thief">
+<input type="file" id="bystander">
+
+<script>
+ window.addEventListener("DOMContentLoaded", function() {
+ if (!document.location.hash) {
+ document.location.hash = "#ready";
+ } else {
+ document.getElementById("thief").type = "file";
+ document.getElementById("reverse_thief").type = "text";
+ }
+ }, {once: true});
+</script>
diff --git a/browser/components/sessionstore/test/browser_467409-backslashplosion.js b/browser/components/sessionstore/test/browser_467409-backslashplosion.js
new file mode 100644
index 0000000000..fe1a821160
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_467409-backslashplosion.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test Summary:
+// 1. Open about:sessionrestore where formdata is a JS object, not a string
+// 1a. Check that #sessionData on the page is readable after JSON.parse (skipped, checking formdata is sufficient)
+// 1b. Check that there are no backslashes in the formdata
+// 1c. Check that formdata doesn't require JSON.parse
+//
+// 2. Use the current state (currently about:sessionrestore with data) and then open that in a new instance of about:sessionrestore
+// 2a. Check that there are no backslashes in the formdata
+// 2b. Check that formdata doesn't require JSON.parse
+//
+// 3. [backwards compat] Use a stringified state as formdata when opening about:sessionrestore
+// 3a. Make sure there are nodes in the tree on about:sessionrestore (skipped, checking formdata is sufficient)
+// 3b. Check that there are no backslashes in the formdata
+// 3c. Check that formdata doesn't require JSON.parse
+
+const CRASH_STATE = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }] },
+ ],
+ },
+ ],
+};
+const STATE = createEntries(CRASH_STATE);
+const STATE2 = createEntries({ windows: [{ tabs: [STATE] }] });
+const STATE3 = createEntries(JSON.stringify(CRASH_STATE));
+
+function createEntries(sessionData) {
+ return {
+ entries: [{ url: "about:sessionrestore", triggeringPrincipal_base64 }],
+ formdata: { id: { sessionData }, url: "about:sessionrestore" },
+ };
+}
+
+add_task(async function test_nested_about_sessionrestore() {
+ // Prepare a blank tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // test 1
+ await promiseTabState(tab, STATE);
+ await checkState("test1", tab);
+
+ // test 2
+ await promiseTabState(tab, STATE2);
+ await checkState("test2", tab);
+
+ // test 3
+ await promiseTabState(tab, STATE3);
+ await checkState("test3", tab);
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+async function checkState(prefix, tab) {
+ // Flush and query tab state.
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ let { formdata } = JSON.parse(ss.getTabState(tab));
+
+ ok(
+ formdata.id.sessionData,
+ prefix + ": we have form data for about:sessionrestore"
+ );
+
+ let sessionData_raw = JSON.stringify(formdata.id.sessionData);
+ ok(
+ !/\\/.test(sessionData_raw),
+ prefix + ": #sessionData contains no backslashes"
+ );
+ info(sessionData_raw);
+
+ let gotError = false;
+ try {
+ JSON.parse(formdata.id.sessionData);
+ } catch (e) {
+ info(prefix + ": got error: " + e);
+ gotError = true;
+ }
+ ok(gotError, prefix + ": attempting to JSON.parse form data threw error");
+}
diff --git a/browser/components/sessionstore/test/browser_477657.js b/browser/components/sessionstore/test/browser_477657.js
new file mode 100644
index 0000000000..f54c270bf5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_477657.js
@@ -0,0 +1,80 @@
+/* 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_sizemodeDefaults() {
+ /** Test for Bug 477657 **/
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
+ await promiseWindowLoaded(newWin);
+ let newState = {
+ windows: [
+ {
+ tabs: [{ entries: [] }],
+ _closedTabs: [
+ {
+ state: { entries: [{ url: "about:" }] },
+ title: "About:",
+ },
+ ],
+ sizemode: "maximized",
+ },
+ ],
+ };
+
+ let uniqueKey = "bug 477657";
+ let uniqueValue = "unik" + Date.now();
+
+ ss.setCustomWindowValue(newWin, uniqueKey, uniqueValue);
+ is(
+ ss.getCustomWindowValue(newWin, uniqueKey),
+ uniqueValue,
+ "window value was set before the window was overwritten"
+ );
+
+ await setWindowState(newWin, newState, true);
+ // use newWin.setTimeout(..., 0) to mirror sss_restoreWindowFeatures
+ await new Promise(resolve => newWin.setTimeout(resolve, 0));
+
+ is(
+ ss.getCustomWindowValue(newWin, uniqueKey),
+ "",
+ "window value was implicitly cleared"
+ );
+
+ is(newWin.windowState, newWin.STATE_MAXIMIZED, "the window was maximized");
+
+ is(
+ ss.getClosedTabDataForWindow(newWin).length,
+ 1,
+ "the closed tab was added before the window was overwritten"
+ );
+ delete newState.windows[0]._closedTabs;
+ delete newState.windows[0].sizemode;
+
+ await setWindowState(newWin, newState, true);
+ await new Promise(resolve => newWin.setTimeout(resolve, 0));
+
+ is(
+ ss.getClosedTabDataForWindow(newWin).length,
+ 0,
+ "closed tabs were implicitly cleared"
+ );
+
+ is(
+ newWin.windowState,
+ newWin.STATE_MAXIMIZED,
+ "the window remains maximized"
+ );
+ newState.windows[0].sizemode = "normal";
+
+ await setWindowState(newWin, newState, true);
+ await new Promise(resolve => newWin.setTimeout(resolve, 0));
+
+ isnot(
+ newWin.windowState,
+ newWin.STATE_MAXIMIZED,
+ "the window was explicitly unmaximized"
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/sessionstore/test/browser_480893.js b/browser/components/sessionstore/test/browser_480893.js
new file mode 100644
index 0000000000..410f986d47
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_480893.js
@@ -0,0 +1,45 @@
+"use strict";
+
+/**
+ * Tests that we get sent to the right page when the user clicks
+ * the "Close" button in about:sessionrestore
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.startup.page", 0]],
+ });
+
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:sessionrestore");
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, "about:sessionrestore");
+
+ let doc = browser.contentDocument;
+
+ // Click on the "Close" button after about:sessionrestore is loaded.
+ doc.getElementById("errorCancel").click();
+
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ // Test that starting a new session loads the homepage (set to http://mochi.test:8888)
+ // if Firefox is configured to display a homepage at startup (browser.startup.page = 1)
+ let homepage = "http://mochi.test:8888/";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.startup.homepage", homepage],
+ ["browser.startup.page", 1],
+ ],
+ });
+
+ BrowserTestUtils.loadURIString(browser, "about:sessionrestore");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:sessionrestore");
+ doc = browser.contentDocument;
+
+ // Click on the "Close" button after about:sessionrestore is loaded.
+ doc.getElementById("errorCancel").click();
+ await BrowserTestUtils.browserLoaded(browser);
+
+ is(browser.currentURI.spec, homepage, "loaded page is the homepage");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_485482.js b/browser/components/sessionstore/test/browser_485482.js
new file mode 100644
index 0000000000..f32523005b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_485482.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+if (gFissionBrowser) {
+ addCoopTask(
+ "browser_485482_sample.html",
+ test_xpath_exp_for_strange_documents,
+ HTTPSROOT
+ );
+}
+addNonCoopTask(
+ "browser_485482_sample.html",
+ test_xpath_exp_for_strange_documents,
+ ROOT
+);
+addNonCoopTask(
+ "browser_485482_sample.html",
+ test_xpath_exp_for_strange_documents,
+ HTTPSROOT
+);
+addNonCoopTask(
+ "browser_485482_sample.html",
+ test_xpath_exp_for_strange_documents,
+ HTTPROOT
+);
+
+/**
+ * Bug 485482 - Make sure that we produce valid XPath expressions even for very
+ * weird HTML documents.
+ */
+async function test_xpath_exp_for_strange_documents(aURL) {
+ // Load a page with weird tag names.
+ let tab = BrowserTestUtils.addTab(gBrowser, aURL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Fill in some values.
+ let uniqueValue = Math.random();
+ await setPropertyOfFormField(
+ browser,
+ "input[type=text]",
+ "value",
+ uniqueValue
+ );
+ await setPropertyOfFormField(
+ browser,
+ "input[type=checkbox]",
+ "checked",
+ true
+ );
+
+ // Duplicate the tab.
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ await promiseTabRestored(tab2);
+
+ // Check that we generated valid XPath expressions to restore form values.
+ let text = await getPropertyOfFormField(
+ browser2,
+ "input[type=text]",
+ "value"
+ );
+ is("" + text, "" + uniqueValue, "generated XPath expression was valid");
+ let checkbox = await getPropertyOfFormField(
+ browser2,
+ "input[type=checkbox]",
+ "checked"
+ );
+ ok(checkbox, "generated XPath expression was valid");
+
+ // Cleanup.
+ gBrowser.removeTab(tab2);
+ gBrowser.removeTab(tab);
+}
diff --git a/browser/components/sessionstore/test/browser_485482_sample.html b/browser/components/sessionstore/test/browser_485482_sample.html
new file mode 100644
index 0000000000..c2097b5930
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_485482_sample.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<title>Test for bug 485482</title>
+
+<bad=name>
+ <input type="text">
+</bad=name>
+
+<worse=name>
+ <l0c@l+na~e"'§>
+ <input type="checkbox" name="check"> Check
+ </l0c@l+na~e"'§>
+</worse=name>
diff --git a/browser/components/sessionstore/test/browser_485563.js b/browser/components/sessionstore/test/browser_485563.js
new file mode 100644
index 0000000000..797100dd98
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_485563.js
@@ -0,0 +1,33 @@
+/* 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 485563 **/
+
+ waitForExplicitFinish();
+
+ let uniqueValue =
+ Math.random() + "\u2028Second line\u2029Second paragraph\u2027";
+
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ promiseBrowserLoaded(tab.linkedBrowser).then(() => {
+ ss.setCustomTabValue(tab, "bug485563", uniqueValue);
+ let tabState = JSON.parse(ss.getTabState(tab));
+ is(
+ tabState.extData.bug485563,
+ uniqueValue,
+ "unicode line separator wasn't over-encoded"
+ );
+ ss.deleteCustomTabValue(tab, "bug485563");
+ ss.setTabState(tab, JSON.stringify(tabState));
+ is(
+ ss.getCustomTabValue(tab, "bug485563"),
+ uniqueValue,
+ "unicode line separator was correctly preserved"
+ );
+
+ gBrowser.removeTab(tab);
+ finish();
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_490040.js b/browser/components/sessionstore/test/browser_490040.js
new file mode 100644
index 0000000000..623a9ea0ab
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_490040.js
@@ -0,0 +1,105 @@
+/* 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/. */
+
+// Only windows with open tabs are restorable. Windows where a lone tab is
+// detached may have _closedTabs, but is left with just an empty tab.
+const STATES = [
+ {
+ shouldBeAdded: true,
+ windowState: {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ {
+ url: "http://example.com",
+ triggeringPrincipal_base64,
+ title: "example.com",
+ },
+ ],
+ },
+ ],
+ selected: 1,
+ _closedTabs: [],
+ },
+ ],
+ },
+ },
+ {
+ shouldBeAdded: false,
+ windowState: {
+ windows: [
+ {
+ tabs: [{ entries: [] }],
+ _closedTabs: [],
+ },
+ ],
+ },
+ },
+ {
+ shouldBeAdded: false,
+ windowState: {
+ windows: [
+ {
+ tabs: [{ entries: [] }],
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ {
+ url: "http://example.com",
+ triggeringPrincipal_base64,
+ index: 1,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ shouldBeAdded: false,
+ windowState: {
+ windows: [
+ {
+ tabs: [{ entries: [] }],
+ _closedTabs: [],
+ extData: { keyname: "pi != " + Math.random() },
+ },
+ ],
+ },
+ },
+];
+
+add_task(async function test_bug_490040() {
+ for (let state of STATES) {
+ // Ensure we can store the window if needed.
+ let startingClosedWindowCount = ss.getClosedWindowCount();
+ await pushPrefs([
+ "browser.sessionstore.max_windows_undo",
+ startingClosedWindowCount + 1,
+ ]);
+
+ let curClosedWindowCount = ss.getClosedWindowCount();
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await setWindowState(win, state.windowState, true);
+ if (state.windowState.windows[0].tabs.length) {
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+
+ is(
+ ss.getClosedWindowCount(),
+ curClosedWindowCount + (state.shouldBeAdded ? 1 : 0),
+ "That window should " +
+ (state.shouldBeAdded ? "" : "not ") +
+ "be restorable"
+ );
+ }
+});
diff --git a/browser/components/sessionstore/test/browser_491168.js b/browser/components/sessionstore/test/browser_491168.js
new file mode 100644
index 0000000000..67bc45d371
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_491168.js
@@ -0,0 +1,112 @@
+"use strict";
+
+const REFERRER1 = "http://example.org/?" + Date.now();
+const REFERRER2 = "http://example.org/?" + Math.random();
+const REFERRER3 = "http://example.org/?" + Math.random();
+
+add_task(async function () {
+ function getExpectedReferrer(referrer) {
+ let defaultPolicy = Services.prefs.getIntPref(
+ "network.http.referer.defaultPolicy"
+ );
+ ok(
+ [2, 3].indexOf(defaultPolicy) > -1,
+ "default referrer policy should be either strict-origin-when-cross-origin(2) or no-referrer-when-downgrade(3)"
+ );
+ if (defaultPolicy == 2) {
+ return referrer.match(/https?:\/\/[^\/]+\/?/i)[0];
+ }
+ return referrer;
+ }
+
+ async function checkDocumentReferrer(referrer, msg) {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ referrer, msg }],
+ async function (args) {
+ Assert.equal(content.document.referrer, args.referrer, args.msg);
+ }
+ );
+ }
+
+ let ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ );
+ // Add a new tab.
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank"
+ ));
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Load a new URI with a specific referrer.
+ let referrerInfo1 = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ Services.io.newURI(REFERRER1)
+ );
+ browser.loadURI(Services.io.newURI("http://example.org"), {
+ referrerInfo: referrerInfo1,
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
+ });
+ await promiseBrowserLoaded(browser);
+
+ await TabStateFlusher.flush(browser);
+ let tabState = JSON.parse(ss.getTabState(tab));
+ let actualReferrerInfo = E10SUtils.deserializeReferrerInfo(
+ tabState.entries[0].referrerInfo
+ );
+ is(
+ actualReferrerInfo.originalReferrer.spec,
+ REFERRER1,
+ "Referrer retrieved via getTabState matches referrer set via loadURI."
+ );
+
+ let referrerInfo2 = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ Services.io.newURI(REFERRER2)
+ );
+
+ tabState.entries[0].referrerInfo =
+ E10SUtils.serializeReferrerInfo(referrerInfo2);
+ await promiseTabState(tab, tabState);
+
+ await checkDocumentReferrer(
+ getExpectedReferrer(REFERRER2),
+ "document.referrer matches referrer set via setTabState using referrerInfo."
+ );
+ gBrowser.removeCurrentTab();
+
+ // Restore the closed tab.
+ tab = ss.undoCloseTab(window, 0);
+ await promiseTabRestored(tab);
+
+ await checkDocumentReferrer(
+ getExpectedReferrer(REFERRER2),
+ "document.referrer is still correct after closing and reopening the tab."
+ );
+
+ tabState.entries[0].referrerInfo = null;
+ tabState.entries[0].referrer = REFERRER3;
+ await promiseTabState(tab, tabState);
+
+ await checkDocumentReferrer(
+ getExpectedReferrer(REFERRER3),
+ "document.referrer matches referrer set via setTabState using referrer."
+ );
+ gBrowser.removeCurrentTab();
+
+ // Restore the closed tab.
+ tab = ss.undoCloseTab(window, 0);
+ await promiseTabRestored(tab);
+
+ await checkDocumentReferrer(
+ getExpectedReferrer(REFERRER3),
+ "document.referrer is still correct after closing and reopening the tab."
+ );
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/sessionstore/test/browser_491577.js b/browser/components/sessionstore/test/browser_491577.js
new file mode 100644
index 0000000000..87d77b71c4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_491577.js
@@ -0,0 +1,212 @@
+/* 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_deleteClosedWindow() {
+ /** Test for Bug 491577 **/
+
+ const REMEMBER = Date.now(),
+ FORGET = Math.random();
+ let test_state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com/", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ selected: 1,
+ },
+ ],
+ _closedWindows: [
+ // _closedWindows[0]
+ {
+ tabs: [
+ {
+ entries: [
+ {
+ url: "http://example.com/",
+ triggeringPrincipal_base64,
+ title: "title",
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ url: "http://mozilla.org/",
+ triggeringPrincipal_base64,
+ title: "title",
+ },
+ ],
+ },
+ ],
+ selected: 2,
+ title: FORGET,
+ _closedTabs: [],
+ },
+ // _closedWindows[1]
+ {
+ tabs: [
+ {
+ entries: [
+ {
+ url: "http://mozilla.org/",
+ triggeringPrincipal_base64,
+ title: "title",
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ url: "http://example.com/",
+ triggeringPrincipal_base64,
+ title: "title",
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ url: "http://mozilla.org/",
+ triggeringPrincipal_base64,
+ title: "title",
+ },
+ ],
+ },
+ ],
+ selected: 3,
+ title: REMEMBER,
+ _closedTabs: [],
+ },
+ // _closedWindows[2]
+ {
+ tabs: [
+ {
+ entries: [
+ {
+ url: "http://example.com/",
+ triggeringPrincipal_base64,
+ title: "title",
+ },
+ ],
+ },
+ ],
+ selected: 1,
+ title: FORGET,
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ {
+ url: "http://mozilla.org/",
+ triggeringPrincipal_base64,
+ title: "title",
+ },
+ {
+ url: "http://mozilla.org/again",
+ triggeringPrincipal_base64,
+ title: "title",
+ },
+ ],
+ },
+ pos: 1,
+ title: "title",
+ },
+ {
+ state: {
+ entries: [
+ {
+ url: "http://example.com",
+ triggeringPrincipal_base64,
+ title: "title",
+ },
+ ],
+ },
+ title: "title",
+ },
+ ],
+ },
+ ],
+ };
+ let remember_count = 1;
+
+ function countByTitle(aClosedWindowList, aTitle) {
+ return aClosedWindowList.filter(aData => aData.title == aTitle).length;
+ }
+
+ function testForError(aFunction) {
+ try {
+ aFunction();
+ return false;
+ } catch (ex) {
+ return ex.name == "NS_ERROR_ILLEGAL_VALUE";
+ }
+ }
+
+ // open a window and add the above closed window list
+ let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
+ await promiseWindowLoaded(newWin);
+ Services.prefs.setIntPref(
+ "browser.sessionstore.max_windows_undo",
+ test_state._closedWindows.length
+ );
+ await setWindowState(newWin, test_state, true);
+
+ let closedWindows = ss.getClosedWindowData();
+ is(
+ closedWindows.length,
+ test_state._closedWindows.length,
+ "Closed window list has the expected length"
+ );
+ is(
+ countByTitle(closedWindows, FORGET),
+ test_state._closedWindows.length - remember_count,
+ "The correct amount of windows are to be forgotten"
+ );
+ is(
+ countByTitle(closedWindows, REMEMBER),
+ remember_count,
+ "Everything is set up."
+ );
+
+ // all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
+ ok(
+ testForError(() => ss.forgetClosedWindow(-1)),
+ "Invalid window for forgetClosedWindow throws"
+ );
+ ok(
+ testForError(() =>
+ ss.forgetClosedWindow(test_state._closedWindows.length + 1)
+ ),
+ "Invalid window for forgetClosedWindow throws"
+ );
+
+ // Remove third window, then first window
+ ss.forgetClosedWindow(2);
+ ss.forgetClosedWindow(null);
+
+ closedWindows = ss.getClosedWindowData();
+ is(
+ closedWindows.length,
+ remember_count,
+ "The correct amount of windows were removed"
+ );
+ is(
+ countByTitle(closedWindows, FORGET),
+ 0,
+ "All windows specifically forgotten were indeed removed"
+ );
+ is(
+ countByTitle(closedWindows, REMEMBER),
+ remember_count,
+ "... and windows not specifically forgetten weren't."
+ );
+
+ // clean up
+ Services.prefs.clearUserPref("browser.sessionstore.max_windows_undo");
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/sessionstore/test/browser_495495.js b/browser/components/sessionstore/test/browser_495495.js
new file mode 100644
index 0000000000..d8779d2f05
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_495495.js
@@ -0,0 +1,47 @@
+/* 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_urlbarFocus() {
+ /** Test for Bug 495495 **/
+
+ let newWin = openDialog(
+ location,
+ "_blank",
+ "chrome,all,dialog=no,toolbar=yes"
+ );
+ await promiseWindowLoaded(newWin);
+ let state1 = ss.getWindowState(newWin);
+ await BrowserTestUtils.closeWindow(newWin);
+
+ newWin = openDialog(
+ location,
+ "_blank",
+ "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar=no,location,personal,directories,dialog=no"
+ );
+ await promiseWindowLoaded(newWin);
+ let state2 = ss.getWindowState(newWin);
+
+ async function testState(state, expected) {
+ let win = openDialog(location, "_blank", "chrome,all,dialog=no");
+ await promiseWindowLoaded(win);
+
+ is(
+ win.gURLBar.readOnly,
+ false,
+ "URL bar should not be read-only before setting the state"
+ );
+ await setWindowState(win, state, true);
+ is(
+ win.gURLBar.readOnly,
+ expected.readOnly,
+ "URL bar read-only state should be restored correctly"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ }
+
+ await BrowserTestUtils.closeWindow(newWin);
+ await testState(state1, { readOnly: false });
+ await testState(state2, { readOnly: true });
+});
diff --git a/browser/components/sessionstore/test/browser_500328.js b/browser/components/sessionstore/test/browser_500328.js
new file mode 100644
index 0000000000..a32917c9c2
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_500328.js
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+async function checkState(browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ // Go back and then forward, and make sure that the state objects received
+ // from the popState event are as we expect them to be.
+ //
+ // We also add a node to the document's body when after going back and make
+ // sure it's still there after we go forward -- this is to test that the two
+ // history entries correspond to the same document.
+
+ // Set some state in the page's window. When we go back(), the page should
+ // be retrieved from bfcache, and this state should still be there.
+ content.testState = "foo";
+ });
+
+ // Now go back. This should trigger the popstate event handler.
+ let popstatePromise = SpecialPowers.spawn(browser, [], async () => {
+ let event = await ContentTaskUtils.waitForEvent(content, "popstate", true);
+ ok(event.state, "Event should have a state property.");
+
+ is(content.testState, "foo", "testState after going back");
+ is(
+ JSON.stringify(content.history.state),
+ JSON.stringify({ obj1: 1 }),
+ "first popstate object."
+ );
+
+ // Add a node with id "new-elem" to the document.
+ let doc = content.document;
+ ok(
+ !doc.getElementById("new-elem"),
+ "doc shouldn't contain new-elem before we add it."
+ );
+ let elem = doc.createElement("div");
+ elem.id = "new-elem";
+ doc.body.appendChild(elem);
+ });
+
+ // Ensure that the message manager has processed the previous task before
+ // going back to prevent racing with it in non-e10s mode.
+ await SpecialPowers.spawn(browser, [], () => {});
+ browser.goBack();
+
+ await popstatePromise;
+
+ popstatePromise = SpecialPowers.spawn(browser, [], async () => {
+ let event = await ContentTaskUtils.waitForEvent(content, "popstate", true);
+
+ // When content fires a PopStateEvent and we observe it from a chrome event
+ // listener (as we do here, and, thankfully, nowhere else in the tree), the
+ // state object will be a cross-compartment wrapper to an object that was
+ // deserialized in the content scope. And in this case, since RegExps are
+ // not currently Xrayable (see bug 1014991), trying to pull |obj3| (a RegExp)
+ // off of an Xrayed Object won't work. So we need to waive.
+ Assert.equal(
+ Cu.waiveXrays(event.state).obj3.toString(),
+ "/^a$/",
+ "second popstate object."
+ );
+
+ // Make sure that the new-elem node is present in the document. If it's
+ // not, then this history entry has a different doc identifier than the
+ // previous entry, which is bad.
+ let doc = content.document;
+ let newElem = doc.getElementById("new-elem");
+ ok(newElem, "doc should contain new-elem.");
+ newElem.remove();
+ ok(!doc.getElementById("new-elem"), "new-elem should be removed.");
+ });
+
+ // Ensure that the message manager has processed the previous task before
+ // going forward to prevent racing with it in non-e10s mode.
+ await SpecialPowers.spawn(browser, [], () => {});
+ browser.goForward();
+ await popstatePromise;
+}
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+
+ // Tests session restore functionality of history.pushState and
+ // history.replaceState(). (Bug 500328)
+
+ // We open a new blank window, let it load, and then load in
+ // http://example.com. We need to load the blank window first, otherwise the
+ // docshell gets confused and doesn't have a current history entry.
+ let state;
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ BrowserTestUtils.loadURIString(browser, "http://example.com");
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // After these push/replaceState calls, the window should have three
+ // history entries:
+ // testURL (state object: null) <-- oldest
+ // testURL (state object: {obj1:1})
+ // testURL?page2 (state object: {obj3:/^a$/}) <-- newest
+ function contentTest() {
+ let history = content.window.history;
+ history.pushState({ obj1: 1 }, "title-obj1");
+ history.pushState({ obj2: 2 }, "title-obj2", "?page2");
+ history.replaceState({ obj3: /^a$/ }, "title-obj3");
+ }
+ await SpecialPowers.spawn(browser, [], contentTest);
+ await TabStateFlusher.flush(browser);
+
+ state = ss.getTabState(gBrowser.getTabForBrowser(browser));
+ }
+ );
+
+ // Restore the state into a new tab. Things don't work well when we
+ // restore into the old tab, but that's not a real use case anyway.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank" },
+ async function (browser) {
+ let tab2 = gBrowser.getTabForBrowser(browser);
+
+ let tabRestoredPromise = promiseTabRestored(tab2);
+ ss.setTabState(tab2, state, true);
+
+ // Run checkState() once the tab finishes loading its restored state.
+ await tabRestoredPromise;
+ await checkState(browser);
+ }
+ );
+});
diff --git a/browser/components/sessionstore/test/browser_506482.js b/browser/components/sessionstore/test/browser_506482.js
new file mode 100644
index 0000000000..a8e628ff7d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_506482.js
@@ -0,0 +1,78 @@
+/* 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 */
+
+function test() {
+ /** Test for Bug 506482 **/
+
+ // test setup
+ waitForExplicitFinish();
+
+ // read the sessionstore.js mtime (picked from browser_248970_a.js)
+ let profilePath = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ function getSessionstoreFile() {
+ let sessionStoreJS = profilePath.clone();
+ sessionStoreJS.append("sessionstore.jsonlz4");
+ return sessionStoreJS;
+ }
+ function getSessionstorejsModificationTime() {
+ let file = getSessionstoreFile();
+ if (file.exists()) {
+ return file.lastModifiedTime;
+ }
+ return -1;
+ }
+
+ // delete existing sessionstore.js, to make sure we're not reading
+ // the mtime of an old one initially.
+ let sessionStoreJS = getSessionstoreFile();
+ if (sessionStoreJS.exists()) {
+ sessionStoreJS.remove(false);
+ }
+
+ // test content URL
+ const TEST_URL =
+ "data:text/html;charset=utf-8," +
+ "<body style='width: 100000px; height: 100000px;'><p>top</p></body>";
+
+ // preferences that we use
+ const PREF_INTERVAL = "browser.sessionstore.interval";
+
+ // make sure sessionstore.js is saved ASAP on all events
+ Services.prefs.setIntPref(PREF_INTERVAL, 0);
+
+ // get the initial sessionstore.js mtime (-1 if it doesn't exist yet)
+ let mtime0 = getSessionstorejsModificationTime();
+
+ // create and select a first tab
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
+ promiseBrowserLoaded(tab.linkedBrowser).then(() => {
+ // step1: the above has triggered some saveStateDelayed(), sleep until
+ // it's done, and get the initial sessionstore.js mtime
+ setTimeout(function step1() {
+ let mtime1 = getSessionstorejsModificationTime();
+ isnot(mtime1, mtime0, "initial sessionstore.js update");
+
+ // step2: test sessionstore.js is not updated on tab selection
+ // or content scrolling
+ gBrowser.selectedTab = tab;
+ tab.linkedBrowser.contentWindow.scrollTo(1100, 1200);
+ setTimeout(function step2() {
+ let mtime2 = getSessionstorejsModificationTime();
+ is(
+ mtime2,
+ mtime1,
+ "tab selection and scrolling: sessionstore.js not updated"
+ );
+
+ // ok, done, cleanup and finish
+ if (Services.prefs.prefHasUserValue(PREF_INTERVAL)) {
+ Services.prefs.clearUserPref(PREF_INTERVAL);
+ }
+ gBrowser.removeTab(tab);
+ finish();
+ }, 3500); // end of sleep after tab selection and scrolling
+ }, 3500); // end of sleep after initial saveStateDelayed()
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_514751.js b/browser/components/sessionstore/test/browser_514751.js
new file mode 100644
index 0000000000..96bf9b08c8
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_514751.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function test_malformedURI() {
+ /** Test for Bug 514751 (Wallpaper) **/
+
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ {
+ url: "about:mozilla",
+ triggeringPrincipal_base64,
+ title: "Mozilla",
+ },
+ {},
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ var theWin = openDialog(location, "", "chrome,all,dialog=no");
+ await promiseWindowLoaded(theWin);
+
+ var gotError = false;
+ try {
+ await setWindowState(theWin, state, true);
+ } catch (e) {
+ if (/NS_ERROR_MALFORMED_URI/.test(e)) {
+ gotError = true;
+ }
+ }
+
+ ok(!gotError, "Didn't get a malformed URI error.");
+ await BrowserTestUtils.closeWindow(theWin);
+});
diff --git a/browser/components/sessionstore/test/browser_522375.js b/browser/components/sessionstore/test/browser_522375.js
new file mode 100644
index 0000000000..ccc8f9dc64
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_522375.js
@@ -0,0 +1,22 @@
+function test() {
+ var startup_info = Services.startup.getStartupInfo();
+ // No .process info on mac
+
+ ok(
+ startup_info.process <= startup_info.main,
+ "process created before main is run " + uneval(startup_info)
+ );
+
+ // on linux firstPaint can happen after everything is loaded (especially with remote X)
+ if (startup_info.firstPaint) {
+ ok(
+ startup_info.main <= startup_info.firstPaint,
+ "main ran before first paint " + uneval(startup_info)
+ );
+ }
+
+ ok(
+ startup_info.main < startup_info.sessionRestored,
+ "Session restored after main " + uneval(startup_info)
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_522545.js b/browser/components/sessionstore/test/browser_522545.js
new file mode 100644
index 0000000000..5ebe3b9d5f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_522545.js
@@ -0,0 +1,443 @@
+/* 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 522545 **/
+
+ waitForExplicitFinish();
+ requestLongerTimeout(4);
+
+ // This tests the following use case:
+ // User opens a new tab which gets focus. The user types something into the
+ // address bar, then crashes or quits.
+ function test_newTabFocused() {
+ let state = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }] },
+ { entries: [], userTypedValue: "example.com", userTypedClear: 0 },
+ ],
+ selected: 2,
+ },
+ ],
+ };
+
+ waitForBrowserState(state, function () {
+ let browser = gBrowser.selectedBrowser;
+ is(
+ browser.currentURI.spec,
+ "about:blank",
+ "No history entries still sets currentURI to about:blank"
+ );
+ is(
+ browser.userTypedValue,
+ "example.com",
+ "userTypedValue was correctly restored"
+ );
+ ok(
+ !browser.didStartLoadSinceLastUserTyping(),
+ "We still know that no load is ongoing"
+ );
+ is(
+ gURLBar.value,
+ "example.com",
+ "Address bar's value correctly restored"
+ );
+
+ // Change tabs to make sure address bar value gets updated. If tab is
+ // lazy, wait for SSTabRestored to ensure address bar has time to update.
+ let tabToSelect = gBrowser.tabContainer.getItemAtIndex(0);
+ if (tabToSelect.linkedBrowser.isConnected) {
+ gBrowser.selectedTab = tabToSelect;
+ is(
+ gURLBar.value,
+ "about:mozilla",
+ "Address bar's value correctly updated"
+ );
+ runNextTest();
+ } else {
+ gBrowser.tabContainer.addEventListener(
+ "SSTabRestored",
+ function SSTabRestored(event) {
+ if (event.target == tabToSelect) {
+ gBrowser.tabContainer.removeEventListener(
+ "SSTabRestored",
+ SSTabRestored,
+ true
+ );
+ is(
+ gURLBar.value,
+ "about:mozilla",
+ "Address bar's value correctly updated"
+ );
+ runNextTest();
+ }
+ },
+ true
+ );
+ gBrowser.selectedTab = tabToSelect;
+ }
+ });
+ }
+
+ // This tests the following use case:
+ // User opens a new tab which gets focus. The user types something into the
+ // address bar, switches back to the first tab, then crashes or quits.
+ function test_newTabNotFocused() {
+ let state = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }] },
+ { entries: [], userTypedValue: "example.org", userTypedClear: 0 },
+ ],
+ selected: 1,
+ },
+ ],
+ };
+
+ waitForBrowserState(state, function () {
+ let browser = gBrowser.getBrowserAtIndex(1);
+ is(
+ browser.currentURI.spec,
+ "about:blank",
+ "No history entries still sets currentURI to about:blank"
+ );
+ is(
+ browser.userTypedValue,
+ "example.org",
+ "userTypedValue was correctly restored"
+ );
+ // didStartLoadSinceLastUserTyping does not exist on lazy tabs.
+ if (browser.didStartLoadSinceLastUserTyping) {
+ ok(
+ !browser.didStartLoadSinceLastUserTyping(),
+ "We still know that no load is ongoing"
+ );
+ }
+ is(
+ gURLBar.value,
+ "about:mozilla",
+ "Address bar's value correctly restored"
+ );
+
+ // Change tabs to make sure address bar value gets updated. If tab is
+ // lazy, wait for SSTabRestored to ensure address bar has time to update.
+ let tabToSelect = gBrowser.tabContainer.getItemAtIndex(1);
+ if (tabToSelect.linkedBrowser.isConnected) {
+ gBrowser.selectedTab = tabToSelect;
+ is(
+ gURLBar.value,
+ "example.org",
+ "Address bar's value correctly updated"
+ );
+ runNextTest();
+ } else {
+ gBrowser.tabContainer.addEventListener(
+ "SSTabRestored",
+ function SSTabRestored(event) {
+ if (event.target == tabToSelect) {
+ gBrowser.tabContainer.removeEventListener(
+ "SSTabRestored",
+ SSTabRestored,
+ true
+ );
+ is(
+ gURLBar.value,
+ "example.org",
+ "Address bar's value correctly updated"
+ );
+ runNextTest();
+ }
+ },
+ true
+ );
+ gBrowser.selectedTab = tabToSelect;
+ }
+ });
+ }
+
+ // This tests the following use case:
+ // User is in a tab with session history, then types something in the
+ // address bar, then crashes or quits.
+ function test_existingSHEnd_noClear() {
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "about:mozilla", triggeringPrincipal_base64 },
+ { url: "about:config", triggeringPrincipal_base64 },
+ ],
+ index: 2,
+ userTypedValue: "example.com",
+ userTypedClear: 0,
+ },
+ ],
+ },
+ ],
+ };
+
+ waitForBrowserState(state, function () {
+ let browser = gBrowser.selectedBrowser;
+ is(
+ browser.currentURI.spec,
+ "about:config",
+ "browser.currentURI set to current entry in SH"
+ );
+ is(
+ browser.userTypedValue,
+ "example.com",
+ "userTypedValue was correctly restored"
+ );
+ ok(
+ !browser.didStartLoadSinceLastUserTyping(),
+ "We still know that no load is ongoing"
+ );
+ is(
+ gURLBar.value,
+ "example.com",
+ "Address bar's value correctly restored to userTypedValue"
+ );
+ runNextTest();
+ });
+ }
+
+ // This tests the following use case:
+ // User is in a tab with session history, presses back at some point, then
+ // types something in the address bar, then crashes or quits.
+ function test_existingSHMiddle_noClear() {
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "about:mozilla", triggeringPrincipal_base64 },
+ { url: "about:config", triggeringPrincipal_base64 },
+ ],
+ index: 1,
+ userTypedValue: "example.org",
+ userTypedClear: 0,
+ },
+ ],
+ },
+ ],
+ };
+
+ waitForBrowserState(state, function () {
+ let browser = gBrowser.selectedBrowser;
+ is(
+ browser.currentURI.spec,
+ "about:mozilla",
+ "browser.currentURI set to current entry in SH"
+ );
+ is(
+ browser.userTypedValue,
+ "example.org",
+ "userTypedValue was correctly restored"
+ );
+ ok(
+ !browser.didStartLoadSinceLastUserTyping(),
+ "We still know that no load is ongoing"
+ );
+ is(
+ gURLBar.value,
+ "example.org",
+ "Address bar's value correctly restored to userTypedValue"
+ );
+ runNextTest();
+ });
+ }
+
+ // This test simulates lots of tabs opening at once and then quitting/crashing.
+ function test_getBrowserState_lotsOfTabsOpening() {
+ gBrowser.stop();
+
+ let uris = [];
+ for (let i = 0; i < 25; i++) {
+ uris.push("http://example.com/" + i);
+ }
+
+ // We're waiting for the first location change, which should indicate
+ // one of the tabs has loaded and the others haven't. So one should
+ // be in a non-userTypedValue case, while others should still have
+ // userTypedValue and userTypedClear set.
+ gBrowser.addTabsProgressListener({
+ onLocationChange(aBrowser) {
+ if (uris.indexOf(aBrowser.currentURI.spec) > -1) {
+ gBrowser.removeTabsProgressListener(this);
+ firstLocationChange();
+ }
+ },
+ });
+
+ function firstLocationChange() {
+ let state = JSON.parse(ss.getBrowserState());
+ let hasUTV = state.windows[0].tabs.some(function (aTab) {
+ return (
+ aTab.userTypedValue && aTab.userTypedClear && !aTab.entries.length
+ );
+ });
+
+ ok(
+ hasUTV,
+ "At least one tab has a userTypedValue with userTypedClear with no loaded URL"
+ );
+
+ BrowserTestUtils.waitForMessage(
+ gBrowser.selectedBrowser.messageManager,
+ "SessionStore:update"
+ ).then(firstLoad);
+ }
+
+ function firstLoad() {
+ let state = JSON.parse(ss.getTabState(gBrowser.selectedTab));
+ let hasSH = !("userTypedValue" in state) && state.entries[0].url;
+ ok(hasSH, "The selected tab has its entry in SH");
+
+ runNextTest();
+ }
+
+ gBrowser.loadTabs(uris, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ }
+
+ // This simulates setting a userTypedValue and ensures that just typing in the
+ // URL bar doesn't set userTypedClear as well.
+ function test_getBrowserState_userTypedValue() {
+ let state = {
+ windows: [
+ {
+ tabs: [{ entries: [] }],
+ },
+ ],
+ };
+
+ waitForBrowserState(state, function () {
+ let browser = gBrowser.selectedBrowser;
+ // Make sure this tab isn't loading and state is clear before we test.
+ is(browser.userTypedValue, null, "userTypedValue is empty to start");
+ ok(
+ !browser.didStartLoadSinceLastUserTyping(),
+ "Initially, no load should be ongoing"
+ );
+
+ let inputText = "example.org";
+ gURLBar.focus();
+ gURLBar.value = inputText.slice(0, -1);
+ EventUtils.sendString(inputText.slice(-1));
+
+ executeSoon(function () {
+ is(
+ browser.userTypedValue,
+ "example.org",
+ "userTypedValue was set when changing URLBar value"
+ );
+ ok(
+ !browser.didStartLoadSinceLastUserTyping(),
+ "No load started since changing URLBar value"
+ );
+
+ // Now make sure ss gets these values too
+ let newState = JSON.parse(ss.getBrowserState());
+ is(
+ newState.windows[0].tabs[0].userTypedValue,
+ "example.org",
+ "sessionstore got correct userTypedValue"
+ );
+ is(
+ newState.windows[0].tabs[0].userTypedClear,
+ 0,
+ "sessionstore got correct userTypedClear"
+ );
+ runNextTest();
+ });
+ });
+ }
+
+ // test_getBrowserState_lotsOfTabsOpening tested userTypedClear in a few cases,
+ // but not necessarily any that had legitimate URIs in the state of loading
+ // (eg, "http://example.com"), so this test will cover that case.
+ function test_userTypedClearLoadURI() {
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [],
+ userTypedValue: "http://example.com",
+ userTypedClear: 2,
+ },
+ ],
+ },
+ ],
+ };
+
+ waitForBrowserState(state, function () {
+ let browser = gBrowser.selectedBrowser;
+ is(
+ browser.currentURI.spec,
+ "http://example.com/",
+ "userTypedClear=2 caused userTypedValue to be loaded"
+ );
+ is(
+ browser.userTypedValue,
+ null,
+ "userTypedValue was null after loading a URI"
+ );
+ ok(
+ !browser.didStartLoadSinceLastUserTyping(),
+ "We should have reset the load state when the tab loaded"
+ );
+ is(
+ gURLBar.value,
+ BrowserUIUtils.trimURL("http://example.com/"),
+ "Address bar's value set after loading URI"
+ );
+ runNextTest();
+ });
+ }
+
+ let tests = [
+ test_newTabFocused,
+ test_newTabNotFocused,
+ test_existingSHEnd_noClear,
+ test_existingSHMiddle_noClear,
+ test_getBrowserState_lotsOfTabsOpening,
+ test_getBrowserState_userTypedValue,
+ test_userTypedClearLoadURI,
+ ];
+ let originalState = JSON.parse(ss.getBrowserState());
+ let state = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ ],
+ },
+ ],
+ };
+ function runNextTest() {
+ if (tests.length) {
+ waitForBrowserState(state, function () {
+ gBrowser.selectedBrowser.userTypedValue = null;
+ gURLBar.setURI();
+ tests.shift()();
+ });
+ } else {
+ waitForBrowserState(originalState, function () {
+ gBrowser.selectedBrowser.userTypedValue = null;
+ gURLBar.setURI();
+ finish();
+ });
+ }
+ }
+
+ // Run the tests!
+ runNextTest();
+}
diff --git a/browser/components/sessionstore/test/browser_524745.js b/browser/components/sessionstore/test/browser_524745.js
new file mode 100644
index 0000000000..dcd0f4ac16
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_524745.js
@@ -0,0 +1,55 @@
+/* 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 524745 **/
+
+ let uniqKey = "bug524745";
+ let uniqVal = Date.now().toString();
+
+ waitForExplicitFinish();
+
+ whenNewWindowLoaded({ private: false }, function (window_B) {
+ waitForFocus(function () {
+ // Add identifying information to window_B
+ ss.setCustomWindowValue(window_B, uniqKey, uniqVal);
+ let state = JSON.parse(ss.getBrowserState());
+ let selectedWindow = state.windows[state.selectedWindow - 1];
+ is(
+ selectedWindow.extData && selectedWindow.extData[uniqKey],
+ uniqVal,
+ "selectedWindow is window_B"
+ );
+
+ // Now minimize window_B. The selected window shouldn't have the secret data
+ window_B.minimize();
+ waitForFocus(async function () {
+ state = JSON.parse(ss.getBrowserState());
+ selectedWindow = state.windows[state.selectedWindow - 1];
+ ok(
+ !selectedWindow.extData || !selectedWindow.extData[uniqKey],
+ "selectedWindow is not window_B after minimizing it"
+ );
+
+ // Now minimize the last open window (assumes no other tests left windows open)
+ let promiseSizeModeChange = BrowserTestUtils.waitForEvent(
+ window,
+ "sizemodechange"
+ );
+ window.minimize();
+ await promiseSizeModeChange;
+ state = JSON.parse(ss.getBrowserState());
+ is(
+ state.selectedWindow,
+ 0,
+ "selectedWindow should be 0 when all windows are minimized"
+ );
+
+ // Cleanup
+ window.restore();
+ BrowserTestUtils.closeWindow(window_B).then(finish);
+ });
+ }, window_B);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_526613.js b/browser/components/sessionstore/test/browser_526613.js
new file mode 100644
index 0000000000..ba3f03ef32
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_526613.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ /** Test for Bug 526613 **/
+
+ // test setup
+ waitForExplicitFinish();
+
+ function browserWindowsCount(expected) {
+ let count = 0;
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ if (!win.closed) {
+ ++count;
+ }
+ }
+ is(
+ count,
+ expected,
+ "number of open browser windows according to nsIWindowMediator"
+ );
+ let state = ss.getBrowserState();
+ info(state);
+ is(
+ JSON.parse(state).windows.length,
+ expected,
+ "number of open browser windows according to getBrowserState"
+ );
+ }
+
+ browserWindowsCount(1);
+
+ // backup old state
+ let oldState = ss.getBrowserState();
+ // create a new state for testing
+ let testState = {
+ windows: [
+ { tabs: [{ entries: [{ url: "http://example.com/" }] }], selected: 1 },
+ { tabs: [{ entries: [{ url: "about:mozilla" }] }], selected: 1 },
+ ],
+ // make sure the first window is focused, otherwise when restoring the
+ // old state, the first window is closed and the test harness gets unloaded
+ selectedWindow: 1,
+ };
+
+ let pass = 1;
+ function observer(aSubject, aTopic, aData) {
+ is(
+ aTopic,
+ "sessionstore-browser-state-restored",
+ "The sessionstore-browser-state-restored notification was observed"
+ );
+
+ if (pass++ == 1) {
+ browserWindowsCount(2);
+
+ // let the first window be focused (see above)
+ function pollMostRecentWindow() {
+ if (Services.wm.getMostRecentWindow("navigator:browser") == window) {
+ ss.setBrowserState(oldState);
+ } else {
+ info("waiting for the current window to become active");
+ setTimeout(pollMostRecentWindow, 0);
+ window.focus(); // XXX Why is this needed?
+ }
+ }
+ pollMostRecentWindow();
+ } else {
+ browserWindowsCount(1);
+ ok(
+ !window.closed,
+ "Restoring the old state should have left this window open"
+ );
+ Services.obs.removeObserver(
+ observer,
+ "sessionstore-browser-state-restored"
+ );
+ finish();
+ }
+ }
+ Services.obs.addObserver(observer, "sessionstore-browser-state-restored");
+
+ // set browser to test state
+ ss.setBrowserState(JSON.stringify(testState));
+}
diff --git a/browser/components/sessionstore/test/browser_528776.js b/browser/components/sessionstore/test/browser_528776.js
new file mode 100644
index 0000000000..37b76a766d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_528776.js
@@ -0,0 +1,27 @@
+function browserWindowsCount(expected) {
+ var count = 0;
+ for (let win of Services.wm.getEnumerator("navigator:browser")) {
+ if (!win.closed) {
+ ++count;
+ }
+ }
+ is(
+ count,
+ expected,
+ "number of open browser windows according to nsIWindowMediator"
+ );
+ is(
+ JSON.parse(ss.getBrowserState()).windows.length,
+ expected,
+ "number of open browser windows according to getBrowserState"
+ );
+}
+
+add_task(async function () {
+ browserWindowsCount(1);
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ browserWindowsCount(2);
+ await BrowserTestUtils.closeWindow(win);
+ browserWindowsCount(1);
+});
diff --git a/browser/components/sessionstore/test/browser_579868.js b/browser/components/sessionstore/test/browser_579868.js
new file mode 100644
index 0000000000..b54aa89cdb
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_579868.js
@@ -0,0 +1,31 @@
+/* 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() {
+ waitForExplicitFinish();
+
+ let tab1 = BrowserTestUtils.addTab(gBrowser, "about:rights");
+ let tab2 = BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+
+ promiseBrowserLoaded(tab1.linkedBrowser).then(() => {
+ // Tell the session storer that the tab is pinned
+ let newTabState =
+ '{"entries":[{"url":"about:rights"}],"pinned":true,"userTypedValue":"Hello World!"}';
+ ss.setTabState(tab1, newTabState);
+
+ // Undo pinning
+ gBrowser.unpinTab(tab1);
+
+ // Close and restore tab
+ gBrowser.removeTab(tab1);
+ let savedState = ss.getClosedTabDataForWindow(window)[0].state;
+ isnot(savedState.pinned, true, "Pinned should not be true");
+ tab1 = ss.undoCloseTab(window, 0);
+
+ isnot(tab1.pinned, true, "Should not be pinned");
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ finish();
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_579879.js b/browser/components/sessionstore/test/browser_579879.js
new file mode 100644
index 0000000000..4def22bb01
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_579879.js
@@ -0,0 +1,31 @@
+"use strict";
+
+add_task(async function () {
+ let tab1 = BrowserTestUtils.addTab(
+ gBrowser,
+ "data:text/plain;charset=utf-8,foo"
+ );
+ gBrowser.pinTab(tab1);
+
+ await promiseBrowserLoaded(tab1.linkedBrowser);
+ let tab2 = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.pinTab(tab2);
+
+ is(
+ Array.prototype.indexOf.call(gBrowser.tabs, tab1),
+ 0,
+ "pinned tab 1 is at the first position"
+ );
+ await promiseRemoveTabAndSessionState(tab1);
+
+ tab1 = undoCloseTab();
+ ok(tab1.pinned, "pinned tab 1 has been restored as a pinned tab");
+ is(
+ Array.prototype.indexOf.call(gBrowser.tabs, tab1),
+ 0,
+ "pinned tab 1 has been restored to the first position"
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/sessionstore/test/browser_580512.js b/browser/components/sessionstore/test/browser_580512.js
new file mode 100644
index 0000000000..1dfd696277
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_580512.js
@@ -0,0 +1,117 @@
+/* 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 URIS_PINNED = ["about:license", "about:about"];
+const URIS_NORMAL_A = ["about:mozilla"];
+const URIS_NORMAL_B = ["about:buildconfig"];
+
+function test() {
+ waitForExplicitFinish();
+
+ isnot(
+ Services.prefs.getIntPref("browser.startup.page"),
+ 3,
+ "pref to save session must not be set for this test"
+ );
+ ok(
+ !Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"),
+ "pref to save session once must not be set for this test"
+ );
+
+ document.documentElement.setAttribute(
+ "windowtype",
+ "navigator:browsertestdummy"
+ );
+
+ openWinWithCb(closeFirstWin, URIS_PINNED.concat(URIS_NORMAL_A));
+}
+
+function closeFirstWin(win) {
+ win.gBrowser.pinTab(win.gBrowser.tabs[0]);
+ win.gBrowser.pinTab(win.gBrowser.tabs[1]);
+
+ let winClosed = BrowserTestUtils.windowClosed(win);
+ // We need to call BrowserTryToCloseWindow in order to trigger
+ // the machinery that chooses whether or not to save the session
+ // for the last window.
+ win.BrowserTryToCloseWindow();
+ ok(win.closed, "window closed");
+
+ winClosed.then(() => {
+ openWinWithCb(
+ checkSecondWin,
+ URIS_NORMAL_B,
+ URIS_PINNED.concat(URIS_NORMAL_B)
+ );
+ });
+}
+
+function checkSecondWin(win) {
+ is(
+ win.gBrowser.browsers[0].currentURI.spec,
+ URIS_PINNED[0],
+ "first pinned tab restored"
+ );
+ is(
+ win.gBrowser.browsers[1].currentURI.spec,
+ URIS_PINNED[1],
+ "second pinned tab restored"
+ );
+ ok(win.gBrowser.tabs[0].pinned, "first pinned tab is still pinned");
+ ok(win.gBrowser.tabs[1].pinned, "second pinned tab is still pinned");
+
+ BrowserTestUtils.closeWindow(win).then(() => {
+ // cleanup
+ document.documentElement.setAttribute("windowtype", "navigator:browser");
+ finish();
+ });
+}
+
+function openWinWithCb(cb, argURIs, expectedURIs) {
+ if (!expectedURIs) {
+ expectedURIs = argURIs;
+ }
+
+ var win = openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,all,dialog=no",
+ argURIs.join("|")
+ );
+
+ win.addEventListener(
+ "load",
+ function () {
+ info("the window loaded");
+
+ var expectedLoads = expectedURIs.length;
+
+ win.gBrowser.addTabsProgressListener({
+ onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (
+ aRequest &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+ expectedURIs.indexOf(
+ aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec
+ ) > -1 &&
+ --expectedLoads <= 0
+ ) {
+ win.gBrowser.removeTabsProgressListener(this);
+ info("all tabs loaded");
+ is(
+ win.gBrowser.tabs.length,
+ expectedURIs.length,
+ "didn't load any unexpected tabs"
+ );
+ executeSoon(function () {
+ cb(win);
+ });
+ }
+ },
+ });
+ },
+ { once: true }
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_581937.js b/browser/components/sessionstore/test/browser_581937.js
new file mode 100644
index 0000000000..66a2159ff1
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_581937.js
@@ -0,0 +1,22 @@
+// Tests that an about:blank tab with no history will not be saved into
+// session store and thus, it will not show up in Recently Closed Tabs.
+
+"use strict";
+
+add_task(async function () {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ await promiseBrowserLoaded(tab.linkedBrowser);
+
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ "about:blank",
+ "we will be removing an about:blank tab"
+ );
+
+ let r = `rand-${Math.random()}`;
+ ss.setCustomTabValue(tab, "foobar", r);
+
+ await promiseRemoveTabAndSessionState(tab);
+ let closedTabData = ss.getClosedTabDataForWindow(window);
+ ok(!closedTabData.includes(r), "tab not stored in _closedTabs");
+});
diff --git a/browser/components/sessionstore/test/browser_586068-apptabs.js b/browser/components/sessionstore/test/browser_586068-apptabs.js
new file mode 100644
index 0000000000..b2f92f760c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-apptabs.js
@@ -0,0 +1,109 @@
+/* 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/. */
+
+requestLongerTimeout(2);
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org/#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ pinned: true,
+ },
+ {
+ entries: [
+ { url: "http://example.org/#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ pinned: true,
+ },
+ {
+ entries: [
+ { url: "http://example.org/#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ pinned: true,
+ },
+ {
+ entries: [
+ { url: "http://example.org/#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#5", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#6", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#7", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 5,
+ },
+ ],
+ };
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (
+ aBrowser,
+ aNeedRestore,
+ aRestoring,
+ aRestored
+ ) {
+ loadCount++;
+
+ // We'll make sure that the loads we get come from pinned tabs or the
+ // the selected tab.
+
+ // get the tab
+ let tab;
+ for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+ if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser) {
+ tab = window.gBrowser.tabs[i];
+ }
+ }
+
+ ok(tab.pinned || tab.selected, "load came from pinned or selected tab");
+
+ // We should get 4 loads: 3 app tabs + 1 normal selected tab
+ if (loadCount < 4) {
+ return;
+ }
+
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ await promiseRestoringTabs;
+
+ // Cleanup.
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js b/browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js
new file mode 100644
index 0000000000..928f2512c6
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js
@@ -0,0 +1,105 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+const PREF_RESTORE_PINNED_TABS_ON_DEMAND =
+ "browser.sessionstore.restore_pinned_tabs_on_demand";
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+ Services.prefs.setBoolPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND, true);
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ Services.prefs.clearUserPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND);
+ });
+
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org/#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ pinned: true,
+ },
+ {
+ entries: [
+ { url: "http://example.org/#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ pinned: true,
+ },
+ {
+ entries: [
+ { url: "http://example.org/#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ pinned: true,
+ },
+ {
+ entries: [
+ { url: "http://example.org/#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#5", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#6", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#7", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 5,
+ },
+ ],
+ };
+
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (
+ aBrowser,
+ aNeedRestore,
+ aRestoring,
+ aRestored
+ ) {
+ // get the tab
+ let tab;
+ for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+ if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser) {
+ tab = window.gBrowser.tabs[i];
+ }
+ }
+
+ // Check that the load only comes from the selected tab.
+ ok(tab.selected, "load came from selected tab");
+ is(aNeedRestore, 6, "six tabs left to restore");
+ is(aRestoring, 1, "one tab is restoring");
+ is(aRestored, 0, "no tabs have been restored, yet");
+
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ await promiseRestoringTabs;
+
+ // Cleanup.
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
new file mode 100644
index 0000000000..b729555ff1
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
@@ -0,0 +1,212 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+requestLongerTimeout(2);
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ // The first state will be loaded using setBrowserState, followed by the 2nd
+ // state also being loaded using setBrowserState, interrupting the first restore.
+ let state1 = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 1,
+ },
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 3,
+ },
+ ],
+ };
+ let state2 = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org#5", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#6", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#7", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#8", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 3,
+ },
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#5", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#6", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#7", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#8", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 1,
+ },
+ ],
+ };
+
+ // interruptedAfter will be set after the selected tab from each window have loaded.
+ let interruptedAfter = 0;
+ let loadedWindow1 = false;
+ let loadedWindow2 = false;
+ let numTabs = state2.windows[0].tabs.length + state2.windows[1].tabs.length;
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (
+ aBrowser,
+ aNeedRestore,
+ aRestoring,
+ aRestored
+ ) {
+ loadCount++;
+
+ if (
+ aBrowser.currentURI.spec == state1.windows[0].tabs[2].entries[0].url
+ ) {
+ loadedWindow1 = true;
+ }
+ if (
+ aBrowser.currentURI.spec == state1.windows[1].tabs[0].entries[0].url
+ ) {
+ loadedWindow2 = true;
+ }
+
+ if (!interruptedAfter && loadedWindow1 && loadedWindow2) {
+ interruptedAfter = loadCount;
+ ss.setBrowserState(JSON.stringify(state2));
+ return;
+ }
+
+ if (loadCount < numTabs + interruptedAfter) {
+ return;
+ }
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs + interruptedAfter, "all tabs were restored");
+ is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+ // Remove the progress listener.
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ // We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened
+ Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let win = aSubject;
+ win.addEventListener(
+ "load",
+ function () {
+ Services.ww.unregisterNotification(observer);
+ win.gBrowser.addTabsProgressListener(gProgressListener);
+ },
+ { once: true }
+ );
+ }
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state1));
+ await promiseRestoringTabs;
+
+ // Cleanup.
+ await promiseAllButPrimaryWindowClosed();
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-cascade.js b/browser/components/sessionstore/test/browser_586068-cascade.js
new file mode 100644
index 0000000000..5f64ae13d6
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-cascade.js
@@ -0,0 +1,107 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ },
+ ],
+ };
+
+ let expectedCounts = [
+ [3, 3, 0],
+ [2, 3, 1],
+ [1, 3, 2],
+ [0, 3, 3],
+ [0, 2, 4],
+ [0, 1, 5],
+ ];
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (
+ aBrowser,
+ aNeedRestore,
+ aRestoring,
+ aRestored
+ ) {
+ loadCount++;
+ let expected = expectedCounts[loadCount - 1];
+
+ is(
+ aNeedRestore,
+ expected[0],
+ "load " + loadCount + " - # tabs that need to be restored"
+ );
+ is(
+ aRestoring,
+ expected[1],
+ "load " + loadCount + " - # tabs that are restoring"
+ );
+ is(
+ aRestored,
+ expected[2],
+ "load " + loadCount + " - # tabs that has been restored"
+ );
+
+ if (loadCount == state.windows[0].tabs.length) {
+ gProgressListener.unsetCallback();
+ resolve();
+ }
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ await promiseRestoringTabs;
+
+ // Cleanup.
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-multi_window.js b/browser/components/sessionstore/test/browser_586068-multi_window.js
new file mode 100644
index 0000000000..bf5d839812
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-multi_window.js
@@ -0,0 +1,115 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ // The first window will be put into the already open window and the second
+ // window will be opened with _openWindowWithState, which is the source of the problem.
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org#0", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 1,
+ },
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#5", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#6", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 4,
+ },
+ ],
+ };
+ let numTabs = state.windows[0].tabs.length + state.windows[1].tabs.length;
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (
+ aBrowser,
+ aNeedRestore,
+ aRestoring,
+ aRestored
+ ) {
+ if (++loadCount == numTabs) {
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs, "all tabs were restored");
+ is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+ gProgressListener.unsetCallback();
+ resolve();
+ }
+ });
+ });
+
+ // We also want to catch the 2nd window, so we need to observe domwindowopened
+ Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let win = aSubject;
+ win.addEventListener(
+ "load",
+ function () {
+ Services.ww.unregisterNotification(observer);
+ win.gBrowser.addTabsProgressListener(gProgressListener);
+ },
+ { once: true }
+ );
+ }
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ await promiseRestoringTabs;
+
+ // Cleanup.
+ await promiseAllButPrimaryWindowClosed();
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-reload.js b/browser/components/sessionstore/test/browser_586068-reload.js
new file mode 100644
index 0000000000..b0b368824d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-reload.js
@@ -0,0 +1,118 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org/#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#5", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#6", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#7", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#8", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org/#9", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 1,
+ },
+ ],
+ };
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gBrowser.tabContainer.addEventListener(
+ "SSTabRestored",
+ function onRestored(event) {
+ let tab = event.target;
+ let browser = tab.linkedBrowser;
+ let tabData = state.windows[0].tabs[loadCount++];
+
+ // double check that this tab was the right one
+ is(
+ browser.currentURI.spec,
+ tabData.entries[0].url,
+ "load " + loadCount + " - browser loaded correct url"
+ );
+ is(
+ ss.getCustomTabValue(tab, "uniq"),
+ tabData.extData.uniq,
+ "load " + loadCount + " - correct tab was restored"
+ );
+
+ if (loadCount == state.windows[0].tabs.length) {
+ gBrowser.tabContainer.removeEventListener(
+ "SSTabRestored",
+ onRestored
+ );
+ resolve();
+ } else {
+ // reload the next tab
+ gBrowser.browsers[loadCount].reload();
+ }
+ }
+ );
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ await promiseRestoringTabs;
+
+ // Cleanup.
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-select.js b/browser/components/sessionstore/test/browser_586068-select.js
new file mode 100644
index 0000000000..57dfa12d28
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-select.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 1,
+ },
+ ],
+ };
+
+ let expectedCounts = [
+ [5, 1, 0],
+ [4, 1, 1],
+ [3, 1, 2],
+ [2, 1, 3],
+ [1, 1, 4],
+ [0, 1, 5],
+ ];
+ let tabOrder = [0, 5, 1, 4, 3, 2];
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (
+ aBrowser,
+ aNeedRestore,
+ aRestoring,
+ aRestored
+ ) {
+ loadCount++;
+ let expected = expectedCounts[loadCount - 1];
+
+ is(
+ aNeedRestore,
+ expected[0],
+ "load " + loadCount + " - # tabs that need to be restored"
+ );
+ is(
+ aRestoring,
+ expected[1],
+ "load " + loadCount + " - # tabs that are restoring"
+ );
+ is(
+ aRestored,
+ expected[2],
+ "load " + loadCount + " - # tabs that has been restored"
+ );
+
+ if (loadCount < state.windows[0].tabs.length) {
+ // double check that this tab was the right one
+ let expectedData =
+ state.windows[0].tabs[tabOrder[loadCount - 1]].extData.uniq;
+ let tab;
+ for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+ if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser) {
+ tab = window.gBrowser.tabs[i];
+ }
+ }
+
+ is(
+ ss.getCustomTabValue(tab, "uniq"),
+ expectedData,
+ "load " + loadCount + " - correct tab was restored"
+ );
+
+ // select the next tab
+ window.gBrowser.selectTabAtIndex(tabOrder[loadCount]);
+ } else {
+ gProgressListener.unsetCallback();
+ resolve();
+ }
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setBrowserState(JSON.stringify(state));
+ await promiseRestoringTabs;
+
+ // Cleanup.
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-window_state.js b/browser/components/sessionstore/test/browser_586068-window_state.js
new file mode 100644
index 0000000000..69c3742a66
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-window_state.js
@@ -0,0 +1,120 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ // We'll use 2 states so that we can make sure calling setWindowState doesn't
+ // wipe out currently restoring data.
+ let state1 = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#3", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#4", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#5", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ let state2 = {
+ 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 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.org#5", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ let numTabs = state1.windows[0].tabs.length + state2.windows[0].tabs.length;
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (
+ aBrowser,
+ aNeedRestore,
+ aRestoring,
+ aRestored
+ ) {
+ // When loadCount == 2, we'll also restore state2 into the window
+ if (++loadCount == 2) {
+ ss.setWindowState(window, JSON.stringify(state2), false);
+ }
+
+ if (loadCount < numTabs) {
+ return;
+ }
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(
+ loadCount,
+ numTabs,
+ "test_setWindowStateNoOverwrite: all tabs were restored"
+ );
+ is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setWindowState(window, JSON.stringify(state1), true);
+ await promiseRestoringTabs;
+
+ // Cleanup.
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586068-window_state_override.js b/browser/components/sessionstore/test/browser_586068-window_state_override.js
new file mode 100644
index 0000000000..8a6eac6de2
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-window_state_override.js
@@ -0,0 +1,118 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+ });
+
+ // We'll use 2 states so that we can make sure calling setWindowState doesn't
+ // wipe out currently restoring data.
+ let state1 = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#3", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#4", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#5", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ let state2 = {
+ 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 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.org#5", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ let numTabs = 2 + state2.windows[0].tabs.length;
+
+ let loadCount = 0;
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (
+ aBrowser,
+ aNeedRestore,
+ aRestoring,
+ aRestored
+ ) {
+ // When loadCount == 2, we'll also restore state2 into the window
+ if (++loadCount == 2) {
+ executeSoon(() =>
+ ss.setWindowState(window, JSON.stringify(state2), true)
+ );
+ }
+
+ if (loadCount < numTabs) {
+ return;
+ }
+
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs, "all tabs were restored");
+ is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+ gProgressListener.unsetCallback();
+ resolve();
+ });
+ });
+
+ let backupState = ss.getBrowserState();
+ ss.setWindowState(window, JSON.stringify(state1), true);
+ await promiseRestoringTabs;
+
+ // Cleanup.
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_586147.js b/browser/components/sessionstore/test/browser_586147.js
new file mode 100644
index 0000000000..aff6c4ce06
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586147.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/. */
+
+function observeOneRestore(callback) {
+ let topic = "sessionstore-browser-state-restored";
+ Services.obs.addObserver(function onRestore() {
+ Services.obs.removeObserver(onRestore, topic);
+ callback();
+ }, topic);
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ // There should be one tab when we start the test
+ let [origTab] = gBrowser.visibleTabs;
+ let hiddenTab = BrowserTestUtils.addTab(gBrowser);
+
+ is(gBrowser.visibleTabs.length, 2, "should have 2 tabs before hiding");
+ gBrowser.showOnlyTheseTabs([origTab]);
+ is(gBrowser.visibleTabs.length, 1, "only 1 after hiding");
+ ok(hiddenTab.hidden, "sanity check that it's hidden");
+
+ BrowserTestUtils.addTab(gBrowser);
+ let state = ss.getBrowserState();
+ let stateObj = JSON.parse(state);
+ let tabs = stateObj.windows[0].tabs;
+ is(tabs.length, 3, "just checking that browser state is correct");
+ ok(!tabs[0].hidden, "first tab is visible");
+ ok(tabs[1].hidden, "second is hidden");
+ ok(!tabs[2].hidden, "third is visible");
+
+ // Make the third tab hidden and then restore the modified state object
+ tabs[2].hidden = true;
+
+ observeOneRestore(function () {
+ is(gBrowser.visibleTabs.length, 1, "only restored 1 visible tab");
+ let restoredTabs = gBrowser.tabs;
+
+ ok(!restoredTabs[0].hidden, "first is still visible");
+ ok(restoredTabs[1].hidden, "second tab is still hidden");
+ ok(restoredTabs[2].hidden, "third tab is now hidden");
+
+ // Restore the original state and clean up now that we're done
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ gBrowser.removeTab(gBrowser.tabs[1]);
+
+ finish();
+ });
+ ss.setBrowserState(JSON.stringify(stateObj));
+}
diff --git a/browser/components/sessionstore/test/browser_588426.js b/browser/components/sessionstore/test/browser_588426.js
new file mode 100644
index 0000000000..e32867fc6b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_588426.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
+ hidden: true,
+ },
+ {
+ entries: [{ url: "about:rights", triggeringPrincipal_base64 }],
+ hidden: true,
+ },
+ ],
+ },
+ ],
+ };
+
+ waitForExplicitFinish();
+
+ newWindowWithState(state, function (win) {
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ is(win.gBrowser.tabs.length, 2, "two tabs were restored");
+ is(win.gBrowser.visibleTabs.length, 1, "one tab is visible");
+
+ let tab = win.gBrowser.visibleTabs[0];
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ "about:mozilla",
+ "visible tab is about:mozilla"
+ );
+
+ finish();
+ });
+}
+
+function newWindowWithState(state, callback) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(AppConstants.BROWSER_CHROME_URL, "_blank", opts);
+
+ win.addEventListener(
+ "load",
+ function () {
+ executeSoon(function () {
+ win.addEventListener(
+ "SSWindowStateReady",
+ function () {
+ promiseTabRestored(win.gBrowser.tabs[0]).then(() => callback(win));
+ },
+ { once: true }
+ );
+
+ ss.setWindowState(win, JSON.stringify(state), true);
+ });
+ },
+ { once: true }
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_589246.js b/browser/components/sessionstore/test/browser_589246.js
new file mode 100644
index 0000000000..2fd92b2b82
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_589246.js
@@ -0,0 +1,286 @@
+/* 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/. */
+
+// Mirrors WINDOW_ATTRIBUTES IN SessionStore.sys.mjs
+const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
+
+var stateBackup = ss.getBrowserState();
+
+var originalWarnOnClose = Services.prefs.getBoolPref(
+ "browser.tabs.warnOnClose"
+);
+var originalStartupPage = Services.prefs.getIntPref("browser.startup.page");
+var originalWindowType = document.documentElement.getAttribute("windowtype");
+
+var gotLastWindowClosedTopic = false;
+var shouldPinTab = false;
+var shouldOpenTabs = false;
+var shouldCloseTab = false;
+var testNum = 0;
+var afterTestCallback;
+
+// Set state so we know the closed windows content
+var testState = {
+ windows: [{ tabs: [{ entries: [{ url: "http://example.org" }] }] }],
+ _closedWindows: [],
+};
+
+// We'll push a set of conditions and callbacks into this array
+// Ideally we would also test win/linux under a complete set of conditions, but
+// the tests for osx mirror the other set of conditions possible on win/linux.
+var tests = [];
+
+// the third & fourth test share a condition check, keep it DRY
+function checkOSX34Generator(num) {
+ return function (aPreviousState, aCurState) {
+ // In here, we should have restored the pinned tab, so only the unpinned tab
+ // should be in aCurState. So let's shape our expectations.
+ let expectedState = JSON.parse(aPreviousState);
+ expectedState[0].tabs.shift();
+ // size attributes are stripped out in _prepDataForDeferredRestore in SessionStore.sys.mjs.
+ // This isn't the best approach, but neither is comparing JSON strings
+ WINDOW_ATTRIBUTES.forEach(attr => delete expectedState[0][attr]);
+
+ is(
+ aCurState,
+ JSON.stringify(expectedState),
+ "test #" + num + ": closedWindowState is as expected"
+ );
+ };
+}
+function checkNoWindowsGenerator(num) {
+ return function (aPreviousState, aCurState) {
+ is(
+ aCurState,
+ "[]",
+ "test #" + num + ": there should be no closedWindowsLeft"
+ );
+ };
+}
+
+// The first test has 0 pinned tabs and 1 unpinned tab
+tests.push({
+ pinned: false,
+ extra: false,
+ close: false,
+ checkWinLin: checkNoWindowsGenerator(1),
+ checkOSX(aPreviousState, aCurState) {
+ is(aCurState, aPreviousState, "test #1: closed window state is unchanged");
+ },
+});
+
+// The second test has 1 pinned tab and 0 unpinned tabs.
+tests.push({
+ pinned: true,
+ extra: false,
+ close: false,
+ checkWinLin: checkNoWindowsGenerator(2),
+ checkOSX: checkNoWindowsGenerator(2),
+});
+
+// The third test has 1 pinned tab and 2 unpinned tabs.
+tests.push({
+ pinned: true,
+ extra: true,
+ close: false,
+ checkWinLin: checkNoWindowsGenerator(3),
+ checkOSX: checkOSX34Generator(3),
+});
+
+// The fourth test has 1 pinned tab, 2 unpinned tabs, and closes one unpinned tab.
+tests.push({
+ pinned: true,
+ extra: true,
+ close: "one",
+ checkWinLin: checkNoWindowsGenerator(4),
+ checkOSX: checkOSX34Generator(4),
+});
+
+// The fifth test has 1 pinned tab, 2 unpinned tabs, and closes both unpinned tabs.
+tests.push({
+ pinned: true,
+ extra: true,
+ close: "both",
+ checkWinLin: checkNoWindowsGenerator(5),
+ checkOSX: checkNoWindowsGenerator(5),
+});
+
+function test() {
+ /** Test for Bug 589246 - Closed window state getting corrupted when closing
+ and reopening last browser window without exiting browser **/
+ waitForExplicitFinish();
+ // windows opening & closing, so extending the timeout
+ requestLongerTimeout(2);
+
+ // We don't want the quit dialog pref
+ Services.prefs.setBoolPref("browser.tabs.warnOnClose", false);
+ // Ensure that we would restore the session (important for Windows)
+ Services.prefs.setIntPref("browser.startup.page", 3);
+
+ runNextTestOrFinish();
+}
+
+function runNextTestOrFinish() {
+ if (tests.length) {
+ setupForTest(tests.shift());
+ } else {
+ // some state is cleaned up at the end of each test, but not all
+ ["browser.tabs.warnOnClose", "browser.startup.page"].forEach(function (p) {
+ if (Services.prefs.prefHasUserValue(p)) {
+ Services.prefs.clearUserPref(p);
+ }
+ });
+
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+ }
+}
+
+function setupForTest(aConditions) {
+ // reset some checks
+ gotLastWindowClosedTopic = false;
+ shouldPinTab = aConditions.pinned;
+ shouldOpenTabs = aConditions.extra;
+ shouldCloseTab = aConditions.close;
+ testNum++;
+
+ // set our test callback
+ afterTestCallback = /Mac/.test(navigator.platform)
+ ? aConditions.checkOSX
+ : aConditions.checkWinLin;
+
+ // Add observers
+ Services.obs.addObserver(
+ onLastWindowClosed,
+ "browser-lastwindow-close-granted"
+ );
+
+ // Set the state
+ Services.obs.addObserver(
+ onStateRestored,
+ "sessionstore-browser-state-restored"
+ );
+ ss.setBrowserState(JSON.stringify(testState));
+}
+
+function onStateRestored(aSubject, aTopic, aData) {
+ info("test #" + testNum + ": onStateRestored");
+ Services.obs.removeObserver(
+ onStateRestored,
+ "sessionstore-browser-state-restored"
+ );
+
+ // change this window's windowtype so that closing a new window will trigger
+ // browser-lastwindow-close-granted.
+ document.documentElement.setAttribute("windowtype", "navigator:testrunner");
+
+ let newWin = openDialog(
+ location,
+ "_blank",
+ "chrome,all,dialog=no",
+ "http://example.com"
+ );
+ newWin.addEventListener(
+ "load",
+ function (aEvent) {
+ promiseBrowserLoaded(newWin.gBrowser.selectedBrowser).then(() => {
+ // pin this tab
+ if (shouldPinTab) {
+ newWin.gBrowser.pinTab(newWin.gBrowser.selectedTab);
+ }
+
+ newWin.addEventListener(
+ "unload",
+ function () {
+ onWindowUnloaded();
+ },
+ { once: true }
+ );
+ // Open a new tab as well. On Windows/Linux this will be restored when the
+ // new window is opened below (in onWindowUnloaded). On OS X we'll just
+ // restore the pinned tabs, leaving the unpinned tab in the closedWindowsData.
+ if (shouldOpenTabs) {
+ let newTab = BrowserTestUtils.addTab(newWin.gBrowser, "about:config");
+ let newTab2 = BrowserTestUtils.addTab(
+ newWin.gBrowser,
+ "about:buildconfig"
+ );
+
+ newTab.linkedBrowser.addEventListener(
+ "load",
+ function () {
+ if (shouldCloseTab == "one") {
+ newWin.gBrowser.removeTab(newTab2);
+ } else if (shouldCloseTab == "both") {
+ newWin.gBrowser.removeTab(newTab);
+ newWin.gBrowser.removeTab(newTab2);
+ }
+ newWin.BrowserTryToCloseWindow();
+ },
+ { capture: true, once: true }
+ );
+ } else {
+ newWin.BrowserTryToCloseWindow();
+ }
+ });
+ },
+ { once: true }
+ );
+}
+
+// This will be called before the window is actually closed
+function onLastWindowClosed(aSubject, aTopic, aData) {
+ info("test #" + testNum + ": onLastWindowClosed");
+ Services.obs.removeObserver(
+ onLastWindowClosed,
+ "browser-lastwindow-close-granted"
+ );
+ gotLastWindowClosedTopic = true;
+}
+
+// This is the unload event listener on the new window (from onStateRestored).
+// Unload is fired after the window is closed, so sessionstore has already
+// updated _closedWindows (which is important). We'll open a new window here
+// which should actually trigger the bug.
+function onWindowUnloaded() {
+ info("test #" + testNum + ": onWindowClosed");
+ ok(
+ gotLastWindowClosedTopic,
+ "test #" + testNum + ": browser-lastwindow-close-granted was notified prior"
+ );
+
+ let previousClosedWindowData = ss.getClosedWindowData();
+
+ // Now we want to open a new window
+ let newWin = openDialog(
+ location,
+ "_blank",
+ "chrome,all,dialog=no",
+ "about:mozilla"
+ );
+ newWin.addEventListener(
+ "load",
+ function (aEvent) {
+ newWin.gBrowser.selectedBrowser.addEventListener(
+ "load",
+ function () {
+ // Good enough for checking the state
+ afterTestCallback(previousClosedWindowData, ss.getClosedWindowData());
+ afterTestCleanup(newWin);
+ },
+ { capture: true, once: true }
+ );
+ },
+ { once: true }
+ );
+}
+
+function afterTestCleanup(aNewWin) {
+ executeSoon(function () {
+ BrowserTestUtils.closeWindow(aNewWin).then(() => {
+ document.documentElement.setAttribute("windowtype", originalWindowType);
+ runNextTestOrFinish();
+ });
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_590268.js b/browser/components/sessionstore/test/browser_590268.js
new file mode 100644
index 0000000000..f3f4a58dfc
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_590268.js
@@ -0,0 +1,155 @@
+/* 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 NUM_TABS = 12;
+
+var stateBackup = ss.getBrowserState();
+
+function test() {
+ /** Test for Bug 590268 - Provide access to sessionstore tab data sooner **/
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ // wasLoaded will be used to keep track of tabs that have already had SSTabRestoring
+ // fired for them.
+ let wasLoaded = {};
+ let restoringTabsCount = 0;
+ let restoredTabsCount = 0;
+ let uniq2 = {};
+ let uniq2Count = 0;
+ let state = { windows: [{ tabs: [] }] };
+ // We're going to put a bunch of tabs into this state
+ for (let i = 0; i < NUM_TABS; i++) {
+ let uniq = r();
+ let tabData = {
+ entries: [
+ { url: "http://example.com/#" + i, triggeringPrincipal_base64 },
+ ],
+ extData: { uniq, baz: "qux" },
+ };
+ state.windows[0].tabs.push(tabData);
+ wasLoaded[uniq] = false;
+ }
+
+ function onSSTabRestoring(aEvent) {
+ restoringTabsCount++;
+ let uniq = ss.getCustomTabValue(aEvent.originalTarget, "uniq");
+ wasLoaded[uniq] = true;
+
+ is(
+ ss.getCustomTabValue(aEvent.originalTarget, "foo"),
+ "",
+ "There is no value for 'foo'"
+ );
+
+ // On the first SSTabRestoring we're going to run the the real test.
+ // We'll keep this listener around so we can keep marking tabs as restored.
+ if (restoringTabsCount == 1) {
+ onFirstSSTabRestoring();
+ } else if (restoringTabsCount == NUM_TABS) {
+ onLastSSTabRestoring();
+ }
+ }
+
+ function onSSTabRestored(aEvent) {
+ if (++restoredTabsCount < NUM_TABS) {
+ return;
+ }
+ cleanup();
+ }
+
+ function onTabOpen(aEvent) {
+ // To test bug 614708, we'll just set a value on the tab here. This value
+ // would previously cause us to not recognize the values in extData until
+ // much later. So testing "uniq" failed.
+ ss.setCustomTabValue(aEvent.originalTarget, "foo", "bar");
+ }
+
+ // This does the actual testing. SSTabRestoring should be firing on tabs from
+ // left to right, so we're going to start with the rightmost tab.
+ function onFirstSSTabRestoring() {
+ info("onFirstSSTabRestoring...");
+ for (let i = gBrowser.tabs.length - 1; i >= 0; i--) {
+ let tab = gBrowser.tabs[i];
+ let actualUniq = ss.getCustomTabValue(tab, "uniq");
+ let expectedUniq = state.windows[0].tabs[i].extData.uniq;
+
+ if (wasLoaded[actualUniq]) {
+ info("tab " + i + ": already restored");
+ continue;
+ }
+ is(actualUniq, expectedUniq, "tab " + i + ": extData was correct");
+
+ // Now we're going to set a piece of data back on the tab so it can be read
+ // to test setting a value "early".
+ uniq2[actualUniq] = r();
+ ss.setCustomTabValue(tab, "uniq2", uniq2[actualUniq]);
+
+ // Delete the value we have for "baz". This tests that deleteCustomTabValue
+ // will delete "early access" values (c.f. bug 617175). If this doesn't throw
+ // then the test is successful.
+ try {
+ ss.deleteCustomTabValue(tab, "baz");
+ } catch (e) {
+ ok(false, "no error calling deleteCustomTabValue - " + e);
+ }
+
+ // This will be used in the final comparison to make sure we checked the
+ // same number as we set.
+ uniq2Count++;
+ }
+ }
+
+ function onLastSSTabRestoring() {
+ let checked = 0;
+ for (let i = 0; i < gBrowser.tabs.length; i++) {
+ let tab = gBrowser.tabs[i];
+ let uniq = ss.getCustomTabValue(tab, "uniq");
+
+ // Look to see if we set a uniq2 value for this uniq value
+ if (uniq in uniq2) {
+ is(
+ ss.getCustomTabValue(tab, "uniq2"),
+ uniq2[uniq],
+ "tab " + i + " has correct uniq2 value"
+ );
+ checked++;
+ }
+ }
+ ok(uniq2Count > 0, "at least 1 tab properly checked 'early access'");
+ is(checked, uniq2Count, "checked the same number of uniq2 as we set");
+ }
+
+ function cleanup() {
+ // remove the event listener and clean up before finishing
+ gBrowser.tabContainer.removeEventListener(
+ "SSTabRestoring",
+ onSSTabRestoring
+ );
+ gBrowser.tabContainer.removeEventListener(
+ "SSTabRestored",
+ onSSTabRestored,
+ true
+ );
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen);
+ // Put this in an executeSoon because we still haven't called restoreNextTab
+ // in sessionstore for the last tab (we'll call it after this). We end up
+ // trying to restore the tab (since we then add a closed tab to the array).
+ executeSoon(function () {
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+ });
+ }
+
+ // Add the event listeners
+ gBrowser.tabContainer.addEventListener("SSTabRestoring", onSSTabRestoring);
+ gBrowser.tabContainer.addEventListener(
+ "SSTabRestored",
+ onSSTabRestored,
+ true
+ );
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen);
+ // Restore state
+ ss.setBrowserState(JSON.stringify(state));
+}
diff --git a/browser/components/sessionstore/test/browser_590563.js b/browser/components/sessionstore/test/browser_590563.js
new file mode 100644
index 0000000000..3b255fd9bd
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_590563.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ let sessionData = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
+ hidden: true,
+ },
+ {
+ entries: [{ url: "about:blank", triggeringPrincipal_base64 }],
+ hidden: false,
+ },
+ ],
+ },
+ ],
+ };
+ let url = "about:sessionrestore";
+ let formdata = { id: { sessionData }, url };
+ let state = {
+ windows: [
+ { tabs: [{ entries: [{ url, triggeringPrincipal_base64 }], formdata }] },
+ ],
+ };
+
+ let win = await newWindowWithState(state);
+
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ is(gBrowser.tabs.length, 1, "The total number of tabs should be 1");
+ is(
+ gBrowser.visibleTabs.length,
+ 1,
+ "The total number of visible tabs should be 1"
+ );
+
+ await new Promise(resolve => executeSoon(resolve));
+ await new Promise(resolve => waitForFocus(resolve, win));
+ await middleClickTest(win);
+});
+
+async function middleClickTest(win) {
+ let browser = win.gBrowser.selectedBrowser;
+ let tabsToggle = browser.contentDocument.getElementById("tabsToggle");
+ EventUtils.synthesizeMouseAtCenter(
+ tabsToggle,
+ { button: 0 },
+ browser.contentWindow
+ );
+ let tree = browser.contentDocument.getElementById("tabList");
+ await BrowserTestUtils.waitForCondition(() => !tree.hasAttribute("hidden"));
+ // Force a layout flush before accessing coordinates.
+ tree.getBoundingClientRect();
+
+ is(tree.view.rowCount, 3, "There should be three items");
+
+ // click on the first tab item
+ var rect = tree.getCoordsForCellItem(1, tree.columns[1], "text");
+ EventUtils.synthesizeMouse(
+ tree.body,
+ rect.x,
+ rect.y,
+ { button: 1 },
+ browser.contentWindow
+ );
+ // click on the second tab item
+ rect = tree.getCoordsForCellItem(2, tree.columns[1], "text");
+ EventUtils.synthesizeMouse(
+ tree.body,
+ rect.x,
+ rect.y,
+ { button: 1 },
+ browser.contentWindow
+ );
+
+ is(
+ win.gBrowser.tabs.length,
+ 3,
+ "The total number of tabs should be 3 after restoring 2 tabs by middle click."
+ );
+ is(
+ win.gBrowser.visibleTabs.length,
+ 3,
+ "The total number of visible tabs should be 3 after restoring 2 tabs by middle click"
+ );
+}
+
+async function newWindowWithState(state) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(AppConstants.BROWSER_CHROME_URL, "_blank", opts);
+
+ await BrowserTestUtils.waitForEvent(win, "load");
+
+ // The form data will be restored before SSTabRestored, so we want to listen
+ // for that on the currently selected tab
+ await new Promise(resolve => {
+ win.gBrowser.tabContainer.addEventListener(
+ "SSTabRestored",
+ function onSSTabRestored(event) {
+ let tab = event.target;
+ if (tab.selected) {
+ win.gBrowser.tabContainer.removeEventListener(
+ "SSTabRestored",
+ onSSTabRestored,
+ true
+ );
+ resolve();
+ }
+ },
+ true
+ );
+
+ executeSoon(() => ss.setWindowState(win, JSON.stringify(state), true));
+ });
+
+ return win;
+}
diff --git a/browser/components/sessionstore/test/browser_595601-restore_hidden.js b/browser/components/sessionstore/test/browser_595601-restore_hidden.js
new file mode 100644
index 0000000000..046723fc3e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_595601-restore_hidden.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var windowState = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#3", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#4", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#5", triggeringPrincipal_base64 },
+ ],
+ hidden: true,
+ },
+ {
+ entries: [
+ { url: "http://example.com#6", triggeringPrincipal_base64 },
+ ],
+ hidden: true,
+ },
+ {
+ entries: [
+ { url: "http://example.com#7", triggeringPrincipal_base64 },
+ ],
+ hidden: true,
+ },
+ {
+ entries: [
+ { url: "http://example.com#8", triggeringPrincipal_base64 },
+ ],
+ hidden: true,
+ },
+ ],
+ },
+ ],
+};
+
+function test() {
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.restore_hidden_tabs");
+ });
+
+ // First stage: restoreHiddenTabs = true
+ // Second stage: restoreHiddenTabs = false
+ test_loadTabs(true, function () {
+ test_loadTabs(false, finish);
+ });
+}
+
+function test_loadTabs(restoreHiddenTabs, callback) {
+ Services.prefs.setBoolPref(
+ "browser.sessionstore.restore_hidden_tabs",
+ restoreHiddenTabs
+ );
+
+ let expectedTabs = restoreHiddenTabs ? 8 : 4;
+ let firstProgress = true;
+
+ newWindowWithState(windowState, function (win, needsRestore, isRestoring) {
+ if (firstProgress) {
+ firstProgress = false;
+ is(isRestoring, 3, "restoring 3 tabs concurrently");
+ } else {
+ ok(isRestoring < 4, "restoring max. 3 tabs concurrently");
+ }
+
+ // We're explicity checking for (isRestoring == 1) here because the test
+ // progress listener is called before the session store one. So when we're
+ // called with one tab left to restore we know that the last tab has
+ // finished restoring and will soon be handled by the SS listener.
+ let tabsNeedingRestore = win.gBrowser.tabs.length - needsRestore;
+ if (isRestoring == 1 && tabsNeedingRestore == expectedTabs) {
+ is(win.gBrowser.visibleTabs.length, 4, "only 4 visible tabs");
+
+ TabsProgressListener.uninit();
+ executeSoon(callback);
+ }
+ });
+}
+
+var TabsProgressListener = {
+ init(win) {
+ this.window = win;
+ Services.obs.addObserver(this, "sessionstore-debug-tab-restored");
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "sessionstore-debug-tab-restored");
+
+ delete this.window;
+ delete this.callback;
+ },
+
+ setCallback(callback) {
+ this.callback = callback;
+ },
+
+ observe(browser) {
+ TabsProgressListener.onRestored(browser);
+ },
+
+ onRestored(browser) {
+ if (
+ this.callback &&
+ ss.getInternalObjectState(browser) == TAB_STATE_RESTORING
+ ) {
+ this.callback.apply(null, [this.window].concat(this.countTabs()));
+ }
+ },
+
+ countTabs() {
+ let needsRestore = 0,
+ isRestoring = 0;
+
+ for (let i = 0; i < this.window.gBrowser.tabs.length; i++) {
+ let state = ss.getInternalObjectState(
+ this.window.gBrowser.tabs[i].linkedBrowser
+ );
+ if (state == TAB_STATE_RESTORING) {
+ isRestoring++;
+ } else if (state == TAB_STATE_NEEDS_RESTORE) {
+ needsRestore++;
+ }
+ }
+
+ return [needsRestore, isRestoring];
+ },
+};
+
+// ----------
+function newWindowWithState(state, callback) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(AppConstants.BROWSER_CHROME_URL, "_blank", opts);
+
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ whenWindowLoaded(win, function onWindowLoaded(aWin) {
+ TabsProgressListener.init(aWin);
+ TabsProgressListener.setCallback(callback);
+
+ ss.setWindowState(aWin, JSON.stringify(state), true);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_597071.js b/browser/components/sessionstore/test/browser_597071.js
new file mode 100644
index 0000000000..953e462491
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_597071.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 597071 - Closed windows should only be resurrected when there is a single
+ * popup window
+ */
+add_task(async function test_close_last_nonpopup_window() {
+ // Purge the list of closed windows.
+ forgetClosedWindows();
+
+ let oldState = ss.getWindowState(window);
+
+ let popupState = {
+ windows: [{ tabs: [{ entries: [] }], isPopup: true, hidden: "toolbar" }],
+ };
+
+ // Set this window to be a popup.
+ ss.setWindowState(window, JSON.stringify(popupState), true);
+
+ // Open a new window with a tab.
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: false });
+ let tab = BrowserTestUtils.addTab(win.gBrowser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Make sure sessionstore sees this window.
+ let state = JSON.parse(ss.getBrowserState());
+ is(state.windows.length, 2, "sessionstore knows about this window");
+
+ // Closed the window and check the closed window count.
+ await BrowserTestUtils.closeWindow(win);
+ is(ss.getClosedWindowCount(), 1, "correct closed window count");
+
+ // Cleanup.
+ ss.setWindowState(window, oldState, true);
+});
diff --git a/browser/components/sessionstore/test/browser_600545.js b/browser/components/sessionstore/test/browser_600545.js
new file mode 100644
index 0000000000..45ad72d2e0
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_600545.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/. */
+
+requestLongerTimeout(2);
+
+var stateBackup = JSON.parse(ss.getBrowserState());
+
+function test() {
+ /** Test for Bug 600545 **/
+ waitForExplicitFinish();
+ testBug600545();
+}
+
+function testBug600545() {
+ // Set the pref to false to cause non-app tabs to be stripped out on a save
+ Services.prefs.setBoolPref("browser.sessionstore.resume_from_crash", false);
+ Services.prefs.setIntPref("browser.sessionstore.interval", 2000);
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.resume_from_crash");
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+ });
+
+ // This tests the following use case: When multiple windows are open
+ // and browser.sessionstore.resume_from_crash preference is false,
+ // tab session data for non-active window is stripped for non-pinned
+ // tabs. This occurs after "sessionstore-state-write-complete"
+ // fires which will only fire in this case if there is at least one
+ // pinned tab.
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org#0", triggeringPrincipal_base64 },
+ ],
+ pinned: true,
+ },
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ selected: 2,
+ },
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#3", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#4", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#5", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#6", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ selected: 3,
+ },
+ ],
+ };
+
+ waitForBrowserState(state, function () {
+ // Need to wait for SessionStore's saveState function to be called
+ // so that non-pinned tabs will be stripped from non-active window
+ waitForSaveState(function () {
+ let expectedNumberOfTabs = getStateTabCount(state);
+ let retrievedState = JSON.parse(ss.getBrowserState());
+ let actualNumberOfTabs = getStateTabCount(retrievedState);
+
+ is(
+ actualNumberOfTabs,
+ expectedNumberOfTabs,
+ "Number of tabs in retreived session data, matches number of tabs set."
+ );
+
+ done();
+ });
+ });
+}
+
+function done() {
+ // Enumerate windows and close everything but our primary window. We can't
+ // use waitForFocus() because apparently it's buggy. See bug 599253.
+ let closeWinPromises = [];
+ for (let currentWindow of Services.wm.getEnumerator("navigator:browser")) {
+ if (currentWindow != window) {
+ closeWinPromises.push(BrowserTestUtils.closeWindow(currentWindow));
+ }
+ }
+
+ Promise.all(closeWinPromises).then(() => {
+ waitForBrowserState(stateBackup, finish);
+ });
+}
+
+// Count up the number of tabs in the state data
+function getStateTabCount(aState) {
+ let tabCount = 0;
+ for (let i in aState.windows) {
+ tabCount += aState.windows[i].tabs.length;
+ }
+ return tabCount;
+}
diff --git a/browser/components/sessionstore/test/browser_601955.js b/browser/components/sessionstore/test/browser_601955.js
new file mode 100644
index 0000000000..90cf6c182b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_601955.js
@@ -0,0 +1,54 @@
+/* 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 tests that pinning/unpinning a tab, on its own, eventually triggers a
+// session store.
+
+function test() {
+ waitForExplicitFinish();
+ // We speed up the interval between session saves to ensure that the test
+ // runs quickly.
+ Services.prefs.setIntPref("browser.sessionstore.interval", 2000);
+
+ // Loading a tab causes a save state and this is meant to catch that event.
+ waitForSaveState(testBug601955_1);
+
+ // Assumption: Only one window is open and it has one tab open.
+ BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+}
+
+function testBug601955_1() {
+ // Because pinned tabs are at the front of |gBrowser.tabs|, pinning tabs
+ // re-arranges the |tabs| array.
+ ok(!gBrowser.tabs[0].pinned, "first tab should not be pinned yet");
+ ok(!gBrowser.tabs[1].pinned, "second tab should not be pinned yet");
+
+ waitForSaveState(testBug601955_2);
+ gBrowser.pinTab(gBrowser.tabs[0]);
+}
+
+function testBug601955_2() {
+ let state = JSON.parse(ss.getBrowserState());
+ ok(state.windows[0].tabs[0].pinned, "first tab should be pinned by now");
+ ok(!state.windows[0].tabs[1].pinned, "second tab should still not be pinned");
+
+ waitForSaveState(testBug601955_3);
+ gBrowser.unpinTab(window.gBrowser.tabs[0]);
+}
+
+function testBug601955_3() {
+ let state = JSON.parse(ss.getBrowserState());
+ ok(!state.windows[0].tabs[0].pinned, "first tab should not be pinned");
+ ok(!state.windows[0].tabs[1].pinned, "second tab should not be pinned");
+
+ done();
+}
+
+function done() {
+ gBrowser.removeTab(window.gBrowser.tabs[1]);
+
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+
+ executeSoon(finish);
+}
diff --git a/browser/components/sessionstore/test/browser_607016.js b/browser/components/sessionstore/test/browser_607016.js
new file mode 100644
index 0000000000..68e9d3fb6e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_607016.js
@@ -0,0 +1,155 @@
+"use strict";
+
+var stateBackup = ss.getBrowserState();
+
+add_task(async function () {
+ /** Bug 607016 - If a tab is never restored, attributes (eg. hidden) aren't updated correctly **/
+ ignoreAllUncaughtExceptions();
+
+ // 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.
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+ // Don't restore tabs lazily.
+ Services.prefs.setBoolPref("browser.sessionstore.restore_tabs_lazily", false);
+
+ let state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ }, // overwriting
+ {
+ entries: [
+ { url: "http://example.org#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ }, // hiding
+ {
+ entries: [
+ { url: "http://example.org#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ }, // adding
+ {
+ entries: [
+ { url: "http://example.org#5", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ }, // deleting
+ {
+ entries: [
+ { url: "http://example.org#6", triggeringPrincipal_base64 },
+ ],
+ }, // creating
+ ],
+ selected: 1,
+ },
+ ],
+ };
+
+ async function progressCallback() {
+ let curState = JSON.parse(ss.getBrowserState());
+ for (let i = 0; i < curState.windows[0].tabs.length; i++) {
+ let tabState = state.windows[0].tabs[i];
+ let tabCurState = curState.windows[0].tabs[i];
+ if (tabState.extData) {
+ is(
+ tabCurState.extData.uniq,
+ tabState.extData.uniq,
+ "sanity check that tab has correct extData"
+ );
+ } else {
+ // We aren't expecting there to be any data on extData, but panorama
+ // may be setting something, so we need to make sure that if we do have
+ // data, we just don't have anything for "uniq".
+ ok(
+ !("extData" in tabCurState) || !("uniq" in tabCurState.extData),
+ "sanity check that tab doesn't have extData or extData doesn't have 'uniq'"
+ );
+ }
+ }
+
+ // Now we'll set a new unique value on 1 of the tabs
+ let newUniq = r();
+ ss.setCustomTabValue(gBrowser.tabs[1], "uniq", newUniq);
+ let tabState = JSON.parse(ss.getTabState(gBrowser.tabs[1]));
+ is(
+ tabState.extData.uniq,
+ newUniq,
+ "(overwriting) new data is stored in extData"
+ );
+
+ // hide the next tab before closing it
+ gBrowser.hideTab(gBrowser.tabs[2]);
+ tabState = JSON.parse(ss.getTabState(gBrowser.tabs[2]));
+ ok(tabState.hidden, "(hiding) tab data has hidden == true");
+
+ // set data that's not in a conflicting key
+ let stillUniq = r();
+ ss.setCustomTabValue(gBrowser.tabs[3], "stillUniq", stillUniq);
+ tabState = JSON.parse(ss.getTabState(gBrowser.tabs[3]));
+ is(
+ tabState.extData.stillUniq,
+ stillUniq,
+ "(adding) new data is stored in extData"
+ );
+
+ // remove the uniq value and make sure it's not there in the closed data
+ ss.deleteCustomTabValue(gBrowser.tabs[4], "uniq");
+ tabState = JSON.parse(ss.getTabState(gBrowser.tabs[4]));
+ // Since Panorama might have put data in, first check if there is extData.
+ // If there is explicitly check that "uniq" isn't in it. Otherwise, we're ok
+ if ("extData" in tabState) {
+ ok(
+ !("uniq" in tabState.extData),
+ "(deleting) uniq not in existing extData"
+ );
+ } else {
+ ok(true, "(deleting) no data is stored in extData");
+ }
+
+ // set unique data on the tab that never had any set, make sure that's saved
+ let newUniq2 = r();
+ ss.setCustomTabValue(gBrowser.tabs[5], "uniq", newUniq2);
+ tabState = JSON.parse(ss.getTabState(gBrowser.tabs[5]));
+ is(
+ tabState.extData.uniq,
+ newUniq2,
+ "(creating) new data is stored in extData where there was none"
+ );
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.tabs[1]);
+ }
+ }
+
+ // Set the test state.
+ await setBrowserState(state);
+
+ // Wait until the selected tab is restored and all others are pending.
+ await Promise.all(
+ Array.from(gBrowser.tabs, tab => {
+ return tab == gBrowser.selectedTab
+ ? promiseTabRestored(tab)
+ : promiseTabRestoring(tab);
+ })
+ );
+
+ // Kick off the actual tests.
+ await progressCallback();
+
+ // Cleanup.
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+ Services.prefs.clearUserPref("browser.sessionstore.restore_tabs_lazily");
+ await promiseBrowserState(stateBackup);
+});
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js
new file mode 100644
index 0000000000..b3ad6d240a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.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/. */
+
+const testState = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ { entries: [{ url: "about:rights", triggeringPrincipal_base64 }] },
+ ],
+ },
+ ],
+};
+
+function test() {
+ /** Test for Bug 615394 - Session Restore should notify when it is beginning and ending a restore **/
+ waitForExplicitFinish();
+
+ waitForBrowserState(testState, test_duplicateTab);
+}
+
+function test_duplicateTab() {
+ let tab = gBrowser.tabs[1];
+ let busyEventCount = 0;
+ let readyEventCount = 0;
+ let newTab;
+
+ // We'll look to make sure this value is on the duplicated tab
+ ss.setCustomTabValue(tab, "foo", "bar");
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ newTab = gBrowser.tabs[2];
+ readyEventCount++;
+ is(ss.getCustomTabValue(newTab, "foo"), "bar");
+ ss.setCustomTabValue(newTab, "baz", "qux");
+ }
+
+ function onSSTabRestoring(aEvent) {
+ if (aEvent.target == newTab) {
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(ss.getCustomTabValue(newTab, "baz"), "qux");
+ is(newTab.linkedBrowser.currentURI.spec, "about:rights");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ gBrowser.tabContainer.removeEventListener(
+ "SSTabRestoring",
+ onSSTabRestoring
+ );
+
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(newTab);
+ finish();
+ }
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ gBrowser.tabContainer.addEventListener("SSTabRestoring", onSSTabRestoring);
+
+ gBrowser._insertBrowser(tab);
+ newTab = ss.duplicateTab(window, tab);
+}
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js
new file mode 100644
index 0000000000..4dfcbc844d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js
@@ -0,0 +1,152 @@
+/* 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 lameMultiWindowState = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 1,
+ },
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 3,
+ },
+ ],
+};
+
+function getOuterWindowID(aWindow) {
+ return aWindow.docShell.outerWindowID;
+}
+
+function test() {
+ /** Test for Bug 615394 - Session Restore should notify when it is beginning and ending a restore **/
+ waitForExplicitFinish();
+
+ // We'll track events per window so we are sure that they are each happening once
+ // pre window.
+ let windowEvents = {};
+ windowEvents[getOuterWindowID(window)] = {
+ busyEventCount: 0,
+ readyEventCount: 0,
+ };
+
+ // waitForBrowserState does it's own observing for windows, but doesn't attach
+ // the listeners we want here, so do it ourselves.
+ let newWindow;
+ function windowObserver(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ Services.ww.unregisterNotification(windowObserver);
+
+ newWindow = aSubject;
+ newWindow.addEventListener(
+ "load",
+ function () {
+ windowEvents[getOuterWindowID(newWindow)] = {
+ busyEventCount: 0,
+ readyEventCount: 0,
+ };
+
+ newWindow.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ newWindow.addEventListener(
+ "SSWindowStateReady",
+ onSSWindowStateReady
+ );
+ },
+ { once: true }
+ );
+ }
+ }
+
+ function onSSWindowStateBusy(aEvent) {
+ windowEvents[getOuterWindowID(aEvent.originalTarget)].busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ windowEvents[getOuterWindowID(aEvent.originalTarget)].readyEventCount++;
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ Services.ww.registerNotification(windowObserver);
+
+ waitForBrowserState(lameMultiWindowState, function () {
+ let checkedWindows = 0;
+ for (let id of Object.keys(windowEvents)) {
+ let winEvents = windowEvents[id];
+ is(
+ winEvents.busyEventCount,
+ 1,
+ "window" + id + " busy event count correct"
+ );
+ is(
+ winEvents.readyEventCount,
+ 1,
+ "window" + id + " ready event count correct"
+ );
+ checkedWindows++;
+ }
+ is(checkedWindows, 2, "checked 2 windows");
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ newWindow.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ newWindow.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+
+ newWindow.close();
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ }
+
+ finish();
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js
new file mode 100644
index 0000000000..a76a8b3dd5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.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/. */
+
+const testState = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ { entries: [{ url: "about:rights", triggeringPrincipal_base64 }] },
+ ],
+ },
+ ],
+};
+
+function test() {
+ /** Test for Bug 615394 - Session Restore should notify when it is beginning and ending a restore **/
+ waitForExplicitFinish();
+
+ waitForBrowserState(testState, test_setTabState);
+}
+
+function test_setTabState() {
+ let tab = gBrowser.tabs[1];
+ let newTabState = JSON.stringify({
+ entries: [{ url: "http://example.org", triggeringPrincipal_base64 }],
+ extData: { foo: "bar" },
+ });
+ let busyEventCount = 0;
+ let readyEventCount = 0;
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ is(ss.getCustomTabValue(tab, "foo"), "bar");
+ ss.setCustomTabValue(tab, "baz", "qux");
+ }
+
+ function onSSTabRestoring(aEvent) {
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(ss.getCustomTabValue(tab, "baz"), "qux");
+ is(tab.linkedBrowser.currentURI.spec, "http://example.org/");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+
+ gBrowser.removeTab(tab);
+ finish();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ tab.addEventListener("SSTabRestoring", onSSTabRestoring, { once: true });
+ // Browser must be inserted in order to restore.
+ gBrowser._insertBrowser(tab);
+ ss.setTabState(tab, newTabState);
+}
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js
new file mode 100644
index 0000000000..c9d4bd00f5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js
@@ -0,0 +1,65 @@
+/* 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 615394 - Session Restore should notify when it is beginning and ending a restore **/
+ waitForExplicitFinish();
+
+ let newState = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
+ extData: { foo: "bar" },
+ },
+ {
+ entries: [
+ { url: "http://example.org", triggeringPrincipal_base64 },
+ ],
+ extData: { baz: "qux" },
+ },
+ ],
+ },
+ ],
+ };
+
+ let busyEventCount = 0,
+ readyEventCount = 0,
+ tabRestoredCount = 0;
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ is(ss.getCustomTabValue(gBrowser.tabs[0], "foo"), "bar");
+ is(ss.getCustomTabValue(gBrowser.tabs[1], "baz"), "qux");
+ }
+
+ function onSSTabRestored(aEvent) {
+ if (++tabRestoredCount < 2) {
+ return;
+ }
+
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+ is(gBrowser.tabs[0].linkedBrowser.currentURI.spec, "about:mozilla");
+ is(gBrowser.tabs[1].linkedBrowser.currentURI.spec, "http://example.org/");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored);
+
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ finish();
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+ gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored);
+
+ ss.setWindowState(window, JSON.stringify(newState), true);
+}
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js
new file mode 100644
index 0000000000..345bba516c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js
@@ -0,0 +1,65 @@
+"use strict";
+
+const testState = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ { entries: [{ url: "about:rights", triggeringPrincipal_base64 }] },
+ ],
+ },
+ ],
+};
+
+// Test for Bug 615394 - Session Restore should notify when it is beginning and
+// ending a restore.
+add_task(async function test_undoCloseTab() {
+ await promiseBrowserState(testState);
+
+ let tab = gBrowser.tabs[1];
+ let busyEventCount = 0;
+ let readyEventCount = 0;
+ // This will be set inside the `onSSWindowStateReady` method.
+ let lastTab;
+
+ ss.setCustomTabValue(tab, "foo", "bar");
+
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ Assert.equal(gBrowser.tabs.length, 2, "Should only have 2 tabs");
+ lastTab = gBrowser.tabs[1];
+ readyEventCount++;
+ Assert.equal(ss.getCustomTabValue(lastTab, "foo"), "bar");
+ ss.setCustomTabValue(lastTab, "baz", "qux");
+ }
+
+ window.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.addEventListener("SSWindowStateReady", onSSWindowStateReady);
+
+ let restoredPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "SSTabRestored"
+ );
+
+ await promiseRemoveTabAndSessionState(tab);
+ let reopenedTab = ss.undoCloseTab(window, 0);
+
+ await Promise.all([
+ restoredPromise,
+ BrowserTestUtils.browserLoaded(reopenedTab.linkedBrowser),
+ ]);
+
+ Assert.equal(reopenedTab, lastTab, "Tabs should be the same one.");
+ Assert.equal(busyEventCount, 1);
+ Assert.equal(readyEventCount, 1);
+ Assert.equal(ss.getCustomTabValue(reopenedTab, "baz"), "qux");
+ Assert.equal(reopenedTab.linkedBrowser.currentURI.spec, "about:rights");
+
+ window.removeEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ window.removeEventListener("SSWindowStateReady", onSSWindowStateReady);
+
+ BrowserTestUtils.removeTab(reopenedTab);
+});
diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js
new file mode 100644
index 0000000000..0a5b07da29
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js
@@ -0,0 +1,146 @@
+/* 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 lameMultiWindowState = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.org#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.org#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 1,
+ },
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#3", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ {
+ entries: [
+ { url: "http://example.com#4", triggeringPrincipal_base64 },
+ ],
+ extData: { uniq: r() },
+ },
+ ],
+ selected: 3,
+ },
+ ],
+};
+
+function test() {
+ /** Test for Bug 615394 - Session Restore should notify when it is beginning and ending a restore **/
+ waitForExplicitFinish();
+
+ let newWindow, reopenedWindow;
+
+ function firstWindowObserver(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ newWindow = aSubject;
+ Services.ww.unregisterNotification(firstWindowObserver);
+ }
+ }
+ Services.ww.registerNotification(firstWindowObserver);
+
+ waitForBrowserState(lameMultiWindowState, function () {
+ // Close the window which isn't window
+ BrowserTestUtils.closeWindow(newWindow).then(() => {
+ // Now give it time to close
+ reopenedWindow = ss.undoCloseWindow(0);
+ reopenedWindow.addEventListener("SSWindowStateBusy", onSSWindowStateBusy);
+ reopenedWindow.addEventListener(
+ "SSWindowStateReady",
+ onSSWindowStateReady
+ );
+
+ reopenedWindow.addEventListener(
+ "load",
+ function () {
+ reopenedWindow.gBrowser.tabContainer.addEventListener(
+ "SSTabRestored",
+ onSSTabRestored
+ );
+ },
+ { once: true }
+ );
+ });
+ });
+
+ let busyEventCount = 0,
+ readyEventCount = 0,
+ tabRestoredCount = 0;
+ // These will listen to the reopened closed window...
+ function onSSWindowStateBusy(aEvent) {
+ busyEventCount++;
+ }
+
+ function onSSWindowStateReady(aEvent) {
+ readyEventCount++;
+ }
+
+ function onSSTabRestored(aEvent) {
+ if (++tabRestoredCount < 4) {
+ return;
+ }
+
+ is(busyEventCount, 1);
+ is(readyEventCount, 1);
+
+ reopenedWindow.removeEventListener(
+ "SSWindowStateBusy",
+ onSSWindowStateBusy
+ );
+ reopenedWindow.removeEventListener(
+ "SSWindowStateReady",
+ onSSWindowStateReady
+ );
+ reopenedWindow.gBrowser.tabContainer.removeEventListener(
+ "SSTabRestored",
+ onSSTabRestored
+ );
+
+ reopenedWindow.close();
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ }
+
+ finish();
+ }
+}
diff --git a/browser/components/sessionstore/test/browser_618151.js b/browser/components/sessionstore/test/browser_618151.js
new file mode 100644
index 0000000000..c38a349818
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_618151.js
@@ -0,0 +1,67 @@
+/* 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 stateBackup = ss.getBrowserState();
+const testState = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ { entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }] },
+ ],
+ },
+ ],
+};
+
+function test() {
+ /** Test for Bug 618151 - Overwriting state can lead to unrestored tabs **/
+ waitForExplicitFinish();
+ runNextTest();
+}
+
+// Just a subset of tests from bug 615394 that causes a timeout.
+var tests = [test_setup, test_hang];
+function runNextTest() {
+ // set an empty state & run the next test, or finish
+ if (tests.length) {
+ // Enumerate windows and close everything but our primary window. We can't
+ // use waitForFocus() because apparently it's buggy. See bug 599253.
+ let closeWinPromises = [];
+ for (let currentWindow of Services.wm.getEnumerator("navigator:browser")) {
+ if (currentWindow != window) {
+ closeWinPromises.push(BrowserTestUtils.closeWindow(currentWindow));
+ }
+ }
+
+ Promise.all(closeWinPromises).then(() => {
+ let currentTest = tests.shift();
+ info("running " + currentTest.name);
+ waitForBrowserState(testState, currentTest);
+ });
+ } else {
+ ss.setBrowserState(stateBackup);
+ executeSoon(finish);
+ }
+}
+
+function test_setup() {
+ function onSSTabRestored(aEvent) {
+ gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored);
+ runNextTest();
+ }
+
+ gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored);
+ ss.setTabState(
+ gBrowser.tabs[1],
+ JSON.stringify({
+ entries: [{ url: "http://example.org", triggeringPrincipal_base64 }],
+ extData: { foo: "bar" },
+ })
+ );
+}
+
+function test_hang() {
+ ok(true, "test didn't time out");
+ runNextTest();
+}
diff --git a/browser/components/sessionstore/test/browser_623779.js b/browser/components/sessionstore/test/browser_623779.js
new file mode 100644
index 0000000000..ce6d1cde1b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_623779.js
@@ -0,0 +1,13 @@
+"use strict";
+
+add_task(async function () {
+ gBrowser.pinTab(gBrowser.selectedTab);
+
+ let newTab = gBrowser.duplicateTab(gBrowser.selectedTab);
+ await promiseTabRestored(newTab);
+
+ ok(!newTab.pinned, "duplicating a pinned tab creates unpinned tab");
+ BrowserTestUtils.removeTab(newTab);
+
+ gBrowser.unpinTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/sessionstore/test/browser_624727.js b/browser/components/sessionstore/test/browser_624727.js
new file mode 100644
index 0000000000..074ea7599d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_624727.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var TEST_STATE = { windows: [{ tabs: [{ url: "about:blank" }] }] };
+
+add_task(async function () {
+ function assertNumberOfTabs(num, msg) {
+ is(gBrowser.tabs.length, num, msg);
+ }
+
+ function assertNumberOfPinnedTabs(num, msg) {
+ is(gBrowser._numPinnedTabs, num, msg);
+ }
+
+ // check prerequisites
+ assertNumberOfTabs(1, "we start off with one tab");
+ assertNumberOfPinnedTabs(0, "no pinned tabs so far");
+
+ // setup
+ BrowserTestUtils.addTab(gBrowser, "about:blank");
+ assertNumberOfTabs(2, "there are two tabs, now");
+
+ let [tab1, tab2] = gBrowser.tabs;
+ gBrowser.pinTab(tab1);
+ gBrowser.pinTab(tab2);
+ assertNumberOfPinnedTabs(2, "both tabs are now pinned");
+
+ // run the test
+ await promiseBrowserState(TEST_STATE);
+
+ assertNumberOfTabs(1, "one tab left after setBrowserState()");
+ assertNumberOfPinnedTabs(0, "there are no pinned tabs");
+});
diff --git a/browser/components/sessionstore/test/browser_625016.js b/browser/components/sessionstore/test/browser_625016.js
new file mode 100644
index 0000000000..8432ce1d64
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_625016.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ /** Test for Bug 625016 - Restore windows closed in succession to quit (non-OSX only) **/
+
+ // We'll test this by opening a new window, waiting for the save
+ // event, then closing that window. We'll observe the
+ // "sessionstore-state-write-complete" notification and check that
+ // the state contains no _closedWindows. We'll then add a new tab
+ // and make sure that the state following that was reset and the
+ // closed window is now in _closedWindows.
+
+ requestLongerTimeout(2);
+
+ await forceSaveState();
+
+ // We'll clear all closed windows to make sure our state is clean
+ // forgetClosedWindow doesn't trigger a delayed save
+ forgetClosedWindows();
+ is(ss.getClosedWindowCount(), 0, "starting with no closed windows");
+});
+
+add_task(async function new_window() {
+ let newWin;
+ try {
+ newWin = await promiseNewWindowLoaded();
+ let tab = BrowserTestUtils.addTab(
+ newWin.gBrowser,
+ "http://example.com/browser_625016.js?" + Math.random()
+ );
+ await promiseBrowserLoaded(tab.linkedBrowser);
+
+ // Double check that we have no closed windows
+ is(ss.getClosedWindowCount(), 0, "no closed windows on first save");
+
+ await BrowserTestUtils.closeWindow(newWin);
+ newWin = null;
+
+ let state = JSON.parse(await promiseRecoveryFileContents());
+ is(state.windows.length, 1, "observe1: 1 window in data written to disk");
+ is(
+ state._closedWindows.length,
+ 0,
+ "observe1: no closed windows in data written to disk"
+ );
+
+ // The API still treats the closed window as closed, so ensure that window is there
+ is(
+ ss.getClosedWindowCount(),
+ 1,
+ "observe1: 1 closed window according to API"
+ );
+ } finally {
+ if (newWin) {
+ await BrowserTestUtils.closeWindow(newWin);
+ }
+ await forceSaveState();
+ }
+});
+
+// We'll open a tab, which should trigger another state save which would wipe
+// the _shouldRestore attribute from the closed window
+add_task(async function new_tab() {
+ let newTab;
+ try {
+ newTab = BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+ await promiseBrowserLoaded(newTab.linkedBrowser);
+ await TabStateFlusher.flush(newTab.linkedBrowser);
+
+ let state = JSON.parse(await promiseRecoveryFileContents());
+ is(
+ state.windows.length,
+ 1,
+ "observe2: 1 window in data being written to disk"
+ );
+ is(
+ state._closedWindows.length,
+ 1,
+ "observe2: 1 closed window in data being written to disk"
+ );
+
+ // The API still treats the closed window as closed, so ensure that window is there
+ is(
+ ss.getClosedWindowCount(),
+ 1,
+ "observe2: 1 closed window according to API"
+ );
+ } finally {
+ if (newTab) {
+ gBrowser.removeTab(newTab);
+ }
+ }
+});
+
+add_task(async function done() {
+ // The API still represents the closed window as closed, so we can clear it
+ // with the API, but just to make sure...
+ // is(ss.getClosedWindowCount(), 1, "1 closed window according to API");
+ await promiseAllButPrimaryWindowClosed();
+ forgetClosedWindows();
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+});
diff --git a/browser/components/sessionstore/test/browser_628270.js b/browser/components/sessionstore/test/browser_628270.js
new file mode 100644
index 0000000000..195e362063
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_628270.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function test() {
+ let assertNumberOfTabs = function (num, msg) {
+ is(gBrowser.tabs.length, num, msg);
+ };
+
+ let assertNumberOfVisibleTabs = function (num, msg) {
+ is(gBrowser.visibleTabs.length, num, msg);
+ };
+
+ waitForExplicitFinish();
+
+ // check prerequisites
+ assertNumberOfTabs(1, "we start off with one tab");
+
+ // setup
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+
+ await promiseBrowserLoaded(tab.linkedBrowser);
+
+ // hide the newly created tab
+ assertNumberOfVisibleTabs(2, "there are two visible tabs");
+ gBrowser.showOnlyTheseTabs([gBrowser.tabs[0]]);
+ assertNumberOfVisibleTabs(1, "there is one visible tab");
+ ok(tab.hidden, "newly created tab is now hidden");
+
+ // close and restore hidden tab
+ await promiseRemoveTabAndSessionState(tab);
+ tab = ss.undoCloseTab(window, 0);
+
+ // check that everything was restored correctly, clean up and finish
+ await promiseBrowserLoaded(tab.linkedBrowser);
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ "about:mozilla",
+ "restored tab has correct url"
+ );
+
+ gBrowser.removeTab(tab);
+ finish();
+}
diff --git a/browser/components/sessionstore/test/browser_635418.js b/browser/components/sessionstore/test/browser_635418.js
new file mode 100644
index 0000000000..c932a3b9f4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_635418.js
@@ -0,0 +1,58 @@
+/* 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 tests that hiding/showing a tab, on its own, eventually triggers a
+// session store.
+
+function test() {
+ waitForExplicitFinish();
+
+ // We speed up the interval between session saves to ensure that the test
+ // runs quickly.
+ Services.prefs.setIntPref("browser.sessionstore.interval", 2000);
+
+ // Loading a tab causes a save state and this is meant to catch that event.
+ waitForSaveState(testBug635418_1);
+
+ // Assumption: Only one window is open and it has one tab open.
+ BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+}
+
+function testBug635418_1() {
+ ok(!gBrowser.tabs[0].hidden, "first tab should not be hidden");
+ ok(!gBrowser.tabs[1].hidden, "second tab should not be hidden");
+
+ waitForSaveState(testBug635418_2);
+
+ // We can't hide the selected tab, so hide the new one
+ gBrowser.hideTab(gBrowser.tabs[1]);
+}
+
+function testBug635418_2() {
+ let state = JSON.parse(ss.getBrowserState());
+ ok(!state.windows[0].tabs[0].hidden, "first tab should still not be hidden");
+ ok(state.windows[0].tabs[1].hidden, "second tab should be hidden by now");
+
+ waitForSaveState(testBug635418_3);
+ gBrowser.showTab(gBrowser.tabs[1]);
+}
+
+function testBug635418_3() {
+ let state = JSON.parse(ss.getBrowserState());
+ ok(
+ !state.windows[0].tabs[0].hidden,
+ "first tab should still still not be hidden"
+ );
+ ok(!state.windows[0].tabs[1].hidden, "second tab should not be hidden again");
+
+ done();
+}
+
+function done() {
+ gBrowser.removeTab(window.gBrowser.tabs[1]);
+
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+
+ executeSoon(finish);
+}
diff --git a/browser/components/sessionstore/test/browser_636279.js b/browser/components/sessionstore/test/browser_636279.js
new file mode 100644
index 0000000000..ea116ee3f7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_636279.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var stateBackup = ss.getBrowserState();
+
+var statePinned = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ pinned: true,
+ },
+ ],
+ },
+ ],
+};
+
+var state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#3", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#4", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+};
+
+function test() {
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function () {
+ TabsProgressListener.uninit();
+ ss.setBrowserState(stateBackup);
+ });
+
+ TabsProgressListener.init();
+
+ window.addEventListener(
+ "SSWindowStateReady",
+ function () {
+ let firstProgress = true;
+
+ TabsProgressListener.setCallback(function (needsRestore, isRestoring) {
+ if (firstProgress) {
+ firstProgress = false;
+ is(isRestoring, 3, "restoring 3 tabs concurrently");
+ } else {
+ ok(isRestoring <= 3, "restoring max. 2 tabs concurrently");
+ }
+
+ if (0 == needsRestore) {
+ TabsProgressListener.unsetCallback();
+ waitForFocus(finish);
+ }
+ });
+
+ ss.setBrowserState(JSON.stringify(state));
+ },
+ { once: true }
+ );
+
+ ss.setBrowserState(JSON.stringify(statePinned));
+}
+
+function countTabs() {
+ let needsRestore = 0,
+ isRestoring = 0;
+ for (let window of Services.wm.getEnumerator("navigator:browser")) {
+ if (window.closed) {
+ continue;
+ }
+
+ for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+ let browserState = ss.getInternalObjectState(
+ window.gBrowser.tabs[i].linkedBrowser
+ );
+ if (browserState == TAB_STATE_RESTORING) {
+ isRestoring++;
+ } else if (browserState == TAB_STATE_NEEDS_RESTORE) {
+ needsRestore++;
+ }
+ }
+ }
+
+ return [needsRestore, isRestoring];
+}
+
+var TabsProgressListener = {
+ init() {
+ Services.obs.addObserver(this, "sessionstore-debug-tab-restored");
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "sessionstore-debug-tab-restored");
+ this.unsetCallback();
+ },
+
+ setCallback(callback) {
+ this.callback = callback;
+ },
+
+ unsetCallback() {
+ delete this.callback;
+ },
+
+ observe(browser, topic, data) {
+ TabsProgressListener.onRestored(browser);
+ },
+
+ onRestored(browser) {
+ if (
+ this.callback &&
+ ss.getInternalObjectState(browser) == TAB_STATE_RESTORING
+ ) {
+ this.callback.apply(null, countTabs());
+ }
+ },
+};
diff --git a/browser/components/sessionstore/test/browser_637020.js b/browser/components/sessionstore/test/browser_637020.js
new file mode 100644
index 0000000000..46bf062002
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_637020.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL =
+ "http://mochi.test:8888/browser/browser/components/" +
+ "sessionstore/test/browser_637020_slow.sjs";
+
+const TEST_STATE = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }] },
+ { entries: [{ url: "about:robots", triggeringPrincipal_base64 }] },
+ ],
+ },
+ {
+ tabs: [
+ { entries: [{ url: TEST_URL, triggeringPrincipal_base64 }] },
+ { entries: [{ url: TEST_URL, triggeringPrincipal_base64 }] },
+ ],
+ },
+ ],
+};
+
+/**
+ * This test ensures that windows that have just been restored will be marked
+ * as dirty, otherwise _getCurrentState() might ignore them when collecting
+ * state for the first time and we'd just save them as empty objects.
+ *
+ * The dirty state acts as a cache to not collect data from all windows all the
+ * time, so at the beginning, each window must be dirty so that we collect
+ * their state at least once.
+ */
+
+add_task(async function test() {
+ // Wait until the new window has been opened.
+ let promiseWindow = new Promise(resolve => {
+ Services.obs.addObserver(function onOpened(subject) {
+ Services.obs.removeObserver(onOpened, "domwindowopened");
+ resolve(subject);
+ }, "domwindowopened");
+ });
+
+ // Set the new browser state that will
+ // restore a window with two slowly loading tabs.
+ let backupState = SessionStore.getBrowserState();
+ SessionStore.setBrowserState(JSON.stringify(TEST_STATE));
+ let win = await promiseWindow;
+ let restoring = promiseWindowRestoring(win);
+ let restored = promiseWindowRestored(win);
+ await restoring;
+ await restored;
+
+ // The window has now been opened. Check the state that is returned,
+ // this should come from the cache while the window isn't restored, yet.
+ info("the window has been opened");
+ checkWindows();
+
+ // The history has now been restored and the tabs are loading. The data must
+ // now come from the window, if it's correctly been marked as dirty before.
+ await new Promise(resolve => whenDelayedStartupFinished(win, resolve));
+ info("the delayed startup has finished");
+ checkWindows();
+
+ // Cleanup.
+ await BrowserTestUtils.closeWindow(win);
+ await promiseBrowserState(backupState);
+});
+
+function checkWindows() {
+ let state = JSON.parse(SessionStore.getBrowserState());
+ is(state.windows[0].tabs.length, 2, "first window has two tabs");
+ is(state.windows[1].tabs.length, 2, "second window has two tabs");
+}
diff --git a/browser/components/sessionstore/test/browser_637020_slow.sjs b/browser/components/sessionstore/test/browser_637020_slow.sjs
new file mode 100644
index 0000000000..abb1dee829
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_637020_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_645428.js b/browser/components/sessionstore/test/browser_645428.js
new file mode 100644
index 0000000000..bbb3b1b299
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_645428.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NOTIFICATION = "sessionstore-browser-state-restored";
+
+function test() {
+ waitForExplicitFinish();
+
+ function observe(subject, topic, data) {
+ if (NOTIFICATION == topic) {
+ finish();
+ ok(true, "TOPIC received");
+ }
+ }
+
+ Services.obs.addObserver(observe, NOTIFICATION);
+ registerCleanupFunction(function () {
+ Services.obs.removeObserver(observe, NOTIFICATION);
+ });
+
+ ss.setBrowserState(JSON.stringify({ windows: [] }));
+}
diff --git a/browser/components/sessionstore/test/browser_659591.js b/browser/components/sessionstore/test/browser_659591.js
new file mode 100644
index 0000000000..1020d2a39d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_659591.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ waitForExplicitFinish();
+
+ let eventReceived = false;
+
+ registerCleanupFunction(function () {
+ ok(eventReceived, "SSWindowClosing event received");
+ });
+
+ newWindow(function (win) {
+ win.addEventListener(
+ "SSWindowClosing",
+ function () {
+ eventReceived = true;
+ },
+ { once: true }
+ );
+
+ BrowserTestUtils.closeWindow(win).then(() => {
+ waitForFocus(finish);
+ });
+ });
+}
+
+function newWindow(callback) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(AppConstants.BROWSER_CHROME_URL, "_blank", opts);
+
+ win.addEventListener(
+ "load",
+ function () {
+ executeSoon(() => callback(win));
+ },
+ { once: true }
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_662743.js b/browser/components/sessionstore/test/browser_662743.js
new file mode 100644
index 0000000000..0a030789e1
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_662743.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This tests that session restore component does restore the right <select> option.
+// Session store should not rely only on previous user's selectedIndex, it should
+// check its value as well.
+
+function test() {
+ /** Tests selected options **/
+ requestLongerTimeout(2);
+ waitForExplicitFinish();
+
+ let testTabCount = 0;
+ let formData = [
+ // default case
+ {},
+
+ // new format
+ // index doesn't match value (testing an option in between (two))
+ { id: { select_id: { selectedIndex: 0, value: "val2" } } },
+ // index doesn't match value (testing an invalid value)
+ { id: { select_id: { selectedIndex: 4, value: "val8" } } },
+ // index doesn't match value (testing an invalid index)
+ { id: { select_id: { selectedIndex: 8, value: "val5" } } },
+ // index and value match position zero
+ { id: { select_id: { selectedIndex: 0, value: "val0" } }, xpath: {} },
+ // index doesn't match value (testing the last option (seven))
+ {
+ id: {},
+ xpath: {
+ "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']": {
+ selectedIndex: 1,
+ value: "val7",
+ },
+ },
+ },
+ // index and value match the default option "selectedIndex":3,"value":"val3"
+ {
+ xpath: {
+ "/xhtml:html/xhtml:body/xhtml:select[@name='select_name']": {
+ selectedIndex: 3,
+ value: "val3",
+ },
+ },
+ },
+ // index matches default option however it doesn't match value
+ { id: { select_id: { selectedIndex: 3, value: "val4" } } },
+ ];
+
+ let expectedValues = [
+ null, // default value
+ "val2",
+ null, // default value (invalid value)
+ "val5", // value is still valid (even it has an invalid index)
+ "val0",
+ "val7",
+ null,
+ "val4",
+ ];
+ let callback = function () {
+ testTabCount--;
+ if (testTabCount == 0) {
+ finish();
+ }
+ };
+
+ for (let i = 0; i < formData.length; i++) {
+ testTabCount++;
+ testTabRestoreData(formData[i], expectedValues[i], callback);
+ }
+}
+
+function testTabRestoreData(aFormData, aExpectedValue, aCallback) {
+ let testURL = getRootDirectory(gTestPath) + "browser_662743_sample.html";
+ let tab = BrowserTestUtils.addTab(gBrowser, testURL);
+
+ aFormData.url = testURL;
+ let tabState = {
+ entries: [{ url: testURL, triggeringPrincipal_base64 }],
+ formdata: aFormData,
+ };
+
+ promiseBrowserLoaded(tab.linkedBrowser).then(() => {
+ promiseTabState(tab, tabState)
+ .then(() => {
+ // Flush to make sure we have the latest form data.
+ return TabStateFlusher.flush(tab.linkedBrowser);
+ })
+ .then(() => {
+ let doc = tab.linkedBrowser.contentDocument;
+ let select = doc.getElementById("select_id");
+ let value = select.options[select.selectedIndex].value;
+ let restoredTabState = JSON.parse(ss.getTabState(tab));
+
+ // If aExpectedValue=null we don't expect any form data to be collected.
+ if (!aExpectedValue) {
+ ok(
+ !restoredTabState.hasOwnProperty("formdata"),
+ "no formdata collected"
+ );
+ gBrowser.removeTab(tab);
+ aCallback();
+ return;
+ }
+
+ // test select options values
+ is(
+ value,
+ aExpectedValue,
+ "Select Option by selectedIndex &/or value has been restored correctly"
+ );
+
+ let restoredFormData = restoredTabState.formdata;
+ let selectIdFormData = restoredFormData.id.select_id;
+ value = restoredFormData.id.select_id.value;
+
+ // test format
+ ok(
+ "id" in restoredFormData || "xpath" in restoredFormData,
+ "FormData format is valid"
+ );
+ // test format
+ ok(
+ "selectedIndex" in selectIdFormData && "value" in selectIdFormData,
+ "select format is valid"
+ );
+ // test set collection values
+ is(value, aExpectedValue, "Collection has been saved correctly");
+
+ // clean up
+ gBrowser.removeTab(tab);
+
+ aCallback();
+ });
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_662743_sample.html b/browser/components/sessionstore/test/browser_662743_sample.html
new file mode 100644
index 0000000000..0fd49417cc
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_662743_sample.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<title>Test 662743</title>
+
+<!-- Select events -->
+<h3>Select options</h3>
+<select id="select_id" name="select_name">
+ <option value="val0">Zero</option>
+ <option value="val1">One</option>
+ <option value="val2">Two</option>
+ <option value="val3" selected="selected">Three</option>
+ <option value="val4">Four</option>
+ <option value="val5">Five</option>
+ <option value="val6">Six</option>
+ <option value="val7">Seven</option>
+</select>
diff --git a/browser/components/sessionstore/test/browser_662812.js b/browser/components/sessionstore/test/browser_662812.js
new file mode 100644
index 0000000000..cb15a14a1c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_662812.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ waitForExplicitFinish();
+
+ window.addEventListener(
+ "SSWindowStateBusy",
+ function () {
+ let state = ss.getWindowState(window);
+ ok(state.windows[0].busy, "window is busy");
+
+ window.addEventListener(
+ "SSWindowStateReady",
+ function () {
+ let state2 = ss.getWindowState(window);
+ ok(!state2.windows[0].busy, "window is not busy");
+
+ executeSoon(() => {
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ finish();
+ });
+ },
+ { once: true }
+ );
+ },
+ { once: true }
+ );
+
+ // create a new tab
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+ let browser = tab.linkedBrowser;
+
+ // close and restore it
+ browser.addEventListener(
+ "load",
+ function () {
+ gBrowser.removeTab(tab);
+ ss.undoCloseTab(window, 0);
+ },
+ { capture: true, once: true }
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_665702-state_session.js b/browser/components/sessionstore/test/browser_665702-state_session.js
new file mode 100644
index 0000000000..fc024e2247
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_665702-state_session.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function compareArray(a, b) {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function test() {
+ let currentState = JSON.parse(ss.getBrowserState());
+ ok(currentState.session, "session data returned by getBrowserState");
+
+ let keys = Object.keys(currentState.session);
+ let expectedKeys = ["lastUpdate", "startTime", "recentCrashes"];
+ ok(
+ compareArray(keys.sort(), expectedKeys.sort()),
+ "session object from getBrowserState has correct keys"
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_682507.js b/browser/components/sessionstore/test/browser_682507.js
new file mode 100644
index 0000000000..00a8ad4cd5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_682507.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+ BrowserTestUtils.addTab(gBrowser, "about:mozilla");
+
+ ss.setTabState(gBrowser.tabs[1], ss.getTabState(gBrowser.tabs[1]));
+ ok(
+ gBrowser.tabs[1].hasAttribute("pending"),
+ "second tab should have 'pending' attribute"
+ );
+
+ gBrowser.selectedTab = gBrowser.tabs[1];
+ ok(
+ !gBrowser.tabs[1].hasAttribute("pending"),
+ "second tab should have not 'pending' attribute"
+ );
+
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+}
diff --git a/browser/components/sessionstore/test/browser_687710.js b/browser/components/sessionstore/test/browser_687710.js
new file mode 100644
index 0000000000..e90c74c881
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_687710.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that sessionrestore handles cycles in the shentry graph properly.
+//
+// These cycles shouldn't be there in the first place, but they cause hangs
+// when they mysteriously appear (bug 687710). Docshell code assumes this
+// graph is a tree and tires to walk to the root. But if there's a cycle,
+// there is no root, and we loop forever.
+
+var stateBackup = ss.getBrowserState();
+
+var state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ {
+ docIdentifier: 1,
+ url: "http://example.com",
+ triggeringPrincipal_base64,
+ children: [
+ {
+ docIdentifier: 2,
+ url: "http://example.com",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ {
+ docIdentifier: 2,
+ url: "http://example.com",
+ triggeringPrincipal_base64,
+ children: [
+ {
+ docIdentifier: 1,
+ url: "http://example.com",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
+
+add_task(async function test() {
+ registerCleanupFunction(function () {
+ ss.setBrowserState(stateBackup);
+ });
+
+ /* This test fails by hanging. */
+ await setBrowserState(state);
+ ok(true, "Didn't hang!");
+});
diff --git a/browser/components/sessionstore/test/browser_687710_2.js b/browser/components/sessionstore/test/browser_687710_2.js
new file mode 100644
index 0000000000..81d3c55379
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_687710_2.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the fix for bug 687710 isn't too aggressive -- shentries which are
+// cousins should be able to share bfcache entries.
+
+var stateBackup = ss.getBrowserState();
+
+var state = {
+ entries: [
+ {
+ docIdentifier: 1,
+ url: "http://example.com?1",
+ triggeringPrincipal_base64,
+ children: [
+ {
+ docIdentifier: 10,
+ url: "http://example.com?10",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ {
+ docIdentifier: 1,
+ url: "http://example.com?1#a",
+ triggeringPrincipal_base64,
+ children: [
+ {
+ docIdentifier: 10,
+ url: "http://example.com?10#aa",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ ],
+};
+
+add_task(async function test() {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ await promiseTabState(tab, state);
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ function compareEntries(i, j, history) {
+ let e1 = history.getEntryAtIndex(i);
+ let e2 = history.getEntryAtIndex(j);
+
+ ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
+ is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
+
+ for (let c = 0; c < e1.childCount; c++) {
+ let c1 = e1.GetChildAt(c);
+ let c2 = e2.GetChildAt(c);
+
+ ok(
+ c1.sharesDocumentWith(c2),
+ `Cousins should share documents. (${i}, ${j}, ${c})`
+ );
+ }
+ }
+
+ let history = docShell.browsingContext.childSessionHistory.legacySHistory;
+
+ is(history.count, 2, "history.count");
+ for (let i = 0; i < history.count; i++) {
+ for (let j = 0; j < history.count; j++) {
+ compareEntries(i, j, history);
+ }
+ }
+ });
+ } else {
+ function compareEntries(i, j, history) {
+ let e1 = history.getEntryAtIndex(i);
+ let e2 = history.getEntryAtIndex(j);
+
+ ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
+ is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
+
+ for (let c = 0; c < e1.childCount; c++) {
+ let c1 = e1.GetChildAt(c);
+ let c2 = e2.GetChildAt(c);
+
+ ok(
+ c1.sharesDocumentWith(c2),
+ `Cousins should share documents. (${i}, ${j}, ${c})`
+ );
+ }
+ }
+
+ let history = tab.linkedBrowser.browsingContext.sessionHistory;
+
+ is(history.count, 2, "history.count");
+ for (let i = 0; i < history.count; i++) {
+ for (let j = 0; j < history.count; j++) {
+ compareEntries(i, j, history);
+ }
+ }
+ }
+
+ ss.setBrowserState(stateBackup);
+});
diff --git a/browser/components/sessionstore/test/browser_694378.js b/browser/components/sessionstore/test/browser_694378.js
new file mode 100644
index 0000000000..fc6619b9ab
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_694378.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test Summary:
+// 1. call ss.setWindowState with a broken state
+// 1a. ensure that it doesn't throw.
+
+add_task(async function test_brokenWindowState() {
+ let brokenState = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }] },
+ ],
+ },
+ ],
+ selectedWindow: 2,
+ };
+
+ let gotError = false;
+ try {
+ await setWindowState(window, brokenState, true);
+ } catch (ex) {
+ gotError = true;
+ info(ex);
+ }
+
+ ok(!gotError, "ss.setWindowState did not throw an error");
+
+ // Make sure that we reset the state. Use a full state just in case things get crazy.
+ let blankState = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ ],
+ },
+ ],
+ };
+ await promiseBrowserState(blankState);
+});
diff --git a/browser/components/sessionstore/test/browser_701377.js b/browser/components/sessionstore/test/browser_701377.js
new file mode 100644
index 0000000000..3f07aea6bb
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_701377.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "http://example.com#1", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "http://example.com#2", triggeringPrincipal_base64 },
+ ],
+ hidden: true,
+ },
+ ],
+ },
+ ],
+};
+
+function test() {
+ waitForExplicitFinish();
+
+ newWindowWithState(state, function (aWindow) {
+ let tab = aWindow.gBrowser.tabs[1];
+ ok(tab.hidden, "the second tab is hidden");
+
+ let tabShown = false;
+ let tabShowCallback = () => (tabShown = true);
+ tab.addEventListener("TabShow", tabShowCallback);
+
+ let tabState = ss.getTabState(tab);
+ ss.setTabState(tab, tabState);
+
+ tab.removeEventListener("TabShow", tabShowCallback);
+ ok(tab.hidden && !tabShown, "tab remains hidden");
+
+ finish();
+ });
+}
+
+// ----------
+function newWindowWithState(aState, aCallback) {
+ let opts = "chrome,all,dialog=no,height=800,width=800";
+ let win = window.openDialog(AppConstants.BROWSER_CHROME_URL, "_blank", opts);
+
+ registerCleanupFunction(() => BrowserTestUtils.closeWindow(win));
+
+ whenWindowLoaded(win, function onWindowLoaded(aWin) {
+ ss.setWindowState(aWin, JSON.stringify(aState), true);
+ executeSoon(() => aCallback(aWin));
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_705597.js b/browser/components/sessionstore/test/browser_705597.js
new file mode 100644
index 0000000000..d497e46a97
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_705597.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+var tabState = {
+ entries: [
+ {
+ url: "about:robots",
+ triggeringPrincipal_base64,
+ children: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
+ },
+ ],
+};
+
+function test() {
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ Services.prefs.setIntPref("browser.sessionstore.interval", 4000);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+ });
+
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+
+ let browser = tab.linkedBrowser;
+
+ promiseTabState(tab, tabState).then(() => {
+ let entry;
+ if (!Services.appinfo.sessionHistoryInParent) {
+ let sessionHistory = browser.sessionHistory;
+ entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
+ } else {
+ let sessionHistory = browser.browsingContext.sessionHistory;
+ entry = sessionHistory.getEntryAtIndex(0);
+ }
+
+ whenChildCount(entry, 1, function () {
+ whenChildCount(entry, 2, function () {
+ promiseBrowserLoaded(browser)
+ .then(() => {
+ return TabStateFlusher.flush(browser);
+ })
+ .then(() => {
+ let { entries } = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "tab has one history entry");
+ ok(!entries[0].children, "history entry has no subframes");
+
+ // Make sure that we reset the state.
+ let blankState = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "about:blank", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ waitForBrowserState(blankState, finish);
+ });
+
+ // Force reload the browser to deprecate the subframes.
+ browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ });
+
+ // Create a dynamic subframe.
+ let doc = browser.contentDocument;
+ let iframe = doc.createElement("iframe");
+ doc.body.appendChild(iframe);
+ iframe.setAttribute("src", "about:mozilla");
+ });
+ });
+}
+
+function whenChildCount(aEntry, aChildCount, aCallback) {
+ if (aEntry.childCount == aChildCount) {
+ aCallback();
+ } else {
+ setTimeout(() => whenChildCount(aEntry, aChildCount, aCallback), 100);
+ }
+}
diff --git a/browser/components/sessionstore/test/browser_707862.js b/browser/components/sessionstore/test/browser_707862.js
new file mode 100644
index 0000000000..765c63257f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_707862.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+var tabState = {
+ entries: [
+ {
+ url: "about:robots",
+ triggeringPrincipal_base64,
+ children: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
+ },
+ ],
+};
+
+function test() {
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ Services.prefs.setIntPref("browser.sessionstore.interval", 4000);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.sessionstore.interval");
+ });
+
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+
+ let browser = tab.linkedBrowser;
+
+ promiseTabState(tab, tabState).then(() => {
+ let entry;
+ if (!Services.appinfo.sessionHistoryInParent) {
+ let sessionHistory = browser.sessionHistory;
+ entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
+ } else {
+ let sessionHistory = browser.browsingContext.sessionHistory;
+ entry = sessionHistory.getEntryAtIndex(0);
+ }
+
+ whenChildCount(entry, 1, function () {
+ whenChildCount(entry, 2, function () {
+ promiseBrowserLoaded(browser).then(() => {
+ let newEntry;
+ if (!Services.appinfo.sessionHistoryInParent) {
+ let newSessionHistory = browser.sessionHistory;
+ newEntry = newSessionHistory.legacySHistory.getEntryAtIndex(0);
+ } else {
+ let newSessionHistory = browser.browsingContext.sessionHistory;
+ newEntry = newSessionHistory.getEntryAtIndex(0);
+ }
+
+ whenChildCount(newEntry, 0, function () {
+ // Make sure that we reset the state.
+ let blankState = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "about:blank", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ waitForBrowserState(blankState, finish);
+ });
+ });
+
+ // Force reload the browser to deprecate the subframes.
+ browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ });
+
+ // Create a dynamic subframe.
+ let doc = browser.contentDocument;
+ let iframe = doc.createElement("iframe");
+ doc.body.appendChild(iframe);
+ iframe.setAttribute("src", "about:mozilla");
+ });
+ });
+
+ // This test relies on the test timing out in order to indicate failure so
+ // let's add a dummy pass.
+ ok(
+ true,
+ "Each test requires at least one pass, fail or todo so here is a pass."
+ );
+}
+
+function whenChildCount(aEntry, aChildCount, aCallback) {
+ if (aEntry.childCount == aChildCount) {
+ aCallback();
+ } else {
+ setTimeout(() => whenChildCount(aEntry, aChildCount, aCallback), 100);
+ }
+}
diff --git a/browser/components/sessionstore/test/browser_739531.js b/browser/components/sessionstore/test/browser_739531.js
new file mode 100644
index 0000000000..507d10a5f1
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_739531.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that attempts made to save/restore ("duplicate") pages
+// using designmode AND make changes to document structure (remove body)
+// don't result in uncaught errors and a broken browser state.
+
+function test() {
+ waitForExplicitFinish();
+
+ let testURL =
+ "http://mochi.test:8888/browser/" +
+ "browser/components/sessionstore/test/browser_739531_sample.html";
+
+ let loadCount = 0;
+ let tab = BrowserTestUtils.addTab(gBrowser, testURL);
+
+ let removeFunc;
+ removeFunc = BrowserTestUtils.addContentEventListener(
+ tab.linkedBrowser,
+ "load",
+ function onLoad(aEvent) {
+ // make sure both the page and the frame are loaded
+ if (++loadCount < 2) {
+ return;
+ }
+ removeFunc();
+
+ // executeSoon to allow the JS to execute on the page
+ executeSoon(function () {
+ let tab2;
+ let caughtError = false;
+ try {
+ tab2 = ss.duplicateTab(window, tab);
+ } catch (e) {
+ caughtError = true;
+ info(e);
+ }
+
+ is(gBrowser.tabs.length, 3, "there should be 3 tabs");
+
+ ok(!caughtError, "duplicateTab didn't throw");
+
+ // if the test fails, we don't want to try to close a tab that doesn't exist
+ if (tab2) {
+ gBrowser.removeTab(tab2);
+ }
+ gBrowser.removeTab(tab);
+
+ finish();
+ });
+ },
+ true
+ );
+}
diff --git a/browser/components/sessionstore/test/browser_739531_frame.html b/browser/components/sessionstore/test/browser_739531_frame.html
new file mode 100644
index 0000000000..10f045a394
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_739531_frame.html
@@ -0,0 +1 @@
+<body><html>1</html></body>
diff --git a/browser/components/sessionstore/test/browser_739531_sample.html b/browser/components/sessionstore/test/browser_739531_sample.html
new file mode 100644
index 0000000000..e6c48ff26c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_739531_sample.html
@@ -0,0 +1,23 @@
+<!-- originally a crash test for bug 713417
+ https://bug713417.bugzilla.mozilla.org/attachment.cgi?id=584240 -->
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+
+function boom() {
+ var w = document.getElementById("f").contentWindow;
+ var d = w.document;
+ d.designMode = "on";
+ var r = d.documentElement;
+ d.removeChild(r);
+ document.adoptNode(r);
+}
+
+</script>
+</head>
+<body onload="boom();">
+<iframe src="browser_739531_frame.html" id="f"></iframe>
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_739805.js b/browser/components/sessionstore/test/browser_739805.js
new file mode 100644
index 0000000000..94be3f3d35
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_739805.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var url = "data:text/html;charset=utf-8,<input%20id='foo'>";
+var tabState = {
+ entries: [{ url, triggeringPrincipal_base64 }],
+ formdata: { id: { foo: "bar" }, url },
+};
+
+function test() {
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+
+ registerCleanupFunction(function () {
+ if (gBrowser.tabs.length > 1) {
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ }
+ Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+ });
+
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+
+ promiseBrowserLoaded(browser).then(() => {
+ isnot(gBrowser.selectedTab, tab, "newly created tab is not selected");
+
+ ss.setTabState(tab, JSON.stringify(tabState));
+ is(
+ ss.getInternalObjectState(browser),
+ TAB_STATE_NEEDS_RESTORE,
+ "tab needs restoring"
+ );
+
+ let { formdata } = JSON.parse(ss.getTabState(tab));
+ is(formdata && formdata.id.foo, "bar", "tab state's formdata is valid");
+
+ promiseTabRestored(tab).then(() => {
+ SpecialPowers.spawn(browser, [], function () {
+ let input = content.document.getElementById("foo");
+ is(input.value, "bar", "formdata has been restored correctly");
+ }).then(() => {
+ finish();
+ });
+ });
+
+ // Restore the tab by selecting it.
+ gBrowser.selectedTab = tab;
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_819510_perwindowpb.js b/browser/components/sessionstore/test/browser_819510_perwindowpb.js
new file mode 100644
index 0000000000..8dd41ef055
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_819510_perwindowpb.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test opening default mochitest-normal-private-normal-private windows
+// (saving the state with last window being private)
+
+requestLongerTimeout(2);
+
+add_task(async function test_1() {
+ let win = await promiseNewWindowLoaded();
+ await promiseTabLoad(win, "http://www.example.com/#1");
+
+ win = await promiseNewWindowLoaded({ private: true });
+ await promiseTabLoad(win, "http://www.example.com/#2");
+
+ win = await promiseNewWindowLoaded();
+ await promiseTabLoad(win, "http://www.example.com/#3");
+
+ win = await promiseNewWindowLoaded({ private: true });
+ await promiseTabLoad(win, "http://www.example.com/#4");
+
+ let curState = JSON.parse(ss.getBrowserState());
+ is(curState.windows.length, 5, "Browser has opened 5 windows");
+ is(curState.windows[2].isPrivate, true, "Window is private");
+ is(curState.windows[4].isPrivate, true, "Last window is private");
+ is(curState.selectedWindow, 5, "Last window opened is the one selected");
+
+ let state = JSON.parse(await promiseRecoveryFileContents());
+
+ is(
+ state.windows.length,
+ 2,
+ "sessionstore state: 2 windows in data being written to disk"
+ );
+ is(
+ state.selectedWindow,
+ 2,
+ "Selected window is updated to match one of the saved windows"
+ );
+ ok(
+ state.windows.every(win2 => !win2.isPrivate),
+ "Saved windows are not private"
+ );
+ is(
+ state._closedWindows.length,
+ 0,
+ "sessionstore state: no closed windows in data being written to disk"
+ );
+
+ // Cleanup.
+ await promiseAllButPrimaryWindowClosed();
+ forgetClosedWindows();
+});
+
+// Test opening default mochitest window + 2 private windows
+add_task(async function test_2() {
+ let win = await promiseNewWindowLoaded({ private: true });
+ await promiseTabLoad(win, "http://www.example.com/#1");
+
+ win = await promiseNewWindowLoaded({ private: true });
+ await promiseTabLoad(win, "http://www.example.com/#2");
+
+ let curState = JSON.parse(ss.getBrowserState());
+ is(curState.windows.length, 3, "Browser has opened 3 windows");
+ is(curState.windows[1].isPrivate, true, "Window 1 is private");
+ is(curState.windows[2].isPrivate, true, "Window 2 is private");
+ is(curState.selectedWindow, 3, "Last window opened is the one selected");
+
+ let state = JSON.parse(await promiseRecoveryFileContents());
+
+ is(
+ state.windows.length,
+ 0,
+ "sessionstore state: no window in data being written to disk"
+ );
+ is(
+ state.selectedWindow,
+ 0,
+ "Selected window updated to 0 given there are no saved windows"
+ );
+ is(
+ state._closedWindows.length,
+ 0,
+ "sessionstore state: no closed windows in data being written to disk"
+ );
+
+ // Cleanup.
+ await promiseAllButPrimaryWindowClosed();
+ forgetClosedWindows();
+});
+
+// Test opening default-normal-private-normal windows and closing a normal window
+add_task(async function test_3() {
+ let normalWindow = await promiseNewWindowLoaded();
+ await promiseTabLoad(normalWindow, "http://www.example.com/#1");
+
+ let win = await promiseNewWindowLoaded({ private: true });
+ await promiseTabLoad(win, "http://www.example.com/#2");
+
+ win = await promiseNewWindowLoaded();
+ await promiseTabLoad(win, "http://www.example.com/#3");
+
+ let curState = JSON.parse(ss.getBrowserState());
+ is(curState.windows.length, 4, "Browser has opened 4 windows");
+ is(curState.windows[2].isPrivate, true, "Window 2 is private");
+ is(curState.selectedWindow, 4, "Last window opened is the one selected");
+
+ await BrowserTestUtils.closeWindow(normalWindow);
+
+ // Pin and unpin a tab before checking the written state so that
+ // the list of restoring windows gets cleared. Otherwise the
+ // window we just closed would be marked as not closed.
+ let tab = win.gBrowser.tabs[0];
+ win.gBrowser.pinTab(tab);
+ win.gBrowser.unpinTab(tab);
+
+ let state = JSON.parse(await promiseRecoveryFileContents());
+
+ is(
+ state.windows.length,
+ 1,
+ "sessionstore state: 1 window in data being written to disk"
+ );
+ is(
+ state.selectedWindow,
+ 1,
+ "Selected window is updated to match one of the saved windows"
+ );
+ ok(
+ state.windows.every(win2 => !win2.isPrivate),
+ "Saved windows are not private"
+ );
+ is(
+ state._closedWindows.length,
+ 1,
+ "sessionstore state: 1 closed window in data being written to disk"
+ );
+ ok(
+ state._closedWindows.every(win2 => !win2.isPrivate),
+ "Closed windows are not private"
+ );
+
+ // Cleanup.
+ await promiseAllButPrimaryWindowClosed();
+ forgetClosedWindows();
+});
+
+async function promiseTabLoad(win, url) {
+ let tab = BrowserTestUtils.addTab(win.gBrowser, url);
+ await promiseBrowserLoaded(tab.linkedBrowser);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+}
diff --git a/browser/components/sessionstore/test/browser_906076_lazy_tabs.js b/browser/components/sessionstore/test/browser_906076_lazy_tabs.js
new file mode 100644
index 0000000000..2fab82ca01
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_906076_lazy_tabs.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_STATE = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ {
+ entries: [{ url: "http://example.com", triggeringPrincipal_base64 }],
+ },
+ ],
+ },
+ ],
+};
+
+const TEST_STATE_2 = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:robots", triggeringPrincipal_base64 }] },
+ {
+ entries: [],
+ userTypedValue: "http://example.com",
+ userTypedClear: 1,
+ },
+ ],
+ },
+ ],
+};
+
+function countNonLazyTabs(win) {
+ win = win || window;
+ let count = 0;
+ for (let browser of win.gBrowser.browsers) {
+ if (browser.isConnected) {
+ count++;
+ }
+ }
+ return count;
+}
+
+/**
+ * Test that lazy browsers do not get prematurely inserted by
+ * code accessing browser bound properties on the unbound browser.
+ */
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.sessionstore.restore_on_demand", true],
+ ["browser.sessionstore.restore_tabs_lazily", true],
+ ],
+ });
+
+ let backupState = SessionStore.getBrowserState();
+
+ await promiseBrowserState(TEST_STATE);
+
+ info(
+ "Check that no lazy browsers get unnecessarily inserted after session restore"
+ );
+ is(countNonLazyTabs(), 1, "Window has only 1 non-lazy tab");
+
+ await TestUtils.topicObserved("sessionstore-state-write-complete");
+
+ // When sessionstore write occurs, tabs are checked for state changes.
+ // Make sure none of them insert their browsers when this happens.
+ info("Check that no lazy browsers get inserted after sessionstore write");
+ is(countNonLazyTabs(), 1, "Window has only 1 non-lazy tab");
+
+ info("Check that lazy browser gets inserted properly");
+ ok(
+ !gBrowser.browsers[1].isConnected,
+ "The browser that we're attempting to insert is indeed lazy"
+ );
+ gBrowser._insertBrowser(gBrowser.tabs[1]);
+ is(countNonLazyTabs(), 2, "Window now has 2 non-lazy tabs");
+
+ // Check if any lazy tabs got inserted when window closes.
+ let newWindow = await promiseNewWindowLoaded();
+
+ SessionStore.setWindowState(newWindow, JSON.stringify(TEST_STATE));
+
+ await new Promise(resolve => {
+ newWindow.addEventListener(
+ "unload",
+ () => {
+ info("Check that no lazy browsers get inserted when window closes");
+ is(countNonLazyTabs(newWindow), 1, "Window has only 1 non-lazy tab");
+
+ info(
+ "Check that it is not possible to insert a lazy browser after the window closed"
+ );
+ ok(
+ !newWindow.gBrowser.browsers[1].isConnected,
+ "The browser that we're attempting to insert is indeed lazy"
+ );
+ newWindow.gBrowser._insertBrowser(newWindow.gBrowser.tabs[1]);
+ is(
+ countNonLazyTabs(newWindow),
+ 1,
+ "Window still has only 1 non-lazy tab"
+ );
+
+ resolve();
+ },
+ { once: true }
+ );
+
+ newWindow.close();
+ });
+
+ // Bug 1365933.
+ info(
+ "Check that session with tab having empty entries array gets restored properly"
+ );
+ await promiseBrowserState(TEST_STATE_2);
+
+ is(gBrowser.tabs.length, 2, "Window has 2 tabs");
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:robots",
+ "Tab has the expected URL"
+ );
+
+ gBrowser.selectedTab = gBrowser.tabs[1];
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "http://example.com/",
+ "Tab has the expected URL"
+ );
+
+ // Cleanup.
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_911547.js b/browser/components/sessionstore/test/browser_911547.js
new file mode 100644
index 0000000000..1068d8e14b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_911547.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test tests that session restore component does restore the right
+// content security policy with the document. (The policy being tested
+// disallows inline scripts).
+
+add_task(async function test() {
+ // allow top level data: URI navigations, otherwise clicking a data: link fails
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", false]],
+ });
+ // create a tab that has a CSP
+ let testURL =
+ "http://mochi.test:8888/browser/browser/components/sessionstore/test/browser_911547_sample.html";
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, testURL));
+ gBrowser.selectedTab = tab;
+
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // this is a baseline to ensure CSP is active
+ // attempt to inject and run a script via inline (pre-restore, allowed)
+ await injectInlineScript(
+ browser,
+ `document.getElementById("test_id1").value = "id1_modified";`
+ );
+
+ let loadedPromise = promiseBrowserLoaded(browser);
+ await SpecialPowers.spawn(browser, [], function () {
+ is(
+ content.document.getElementById("test_id1").value,
+ "id1_initial",
+ "CSP should block the inline script that modifies test_id"
+ );
+ content.document.getElementById("test_data_link").click();
+ });
+
+ await loadedPromise;
+
+ await SpecialPowers.spawn(browser, [], function () {
+ // eslint-disable-line
+ // the data: URI inherits the CSP and the inline script needs to be blocked
+ is(
+ content.document.getElementById("test_id2").value,
+ "id2_initial",
+ "CSP should block the script loaded by the clicked data URI"
+ );
+ });
+
+ // close the tab
+ await promiseRemoveTabAndSessionState(tab);
+
+ // open new tab and recover the state
+ tab = ss.undoCloseTab(window, 0);
+ await promiseTabRestored(tab);
+ browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [], function () {
+ // eslint-disable-line
+ // the data: URI should be restored including the inherited CSP and the
+ // inline script should be blocked.
+ is(
+ content.document.getElementById("test_id2").value,
+ "id2_initial",
+ "CSP should block the script loaded by the clicked data URI after restore"
+ );
+ });
+
+ // clean up
+ gBrowser.removeTab(tab);
+});
+
+// injects an inline script element (with a text body)
+function injectInlineScript(browser, scriptText) {
+ return SpecialPowers.spawn(browser, [scriptText], function (text) {
+ let scriptElt = content.document.createElement("script");
+ scriptElt.type = "text/javascript";
+ scriptElt.text = text;
+ content.document.body.appendChild(scriptElt);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_911547_sample.html b/browser/components/sessionstore/test/browser_911547_sample.html
new file mode 100644
index 0000000000..9d2706c008
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_911547_sample.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test 911547</title>
+ </head>
+<body>
+
+ <!--
+ this element gets modified by an injected script;
+ that script should be blocked by CSP
+ -->
+ <input type="text" id="test_id1" value="id1_initial">
+
+ <a id="test_data_link" href="data:text/html;charset=utf-8,<input type='text' id='test_id2' value='id2_initial'/> <script>document.getElementById('test_id2').value = 'id2_modified';</script>">Test Link</a>
+
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_911547_sample.html^headers^ b/browser/components/sessionstore/test/browser_911547_sample.html^headers^
new file mode 100644
index 0000000000..4623dec303
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_911547_sample.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: script-src 'self'
diff --git a/browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js b/browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js
new file mode 100644
index 0000000000..8bb21620e4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_aboutPrivateBrowsing.js
@@ -0,0 +1,24 @@
+"use strict";
+
+// Tests that an about:privatebrowsing tab with no history will not
+// be saved into session store and thus, it will not show up in
+// Recently Closed Tabs.
+
+add_task(async function () {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:privatebrowsing");
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ is(
+ gBrowser.browsers[1].currentURI.spec,
+ "about:privatebrowsing",
+ "we will be removing an about:privatebrowsing tab"
+ );
+
+ let r = `rand-${Math.random()}`;
+ ss.setCustomTabValue(tab, "foobar", r);
+
+ await promiseRemoveTabAndSessionState(tab);
+ let closedTabData = JSON.stringify(ss.getClosedTabDataForWindow(window));
+ ok(!closedTabData.includes(r), "tab not stored in _closedTabs");
+});
diff --git a/browser/components/sessionstore/test/browser_aboutSessionRestore.js b/browser/components/sessionstore/test/browser_aboutSessionRestore.js
new file mode 100644
index 0000000000..b98d08ae12
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_aboutSessionRestore.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CRASH_URL = "about:mozilla";
+const CRASH_FAVICON = "chrome://branding/content/icon32.png";
+const CRASH_SHENTRY = { url: CRASH_URL, triggeringPrincipal_base64 };
+const CRASH_TAB = { entries: [CRASH_SHENTRY], image: CRASH_FAVICON };
+const CRASH_STATE = { windows: [{ tabs: [CRASH_TAB] }] };
+
+const TAB_URL = "about:sessionrestore";
+const TAB_FORMDATA = { url: TAB_URL, id: { sessionData: CRASH_STATE } };
+const TAB_SHENTRY = { url: TAB_URL, triggeringPrincipal_base64 };
+const TAB_STATE = { entries: [TAB_SHENTRY], formdata: TAB_FORMDATA };
+
+add_task(async function () {
+ // Prepare a blank tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Fake a post-crash tab.
+ ss.setTabState(tab, JSON.stringify(TAB_STATE));
+ await promiseTabRestored(tab);
+
+ ok(gBrowser.tabs.length > 1, "we have more than one tab");
+
+ let tabsToggle = browser.contentDocument.getElementById("tabsToggle");
+ tabsToggle.click();
+ await BrowserTestUtils.waitForCondition(
+ () => browser.contentWindow.gTreeInitialized
+ );
+ let tree = browser.contentDocument.getElementById("tabList");
+ let view = tree.view;
+ ok(view.isContainer(0), "first entry is the window");
+ let titleColumn = tree.columns.title;
+ is(
+ view.getCellProperties(1, titleColumn),
+ "icon",
+ "second entry is the tab and has a favicon"
+ );
+
+ let newWindowOpened = BrowserTestUtils.waitForNewWindow();
+
+ SpecialPowers.spawn(browser.browsingContext, [], () => {
+ content.document.getElementById("errorTryAgain").click();
+ });
+
+ // Wait until the new window was restored.
+ let win = await newWindowOpened;
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+
+ let [
+ {
+ tabs: [
+ {
+ entries: [{ url }],
+ },
+ ],
+ },
+ ] = ss.getClosedWindowData();
+ is(url, CRASH_URL, "session was restored correctly");
+ ss.forgetClosedWindow(0);
+});
diff --git a/browser/components/sessionstore/test/browser_async_duplicate_tab.js b/browser/components/sessionstore/test/browser_async_duplicate_tab.js
new file mode 100644
index 0000000000..e592df93ed
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_duplicate_tab.js
@@ -0,0 +1,87 @@
+"use strict";
+
+const PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://example.com/"
+);
+const URL = PATH + "file_async_duplicate_tab.html";
+
+add_task(async function test_duplicate() {
+ // Create new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ await TabStateFlusher.flush(browser);
+
+ // Click the link to navigate, this will add second shistory entry.
+ await SpecialPowers.spawn(browser, [], async function () {
+ return new Promise(resolve => {
+ docShell.chromeEventHandler.addEventListener(
+ "hashchange",
+ () => resolve(),
+ { once: true, capture: true }
+ );
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Duplicate the tab.
+ let tab2 = ss.duplicateTab(window, tab);
+
+ // Wait until the tab has fully restored.
+ await promiseTabRestored(tab2);
+ await TabStateFlusher.flush(tab2.linkedBrowser);
+
+ // There should be two history entries now.
+ let { entries } = JSON.parse(ss.getTabState(tab2));
+ is(entries.length, 2, "there are two shistory entries");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_duplicate_remove() {
+ // Create new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ await TabStateFlusher.flush(browser);
+
+ // Click the link to navigate, this will add second shistory entry.
+ await SpecialPowers.spawn(browser, [], async function () {
+ return new Promise(resolve => {
+ docShell.chromeEventHandler.addEventListener(
+ "hashchange",
+ () => resolve(),
+ { once: true, capture: true }
+ );
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Duplicate the tab.
+ let tab2 = ss.duplicateTab(window, tab);
+
+ // Before the duplication finished, remove the tab.
+ await Promise.all([
+ promiseRemoveTabAndSessionState(tab),
+ promiseTabRestored(tab2),
+ ]);
+ await TabStateFlusher.flush(tab2.linkedBrowser);
+
+ // There should be two history entries now.
+ let { entries } = JSON.parse(ss.getTabState(tab2));
+ is(entries.length, 2, "there are two shistory entries");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/sessionstore/test/browser_async_flushes.js b/browser/components/sessionstore/test/browser_async_flushes.js
new file mode 100644
index 0000000000..e35593dc30
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_flushes.js
@@ -0,0 +1,131 @@
+"use strict";
+
+const PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://example.com/"
+);
+const URL = PATH + "file_async_flushes.html";
+
+add_task(async function test_flush() {
+ // Create new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ await TabStateFlusher.flush(browser);
+
+ // There should be one history entry.
+ let { entries } = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is a single history entry");
+
+ // Click the link to navigate, this will add second shistory entry.
+ await SpecialPowers.spawn(browser, [], async function () {
+ return new Promise(resolve => {
+ docShell.chromeEventHandler.addEventListener(
+ "hashchange",
+ () => resolve(),
+ { once: true, capture: true }
+ );
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Flush to empty any queued update messages.
+ await TabStateFlusher.flush(browser);
+
+ // There should be two history entries now.
+ ({ entries } = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 2, "there are two shistory entries");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_crash() {
+ if (Services.appinfo.sessionHistoryInParent) {
+ // This test relies on frame script message ordering. Since the frame script
+ // is unused with SHIP, there's no guarantee that we'll crash the frame
+ // before we've started the flush.
+ ok(true, "Test relies on frame script message ordering.");
+ return;
+ }
+
+ // Create new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ await TabStateFlusher.flush(browser);
+
+ // There should be one history entry.
+ let { entries } = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is a single history entry");
+
+ // Click the link to navigate.
+ await SpecialPowers.spawn(browser, [], async function () {
+ return new Promise(resolve => {
+ docShell.chromeEventHandler.addEventListener(
+ "hashchange",
+ () => resolve(),
+ { once: true, capture: true }
+ );
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Crash the browser and flush. Both messages are async and will be sent to
+ // the content process. The "crash" message makes it first so that we don't
+ // get a chance to process the flush. The TabStateFlusher however should be
+ // notified so that the flush still completes.
+ let promise1 = BrowserTestUtils.crashFrame(browser);
+ let promise2 = TabStateFlusher.flush(browser);
+ await Promise.all([promise1, promise2]);
+
+ // The pending update should be lost.
+ ({ entries } = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 1, "still only one history entry");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_remove() {
+ // Create new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Flush to empty any queued update messages.
+ await TabStateFlusher.flush(browser);
+
+ // There should be one history entry.
+ let { entries } = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is a single history entry");
+
+ // Click the link to navigate.
+ await SpecialPowers.spawn(browser, [], async function () {
+ return new Promise(resolve => {
+ docShell.chromeEventHandler.addEventListener(
+ "hashchange",
+ () => resolve(),
+ { once: true, capture: true }
+ );
+
+ // Click the link.
+ content.document.querySelector("a").click();
+ });
+ });
+
+ // Request a flush and remove the tab. The flush should still complete.
+ await Promise.all([
+ TabStateFlusher.flush(browser),
+ promiseRemoveTabAndSessionState(tab),
+ ]);
+});
diff --git a/browser/components/sessionstore/test/browser_async_remove_tab.js b/browser/components/sessionstore/test/browser_async_remove_tab.js
new file mode 100644
index 0000000000..6f744ade3c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_remove_tab.js
@@ -0,0 +1,209 @@
+"use strict";
+
+async function createTabWithRandomValue(url) {
+ let tab = BrowserTestUtils.addTab(gBrowser, url);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Set a random value.
+ let r = `rand-${Math.random()}`;
+ ss.setCustomTabValue(tab, "foobar", r);
+
+ // Flush to ensure there are no scheduled messages.
+ await TabStateFlusher.flush(browser);
+
+ return { tab, r };
+}
+
+function isValueInClosedData(rval) {
+ return JSON.stringify(ss.getClosedTabDataForWindow(window)).includes(rval);
+}
+
+function restoreClosedTabWithValue(rval) {
+ let closedTabData = ss.getClosedTabDataForWindow(window);
+ let index = closedTabData.findIndex(function (data) {
+ return (data.state.extData && data.state.extData.foobar) == rval;
+ });
+
+ if (index == -1) {
+ throw new Error("no closed tab found for given rval");
+ }
+
+ return ss.undoCloseTab(window, index);
+}
+
+add_task(async function dont_save_empty_tabs() {
+ let { tab, r } = await createTabWithRandomValue("about:blank");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTabAndSessionState(tab);
+
+ // No tab state worth saving.
+ ok(!isValueInClosedData(r), "closed tab not saved");
+ await promise;
+
+ // Still no tab state worth saving.
+ ok(!isValueInClosedData(r), "closed tab not saved");
+});
+
+add_task(async function save_worthy_tabs_remote() {
+ let { tab, r } = await createTabWithRandomValue("https://example.com/");
+ ok(tab.linkedBrowser.isRemoteBrowser, "browser is remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTabAndSessionState(tab);
+
+ // Tab state deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+ await promise;
+
+ // Tab state still deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(async function save_worthy_tabs_nonremote() {
+ let { tab, r } = await createTabWithRandomValue("about:robots");
+ ok(!tab.linkedBrowser.isRemoteBrowser, "browser is not remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTabAndSessionState(tab);
+
+ // Tab state deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+ await promise;
+
+ // Tab state still deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(async function save_worthy_tabs_remote_final() {
+ let { tab, r } = await createTabWithRandomValue("about:blank");
+ let browser = tab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "browser is remote");
+
+ // Replace about:blank with a new remote page.
+ let entryReplaced = promiseOnHistoryReplaceEntry(browser);
+ browser.loadURI(Services.io.newURI("https://example.com/"), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ await entryReplaced;
+
+ // Remotness shouldn't have changed.
+ ok(browser.isRemoteBrowser, "browser is still remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTabAndSessionState(tab);
+
+ // With SHIP, we'll do the final tab state update sooner than we did before.
+ if (!Services.appinfo.sessionHistoryInParent) {
+ // No tab state worth saving (that we know about yet).
+ ok(!isValueInClosedData(r), "closed tab not saved");
+ }
+
+ await promise;
+
+ // Turns out there is a tab state worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(async function save_worthy_tabs_nonremote_final() {
+ let { tab, r } = await createTabWithRandomValue("about:blank");
+ let browser = tab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "browser is remote");
+
+ // Replace about:blank with a non-remote entry.
+ BrowserTestUtils.loadURIString(browser, "about:robots");
+ await BrowserTestUtils.browserLoaded(browser);
+ ok(!browser.isRemoteBrowser, "browser is not remote anymore");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTabAndSessionState(tab);
+
+ // With SHIP, we'll do the final tab state update sooner than we did before.
+ if (!Services.appinfo.sessionHistoryInParent) {
+ // No tab state worth saving (that we know about yet).
+ ok(!isValueInClosedData(r), "closed tab not saved");
+ }
+
+ await promise;
+
+ // Turns out there is a tab state worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+});
+
+add_task(async function dont_save_empty_tabs_final() {
+ let { tab, r } = await createTabWithRandomValue("https://example.com/");
+ let browser = tab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "browser is remote");
+
+ // Replace the current page with an about:blank entry.
+ let entryReplaced = promiseOnHistoryReplaceEntry(browser);
+
+ // We're doing a cross origin navigation, so we can't reliably use a
+ // SpecialPowers task here. Instead we just emulate a location.replace() call.
+ browser.loadURI(Services.io.newURI("about:blank"), {
+ loadFlags:
+ Ci.nsIWebNavigation.LOAD_FLAGS_STOP_CONTENT |
+ Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ await entryReplaced;
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTabAndSessionState(tab);
+
+ // With SHIP, we'll do the final tab state update sooner than we did before.
+ if (!Services.appinfo.sessionHistoryInParent) {
+ // Tab state deemed worth saving (yet).
+ ok(isValueInClosedData(r), "closed tab saved");
+ }
+
+ await promise;
+
+ // Turns out we don't want to save the tab state.
+ ok(!isValueInClosedData(r), "closed tab not saved");
+});
+
+add_task(async function undo_worthy_tabs() {
+ let { tab, r } = await createTabWithRandomValue("https://example.com/");
+ ok(tab.linkedBrowser.isRemoteBrowser, "browser is remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTabAndSessionState(tab);
+
+ // Tab state deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+
+ // Restore the closed tab before receiving its final message.
+ tab = restoreClosedTabWithValue(r);
+
+ // Wait for the final update message.
+ await promise;
+
+ // Check we didn't add the tab back to the closed list.
+ ok(!isValueInClosedData(r), "tab no longer closed");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function forget_worthy_tabs_remote() {
+ let { tab, r } = await createTabWithRandomValue("https://example.com/");
+ ok(tab.linkedBrowser.isRemoteBrowser, "browser is remote");
+
+ // Remove the tab before the update arrives.
+ let promise = promiseRemoveTabAndSessionState(tab);
+
+ // Tab state deemed worth saving.
+ ok(isValueInClosedData(r), "closed tab saved");
+
+ // Forget the closed tab.
+ ss.forgetClosedTab(window, 0);
+
+ // Wait for the final update message.
+ await promise;
+
+ // Check we didn't add the tab back to the closed list.
+ ok(!isValueInClosedData(r), "we forgot about the tab");
+});
diff --git a/browser/components/sessionstore/test/browser_async_window_flushing.js b/browser/components/sessionstore/test/browser_async_window_flushing.js
new file mode 100644
index 0000000000..56781896d8
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_window_flushing.js
@@ -0,0 +1,208 @@
+"use strict";
+
+const PAGE = "http://example.com/";
+
+/**
+ * Tests that if we initially discard a window as not interesting
+ * to save in the closed windows array, that we revisit that decision
+ * after a window flush has completed.
+ */
+add_task(async function test_add_interesting_window() {
+ // We want to suppress all non-final updates from the browser tabs
+ // so as to eliminate any racy-ness with this test.
+ await pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+ // Depending on previous tests, we might already have some closed
+ // windows stored. We'll use its length to determine whether or not
+ // the window was added or not.
+ let initialClosedWindows = ss.getClosedWindowCount();
+
+ // Make sure we can actually store another closed window
+ await pushPrefs([
+ "browser.sessionstore.max_windows_undo",
+ initialClosedWindows + 1,
+ ]);
+
+ // Create a new browser window. Since the default window will start
+ // at about:blank, SessionStore should find this tab (and therefore the
+ // whole window) uninteresting, and should not initially put it into
+ // the closed windows array.
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ let browser = newWin.gBrowser.selectedBrowser;
+
+ // Send a message that will cause the content to change its location
+ // to someplace more interesting. We've disabled auto updates from
+ // the browser, so the parent won't know about this
+ await SpecialPowers.spawn(browser, [PAGE], async function (newPage) {
+ content.location = newPage;
+ });
+
+ await promiseOnHistoryReplaceEntry(browser);
+
+ // Clear out the userTypedValue so that the new window looks like
+ // it's really not worth restoring.
+ browser.userTypedValue = null;
+
+ // Once this windowClosed Promise resolves, we should have finished
+ // the flush and revisited our decision to put this window into
+ // the closed windows array.
+ let windowClosed = BrowserTestUtils.windowClosed(newWin);
+
+ let handled = false;
+ whenDomWindowClosedHandled(() => {
+ // SessionStore's onClose handler should have just run.
+ let currentClosedWindows = ss.getClosedWindowCount();
+ is(
+ currentClosedWindows,
+ initialClosedWindows,
+ "We should not have added the window to the closed windows array"
+ );
+
+ handled = true;
+ });
+
+ // Ok, let's close the window.
+ newWin.close();
+
+ await windowClosed;
+
+ ok(handled, "domwindowclosed should already be handled here");
+
+ // The window flush has finished
+ let currentClosedWindows = ss.getClosedWindowCount();
+ is(
+ currentClosedWindows,
+ initialClosedWindows + 1,
+ "We should have added the window to the closed windows array"
+ );
+});
+
+/**
+ * Tests that if we initially store a closed window as interesting
+ * to save in the closed windows array, that we revisit that decision
+ * after a window flush has completed, and stop storing a window that
+ * we've deemed no longer interesting.
+ */
+add_task(async function test_remove_uninteresting_window() {
+ // We want to suppress all non-final updates from the browser tabs
+ // so as to eliminate any racy-ness with this test.
+ await pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+ // Depending on previous tests, we might already have some closed
+ // windows stored. We'll use its length to determine whether or not
+ // the window was added or not.
+ let initialClosedWindows = ss.getClosedWindowCount();
+
+ // Make sure we can actually store another closed window
+ await pushPrefs([
+ "browser.sessionstore.max_windows_undo",
+ initialClosedWindows + 1,
+ ]);
+
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Now browse the initial tab of that window to an interesting
+ // site.
+ let tab = newWin.gBrowser.selectedTab;
+ let browser = tab.linkedBrowser;
+ BrowserTestUtils.loadURIString(browser, PAGE);
+
+ await BrowserTestUtils.browserLoaded(browser, false, PAGE);
+ await TabStateFlusher.flush(browser);
+
+ // Send a message that will cause the content to purge its
+ // history entries and make itself seem uninteresting.
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Epic hackery to make this browser seem suddenly boring.
+ docShell.setCurrentURIForSessionStore(Services.io.newURI("about:blank"));
+
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ let { sessionHistory } = docShell.QueryInterface(Ci.nsIWebNavigation);
+ sessionHistory.legacySHistory.purgeHistory(sessionHistory.count);
+ }
+ });
+
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ let { sessionHistory } = browser.browsingContext;
+ sessionHistory.purgeHistory(sessionHistory.count);
+ }
+
+ // Once this windowClosed Promise resolves, we should have finished
+ // the flush and revisited our decision to put this window into
+ // the closed windows array.
+ let windowClosed = BrowserTestUtils.windowClosed(newWin);
+
+ let handled = false;
+ whenDomWindowClosedHandled(() => {
+ // SessionStore's onClose handler should have just run.
+ let currentClosedWindows = ss.getClosedWindowCount();
+ is(
+ currentClosedWindows,
+ initialClosedWindows + 1,
+ "We should have added the window to the closed windows array"
+ );
+
+ handled = true;
+ });
+
+ // Ok, let's close the window.
+ newWin.close();
+
+ await windowClosed;
+
+ ok(handled, "domwindowclosed should already be handled here");
+
+ // The window flush has finished
+ let currentClosedWindows = ss.getClosedWindowCount();
+ is(
+ currentClosedWindows,
+ initialClosedWindows,
+ "We should have removed the window from the closed windows array"
+ );
+});
+
+/**
+ * Tests that when we close a window, it is immediately removed from the
+ * _windows array.
+ */
+add_task(async function test_synchronously_remove_window_state() {
+ // Depending on previous tests, we might already have some closed
+ // windows stored. We'll use its length to determine whether or not
+ // the window was added or not.
+ let state = JSON.parse(ss.getBrowserState());
+ ok(state, "Make sure we can get the state");
+ let initialWindows = state.windows.length;
+
+ // Open a new window and send the first tab somewhere
+ // interesting.
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let browser = newWin.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURIString(browser, PAGE);
+ await BrowserTestUtils.browserLoaded(browser, false, PAGE);
+ await TabStateFlusher.flush(browser);
+
+ state = JSON.parse(ss.getBrowserState());
+ is(
+ state.windows.length,
+ initialWindows + 1,
+ "The new window to be in the state"
+ );
+
+ // Now close the window, and make sure that the window was removed
+ // from the windows list from the SessionState. We're specifically
+ // testing the case where the window is _not_ removed in between
+ // the close-initiated flush request and the flush response.
+ let windowClosed = BrowserTestUtils.windowClosed(newWin);
+ newWin.close();
+
+ state = JSON.parse(ss.getBrowserState());
+ is(
+ state.windows.length,
+ initialWindows,
+ "The new window should have been removed from the state"
+ );
+
+ // Wait for our window to go away
+ await windowClosed;
+});
diff --git a/browser/components/sessionstore/test/browser_attributes.js b/browser/components/sessionstore/test/browser_attributes.js
new file mode 100644
index 0000000000..336573a508
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_attributes.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test makes sure that we correctly preserve tab attributes when storing
+ * and restoring tabs. It also ensures that we skip special attributes like
+ * 'image', 'muted', and 'pending' that need to be
+ * handled differently or internally.
+ */
+
+const PREF = "browser.sessionstore.restore_on_demand";
+
+add_task(async function test() {
+ Services.prefs.setBoolPref(PREF, true);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
+
+ // Add a new tab with a nice icon.
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:robots");
+ await promiseBrowserLoaded(tab.linkedBrowser);
+
+ // Because there is debounce logic in ContentLinkHandler.jsm 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
+ );
+
+ // Check that the tab has an 'image' attribute.
+ ok(tab.hasAttribute("image"), "tab.image exists");
+
+ tab.toggleMuteAudio();
+ // Check that the tab has a 'muted' attribute.
+ ok(tab.hasAttribute("muted"), "tab.muted exists");
+
+ // Make sure we do not persist 'image' and 'muted' attributes.
+ ss.persistTabAttribute("image");
+ ss.persistTabAttribute("muted");
+ let { attributes } = JSON.parse(ss.getTabState(tab));
+ ok(!("image" in attributes), "'image' attribute not saved");
+ ok(!("muted" in attributes), "'muted' attribute not saved");
+ ok(!("custom" in attributes), "'custom' attribute not saved");
+
+ // Test persisting a custom attribute.
+ tab.setAttribute("custom", "foobar");
+ ss.persistTabAttribute("custom");
+
+ ({ attributes } = JSON.parse(ss.getTabState(tab)));
+ is(attributes.custom, "foobar", "'custom' attribute is correct");
+
+ // Make sure we're backwards compatible and restore old 'image' attributes.
+ let state = {
+ entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
+ attributes: { custom: "foobaz" },
+ image: gBrowser.getIcon(tab),
+ };
+
+ // Prepare a pending tab waiting to be restored.
+ let promise = promiseTabRestoring(tab);
+ ss.setTabState(tab, JSON.stringify(state));
+ await promise;
+
+ ok(tab.hasAttribute("pending"), "tab is pending");
+ is(gBrowser.getIcon(tab), state.image, "tab has correct icon");
+ ok(!state.attributes.image, "'image' attribute not saved");
+
+ // Let the pending tab load.
+ gBrowser.selectedTab = tab;
+ await promiseTabRestored(tab);
+
+ // Ensure no 'image' or 'pending' attributes are stored.
+ ({ attributes } = JSON.parse(ss.getTabState(tab)));
+ ok(!("image" in attributes), "'image' attribute not saved");
+ ok(!("pending" in attributes), "'pending' attribute not saved");
+ is(attributes.custom, "foobaz", "'custom' attribute is correct");
+
+ // Clean up.
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_background_tab_crash.js b/browser/components/sessionstore/test/browser_background_tab_crash.js
new file mode 100644
index 0000000000..91646b9550
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_background_tab_crash.js
@@ -0,0 +1,262 @@
+"use strict";
+
+/**
+ * These tests the behaviour of the browser when background tabs crash,
+ * while the foreground tab remains.
+ *
+ * The current behavioural rule is this: if only background tabs crash,
+ * then only the first tab shown of that group should show the tab crash
+ * page, and subsequent ones should restore on demand.
+ */
+
+/**
+ * Makes the current browser tab non-remote, and then sets up two remote
+ * background tabs, ensuring that both belong to the same content process.
+ * Callers should pass in a testing function that will execute (and possibly
+ * yield Promises) taking the created background tabs as arguments. Once
+ * the testing function completes, this function will take care of closing
+ * the opened tabs.
+ *
+ * @param testFn (function)
+ * A Promise-generating function that will be called once the tabs
+ * are opened and ready.
+ * @return Promise
+ * Resolves once the testing function completes and the opened tabs
+ * have been completely closed.
+ */
+async function setupBackgroundTabs(testFn) {
+ const REMOTE_PAGE = "http://www.example.com";
+ const NON_REMOTE_PAGE = "about:mozilla";
+
+ // Browse the initial tab to a non-remote page, which we'll have in the
+ // foreground.
+ let initialTab = gBrowser.selectedTab;
+ let initialBrowser = initialTab.linkedBrowser;
+ BrowserTestUtils.loadURIString(initialBrowser, NON_REMOTE_PAGE);
+ await BrowserTestUtils.browserLoaded(initialBrowser);
+ // Quick sanity check - the browser should be non remote.
+ Assert.ok(
+ !initialBrowser.isRemoteBrowser,
+ "Initial browser should not be remote."
+ );
+
+ // Open some tabs that should be running in the content process.
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REMOTE_PAGE);
+ let remoteBrowser1 = tab1.linkedBrowser;
+ await TabStateFlusher.flush(remoteBrowser1);
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REMOTE_PAGE);
+ let remoteBrowser2 = tab2.linkedBrowser;
+ await TabStateFlusher.flush(remoteBrowser2);
+
+ // Quick sanity check - the two browsers should be remote and share the
+ // same childID, or else this test is not going to work.
+ Assert.ok(
+ remoteBrowser1.isRemoteBrowser,
+ "Browser should be remote in order to crash."
+ );
+ Assert.ok(
+ remoteBrowser2.isRemoteBrowser,
+ "Browser should be remote in order to crash."
+ );
+ Assert.equal(
+ remoteBrowser1.frameLoader.childID,
+ remoteBrowser2.frameLoader.childID,
+ "Both remote browsers should share the same content process."
+ );
+
+ // Now switch back to the non-remote browser...
+ await BrowserTestUtils.switchTab(gBrowser, initialTab);
+
+ await testFn([tab1, tab2]);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+}
+
+/**
+ * Takes some set of background tabs that are assumed to all belong to
+ * the same content process, and crashes them.
+ *
+ * @param tabs (Array(<xul:tab>))
+ * The tabs to crash.
+ * @return Promise
+ * Resolves once the tabs have crashed and entered the pending
+ * background state.
+ */
+async function crashBackgroundTabs(tabs) {
+ Assert.ok(!!tabs.length, "Need to crash at least one tab.");
+ for (let tab of tabs) {
+ Assert.ok(tab.linkedBrowser.isRemoteBrowser, "tab is remote");
+ }
+
+ let remotenessChangePromises = tabs.map(t => {
+ return BrowserTestUtils.waitForEvent(t, "TabRemotenessChange");
+ });
+
+ let tabsRevived = tabs.map(t => {
+ return promiseTabRestoring(t);
+ });
+
+ await BrowserTestUtils.crashFrame(tabs[0].linkedBrowser, false);
+ await Promise.all(remotenessChangePromises);
+ await Promise.all(tabsRevived);
+
+ // Both background tabs should now be in the pending restore
+ // state.
+ for (let tab of tabs) {
+ Assert.ok(!tab.linkedBrowser.isRemoteBrowser, "tab is not remote");
+ Assert.ok(!tab.linkedBrowser.hasAttribute("crashed"), "tab is not crashed");
+ Assert.ok(tab.hasAttribute("pending"), "tab is pending");
+ }
+}
+
+add_setup(async function () {
+ // We'll simplify by making sure we only ever one content process for this
+ // test.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["dom.ipc.processCount.webIsolated", 1],
+ ],
+ });
+
+ // On debug builds, crashing tabs results in much thinking, which
+ // slows down the test and results in intermittent test timeouts,
+ // so we'll pump up the expected timeout for this test.
+ requestLongerTimeout(5);
+});
+
+/**
+ * Tests that if a content process crashes taking down only
+ * background tabs, then the first of those tabs that the user
+ * selects will show the tab crash page, but the rest will restore
+ * on demand.
+ */
+add_task(async function test_background_crash_simple() {
+ await setupBackgroundTabs(async function ([tab1, tab2]) {
+ // Let's crash one of those background tabs now...
+ await crashBackgroundTabs([tab1, tab2]);
+
+ // Selecting the first tab should now send it to the tab crashed page.
+ let tabCrashedPagePromise = BrowserTestUtils.waitForContentEvent(
+ tab1.linkedBrowser,
+ "AboutTabCrashedReady",
+ false,
+ null,
+ true
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await tabCrashedPagePromise;
+
+ // Selecting the second tab should restore it.
+ let tabRestored = promiseTabRestored(tab2);
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await tabRestored;
+ });
+});
+
+/**
+ * Tests that if a content process crashes taking down only
+ * background tabs, and the user is configured to send backlogged
+ * crash reports automatically, that the tab crashed page is not
+ * shown.
+ */
+add_task(async function test_background_crash_autosubmit_backlogged() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.crashReports.unsubmittedCheck.autoSubmit2", true]],
+ });
+
+ await setupBackgroundTabs(async function ([tab1, tab2]) {
+ // Let's crash one of those background tabs now...
+ await crashBackgroundTabs([tab1, tab2]);
+
+ // Selecting the first tab should restore it.
+ let tabRestored = promiseTabRestored(tab1);
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await tabRestored;
+
+ // Selecting the second tab should restore it.
+ tabRestored = promiseTabRestored(tab2);
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await tabRestored;
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that if there are two background tab crashes in a row, that
+ * the two sets of background crashes don't interfere with one another.
+ *
+ * Specifically, if we start with two background tabs (1, 2) which crash,
+ * and we visit 1, 1 should go to the tab crashed page. If we then have
+ * two new background tabs (3, 4) crash, visiting 2 should still restore.
+ * Visiting 4 should show us the tab crashed page, and then visiting 3
+ * should restore.
+ */
+add_task(async function test_background_crash_multiple() {
+ let initialTab = gBrowser.selectedTab;
+
+ await setupBackgroundTabs(async function ([tab1, tab2]) {
+ // Let's crash one of those background tabs now...
+ await crashBackgroundTabs([tab1, tab2]);
+
+ // Selecting the first tab should now send it to the tab crashed page.
+ let tabCrashedPagePromise = BrowserTestUtils.waitForContentEvent(
+ tab1.linkedBrowser,
+ "AboutTabCrashedReady",
+ false,
+ null,
+ true
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await tabCrashedPagePromise;
+
+ // Now switch back to the original non-remote tab...
+ await BrowserTestUtils.switchTab(gBrowser, initialTab);
+
+ await setupBackgroundTabs(async function ([tab3, tab4]) {
+ await crashBackgroundTabs([tab3, tab4]);
+
+ // Selecting the second tab should restore it.
+ let tabRestored = promiseTabRestored(tab2);
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+ await tabRestored;
+
+ // Selecting the fourth tab should now send it to the tab crashed page.
+ tabCrashedPagePromise = BrowserTestUtils.waitForContentEvent(
+ tab4.linkedBrowser,
+ "AboutTabCrashedReady",
+ false,
+ null,
+ true
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tab4);
+ await tabCrashedPagePromise;
+
+ // Selecting the third tab should restore it.
+ tabRestored = promiseTabRestored(tab3);
+ await BrowserTestUtils.switchTab(gBrowser, tab3);
+ await tabRestored;
+ });
+ });
+});
+
+// Tests that crashed preloaded tabs are removed and no unexpected errors are
+// thrown.
+add_task(async function test_preload_crash() {
+ if (!Services.prefs.getBoolPref("browser.newtab.preload")) {
+ return;
+ }
+
+ // Release any existing preloaded browser
+ NewTabPagePreloading.removePreloadedBrowser(window);
+
+ // Create a fresh preloaded browser
+ await BrowserTestUtils.maybeCreatePreloadedBrowser(gBrowser);
+
+ await BrowserTestUtils.crashFrame(gBrowser.preloadedBrowser, false);
+
+ Assert.ok(!gBrowser.preloadedBrowser);
+});
diff --git a/browser/components/sessionstore/test/browser_backup_recovery.js b/browser/components/sessionstore/test/browser_backup_recovery.js
new file mode 100644
index 0000000000..0610c3cf83
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_backup_recovery.js
@@ -0,0 +1,307 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This tests are for a sessionstore.js atomic backup.
+// Each test will wait for a write to the Session Store
+// before executing.
+
+const PREF_SS_INTERVAL = "browser.sessionstore.interval";
+const Paths = SessionFile.Paths;
+
+// Global variables that contain sessionstore.jsonlz4 and sessionstore.baklz4 data for
+// comparison between tests.
+var gSSData;
+var gSSBakData;
+
+function promiseRead(path) {
+ return IOUtils.readUTF8(path, { decompress: true });
+}
+
+async function reInitSessionFile() {
+ await SessionFile.wipe();
+ await SessionFile.read();
+}
+
+add_setup(async function () {
+ // Make sure that we are not racing with SessionSaver's time based
+ // saves.
+ Services.prefs.setIntPref(PREF_SS_INTERVAL, 10000000);
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF_SS_INTERVAL));
+});
+
+add_task(async function test_creation() {
+ // Cancel all pending session saves so they won't get in our way.
+ SessionSaver.cancel();
+
+ // Create dummy sessionstore backups
+ let OLD_BACKUP = PathUtils.join(PathUtils.profileDir, "sessionstore.baklz4");
+ let OLD_UPGRADE_BACKUP = PathUtils.join(
+ PathUtils.profileDir,
+ "sessionstore.baklz4-0000000"
+ );
+
+ await IOUtils.writeUTF8(OLD_BACKUP, "sessionstore.bak");
+ await IOUtils.writeUTF8(OLD_UPGRADE_BACKUP, "sessionstore upgrade backup");
+
+ await reInitSessionFile();
+
+ // Ensure none of the sessionstore files and backups exists
+ for (let k of Paths.loadOrder) {
+ ok(
+ !(await IOUtils.exists(Paths[k])),
+ "After wipe " + k + " sessionstore file doesn't exist"
+ );
+ }
+ ok(
+ !(await IOUtils.exists(OLD_BACKUP)),
+ "After wipe, old backup doesn't exist"
+ );
+ ok(
+ !(await IOUtils.exists(OLD_UPGRADE_BACKUP)),
+ "After wipe, old upgrade backup doesn't exist"
+ );
+
+ // Open a new tab, save session, ensure that the correct files exist.
+ let URL_BASE =
+ "http://example.com/?atomic_backup_test_creation=" + Math.random();
+ let URL = URL_BASE + "?first_write";
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+
+ info("Testing situation after a single write");
+ await promiseBrowserLoaded(tab.linkedBrowser);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ await SessionSaver.run();
+
+ ok(
+ await IOUtils.exists(Paths.recovery),
+ "After write, recovery sessionstore file exists again"
+ );
+ ok(
+ !(await IOUtils.exists(Paths.recoveryBackup)),
+ "After write, recoveryBackup sessionstore doesn't exist"
+ );
+ ok(
+ (await promiseRead(Paths.recovery)).includes(URL),
+ "Recovery sessionstore file contains the required tab"
+ );
+ ok(
+ !(await IOUtils.exists(Paths.clean)),
+ "After first write, clean shutdown " +
+ "sessionstore doesn't exist, since we haven't shutdown yet"
+ );
+
+ // Open a second tab, save session, ensure that the correct files exist.
+ info("Testing situation after a second write");
+ let URL2 = URL_BASE + "?second_write";
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, URL2);
+ await promiseBrowserLoaded(tab.linkedBrowser);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ await SessionSaver.run();
+
+ ok(
+ await IOUtils.exists(Paths.recovery),
+ "After second write, recovery sessionstore file still exists"
+ );
+ ok(
+ (await promiseRead(Paths.recovery)).includes(URL2),
+ "Recovery sessionstore file contains the latest url"
+ );
+ ok(
+ await IOUtils.exists(Paths.recoveryBackup),
+ "After write, recoveryBackup sessionstore now exists"
+ );
+ let backup = await promiseRead(Paths.recoveryBackup);
+ ok(!backup.includes(URL2), "Recovery backup doesn't contain the latest url");
+ ok(backup.includes(URL), "Recovery backup contains the original url");
+ ok(
+ !(await IOUtils.exists(Paths.clean)),
+ "After first write, clean shutdown " +
+ "sessionstore doesn't exist, since we haven't shutdown yet"
+ );
+
+ info("Reinitialize, ensure that we haven't leaked sensitive files");
+ await SessionFile.read(); // Reinitializes SessionFile
+ await SessionSaver.run();
+ ok(
+ !(await IOUtils.exists(Paths.clean)),
+ "After second write, clean shutdown " +
+ "sessionstore doesn't exist, since we haven't shutdown yet"
+ );
+ ok(
+ Paths.upgradeBackup === "",
+ "After second write, clean " +
+ "shutdown sessionstore doesn't exist, since we haven't shutdown yet"
+ );
+ ok(
+ !(await IOUtils.exists(Paths.nextUpgradeBackup)),
+ "After second write, clean " +
+ "shutdown sessionstore doesn't exist, since we haven't shutdown yet"
+ );
+
+ gBrowser.removeTab(tab);
+});
+
+var promiseSource = async function (name) {
+ let URL =
+ "http://example.com/?atomic_backup_test_recovery=" +
+ Math.random() +
+ "&name=" +
+ name;
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+
+ await promiseBrowserLoaded(tab.linkedBrowser);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ await SessionSaver.run();
+ gBrowser.removeTab(tab);
+
+ let SOURCE = await promiseRead(Paths.recovery);
+ await SessionFile.wipe();
+ return SOURCE;
+};
+
+add_task(async function test_recovery() {
+ await reInitSessionFile();
+ info("Attempting to recover from the recovery file");
+
+ // Create Paths.recovery, ensure that we can recover from it.
+ let SOURCE = await promiseSource("Paths.recovery");
+ await IOUtils.makeDirectory(Paths.backups);
+ await IOUtils.writeUTF8(Paths.recovery, SOURCE, { compress: true });
+ is(
+ (await SessionFile.read()).source,
+ SOURCE,
+ "Recovered the correct source from the recovery file"
+ );
+
+ info("Corrupting recovery file, attempting to recover from recovery backup");
+ SOURCE = await promiseSource("Paths.recoveryBackup");
+ await IOUtils.makeDirectory(Paths.backups);
+ await IOUtils.writeUTF8(Paths.recoveryBackup, SOURCE, { compress: true });
+ await IOUtils.writeUTF8(Paths.recovery, "<Invalid JSON>", { compress: true });
+ is(
+ (await SessionFile.read()).source,
+ SOURCE,
+ "Recovered the correct source from the recovery file"
+ );
+});
+
+add_task(async function test_recovery_inaccessible() {
+ // Can't do chmod() on non-UNIX platforms, we need that for this test.
+ if (AppConstants.platform != "macosx" && AppConstants.platform != "linux") {
+ return;
+ }
+
+ await reInitSessionFile();
+ info(
+ "Making recovery file inaccessible, attempting to recover from recovery backup"
+ );
+ let SOURCE_RECOVERY = await promiseSource("Paths.recovery");
+ let SOURCE = await promiseSource("Paths.recoveryBackup");
+ await IOUtils.makeDirectory(Paths.backups);
+ await IOUtils.writeUTF8(Paths.recoveryBackup, SOURCE, { compress: true });
+
+ // Write a valid recovery file but make it inaccessible.
+ await IOUtils.writeUTF8(Paths.recovery, SOURCE_RECOVERY, { compress: true });
+ await IOUtils.setPermissions(Paths.recovery, 0);
+
+ is(
+ (await SessionFile.read()).source,
+ SOURCE,
+ "Recovered the correct source from the recovery file"
+ );
+ await IOUtils.setPermissions(Paths.recovery, 0o644);
+});
+
+add_task(async function test_clean() {
+ await reInitSessionFile();
+ let SOURCE = await promiseSource("Paths.clean");
+ await IOUtils.writeUTF8(Paths.clean, SOURCE, { compress: true });
+ await SessionFile.read();
+ await SessionSaver.run();
+ is(
+ await promiseRead(Paths.cleanBackup),
+ SOURCE,
+ "After first read/write, " +
+ "clean shutdown file has been moved to cleanBackup"
+ );
+});
+
+/**
+ * Tests loading of sessionstore when format version is known.
+ */
+add_task(async function test_version() {
+ info("Preparing sessionstore");
+ let SOURCE = await promiseSource("Paths.clean");
+
+ // Check there's a format version number
+ is(
+ JSON.parse(SOURCE).version[0],
+ "sessionrestore",
+ "Found sessionstore format version"
+ );
+
+ // Create Paths.clean file
+ await IOUtils.makeDirectory(Paths.backups);
+ await IOUtils.writeUTF8(Paths.clean, SOURCE, { compress: true });
+
+ info("Attempting to recover from the clean file");
+ // Ensure that we can recover from Paths.recovery
+ is(
+ (await SessionFile.read()).source,
+ SOURCE,
+ "Recovered the correct source from the clean file"
+ );
+});
+
+/**
+ * Tests fallback to previous backups if format version is unknown.
+ */
+add_task(async function test_version_fallback() {
+ await reInitSessionFile();
+ info("Preparing data, making sure that it has a version number");
+ let SOURCE = await promiseSource("Paths.clean");
+ let BACKUP_SOURCE = await promiseSource("Paths.cleanBackup");
+
+ is(
+ JSON.parse(SOURCE).version[0],
+ "sessionrestore",
+ "Found sessionstore format version"
+ );
+ is(
+ JSON.parse(BACKUP_SOURCE).version[0],
+ "sessionrestore",
+ "Found backup sessionstore format version"
+ );
+
+ await IOUtils.makeDirectory(Paths.backups);
+
+ info(
+ "Modifying format version number to something incorrect, to make sure that we disregard the file."
+ );
+ let parsedSource = JSON.parse(SOURCE);
+ parsedSource.version[0] = "bookmarks";
+ await IOUtils.writeJSON(Paths.clean, parsedSource, { compress: true });
+ await IOUtils.writeUTF8(Paths.cleanBackup, BACKUP_SOURCE, { compress: true });
+ is(
+ (await SessionFile.read()).source,
+ BACKUP_SOURCE,
+ "Recovered the correct source from the backup recovery file"
+ );
+
+ info(
+ "Modifying format version number to a future version, to make sure that we disregard the file."
+ );
+ parsedSource = JSON.parse(SOURCE);
+ parsedSource.version[1] = Number.MAX_SAFE_INTEGER;
+ await IOUtils.writeJSON(Paths.clean, parsedSource, { compress: true });
+ await IOUtils.writeUTF8(Paths.cleanBackup, BACKUP_SOURCE, { compress: true });
+ is(
+ (await SessionFile.read()).source,
+ BACKUP_SOURCE,
+ "Recovered the correct source from the backup recovery file"
+ );
+});
+
+add_task(async function cleanup() {
+ await reInitSessionFile();
+});
diff --git a/browser/components/sessionstore/test/browser_bfcache_telemetry.js b/browser/components/sessionstore/test/browser_bfcache_telemetry.js
new file mode 100644
index 0000000000..daa4bcf44e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_bfcache_telemetry.js
@@ -0,0 +1,45 @@
+const URL1 = "data:text/html;charset=utf-8,<body><p>Hello1</p></body>";
+const URL2 = "data:text/html;charset=utf-8,<body><p>Hello2</p></body>";
+
+async function getBFCacheComboTelemetry(probeInParent) {
+ let bfcacheCombo;
+ await TestUtils.waitForCondition(() => {
+ let histograms;
+ if (probeInParent) {
+ histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ } else {
+ histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).content;
+ }
+ bfcacheCombo = histograms.BFCACHE_COMBO;
+ return bfcacheCombo;
+ });
+ return bfcacheCombo;
+}
+
+async function test_bfcache_telemetry(probeInParent) {
+ Services.telemetry.getHistogramById("BFCACHE_COMBO").clear();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL1);
+
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, URL2);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ let bfcacheCombo = await getBFCacheComboTelemetry(probeInParent);
+
+ is(bfcacheCombo.values[0], 1, "1 bfcache success");
+
+ gBrowser.removeTab(tab);
+}
+
+add_task(async () => {
+ await test_bfcache_telemetry(
+ Services.appinfo.sessionHistoryInParent &&
+ Services.prefs.getBoolPref("fission.bfcacheInParent")
+ );
+});
diff --git a/browser/components/sessionstore/test/browser_broadcast.js b/browser/components/sessionstore/test/browser_broadcast.js
new file mode 100644
index 0000000000..02f3bc59e7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_broadcast.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const INITIAL_VALUE = "browser_broadcast.js-initial-value-" + Date.now();
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * closing a tab.
+ */
+add_task(async function flush_on_tabclose() {
+ let tab = await createTabWithStorageData(["http://example.com/"]);
+ let browser = tab.linkedBrowser;
+
+ await modifySessionStorage(browser, { test: "on-tab-close" });
+ await TabStateFlusher.flush(browser);
+ await promiseRemoveTabAndSessionState(tab);
+
+ let [
+ {
+ state: { storage },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ is(
+ storage["http://example.com"].test,
+ "on-tab-close",
+ "sessionStorage data has been flushed on TabClose"
+ );
+});
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * duplicating a tab.
+ */
+add_task(async function flush_on_duplicate() {
+ let tab = await createTabWithStorageData(["http://example.com/"]);
+ let browser = tab.linkedBrowser;
+
+ await modifySessionStorage(browser, { test: "on-duplicate" });
+ let tab2 = ss.duplicateTab(window, tab);
+ await promiseTabRestored(tab2);
+
+ await promiseRemoveTabAndSessionState(tab2);
+ let [
+ {
+ state: { storage },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ is(
+ storage["http://example.com"].test,
+ "on-duplicate",
+ "sessionStorage data has been flushed when duplicating tabs"
+ );
+
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * a window is closed.
+ */
+add_task(async function flush_on_windowclose() {
+ let win = await promiseNewWindow();
+ let tab = await createTabWithStorageData(["http://example.com/"], win);
+ let browser = tab.linkedBrowser;
+
+ await modifySessionStorage(browser, { test: "on-window-close" });
+ await BrowserTestUtils.closeWindow(win);
+
+ let [
+ {
+ tabs: [, { storage }],
+ },
+ ] = ss.getClosedWindowData();
+ is(
+ storage["http://example.com"].test,
+ "on-window-close",
+ "sessionStorage data has been flushed when closing a window"
+ );
+});
+
+/**
+ * This test ensures that stale tab data is ignored when reusing a tab
+ * (via e.g. setTabState) and does not overwrite the new data.
+ */
+add_task(async function flush_on_settabstate() {
+ let tab = await createTabWithStorageData(["http://example.com/"]);
+ let browser = tab.linkedBrowser;
+
+ // Flush to make sure our tab state is up-to-date.
+ await TabStateFlusher.flush(browser);
+
+ let state = ss.getTabState(tab);
+ await modifySessionStorage(browser, { test: "on-set-tab-state" });
+
+ // Flush all data contained in the content script but send it using
+ // asynchronous messages.
+ TabStateFlusher.flush(browser);
+
+ await promiseTabState(tab, state);
+
+ let { storage } = JSON.parse(ss.getTabState(tab));
+ is(
+ storage["http://example.com"].test,
+ INITIAL_VALUE,
+ "sessionStorage data has not been overwritten"
+ );
+
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures that we won't lose tab data that has been sent
+ * asynchronously just before closing a tab. Flushing must re-send all data
+ * that hasn't been received by chrome, yet.
+ */
+add_task(async function flush_on_tabclose_racy() {
+ let tab = await createTabWithStorageData(["http://example.com/"]);
+ let browser = tab.linkedBrowser;
+
+ // Flush to make sure we start with an empty queue.
+ await TabStateFlusher.flush(browser);
+
+ await modifySessionStorage(browser, { test: "on-tab-close-racy" });
+
+ // Flush all data contained in the content script but send it using
+ // asynchronous messages.
+ TabStateFlusher.flush(browser);
+ await promiseRemoveTabAndSessionState(tab);
+
+ let [
+ {
+ state: { storage },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ is(
+ storage["http://example.com"].test,
+ "on-tab-close-racy",
+ "sessionStorage data has been merged correctly to prevent data loss"
+ );
+});
+
+function promiseNewWindow() {
+ return new Promise(resolve => {
+ whenNewWindowLoaded({ private: false }, resolve);
+ });
+}
+
+async function createTabWithStorageData(urls, win = window) {
+ let tab = BrowserTestUtils.addTab(win.gBrowser);
+ let browser = tab.linkedBrowser;
+
+ for (let url of urls) {
+ BrowserTestUtils.loadURIString(browser, url);
+ await promiseBrowserLoaded(browser, true, url);
+ dump("Loaded url: " + url + "\n");
+ await modifySessionStorage(browser, { test: INITIAL_VALUE });
+ }
+
+ return tab;
+}
diff --git a/browser/components/sessionstore/test/browser_capabilities.js b/browser/components/sessionstore/test/browser_capabilities.js
new file mode 100644
index 0000000000..f02b0ede76
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_capabilities.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * These tests ensures that disabling features by flipping nsIDocShell.allow*
+ * properties are (re)stored as disabled. Disallowed features must be
+ * re-enabled when the tab is re-used by another tab restoration.
+ */
+add_task(async function docshell_capabilities() {
+ let tab = await createTab();
+ let browser = tab.linkedBrowser;
+ let { browsingContext, docShell } = browser;
+
+ // Get the list of capabilities for docShells.
+ let flags = Object.keys(docShell).filter(k => k.startsWith("allow"));
+
+ // Check that everything is allowed by default for new tabs.
+ let state = JSON.parse(ss.getTabState(tab));
+ ok(!("disallow" in state), "everything allowed by default");
+ ok(
+ flags.every(f => docShell[f]),
+ "all flags set to true"
+ );
+
+ // Flip a couple of allow* flags.
+ docShell.allowImages = false;
+ docShell.allowMetaRedirects = false;
+ browsingContext.allowJavascript = false;
+
+ // Now reload the document to ensure that these capabilities
+ // are taken into account.
+ browser.reload();
+ await promiseBrowserLoaded(browser);
+
+ // Flush to make sure chrome received all data.
+ await TabStateFlusher.flush(browser);
+
+ // Check that we correctly save disallowed features.
+ let disallowedState = JSON.parse(ss.getTabState(tab));
+ let disallow = new Set(disallowedState.disallow.split(","));
+ ok(disallow.has("Images"), "images not allowed");
+ ok(disallow.has("MetaRedirects"), "meta redirects not allowed");
+ is(disallow.size, 2, "two capabilities disallowed");
+
+ // Reuse the tab to restore a new, clean state into it.
+ await promiseTabState(tab, {
+ entries: [{ url: "about:robots", triggeringPrincipal_base64 }],
+ });
+
+ // Flush to make sure chrome received all data.
+ await TabStateFlusher.flush(browser);
+
+ // After restoring disallowed features must be available again.
+ state = JSON.parse(ss.getTabState(tab));
+ ok(!("disallow" in state), "everything allowed again");
+ ok(
+ flags.every(f => docShell[f]),
+ "all flags set to true"
+ );
+
+ // Restore the state with disallowed features.
+ await promiseTabState(tab, disallowedState);
+
+ // Check that docShell flags are set.
+ ok(!docShell.allowImages, "images not allowed");
+ ok(!docShell.allowMetaRedirects, "meta redirects not allowed");
+
+ // Check that docShell allowJavascript flag is not set.
+ ok(browsingContext.allowJavascript, "Javascript still allowed");
+
+ // Check that we correctly restored features as disabled.
+ state = JSON.parse(ss.getTabState(tab));
+ disallow = new Set(state.disallow.split(","));
+ ok(disallow.has("Images"), "images not allowed anymore");
+ ok(disallow.has("MetaRedirects"), "meta redirects not allowed anymore");
+ ok(!disallow.has("Javascript"), "Javascript still allowed");
+ is(disallow.size, 2, "two capabilities disallowed");
+
+ // Clean up after ourselves.
+ gBrowser.removeTab(tab);
+});
+
+async function createTab() {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:rights");
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+ return tab;
+}
diff --git a/browser/components/sessionstore/test/browser_cleaner.js b/browser/components/sessionstore/test/browser_cleaner.js
new file mode 100644
index 0000000000..456f628d1e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cleaner.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * This test ensures that Session Restore eventually forgets about
+ * tabs and windows that have been closed a long time ago.
+ */
+
+"use strict";
+
+const LONG_TIME_AGO = 1;
+
+const URL_TAB1 =
+ "http://example.com/browser_cleaner.js?newtab1=" + Math.random();
+const URL_TAB2 =
+ "http://example.com/browser_cleaner.js?newtab2=" + Math.random();
+const URL_NEWWIN =
+ "http://example.com/browser_cleaner.js?newwin=" + Math.random();
+
+function isRecent(stamp) {
+ is(typeof stamp, "number", "This is a timestamp");
+ return Date.now() - stamp <= 60000;
+}
+
+function promiseCleanup() {
+ info("Cleaning up browser");
+
+ return promiseBrowserState(getClosedState());
+}
+
+function getClosedState() {
+ return Cu.cloneInto(CLOSED_STATE, {});
+}
+
+var CLOSED_STATE;
+
+add_setup(async function () {
+ forgetClosedWindows();
+ forgetClosedTabs(window);
+});
+
+add_task(async function test_open_and_close() {
+ let newTab1 = BrowserTestUtils.addTab(gBrowser, URL_TAB1);
+ await promiseBrowserLoaded(newTab1.linkedBrowser);
+
+ let newTab2 = BrowserTestUtils.addTab(gBrowser, URL_TAB2);
+ await promiseBrowserLoaded(newTab2.linkedBrowser);
+
+ let newWin = await promiseNewWindowLoaded();
+ let tab = BrowserTestUtils.addTab(newWin.gBrowser, URL_NEWWIN);
+
+ await promiseBrowserLoaded(tab.linkedBrowser);
+
+ await TabStateFlusher.flushWindow(window);
+ await TabStateFlusher.flushWindow(newWin);
+
+ info("1. Making sure that before closing, we don't have closedAt");
+ // For the moment, no "closedAt"
+ let state = JSON.parse(ss.getBrowserState());
+ is(
+ state.windows[0].closedAt || false,
+ false,
+ "1. Main window doesn't have closedAt"
+ );
+ is(
+ state.windows[1].closedAt || false,
+ false,
+ "1. Second window doesn't have closedAt"
+ );
+ is(
+ state.windows[0].tabs[0].closedAt || false,
+ false,
+ "1. First tab doesn't have closedAt"
+ );
+ is(
+ state.windows[0].tabs[1].closedAt || false,
+ false,
+ "1. Second tab doesn't have closedAt"
+ );
+
+ info("2. Making sure that after closing, we have closedAt");
+
+ // Now close stuff, this should add closeAt
+ await BrowserTestUtils.closeWindow(newWin);
+ await promiseRemoveTabAndSessionState(newTab1);
+ await promiseRemoveTabAndSessionState(newTab2);
+
+ state = CLOSED_STATE = JSON.parse(ss.getBrowserState());
+
+ is(
+ state.windows[0].closedAt || false,
+ false,
+ "2. Main window doesn't have closedAt"
+ );
+ ok(
+ isRecent(state._closedWindows[0].closedAt),
+ "2. Second window was closed recently"
+ );
+ ok(
+ isRecent(state.windows[0]._closedTabs[0].closedAt),
+ "2. First tab was closed recently"
+ );
+ ok(
+ isRecent(state.windows[0]._closedTabs[1].closedAt),
+ "2. Second tab was closed recently"
+ );
+});
+
+add_task(async function test_restore() {
+ info("3. Making sure that after restoring, we don't have closedAt");
+ await promiseBrowserState(CLOSED_STATE);
+
+ let newWin = ss.undoCloseWindow(0);
+ await promiseDelayedStartupFinished(newWin);
+
+ let newTab2 = ss.undoCloseTab(window, 0);
+ await promiseTabRestored(newTab2);
+
+ let newTab1 = ss.undoCloseTab(window, 0);
+ await promiseTabRestored(newTab1);
+
+ let state = JSON.parse(ss.getBrowserState());
+
+ is(
+ state.windows[0].closedAt || false,
+ false,
+ "3. Main window doesn't have closedAt"
+ );
+ is(
+ state.windows[1].closedAt || false,
+ false,
+ "3. Second window doesn't have closedAt"
+ );
+ is(
+ state.windows[0].tabs[0].closedAt || false,
+ false,
+ "3. First tab doesn't have closedAt"
+ );
+ is(
+ state.windows[0].tabs[1].closedAt || false,
+ false,
+ "3. Second tab doesn't have closedAt"
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+ gBrowser.removeTab(newTab1);
+ gBrowser.removeTab(newTab2);
+});
+
+add_task(async function test_old_data() {
+ info(
+ "4. Removing closedAt from the sessionstore, making sure that it is added upon idle-daily"
+ );
+
+ let state = getClosedState();
+ delete state._closedWindows[0].closedAt;
+ delete state.windows[0]._closedTabs[0].closedAt;
+ delete state.windows[0]._closedTabs[1].closedAt;
+ await promiseBrowserState(state);
+
+ info("Sending idle-daily");
+ Services.obs.notifyObservers(null, "idle-daily");
+ info("Sent idle-daily");
+
+ state = JSON.parse(ss.getBrowserState());
+ is(
+ state.windows[0].closedAt || false,
+ false,
+ "4. Main window doesn't have closedAt"
+ );
+ ok(
+ isRecent(state._closedWindows[0].closedAt),
+ "4. Second window was closed recently"
+ );
+ ok(
+ isRecent(state.windows[0]._closedTabs[0].closedAt),
+ "4. First tab was closed recently"
+ );
+ ok(
+ isRecent(state.windows[0]._closedTabs[1].closedAt),
+ "4. Second tab was closed recently"
+ );
+ await promiseCleanup();
+});
+
+add_task(async function test_cleanup() {
+ info(
+ "5. Altering closedAt to an old date, making sure that stuff gets collected, eventually"
+ );
+ await promiseCleanup();
+
+ let state = getClosedState();
+ state._closedWindows[0].closedAt = LONG_TIME_AGO;
+ state.windows[0]._closedTabs[0].closedAt = LONG_TIME_AGO;
+ state.windows[0]._closedTabs[1].closedAt = Date.now();
+ let url = state.windows[0]._closedTabs[1].state.entries[0].url;
+
+ await promiseBrowserState(state);
+
+ info("Sending idle-daily");
+ Services.obs.notifyObservers(null, "idle-daily");
+ info("Sent idle-daily");
+
+ state = JSON.parse(ss.getBrowserState());
+ is(state._closedWindows[0], undefined, "5. Second window was forgotten");
+
+ is(state.windows[0]._closedTabs.length, 1, "5. Only one closed tab left");
+ is(
+ state.windows[0]._closedTabs[0].state.entries[0].url,
+ url,
+ "5. The second tab is still here"
+ );
+ await promiseCleanup();
+});
diff --git a/browser/components/sessionstore/test/browser_closedId.js b/browser/components/sessionstore/test/browser_closedId.js
new file mode 100644
index 0000000000..6e5c9d7543
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_closedId.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BACKUP_STATE = SessionStore.getBrowserState();
+
+async function add_new_tab(URL) {
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ return tab;
+}
+
+add_task(async function test_closedId_order() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.sessionstore.restore_on_demand", true],
+ ["browser.sessionstore.restore_tabs_lazily", true],
+ ],
+ });
+ // reset to 0
+ SessionStore.resetNextClosedId();
+ await promiseBrowserState({
+ windows: [
+ {
+ selected: 1, // SessionStore uses 1-based indexing.
+ tabs: [
+ {
+ entries: [],
+ },
+ ],
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ {
+ url: "https://www.example.com/",
+ triggeringPrincipal_base64,
+ },
+ ],
+ selected: 1,
+ },
+ closedId: 0,
+ closedAt: Date.now() - 100,
+ title: "Example",
+ },
+ {
+ state: {
+ entries: [
+ {
+ url: "about:mozilla",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ closedId: 1,
+ closedAt: Date.now() - 50,
+ title: "about:mozilla",
+ },
+ {
+ state: {
+ entries: [
+ {
+ url: "https://www.example.net/",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ closedId: 2,
+ closedAt: Date.now(),
+ title: "Example",
+ },
+ ],
+ },
+ ],
+ });
+
+ let tab = await add_new_tab("about:firefoxview");
+
+ is(
+ SessionStore.getClosedTabCountForWindow(window),
+ 3,
+ "Closed tab count after restored session is 3"
+ );
+
+ let initialClosedId =
+ SessionStore.getClosedTabDataForWindow(window)[0].closedId;
+
+ // If this fails, that means one of the closedId's in the stubbed data in this test needs to be updated
+ // to reflect what the initial closedId is when a new tab is open and closed (which may change as more tests
+ // for session store are added here). You can manually verify a change to stubbed data by commenting out
+ // this._resetClosedIds in SessionStore.sys.mjs temporarily and then the "Each tab has a unique closedId" case should fail.
+ is(initialClosedId, 0, "Initial closedId is 0");
+
+ await openAndCloseTab(window, "about:robots"); // closedId should be higher than the ones we just restored.
+
+ let closedData = SessionStore.getClosedTabDataForWindow(window);
+ is(closedData.length, 4, "Should have data for 4 closed tabs.");
+ is(
+ new Set(closedData.map(t => t.closedId)).size,
+ 4,
+ "Each tab has a unique closedId"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Clean up for the next task.
+ await promiseBrowserState(BACKUP_STATE);
+});
diff --git a/browser/components/sessionstore/test/browser_closed_objects_changed_notifications_tabs.js b/browser/components/sessionstore/test/browser_closed_objects_changed_notifications_tabs.js
new file mode 100644
index 0000000000..7ad3955034
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_closed_objects_changed_notifications_tabs.js
@@ -0,0 +1,136 @@
+"use strict";
+
+/**
+ * This test is for the sessionstore-closed-objects-changed notifications.
+ */
+
+const MAX_TABS_UNDO_PREF = "browser.sessionstore.max_tabs_undo";
+const TOPIC = "sessionstore-closed-objects-changed";
+
+let notificationsCount = 0;
+
+async function openWindow(url) {
+ let win = await promiseNewWindowLoaded();
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, url, { flags });
+ await promiseBrowserLoaded(win.gBrowser.selectedBrowser, true, url);
+ return win;
+}
+
+async function closeWindow(win) {
+ await awaitNotification(() => BrowserTestUtils.closeWindow(win));
+}
+
+async function openAndCloseWindow(url) {
+ let win = await openWindow(url);
+ await closeWindow(win);
+}
+
+async function openTab(window, url) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, url);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ return tab;
+}
+
+async function openAndCloseTab(window, url) {
+ let tab = await openTab(window, url);
+ await promiseRemoveTabAndSessionState(tab);
+}
+
+function countingObserver() {
+ notificationsCount++;
+}
+
+function assertNotificationCount(count) {
+ is(
+ notificationsCount,
+ count,
+ "The expected number of notifications was received."
+ );
+}
+
+async function awaitNotification(callback) {
+ let notification = TestUtils.topicObserved(TOPIC);
+ executeSoon(callback);
+ await notification;
+}
+
+add_task(async function test_closedObjectsChangedNotifications() {
+ // Create a closed window so that when we do the purge we can expect a notification.
+ await openAndCloseWindow("about:robots");
+
+ // Forget any previous closed windows or tabs from other tests that may have
+ // run in the same session.
+ await awaitNotification(() =>
+ Services.obs.notifyObservers(null, "browser:purge-session-history")
+ );
+
+ // Add an observer to count the number of notifications.
+ Services.obs.addObserver(countingObserver, TOPIC);
+
+ // Open a new window.
+ let win = await openWindow("about:robots");
+
+ info("Opening and closing a tab.");
+ await openAndCloseTab(win, "about:mozilla");
+ assertNotificationCount(1);
+
+ info("Opening and closing a second tab.");
+ await openAndCloseTab(win, "about:mozilla");
+ assertNotificationCount(2);
+
+ info(`Changing the ${MAX_TABS_UNDO_PREF} pref.`);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(MAX_TABS_UNDO_PREF);
+ });
+ await awaitNotification(() =>
+ Services.prefs.setIntPref(MAX_TABS_UNDO_PREF, 1)
+ );
+ assertNotificationCount(3);
+
+ info("Undoing close of remaining closed tab.");
+ let tab = SessionStore.undoCloseTab(win, 0);
+ await promiseTabRestored(tab);
+ assertNotificationCount(4);
+
+ info("Closing tab again.");
+ await promiseRemoveTabAndSessionState(tab);
+ assertNotificationCount(5);
+
+ info("Purging session history.");
+ await awaitNotification(() =>
+ Services.obs.notifyObservers(null, "browser:purge-session-history")
+ );
+ assertNotificationCount(6);
+
+ info("Opening and closing another tab.");
+ await openAndCloseTab(win, "http://example.com/");
+ assertNotificationCount(7);
+
+ info("Purging domain data with no matches.");
+ Services.obs.notifyObservers(
+ null,
+ "browser:purge-session-history-for-domain",
+ "mozilla.com"
+ );
+ assertNotificationCount(7);
+
+ info("Purging domain data with matches.");
+ await awaitNotification(() =>
+ Services.obs.notifyObservers(
+ null,
+ "browser:purge-session-history-for-domain",
+ "example.com"
+ )
+ );
+ assertNotificationCount(8);
+
+ info("Opening and closing another tab.");
+ await openAndCloseTab(win, "http://example.com/");
+ assertNotificationCount(9);
+
+ await closeWindow(win);
+ assertNotificationCount(10);
+
+ Services.obs.removeObserver(countingObserver, TOPIC);
+});
diff --git a/browser/components/sessionstore/test/browser_closed_objects_changed_notifications_windows.js b/browser/components/sessionstore/test/browser_closed_objects_changed_notifications_windows.js
new file mode 100644
index 0000000000..9f2a24793d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_closed_objects_changed_notifications_windows.js
@@ -0,0 +1,129 @@
+"use strict";
+
+/**
+ * This test is for the sessionstore-closed-objects-changed notifications.
+ */
+
+requestLongerTimeout(2);
+
+const MAX_WINDOWS_UNDO_PREF = "browser.sessionstore.max_windows_undo";
+const TOPIC = "sessionstore-closed-objects-changed";
+
+let notificationsCount = 0;
+
+async function openWindow(url) {
+ let win = await promiseNewWindowLoaded();
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, url, { flags });
+ await promiseBrowserLoaded(win.gBrowser.selectedBrowser, true, url);
+ return win;
+}
+
+async function closeWindow(win) {
+ await awaitNotification(() => BrowserTestUtils.closeWindow(win));
+}
+
+async function openAndCloseWindow(url) {
+ let win = await openWindow(url);
+ await closeWindow(win);
+}
+
+function countingObserver() {
+ notificationsCount++;
+}
+
+function assertNotificationCount(count) {
+ is(
+ notificationsCount,
+ count,
+ "The expected number of notifications was received."
+ );
+}
+
+async function awaitNotification(callback) {
+ let notification = TestUtils.topicObserved(TOPIC);
+ executeSoon(callback);
+ await notification;
+}
+
+add_task(async function test_closedObjectsChangedNotifications() {
+ // Create a closed window so that when we do the purge we know to expect a notification
+ await openAndCloseWindow("about:robots");
+
+ // Forget any previous closed windows or tabs from other tests that may have
+ // run in the same session.
+ await awaitNotification(() =>
+ Services.obs.notifyObservers(null, "browser:purge-session-history")
+ );
+
+ // Add an observer to count the number of notifications.
+ Services.obs.addObserver(countingObserver, TOPIC);
+
+ info("Opening and closing initial window.");
+ await openAndCloseWindow("about:robots");
+ assertNotificationCount(1);
+
+ // Store state with a single closed window for use in later tests.
+ let closedState = Cu.cloneInto(JSON.parse(ss.getBrowserState()), {});
+
+ info("Undoing close of initial window.");
+ let win = SessionStore.undoCloseWindow(0);
+ await promiseDelayedStartupFinished(win);
+ assertNotificationCount(2);
+
+ // Open a second window.
+ let win2 = await openWindow("about:mozilla");
+
+ info("Closing both windows.");
+ await closeWindow(win);
+ assertNotificationCount(3);
+ await closeWindow(win2);
+ assertNotificationCount(4);
+
+ info(`Changing the ${MAX_WINDOWS_UNDO_PREF} pref.`);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(MAX_WINDOWS_UNDO_PREF);
+ });
+ await awaitNotification(() =>
+ Services.prefs.setIntPref(MAX_WINDOWS_UNDO_PREF, 1)
+ );
+ assertNotificationCount(5);
+
+ info("Forgetting a closed window.");
+ await awaitNotification(() => SessionStore.forgetClosedWindow());
+ assertNotificationCount(6);
+
+ info("Opening and closing another window.");
+ await openAndCloseWindow("about:robots");
+ assertNotificationCount(7);
+
+ info("Setting browser state to trigger change onIdleDaily.");
+ let state = Cu.cloneInto(JSON.parse(ss.getBrowserState()), {});
+ state._closedWindows[0].closedAt = 1;
+ await promiseBrowserState(state);
+ assertNotificationCount(8);
+
+ info("Sending idle-daily");
+ await awaitNotification(() =>
+ Services.obs.notifyObservers(null, "idle-daily")
+ );
+ assertNotificationCount(9);
+
+ info("Opening and closing another window.");
+ await openAndCloseWindow("about:robots");
+ assertNotificationCount(10);
+
+ info("Purging session history.");
+ await awaitNotification(() =>
+ Services.obs.notifyObservers(null, "browser:purge-session-history")
+ );
+ assertNotificationCount(11);
+
+ info("Setting window state.");
+ win = await openWindow("about:mozilla");
+ await awaitNotification(() => SessionStore.setWindowState(win, closedState));
+ assertNotificationCount(12);
+
+ Services.obs.removeObserver(countingObserver, TOPIC);
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/sessionstore/test/browser_closed_tabs_windows.js b/browser/components/sessionstore/test/browser_closed_tabs_windows.js
new file mode 100644
index 0000000000..8fb99112f9
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_closed_tabs_windows.js
@@ -0,0 +1,191 @@
+const ORIG_STATE = ss.getBrowserState();
+
+const multiWindowState = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "https://example.com#0", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ {
+ url: "https://example.com#closed0",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ },
+ {
+ state: {
+ entries: [
+ {
+ url: "https://example.com#closed1",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ },
+ ],
+ selected: 1,
+ },
+ {
+ tabs: [
+ {
+ entries: [
+ { url: "https://example.org#1", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "https://example.org#2", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ {
+ url: "https://example.org#closed0",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ },
+ ],
+ selected: 1,
+ },
+ ],
+};
+
+async function setupWithBrowserState(browserState) {
+ let numTabs = browserState.windows.reduce((count, winData) => {
+ return count + winData.tabs.length;
+ }, 0);
+ let loadCount = 0;
+ let windowOpenedCount = 1; // pre-count the first window
+ let promiseRestoringTabs = new Promise(resolve => {
+ gProgressListener.setCallback(function (
+ aBrowser,
+ aNeedRestore,
+ aRestoring,
+ aRestored
+ ) {
+ if (++loadCount == numTabs) {
+ // We don't actually care about load order in this test, just that they all
+ // do load.
+ is(loadCount, numTabs, "all tabs were restored");
+ is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+ gProgressListener.unsetCallback();
+ resolve();
+ }
+ });
+ });
+
+ // We also want to catch the 2nd window, so we need to observe domwindowopened
+ Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let win = aSubject;
+ win.addEventListener(
+ "load",
+ function () {
+ if (++windowOpenedCount == browserState.windows.length) {
+ Services.ww.unregisterNotification(observer);
+ win.gBrowser.addTabsProgressListener(gProgressListener);
+ }
+ },
+ { once: true }
+ );
+ }
+ });
+
+ const stateRestored = TestUtils.topicObserved(
+ "sessionstore-browser-state-restored"
+ );
+ await ss.setBrowserState(JSON.stringify(browserState));
+ await stateRestored;
+ await promiseRestoringTabs;
+}
+add_setup(async function testSetup() {
+ await setupWithBrowserState(multiWindowState);
+});
+
+add_task(async function test_ClosedTabMethods() {
+ let sessionStoreUpdated;
+ const browserWindows = BrowserWindowTracker.orderedWindows;
+ Assert.equal(
+ browserWindows.length,
+ multiWindowState.windows.length,
+ `We expect ${multiWindowState.windows} open browser windows`
+ );
+
+ for (let idx = 0; idx < browserWindows.length; idx++) {
+ const win = browserWindows[idx];
+ const winData = multiWindowState.windows[idx];
+ info(`window ${idx}: ${win.gBrowser.selectedBrowser.currentURI.spec}`);
+ Assert.equal(
+ winData._closedTabs.length,
+ SessionStore.getClosedTabDataForWindow(win).length,
+ `getClosedTabDataForWindow() for window ${idx} returned the expected number of objects`
+ );
+ }
+
+ let closedCount;
+ closedCount = SessionStore.getClosedTabCountForWindow(browserWindows[0]);
+ Assert.equal(2, closedCount, "2 closed tab for this window");
+
+ closedCount = SessionStore.getClosedTabCountForWindow(browserWindows[1]);
+ Assert.equal(1, closedCount, "1 closed tab for this window");
+
+ sessionStoreUpdated = TestUtils.topicObserved(
+ "sessionstore-closed-objects-changed"
+ );
+ SessionStore.undoCloseTab(browserWindows[0], 0);
+ await sessionStoreUpdated;
+
+ Assert.equal(
+ 1,
+ SessionStore.getClosedTabCountForWindow(browserWindows[0]),
+ "Now theres one closed tab"
+ );
+ Assert.equal(
+ 1,
+ SessionStore.getClosedTabCountForWindow(browserWindows[1]),
+ "Theres still one closed tab in the 2nd window"
+ );
+
+ sessionStoreUpdated = TestUtils.topicObserved(
+ "sessionstore-closed-objects-changed"
+ );
+ SessionStore.forgetClosedTab(browserWindows[0], 0);
+ await sessionStoreUpdated;
+
+ Assert.equal(
+ 0,
+ SessionStore.getClosedTabCountForWindow(browserWindows[0]),
+ "No closed tabs after forgetting the last one"
+ );
+ Assert.equal(
+ 1,
+ SessionStore.getClosedTabCountForWindow(browserWindows[1]),
+ "Theres still one closed tab in the 2nd window"
+ );
+
+ await promiseAllButPrimaryWindowClosed();
+
+ Assert.equal(
+ 0,
+ SessionStore.getClosedTabCountForWindow(browserWindows[0]),
+ "Closed tab count is unchanged after closing the other browser window"
+ );
+
+ // Cleanup.
+ await promiseBrowserState(ORIG_STATE);
+});
diff --git a/browser/components/sessionstore/test/browser_cookies.js b/browser/components/sessionstore/test/browser_cookies.js
new file mode 100644
index 0000000000..f514efc777
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cookies.js
@@ -0,0 +1,81 @@
+"use strict";
+
+function promiseSetCookie(cookie) {
+ info(`Set-Cookie: ${cookie}`);
+ return Promise.all([
+ waitForCookieChanged(),
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [cookie],
+ passedCookie => (content.document.cookie = passedCookie)
+ ),
+ ]);
+}
+
+function waitForCookieChanged() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subj, topic, data) {
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }, "session-cookie-changed");
+ });
+}
+
+function cookieExists(host, name, value) {
+ let {
+ cookies: [c],
+ } = JSON.parse(ss.getBrowserState());
+ return c && c.host == host && c.name == name && c.value == value;
+}
+
+// Setup and cleanup.
+add_task(async function test_setup() {
+ registerCleanupFunction(() => {
+ Services.cookies.removeAll();
+ });
+});
+
+// Test session cookie storage.
+add_task(async function test_run() {
+ Services.cookies.removeAll();
+
+ // Add a new tab for testing.
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await promiseBrowserLoaded(gBrowser.selectedBrowser);
+
+ // Add a session cookie.
+ await promiseSetCookie("foo=bar");
+ ok(cookieExists("example.com", "foo", "bar"), "cookie was added");
+
+ // Modify a session cookie.
+ await promiseSetCookie("foo=baz");
+ ok(cookieExists("example.com", "foo", "baz"), "cookie was modified");
+
+ // Turn the session cookie into a normal one.
+ let expiry = new Date();
+ expiry.setDate(expiry.getDate() + 2);
+ await promiseSetCookie(`foo=baz; Expires=${expiry.toUTCString()}`);
+ ok(!cookieExists("example.com", "foo", "baz"), "no longer a session cookie");
+
+ // Turn it back into a session cookie.
+ await promiseSetCookie("foo=bar");
+ ok(cookieExists("example.com", "foo", "bar"), "again a session cookie");
+
+ // Remove the session cookie.
+ await promiseSetCookie("foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT");
+ ok(!cookieExists("example.com", "foo", ""), "cookie was removed");
+
+ // Add a session cookie.
+ await promiseSetCookie("foo=bar");
+ ok(cookieExists("example.com", "foo", "bar"), "cookie was added");
+
+ // Clear all session cookies.
+ Services.cookies.removeAll();
+ ok(!cookieExists("example.com", "foo", "bar"), "cookies were cleared");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/sessionstore/test/browser_cookies_legacy.js b/browser/components/sessionstore/test/browser_cookies_legacy.js
new file mode 100644
index 0000000000..087c2d516f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cookies_legacy.js
@@ -0,0 +1,75 @@
+"use strict";
+
+function createTestState() {
+ let r = Math.round(Math.random() * 100000);
+
+ let cookie = {
+ host: "http://example.com",
+ path: "/",
+ name: `name${r}`,
+ value: `value${r}`,
+ };
+
+ let state = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:robots", triggeringPrincipal_base64 }] },
+ ],
+ cookies: [cookie],
+ },
+ ],
+ };
+
+ return [state, cookie];
+}
+
+function waitForNewCookie({ host, name, value }) {
+ info(`waiting for cookie ${name}=${value} from ${host}...`);
+
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subj, topic, data) {
+ if (data != "added") {
+ return;
+ }
+
+ let cookie = subj.QueryInterface(Ci.nsICookie);
+ if (cookie.host == host && cookie.name == name && cookie.value == value) {
+ ok(true, "cookie added by the cookie service");
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }
+ }, "session-cookie-changed");
+ });
+}
+
+// Setup and cleanup.
+add_task(async function test_setup() {
+ Services.cookies.removeAll();
+
+ registerCleanupFunction(() => {
+ Services.cookies.removeAll();
+ });
+});
+
+// Test that calling ss.setWindowState() with a legacy state object that
+// contains cookies in the window state restores session cookies properly.
+add_task(async function test_window() {
+ let [state, cookie] = createTestState();
+ let win = await promiseNewWindowLoaded();
+
+ let promiseCookie = waitForNewCookie(cookie);
+ ss.setWindowState(win, JSON.stringify(state), true);
+ await promiseCookie;
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Test that calling ss.setBrowserState() with a legacy state object that
+// contains cookies in the window state restores session cookies properly.
+add_task(async function test_browser() {
+ let backupState = ss.getBrowserState();
+ let [state, cookie] = createTestState();
+ await Promise.all([waitForNewCookie(cookie), promiseBrowserState(state)]);
+ await promiseBrowserState(backupState);
+});
diff --git a/browser/components/sessionstore/test/browser_cookies_privacy.js b/browser/components/sessionstore/test/browser_cookies_privacy.js
new file mode 100644
index 0000000000..2c588c8a49
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cookies_privacy.js
@@ -0,0 +1,125 @@
+"use strict";
+
+// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
+const MAX_EXPIRY = Math.pow(2, 62);
+
+function addCookie(scheme, secure = false) {
+ let cookie = createTestCookie(scheme, secure);
+ Services.cookies.add(
+ cookie.host,
+ cookie.path,
+ cookie.name,
+ cookie.value,
+ cookie.secure,
+ /* isHttpOnly = */ false,
+ /* isSession = */ true,
+ MAX_EXPIRY,
+ /* originAttributes = */ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ return cookie;
+}
+
+function createTestCookie(scheme, secure = false) {
+ let r = Math.round(Math.random() * 100000);
+
+ let cookie = {
+ host: `${scheme}://example.com`,
+ path: "/",
+ name: `name${r}`,
+ value: `value${r}`,
+ secure,
+ };
+
+ return cookie;
+}
+
+function getCookie() {
+ let state = JSON.parse(ss.getBrowserState());
+ let cookies = state.cookies || [];
+ return cookies[0];
+}
+
+function compareCookies(a) {
+ let b = getCookie();
+ return a.host == b.host && a.name == b.name && a.value == b.value;
+}
+
+// Setup and cleanup.
+add_task(async function test_setup() {
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+ Services.cookies.removeAll();
+ });
+});
+
+// Test privacy_level=none (default). We store all session cookies.
+add_task(async function test_level_none() {
+ Services.cookies.removeAll();
+
+ // Set level=none, store all cookies.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 0);
+
+ // With the default privacy level we collect all cookies.
+ ok(compareCookies(addCookie("http")), "non-secure http cookie stored");
+ Services.cookies.removeAll();
+
+ // With the default privacy level we collect all cookies.
+ ok(compareCookies(addCookie("https")), "non-secure https cookie stored");
+ Services.cookies.removeAll();
+
+ // With the default privacy level we collect all cookies.
+ ok(compareCookies(addCookie("https", true)), "secure https cookie stored");
+ Services.cookies.removeAll();
+});
+
+// Test privacy_level=encrypted. We store all non-secure session cookies.
+add_task(async function test_level_encrypted() {
+ Services.cookies.removeAll();
+
+ // Set level=encrypted, don't store any secure cookies.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
+
+ // With level=encrypted, non-secure cookies will be stored.
+ ok(compareCookies(addCookie("http")), "non-secure http cookie stored");
+ Services.cookies.removeAll();
+
+ // With level=encrypted, non-secure cookies will be stored,
+ // even if sent by an HTTPS site.
+ ok(compareCookies(addCookie("https")), "non-secure https cookie stored");
+ Services.cookies.removeAll();
+
+ // With level=encrypted, non-secure cookies will be stored,
+ // even if sent by an HTTPS site.
+ ok(
+ addCookie("https", true) && !getCookie(),
+ "secure https cookie not stored"
+ );
+ Services.cookies.removeAll();
+});
+
+// Test privacy_level=full. We store no session cookies.
+add_task(async function test_level_full() {
+ Services.cookies.removeAll();
+
+ // Set level=full, don't store any cookies.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+ // With level=full we must not store any cookies.
+ ok(addCookie("http") && !getCookie(), "non-secure http cookie not stored");
+ Services.cookies.removeAll();
+
+ // With level=full we must not store any cookies.
+ ok(addCookie("https") && !getCookie(), "non-secure https cookie not stored");
+ Services.cookies.removeAll();
+
+ // With level=full we must not store any cookies.
+ ok(
+ addCookie("https", true) && !getCookie(),
+ "secure https cookie not stored"
+ );
+ Services.cookies.removeAll();
+});
diff --git a/browser/components/sessionstore/test/browser_cookies_sameSite.js b/browser/components/sessionstore/test/browser_cookies_sameSite.js
new file mode 100644
index 0000000000..33a44cb2c5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_cookies_sameSite.js
@@ -0,0 +1,89 @@
+"use strict";
+
+const TEST_HTTP_URL = "http://example.com";
+const TEST_HTTPS_URL = "https://example.com";
+const MAX_EXPIRY = Math.pow(2, 62);
+
+function getSingleCookie() {
+ let cookies = Array.from(Services.cookies.cookies);
+ Assert.equal(cookies.length, 1, "expected one cookie");
+ return cookies[0];
+}
+
+async function verifyRestore(url, sameSiteSetting) {
+ 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, url);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Add a cookie with specific same-site setting.
+ let r = Math.floor(Math.random() * MAX_EXPIRY);
+ Services.cookies.add(
+ url,
+ "/",
+ "name" + r,
+ "value" + r,
+ false,
+ false,
+ true,
+ MAX_EXPIRY,
+ {},
+ sameSiteSetting,
+ url.startsWith("https:")
+ ? Ci.nsICookie.SCHEME_HTTPS
+ : Ci.nsICookie.SCHEME_HTTP
+ );
+ await TabStateFlusher.flush(tab.linkedBrowser);
+
+ // Get the sessionstore state for the window.
+ let state = ss.getBrowserState();
+
+ // Verify our cookie got set.
+ let cookie = getSingleCookie();
+
+ // Remove the cookie.
+ Services.cookies.removeAll();
+
+ // Restore the window state.
+ await setBrowserState(state);
+
+ // At this point, the cookie should be restored.
+ let cookie2 = getSingleCookie();
+
+ is(
+ cookie2.sameSite,
+ cookie.sameSite,
+ "cookie same-site flag successfully restored"
+ );
+
+ is(
+ cookie2.schemeMap,
+ cookie.schemeMap,
+ "cookie schemeMap flag successfully restored"
+ );
+
+ // Clean up.
+ Services.cookies.removeAll();
+ BrowserTestUtils.removeTab(gBrowser.tabs[1]);
+}
+
+/**
+ * Tests that cookie.sameSite flag is stored and restored correctly by
+ * sessionstore.
+ */
+add_task(async function () {
+ // Test for various possible values of cookie.sameSite and schemeMap.
+ await verifyRestore(TEST_HTTP_URL, Ci.nsICookie.SAMESITE_NONE);
+ await verifyRestore(TEST_HTTP_URL, Ci.nsICookie.SAMESITE_LAX);
+ await verifyRestore(TEST_HTTP_URL, Ci.nsICookie.SAMESITE_STRICT);
+
+ await verifyRestore(TEST_HTTPS_URL, Ci.nsICookie.SAMESITE_NONE);
+ await verifyRestore(TEST_HTTPS_URL, Ci.nsICookie.SAMESITE_LAX);
+ await verifyRestore(TEST_HTTPS_URL, Ci.nsICookie.SAMESITE_STRICT);
+});
diff --git a/browser/components/sessionstore/test/browser_crashedTabs.js b/browser/components/sessionstore/test/browser_crashedTabs.js
new file mode 100644
index 0000000000..a49e682d9f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_crashedTabs.js
@@ -0,0 +1,501 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file spawns content tasks.
+
+"use strict";
+
+requestLongerTimeout(10);
+
+const PAGE_1 =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const PAGE_2 =
+ "data:text/html,<html><body>Another%20regular,%20everyday,%20normal%20page.";
+
+// Turn off tab animations for testing and use a single content process
+// for these tests since we want to test tabs within the crashing process here.
+gReduceMotionOverride = true;
+
+// Allow tabs to restore on demand so we can test pending states
+Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+
+function clickButton(browser, id) {
+ info("Clicking " + id);
+ return SpecialPowers.spawn(browser, [id], buttonId => {
+ let button = content.document.getElementById(buttonId);
+ button.click();
+ });
+}
+
+/**
+ * Checks the documentURI of the root document of a remote browser
+ * to see if it equals URI.
+ *
+ * @param browser
+ * The remote <xul:browser> to check the root document URI in.
+ * @param URI
+ * A string to match the root document URI against.
+ * @return Promise
+ */
+async function promiseContentDocumentURIEquals(browser, URI) {
+ let contentURI = await SpecialPowers.spawn(browser, [], () => {
+ return content.document.documentURI;
+ });
+ is(
+ contentURI,
+ URI,
+ `Content has URI ${contentURI} which does not match ${URI}`
+ );
+}
+
+/**
+ * Checks the window.history.length of the root window of a remote
+ * browser to see if it equals length.
+ *
+ * @param browser
+ * The remote <xul:browser> to check the root window.history.length
+ * @param length
+ * The expected history length
+ * @return Promise
+ */
+async function promiseHistoryLength(browser, length) {
+ let contentLength = await SpecialPowers.spawn(browser, [], () => {
+ return content.history.length;
+ });
+ is(
+ contentLength,
+ length,
+ `Content has window.history.length ${contentLength} which does ` +
+ `not equal expected ${length}`
+ );
+}
+
+/**
+ * Returns a Promise that resolves when a browser has fired the
+ * AboutTabCrashedReady event.
+ *
+ * @param browser
+ * The remote <xul:browser> that will fire the event.
+ * @return Promise
+ */
+function promiseTabCrashedReady(browser) {
+ return new Promise(resolve => {
+ browser.addEventListener(
+ "AboutTabCrashedReady",
+ function ready(e) {
+ browser.removeEventListener("AboutTabCrashedReady", ready, false, true);
+ resolve();
+ },
+ false,
+ true
+ );
+ });
+}
+
+/**
+ * Checks that if a tab crashes, that information about the tab crashed
+ * page does not get added to the tab history.
+ */
+add_task(async function test_crash_page_not_in_history() {
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+ await TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ await BrowserTestUtils.crashFrame(browser);
+
+ // Check the tab state and make sure the tab crashed page isn't
+ // mentioned.
+ let { entries } = JSON.parse(ss.getTabState(newTab));
+ is(entries.length, 1, "Should have a single history entry");
+ is(
+ entries[0].url,
+ PAGE_1,
+ "Single entry should be the page we visited before crashing"
+ );
+
+ gBrowser.removeTab(newTab);
+});
+
+/**
+ * Checks that if a tab crashes, that when we browse away from that page
+ * to a non-blacklisted site (so the browser becomes remote again), that
+ * we record history for that new visit.
+ */
+add_task(async function test_revived_history_from_remote() {
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+ await TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ await BrowserTestUtils.crashFrame(browser);
+
+ // Browse to a new site that will cause the browser to
+ // become remote again.
+ BrowserTestUtils.loadURIString(browser, PAGE_2);
+ await promiseBrowserLoaded(browser);
+ ok(
+ !newTab.hasAttribute("crashed"),
+ "Tab shouldn't be marked as crashed anymore."
+ );
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ await TabStateFlusher.flush(browser);
+
+ // Check the tab state and make sure the tab crashed page isn't
+ // mentioned.
+ let { entries } = JSON.parse(ss.getTabState(newTab));
+ is(entries.length, 2, "Should have two history entries");
+ is(
+ entries[0].url,
+ PAGE_1,
+ "First entry should be the page we visited before crashing"
+ );
+ is(
+ entries[1].url,
+ PAGE_2,
+ "Second entry should be the page we visited after crashing"
+ );
+
+ gBrowser.removeTab(newTab);
+});
+
+/**
+ * Checks that if a tab crashes, that when we browse away from that page
+ * to a blacklisted site (so the browser stays non-remote), that
+ * we record history for that new visit.
+ */
+add_task(async function test_revived_history_from_non_remote() {
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+ await TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ await BrowserTestUtils.crashFrame(browser);
+
+ // Browse to a new site that will not cause the browser to
+ // become remote again.
+ BrowserTestUtils.loadURIString(browser, "about:mozilla");
+ await promiseBrowserLoaded(browser);
+ ok(
+ !newTab.hasAttribute("crashed"),
+ "Tab shouldn't be marked as crashed anymore."
+ );
+ ok(!browser.isRemoteBrowser, "Should not be a remote browser");
+ await TabStateFlusher.flush(browser);
+
+ // Check the tab state and make sure the tab crashed page isn't
+ // mentioned.
+ let { entries } = JSON.parse(ss.getTabState(newTab));
+ is(entries.length, 2, "Should have two history entries");
+ is(
+ entries[0].url,
+ PAGE_1,
+ "First entry should be the page we visited before crashing"
+ );
+ is(
+ entries[1].url,
+ "about:mozilla",
+ "Second entry should be the page we visited after crashing"
+ );
+
+ gBrowser.removeTab(newTab);
+});
+
+/**
+ * Checks that we can revive a crashed tab back to the page that
+ * it was on when it crashed.
+ */
+add_task(async function test_revive_tab_from_session_store() {
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+
+ let newTab2 = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ remoteType: browser.remoteType,
+ initialBrowsingContextGroupId: browser.browsingContext.group.id,
+ });
+ let browser2 = newTab2.linkedBrowser;
+ ok(browser2.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser2);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_2);
+ await promiseBrowserLoaded(browser);
+
+ await TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ await BrowserTestUtils.crashFrame(browser);
+ // Background tabs should not be crashed, but should be in the "to be restored"
+ // state.
+ ok(!newTab2.hasAttribute("crashed"), "Second tab should not be crashed.");
+ ok(newTab2.hasAttribute("pending"), "Second tab should be pending.");
+
+ // Use SessionStore to revive the first tab
+ let tabRestoredPromise = promiseTabRestored(newTab);
+ await clickButton(browser, "restoreTab");
+ await tabRestoredPromise;
+ ok(
+ !newTab.hasAttribute("crashed"),
+ "Tab shouldn't be marked as crashed anymore."
+ );
+ ok(newTab2.hasAttribute("pending"), "Second tab should still be pending.");
+
+ // We can't just check browser.currentURI.spec, because from
+ // the outside, a crashed tab has the same URI as the page
+ // it crashed on (much like an about:neterror page). Instead,
+ // we have to use the documentURI on the content.
+ await promiseContentDocumentURIEquals(browser, PAGE_2);
+
+ // We should also have two entries in the browser history.
+ await promiseHistoryLength(browser, 2);
+
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(newTab2);
+});
+
+/**
+ * Checks that we can revive multiple crashed tabs back to the pages
+ * that they were on when they crashed.
+ */
+add_task(async function test_revive_all_tabs_from_session_store() {
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+
+ // In order to see a second about:tabcrashed page, we'll need
+ // a second window, since only selected tabs will show
+ // about:tabcrashed.
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+ let newTab2 = BrowserTestUtils.addTab(win2.gBrowser, PAGE_1, {
+ remoteType: browser.remoteType,
+ initialBrowsingContextGroupId: browser.browsingContext.group.id,
+ });
+ win2.gBrowser.selectedTab = newTab2;
+ let browser2 = newTab2.linkedBrowser;
+ ok(browser2.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser2);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_2);
+ await promiseBrowserLoaded(browser);
+
+ await TabStateFlusher.flush(browser);
+ await TabStateFlusher.flush(browser2);
+
+ // Crash the tab
+ await BrowserTestUtils.crashFrame(browser);
+ // Both tabs should now be crashed.
+ is(newTab.getAttribute("crashed"), "true", "First tab should be crashed");
+ is(
+ newTab2.getAttribute("crashed"),
+ "true",
+ "Second window tab should be crashed"
+ );
+
+ // Use SessionStore to revive all the tabs
+ let tabRestoredPromises = Promise.all([
+ promiseTabRestored(newTab),
+ promiseTabRestored(newTab2),
+ ]);
+ await clickButton(browser, "restoreAll");
+ await tabRestoredPromises;
+
+ ok(
+ !newTab.hasAttribute("crashed"),
+ "Tab shouldn't be marked as crashed anymore."
+ );
+ ok(!newTab.hasAttribute("pending"), "Tab shouldn't be pending.");
+ ok(
+ !newTab2.hasAttribute("crashed"),
+ "Second tab shouldn't be marked as crashed anymore."
+ );
+ ok(!newTab2.hasAttribute("pending"), "Second tab shouldn't be pending.");
+
+ // We can't just check browser.currentURI.spec, because from
+ // the outside, a crashed tab has the same URI as the page
+ // it crashed on (much like an about:neterror page). Instead,
+ // we have to use the documentURI on the content.
+ await promiseContentDocumentURIEquals(browser, PAGE_2);
+ await promiseContentDocumentURIEquals(browser2, PAGE_1);
+
+ // We should also have two entries in the browser history.
+ await promiseHistoryLength(browser, 2);
+
+ await BrowserTestUtils.closeWindow(win2);
+ gBrowser.removeTab(newTab);
+});
+
+/**
+ * Checks that about:tabcrashed can close the current tab
+ */
+add_task(async function test_close_tab_after_crash() {
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+
+ await TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ await BrowserTestUtils.crashFrame(browser);
+
+ let promise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabClose"
+ );
+
+ // Click the close tab button
+ await clickButton(browser, "closeTab");
+ await promise;
+
+ is(gBrowser.tabs.length, 1, "Should have closed the tab");
+});
+
+/**
+ * Checks that "restore all" button is only shown if more than one tab
+ * is showing about:tabcrashed
+ */
+add_task(async function test_hide_restore_all_button() {
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+
+ await TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ await BrowserTestUtils.crashFrame(browser);
+
+ let doc = browser.contentDocument;
+ let restoreAllButton = doc.getElementById("restoreAll");
+ let restoreOneButton = doc.getElementById("restoreTab");
+
+ let restoreAllStyles = window.getComputedStyle(restoreAllButton);
+ is(restoreAllStyles.display, "none", "Restore All button should be hidden");
+ ok(
+ restoreOneButton.classList.contains("primary"),
+ "Restore Tab button should have the primary class"
+ );
+
+ let newTab2 = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+
+ BrowserTestUtils.loadURIString(browser, PAGE_2);
+ await promiseBrowserLoaded(browser);
+
+ // Load up a second window so we can get another tab to show
+ // about:tabcrashed
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+ let newTab3 = BrowserTestUtils.addTab(win2.gBrowser, PAGE_2, {
+ remoteType: browser.remoteType,
+ initialBrowsingContextGroupId: browser.browsingContext.group.id,
+ });
+ win2.gBrowser.selectedTab = newTab3;
+ let otherWinBrowser = newTab3.linkedBrowser;
+ await promiseBrowserLoaded(otherWinBrowser);
+ // We'll need to make sure the second tab's browser has finished
+ // sending its AboutTabCrashedReady event before we know for
+ // sure whether or not we're showing the right Restore buttons.
+ let otherBrowserReady = promiseTabCrashedReady(otherWinBrowser);
+
+ // Crash the first tab.
+ await BrowserTestUtils.crashFrame(browser);
+ await otherBrowserReady;
+
+ doc = browser.contentDocument;
+ restoreAllButton = doc.getElementById("restoreAll");
+ restoreOneButton = doc.getElementById("restoreTab");
+
+ restoreAllStyles = window.getComputedStyle(restoreAllButton);
+ isnot(
+ restoreAllStyles.display,
+ "none",
+ "Restore All button should not be hidden"
+ );
+ ok(
+ !restoreOneButton.classList.contains("primary"),
+ "Restore Tab button should not have the primary class"
+ );
+
+ await BrowserTestUtils.closeWindow(win2);
+ gBrowser.removeTab(newTab);
+ gBrowser.removeTab(newTab2);
+});
+
+add_task(async function test_aboutcrashedtabzoom() {
+ let newTab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+ ok(browser.isRemoteBrowser, "Should be a remote browser");
+ await promiseBrowserLoaded(browser);
+
+ BrowserTestUtils.loadURIString(browser, PAGE_1);
+ await promiseBrowserLoaded(browser);
+
+ FullZoom.enlarge();
+ let zoomLevel = ZoomManager.getZoomForBrowser(browser);
+ ok(zoomLevel !== 1, "should have enlarged");
+
+ await TabStateFlusher.flush(browser);
+
+ // Crash the tab
+ await BrowserTestUtils.crashFrame(browser);
+
+ ok(
+ ZoomManager.getZoomForBrowser(browser) === 1,
+ "zoom should have reset on crash"
+ );
+
+ let tabRestoredPromise = promiseTabRestored(newTab);
+ await clickButton(browser, "restoreTab");
+ await tabRestoredPromise;
+
+ ok(
+ ZoomManager.getZoomForBrowser(browser) === zoomLevel,
+ "zoom should have gone back to enlarged"
+ );
+ FullZoom.reset();
+
+ gBrowser.removeTab(newTab);
+});
diff --git a/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js b/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js
new file mode 100644
index 0000000000..2414e04276
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js
@@ -0,0 +1,104 @@
+// First test - open a tab and duplicate it, using session restore to restore the history into the new tab.
+add_task(async function duplicateTab() {
+ const TEST_URL = "data:text/html,foo";
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ if (!Services.appinfo.sessionHistoryInParent) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let docshell = content.window.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ );
+ let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0);
+ is(shEntry.docshellID.toString(), docshell.historyID.toString());
+ });
+ } else {
+ let historyID = tab.linkedBrowser.browsingContext.historyID;
+ let shEntry =
+ tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
+ is(shEntry.docshellID.toString(), historyID.toString());
+ }
+
+ let tab2 = gBrowser.duplicateTab(tab);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
+
+ if (!Services.appinfo.sessionHistoryInParent) {
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], function () {
+ let docshell = content.window.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ );
+ let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0);
+ is(shEntry.docshellID.toString(), docshell.historyID.toString());
+ });
+ } else {
+ let historyID = tab2.linkedBrowser.browsingContext.historyID;
+ let shEntry =
+ tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
+ is(shEntry.docshellID.toString(), historyID.toString());
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+// Second test - open a tab and navigate across processes, which triggers sessionrestore to persist history.
+add_task(async function contentToChromeNavigate() {
+ const TEST_URL = "data:text/html,foo";
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ if (!Services.appinfo.sessionHistoryInParent) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let docshell = content.window.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ );
+ let sh = docshell.sessionHistory;
+ is(sh.count, 1);
+ is(
+ sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(),
+ docshell.historyID.toString()
+ );
+ });
+ } else {
+ let historyID = tab.linkedBrowser.browsingContext.historyID;
+ let sh = tab.linkedBrowser.browsingContext.sessionHistory;
+ is(sh.count, 1);
+ is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString());
+ }
+
+ // Force the browser to navigate to the chrome process.
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:config");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Check to be sure that we're in the chrome process.
+ let docShell = tab.linkedBrowser.frameLoader.docShell;
+
+ // 'cause we're in the chrome process, we can just directly poke at the shistory.
+ if (!Services.appinfo.sessionHistoryInParent) {
+ let sh = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
+
+ is(sh.count, 2);
+ is(
+ sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(),
+ docShell.historyID.toString()
+ );
+ is(
+ sh.legacySHistory.getEntryAtIndex(1).docshellID.toString(),
+ docShell.historyID.toString()
+ );
+ } else {
+ let sh = docShell.browsingContext.sessionHistory;
+
+ is(sh.count, 2);
+ is(
+ sh.getEntryAtIndex(0).docshellID.toString(),
+ docShell.historyID.toString()
+ );
+ is(
+ sh.getEntryAtIndex(1).docshellID.toString(),
+ docShell.historyID.toString()
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_duplicate_history.js b/browser/components/sessionstore/test/browser_duplicate_history.js
new file mode 100644
index 0000000000..6bb2511390
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_duplicate_history.js
@@ -0,0 +1,30 @@
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
+});
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "http://example.com",
+ async function (aBrowser) {
+ let tab = gBrowser.getTabForBrowser(aBrowser);
+ await TabStateFlusher.flush(aBrowser);
+ let before = TabStateCache.get(aBrowser.permanentKey);
+
+ let newTab = SessionStore.duplicateTab(window, tab);
+ await Promise.all([
+ BrowserTestUtils.browserLoaded(newTab.linkedBrowser),
+ TestUtils.topicObserved("sessionstore-debug-tab-restored"),
+ ]);
+ let after = TabStateCache.get(newTab.linkedBrowser.permanentKey);
+
+ isnot(
+ before.history.entries,
+ after.history.entries,
+ "The entry objects should not be shared"
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+ }
+ );
+});
diff --git a/browser/components/sessionstore/test/browser_duplicate_tab_in_new_window.js b/browser/components/sessionstore/test/browser_duplicate_tab_in_new_window.js
new file mode 100644
index 0000000000..b3e5bfcdba
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_duplicate_tab_in_new_window.js
@@ -0,0 +1,37 @@
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+});
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (aBrowser) {
+ BrowserTestUtils.loadURIString(aBrowser, "https://example.org");
+ await BrowserTestUtils.browserLoaded(aBrowser);
+
+ let windowOpened = BrowserTestUtils.waitForNewWindow(
+ "https://example.org"
+ );
+ let newWindow = gBrowser.replaceTabWithWindow(
+ gBrowser.getTabForBrowser(aBrowser)
+ );
+ await windowOpened;
+ let newTab = SessionStore.duplicateTab(
+ newWindow,
+ newWindow.gBrowser.selectedTab
+ );
+
+ await BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+
+ await SpecialPowers.spawn(
+ newTab.linkedBrowser,
+ ["https://example.org"],
+ async ORIGIN => {
+ is(content.window.origin, ORIGIN);
+ }
+ );
+
+ BrowserTestUtils.closeWindow(newWindow);
+ }
+ );
+});
diff --git a/browser/components/sessionstore/test/browser_dying_cache.js b/browser/components/sessionstore/test/browser_dying_cache.js
new file mode 100644
index 0000000000..3b8a5b1197
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_dying_cache.js
@@ -0,0 +1,80 @@
+"use strict";
+
+/**
+ * This test ensures that after closing a window we keep its state data around
+ * as long as something keeps a reference to it. It should only be possible to
+ * read data after closing - writing should fail.
+ */
+
+add_task(async function test() {
+ // Open a new window.
+ let win = await promiseNewWindowLoaded();
+
+ // Load some URL in the current tab.
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, "about:robots", {
+ flags,
+ });
+ await promiseBrowserLoaded(win.gBrowser.selectedBrowser);
+
+ // Open a second tab and close the first one.
+ let tab = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla");
+ await promiseBrowserLoaded(tab.linkedBrowser);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ await promiseRemoveTabAndSessionState(win.gBrowser.tabs[0]);
+
+ // Make sure our window is still tracked by sessionstore
+ // and the window state is as expected.
+ ok("__SSi" in win, "window is being tracked by sessionstore");
+ ss.setCustomWindowValue(win, "foo", "bar");
+ checkWindowState(win);
+
+ // Close our window.
+ await BrowserTestUtils.closeWindow(win);
+
+ // SessionStore should no longer track our window
+ // but it should still report the same state.
+ ok(!("__SSi" in win), "sessionstore does no longer track our window");
+ checkWindowState(win);
+
+ // Make sure we're not allowed to modify state data.
+ Assert.throws(
+ () => ss.setWindowState(win, {}),
+ /Window is not tracked/,
+ "we're not allowed to modify state data anymore"
+ );
+ Assert.throws(
+ () => ss.setCustomWindowValue(win, "foo", "baz"),
+ /Window is not tracked/,
+ "we're not allowed to modify state data anymore"
+ );
+});
+
+function checkWindowState(window) {
+ let {
+ windows: [{ tabs }],
+ } = ss.getWindowState(window);
+ is(tabs.length, 1, "the window has a single tab");
+ is(tabs[0].entries[0].url, "about:mozilla", "the tab is about:mozilla");
+
+ is(ss.getClosedTabCountForWindow(window), 1, "the window has one closed tab");
+ let [
+ {
+ state: {
+ entries: [{ url }],
+ },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ is(url, "about:robots", "the closed tab is about:robots");
+
+ is(ss.getCustomWindowValue(window, "foo"), "bar", "correct extData value");
+}
+
+function shouldThrow(f) {
+ try {
+ f();
+ } catch (e) {
+ return true;
+ }
+ return null;
+}
diff --git a/browser/components/sessionstore/test/browser_dynamic_frames.js b/browser/components/sessionstore/test/browser_dynamic_frames.js
new file mode 100644
index 0000000000..bb5debf83d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_dynamic_frames.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure that static frames of framesets are serialized but dynamically
+ * inserted iframes are ignored.
+ */
+add_task(async function () {
+ // allow top level data: URI navigations, otherwise clicking a data: link fails
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", false]],
+ });
+ // This URL has the following frames:
+ // + data:text/html,A (static)
+ // + data:text/html,B (static)
+ // + data:text/html,C (dynamic iframe)
+ const URL =
+ "data:text/html;charset=utf-8," +
+ "<frameset cols=50%25,50%25><frame src='data:text/html,A'>" +
+ "<frame src='data:text/html,B'></frameset>" +
+ "<script>var i=document.createElement('iframe');" +
+ "i.setAttribute('src', 'data:text/html,C');" +
+ "document.body.appendChild(i);</script>";
+
+ // Add a new tab with two "static" and one "dynamic" frame.
+ 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 URLs.
+ ok(entries[0].url.startsWith("data:text/html"), "correct root url");
+ is(
+ entries[0].children[0].url,
+ "data:text/html,A",
+ "correct url for 1st frame"
+ );
+ is(
+ entries[0].children[1].url,
+ "data:text/html,B",
+ "correct url for 2nd frame"
+ );
+
+ // Check the number of children.
+ is(entries.length, 1, "there is one root entry ...");
+ is(entries[0].children.length, 2, "... with two child entries");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that iframes created by the network parser are serialized but
+ * dynamically inserted iframes are ignored. Navigating a subframe should
+ * create a second root entry that doesn't contain any dynamic children either.
+ */
+add_task(async function () {
+ // allow top level data: URI navigations, otherwise clicking a data: link fails
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", false]],
+ });
+ // This URL has the following frames:
+ // + data:text/html,A (static)
+ // + data:text/html,C (dynamic iframe)
+ const URL =
+ "data:text/html;charset=utf-8," +
+ "<iframe name=t src='data:text/html,A'></iframe>" +
+ "<a id=lnk href='data:text/html,B' target=t>clickme</a>" +
+ "<script>var i=document.createElement('iframe');" +
+ "i.setAttribute('src', 'data:text/html,C');" +
+ "document.body.appendChild(i);</script>";
+
+ // Add a new tab with one "static" and one "dynamic" frame.
+ 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 URLs.
+ ok(entries[0].url.startsWith("data:text/html"), "correct root url");
+ ok(!entries[0].children, "no children collected");
+
+ // Navigate the subframe.
+ await BrowserTestUtils.synthesizeMouseAtCenter("#lnk", {}, browser);
+ await promiseBrowserLoaded(browser, false /* don't ignore subframes */);
+
+ await TabStateFlusher.flush(browser);
+ ({ entries } = JSON.parse(ss.getTabState(tab)));
+
+ // Check URLs.
+ ok(entries[0].url.startsWith("data:text/html"), "correct 1st root url");
+ ok(entries[1].url.startsWith("data:text/html"), "correct 2nd root url");
+ ok(!entries.children, "no children collected");
+ ok(!entries[0].children, "no children collected");
+ ok(!entries[1].children, "no children collected");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_firefoxView_restore.js b/browser/components/sessionstore/test/browser_firefoxView_restore.js
new file mode 100644
index 0000000000..9af181e0f5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_firefoxView_restore.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CLOSED_URI = "https://www.example.com/";
+
+add_task(async function test_TODO() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, CLOSED_URI);
+
+ Assert.equal(gBrowser.tabs[0].linkedBrowser.currentURI.filePath, "blank");
+
+ Assert.equal(gBrowser.tabs[1].linkedBrowser.currentURI.spec, CLOSED_URI);
+
+ Assert.ok(gBrowser.selectedTab == tab);
+
+ let state = ss.getCurrentState(true);
+
+ // SessionStore uses one-based indexes
+ Assert.equal(state.windows[0].selected, 2);
+
+ await EventUtils.synthesizeMouseAtCenter(
+ window.document.getElementById("firefox-view-button"),
+ { type: "mousedown" },
+ window
+ );
+ Assert.ok(window.FirefoxViewHandler.tab.selected);
+
+ Assert.equal(gBrowser.tabs[2], window.FirefoxViewHandler.tab);
+
+ state = ss.getCurrentState(true);
+
+ // The FxView tab doesn't get recorded in the session state, but if it's the last selected tab when a window is closed
+ // we want to point to the first tab in the tab strip upon restore
+ Assert.equal(state.windows[0].selected, 1);
+
+ gBrowser.removeTab(window.FirefoxViewHandler.tab);
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_firefoxView_selected_restore.js b/browser/components/sessionstore/test/browser_firefoxView_selected_restore.js
new file mode 100644
index 0000000000..c5da71d96d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_firefoxView_selected_restore.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { _LastSession } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionStore.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ {
+ url: "https://example.org/",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ ],
+ selected: 2,
+ },
+ ],
+};
+
+add_task(async function test_firefox_view_selected_tab() {
+ let fxViewBtn = document.getElementById("firefox-view-button");
+ ok(fxViewBtn, "Got the Firefox View button");
+ fxViewBtn.click();
+
+ await BrowserTestUtils.browserLoaded(
+ window.FirefoxViewHandler.tab.linkedBrowser
+ );
+
+ let allTabsRestored = promiseSessionStoreLoads(1);
+
+ _LastSession.setState(state);
+
+ is(gBrowser.tabs.length, 2, "Number of tabs is 2");
+
+ ss.restoreLastSession();
+
+ await allTabsRestored;
+
+ ok(
+ window.FirefoxViewHandler.tab.selected,
+ "The Firefox View tab is selected and the browser window did not close"
+ );
+ is(gBrowser.tabs.length, 3, "Number of tabs is 3");
+
+ gBrowser.removeTab(window.FirefoxViewHandler.tab);
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_firefox_view_previously_selected() {
+ let fxViewBtn = document.getElementById("firefox-view-button");
+ ok(fxViewBtn, "Got the Firefox View button");
+ fxViewBtn.click();
+
+ await BrowserTestUtils.browserLoaded(
+ window.FirefoxViewHandler.tab.linkedBrowser
+ );
+
+ let tab = gBrowser.tabs[1];
+ gBrowser.selectedTab = tab;
+
+ let allTabsRestored = promiseSessionStoreLoads(1);
+
+ _LastSession.setState(state);
+
+ is(gBrowser.tabs.length, 2, "Number of tabs is 2");
+
+ ss.restoreLastSession();
+
+ await allTabsRestored;
+
+ ok(
+ window.FirefoxViewHandler.tab.selected,
+ "The Firefox View tab is selected and the browser window did not close"
+ );
+ is(gBrowser.tabs.length, 3, "Number of tabs is 3");
+
+ gBrowser.removeTab(window.FirefoxViewHandler.tab);
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/sessionstore/test/browser_focus_after_restore.js b/browser/components/sessionstore/test/browser_focus_after_restore.js
new file mode 100644
index 0000000000..220827657e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_focus_after_restore.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test() {
+ gURLBar.focus();
+ is(
+ document.activeElement,
+ gURLBar.inputField,
+ "urlbar is focused before restoring"
+ );
+
+ await promiseBrowserState({
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ {
+ url: "http://example.org/",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ ],
+ selected: 1,
+ },
+ ],
+ });
+ is(
+ document.activeElement,
+ gBrowser.selectedBrowser,
+ "content area is focused after restoring"
+ );
+});
diff --git a/browser/components/sessionstore/test/browser_forget_async_closings.js b/browser/components/sessionstore/test/browser_forget_async_closings.js
new file mode 100644
index 0000000000..1d6fd42df2
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_forget_async_closings.js
@@ -0,0 +1,163 @@
+"use strict";
+
+const PAGE = "http://example.com/";
+
+/**
+ * Creates a tab in the current window worth storing in the
+ * closedTabs array, and then closes it. Runs a synchronous
+ * forgetFn passed in that should cause us to forget the tab,
+ * and then ensures that after the tab has sent its final
+ * update message that we didn't accidentally store it in
+ * the closedTabs array.
+ *
+ * @param forgetFn (function)
+ * A synchronous function that should cause the tab
+ * to be forgotten.
+ * @returns Promise
+ */
+let forgetTabHelper = async function (forgetFn) {
+ // We want to suppress all non-final updates from the browser tabs
+ // so as to eliminate any racy-ness with this test.
+ await pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+ // Forget any previous closed tabs from other tests that may have
+ // run in the same session.
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+
+ is(
+ ss.getClosedTabCountForWindow(window),
+ 0,
+ "We should have 0 closed tabs being stored."
+ );
+
+ // Create a tab worth remembering.
+ let tab = BrowserTestUtils.addTab(gBrowser, PAGE);
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser, false, PAGE);
+ await TabStateFlusher.flush(browser);
+
+ // Now close the tab, and immediately choose to forget it.
+ let promise = promiseRemoveTabAndSessionState(tab);
+
+ // At this point, the tab will have closed, but the final update
+ // to SessionStore hasn't come up yet. Now do the operation that
+ // should cause us to forget the tab.
+ forgetFn();
+
+ is(
+ ss.getClosedTabCountForWindow(window),
+ 0,
+ "Should have forgotten the closed tab"
+ );
+
+ // Now wait for the final update to come up.
+ await promise;
+
+ is(
+ ss.getClosedTabCountForWindow(window),
+ 0,
+ "Should not have stored the forgotten closed tab"
+ );
+};
+
+/**
+ * Creates a new window worth storing in the closeWIndows array,
+ * and then closes it. Runs a synchronous forgetFn passed in that
+ * should cause us to forget the window, and then ensures that after
+ * the window has sent its final update message that we didn't
+ * accidentally store it in the closedWindows array.
+ *
+ * @param forgetFn (function)
+ * A synchronous function that should cause the window
+ * to be forgotten.
+ * @returns Promise
+ */
+let forgetWinHelper = async function (forgetFn) {
+ // We want to suppress all non-final updates from the browser tabs
+ // so as to eliminate any racy-ness with this test.
+ await pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+ // Forget any previous closed windows from other tests that may have
+ // run in the same session.
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+
+ is(
+ ss.getClosedWindowCount(),
+ 0,
+ "We should have 0 closed windows being stored."
+ );
+
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Create a tab worth remembering.
+ let tab = newWin.gBrowser.selectedTab;
+ let browser = tab.linkedBrowser;
+ BrowserTestUtils.loadURIString(browser, PAGE);
+ await BrowserTestUtils.browserLoaded(browser, false, PAGE);
+ await TabStateFlusher.flush(browser);
+
+ // Now close the window and immediately choose to forget it.
+ let windowClosed = BrowserTestUtils.windowClosed(newWin);
+
+ let handled = false;
+ whenDomWindowClosedHandled(() => {
+ // At this point, the window will have closed and the onClose handler
+ // has run, but the final update to SessionStore hasn't come up yet.
+ // Now do the oepration that should cause us to forget the window.
+ forgetFn();
+
+ is(ss.getClosedWindowCount(), 0, "Should have forgotten the closed window");
+
+ handled = true;
+ });
+
+ newWin.close();
+
+ // Now wait for the final update to come up.
+ await windowClosed;
+
+ ok(handled, "domwindowclosed should already be handled here");
+
+ is(ss.getClosedWindowCount(), 0, "Should not have stored the closed window");
+};
+
+/**
+ * Tests that if we choose to forget a tab while waiting for its
+ * final flush to complete, we don't accidentally store it.
+ */
+add_task(async function test_forget_closed_tab() {
+ await forgetTabHelper(() => {
+ ss.forgetClosedTab(window, 0);
+ });
+});
+
+/**
+ * Tests that if we choose to forget a tab while waiting for its
+ * final flush to complete, we don't accidentally store it.
+ */
+add_task(async function test_forget_closed_window() {
+ await forgetWinHelper(() => {
+ ss.forgetClosedWindow(0);
+ });
+});
+
+/**
+ * Tests that if we choose to purge history while waiting for a
+ * final flush of a tab to complete, we don't accidentally store it.
+ */
+add_task(async function test_forget_purged_tab() {
+ await forgetTabHelper(() => {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ });
+});
+
+/**
+ * Tests that if we choose to purge history while waiting for a
+ * final flush of a window to complete, we don't accidentally
+ * store it.
+ */
+add_task(async function test_forget_purged_window() {
+ await forgetWinHelper(() => {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ });
+});
diff --git a/browser/components/sessionstore/test/browser_formdata.js b/browser/components/sessionstore/test/browser_formdata.js
new file mode 100644
index 0000000000..7e814e1423
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+/**
+ * This test ensures that form data collection respects the privacy level as
+ * set by the user.
+ */
+add_task(async function test_formdata() {
+ const URL =
+ "http://mochi.test:8888/browser/browser/components/" +
+ "sessionstore/test/browser_formdata_sample.html";
+
+ const OUTER_VALUE = "browser_formdata_" + Math.random();
+ const INNER_VALUE = "browser_formdata_" + Math.random();
+
+ // Creates a tab, loads a page with some form fields,
+ // modifies their values and closes the tab.
+ async function createAndRemoveTab() {
+ // Create a new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Modify form data.
+ await setPropertyOfFormField(browser, "#txt", "value", OUTER_VALUE);
+ await setPropertyOfFormField(
+ browser.browsingContext.children[0],
+ "#txt",
+ "value",
+ INNER_VALUE
+ );
+
+ // Remove the tab.
+ await promiseRemoveTabAndSessionState(tab);
+ }
+
+ await createAndRemoveTab();
+ let [
+ {
+ state: { formdata },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
+ is(formdata.children[0].id.txt, INNER_VALUE, "inner value is correct");
+
+ // Disable saving data for encrypted sites.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
+
+ await createAndRemoveTab();
+ [
+ {
+ state: { formdata },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
+ ok(!formdata.children, "inner value was *not* stored");
+
+ // Disable saving data for any site.
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+ await createAndRemoveTab();
+ [
+ {
+ state: { formdata },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ ok(!formdata, "form data has *not* been stored");
+
+ // Restore the default privacy level.
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+});
+
+/**
+ * This test ensures that a malicious website can't trick us into restoring
+ * form data into a wrong website and that we always check the stored URL
+ * before doing so.
+ */
+add_task(async function test_url_check() {
+ const URL = "data:text/html;charset=utf-8,<input id=input>";
+ const VALUE = "value-" + Math.random();
+
+ // Create a tab with an iframe containing an input field.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Restore a tab state with a given form data url.
+ async function restoreStateWithURL(url) {
+ let state = {
+ entries: [{ url: URL, triggeringPrincipal_base64 }],
+ formdata: { id: { input: VALUE } },
+ };
+
+ if (url) {
+ state.formdata.url = url;
+ }
+
+ await promiseTabState(tab, state);
+ return getPropertyOfFormField(browser, "#input", "value");
+ }
+
+ // Check that the form value is restored with the correct URL.
+ is(await restoreStateWithURL(URL), VALUE, "form data restored");
+
+ // Check that the form value is *not* restored with the wrong URL.
+ is(await restoreStateWithURL(URL + "?"), "", "form data not restored");
+ is(await restoreStateWithURL(), "", "form data not restored");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures that collecting form data works as expected when having
+ * nested frame sets.
+ */
+add_task(async function test_nested() {
+ const URL =
+ "data:text/html;charset=utf-8," +
+ "<iframe src='data:text/html;charset=utf-8,<input/>'/>";
+
+ const FORM_DATA = {
+ children: [
+ {
+ url: "data:text/html;charset=utf-8,<input/>",
+ xpath: { "/xhtml:html/xhtml:body/xhtml:input": "m" },
+ },
+ ],
+ };
+
+ // Create a tab with an iframe containing an input field.
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, URL));
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser, false /* don't ignore subframes */);
+
+ const iframe = await SpecialPowers.spawn(browser, [], () => {
+ return content.document.querySelector("iframe").browsingContext;
+ });
+ await SpecialPowers.spawn(iframe, [], async () => {
+ const input = content.document.querySelector("input");
+ const focusPromise = new Promise(resolve => {
+ input.addEventListener("focus", resolve, { once: true });
+ });
+ input.focus();
+ await focusPromise;
+ });
+
+ // Modify the input field's value.
+ await BrowserTestUtils.synthesizeKey("m", {}, browser);
+
+ // Remove the tab and check that we stored form data correctly.
+ await promiseRemoveTabAndSessionState(tab);
+ let [
+ {
+ state: { formdata },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ is(
+ JSON.stringify(formdata),
+ JSON.stringify(FORM_DATA),
+ "formdata for iframe stored correctly"
+ );
+
+ // Restore the closed tab.
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ await promiseTabRestored(tab);
+
+ // Check that the input field has the right value.
+ await TabStateFlusher.flush(browser);
+ ({ formdata } = JSON.parse(ss.getTabState(tab)));
+ is(
+ JSON.stringify(formdata),
+ JSON.stringify(FORM_DATA),
+ "formdata for iframe restored correctly"
+ );
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures that collecting form data for documents with
+ * designMode=on works as expected.
+ */
+add_task(async function test_design_mode() {
+ const URL =
+ "data:text/html;charset=utf-8,<h1>mozilla</h1>" +
+ "<script>document.designMode='on'</script>";
+
+ // Load a tab with an editable document.
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, URL));
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Modify the document content.
+ await BrowserTestUtils.synthesizeKey("m", {}, browser);
+
+ // Close and restore the tab.
+ await promiseRemoveTabAndSessionState(tab);
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ await promiseTabRestored(tab);
+
+ // Check that the innerHTML value was restored.
+ let html = await getPropertyOfFormField(browser, "body", "innerHTML");
+ let expected = "<h1>mmozilla</h1><script>document.designMode='on'</script>";
+ is(html, expected, "editable document has been restored correctly");
+
+ // Close and restore the tab.
+ await promiseRemoveTabAndSessionState(tab);
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ await promiseTabRestored(tab);
+
+ // Check that the innerHTML value was restored.
+ html = await getPropertyOfFormField(browser, "body", "innerHTML");
+ expected = "<h1>mmozilla</h1><script>document.designMode='on'</script>";
+ is(html, expected, "editable document has been restored correctly");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_formdata_cc.js b/browser/components/sessionstore/test/browser_formdata_cc.js
new file mode 100644
index 0000000000..7f0dac8115
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_cc.js
@@ -0,0 +1,107 @@
+"use strict";
+
+const URL =
+ "http://mochi.test:8888/browser/browser/components/" +
+ "sessionstore/test/browser_formdata_sample.html";
+
+requestLongerTimeout(3);
+
+/**
+ * This test ensures that credit card numbers in form data will not be
+ * collected, while numbers that don't look like credit card numbers will
+ * still be collected.
+ */
+add_task(async function () {
+ const validCCNumbers = [
+ // 15 digits
+ "930771457288760",
+ "474915027480942",
+ "924894781317325",
+ "714816113937185",
+ "790466087343106",
+ "474320195408363",
+ "219211148122351",
+ "633038472250799",
+ "354236732906484",
+ "095347810189325",
+ // 16 digits
+ "3091269135815020",
+ "5471839082338112",
+ "0580828863575793",
+ "5015290610002932",
+ "9465714503078607",
+ "4302068493801686",
+ "2721398408985465",
+ "6160334316984331",
+ "8643619970075142",
+ "0218246069710785",
+ ];
+
+ const invalidCCNumbers = [
+ // 15 digits
+ "526931005800649",
+ "724952425140686",
+ "379761391174135",
+ "030551436468583",
+ "947377014076746",
+ "254848023655752",
+ "226871580283345",
+ "708025346034339",
+ "917585839076788",
+ "918632588027666",
+ // 16 digits
+ "9946177098017064",
+ "4081194386488872",
+ "3095975979578034",
+ "3662215692222536",
+ "6723210018630429",
+ "4411962856225025",
+ "8276996369036686",
+ "4449796938248871",
+ "3350852696538147",
+ "5011802870046957",
+ ];
+
+ // Creates a tab, loads a page with a form field, sets the value of the
+ // field, and then removes the tab to trigger data collection.
+ async function createAndRemoveTab(formValue) {
+ // Create a new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Set form value.
+ await setInputValue(browser, formValue);
+
+ // Remove the tab.
+ await promiseRemoveTabAndSessionState(tab);
+ }
+
+ // Test that valid CC numbers are not collected.
+ for (let number of validCCNumbers) {
+ await createAndRemoveTab(number);
+ let [{ state }] = ss.getClosedTabDataForWindow(window);
+ ok(!("formdata" in state), "valid CC numbers are not collected");
+ }
+
+ // Test that non-CC numbers are still collected.
+ for (let number of invalidCCNumbers) {
+ await createAndRemoveTab(number);
+ let [
+ {
+ state: { formdata },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ is(
+ formdata.id.txt,
+ number,
+ "numbers that are not valid CC numbers are still collected"
+ );
+ }
+});
+
+function setInputValue(browser, formValue) {
+ return SpecialPowers.spawn(browser, [formValue], async function (newValue) {
+ content.document.getElementById("txt").setUserInput(newValue);
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_formdata_format.js b/browser/components/sessionstore/test/browser_formdata_format.js
new file mode 100644
index 0000000000..d4ac1f6e1b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_format.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ /** Tests formdata format **/
+ waitForExplicitFinish();
+
+ let formData = [
+ {},
+ // old format
+ { "#input1": "value0" },
+ {
+ "#input1": "value1",
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value2",
+ },
+ { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value3" },
+ // new format
+ { id: { input1: "value4" } },
+ { id: { input1: "value5" }, xpath: {} },
+ {
+ id: { input1: "value6" },
+ xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value7" },
+ },
+ {
+ id: {},
+ xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value8" },
+ },
+ {
+ xpath: { "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value9" },
+ },
+ // combinations
+ { "#input1": "value10", id: { input1: "value11" } },
+ {
+ "#input1": "value12",
+ id: { input1: "value13" },
+ xpath: {
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value14",
+ },
+ },
+ {
+ "#input1": "value15",
+ xpath: {
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value16",
+ },
+ },
+ {
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value17",
+ id: { input1: "value18" },
+ },
+ {
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value19",
+ id: { input1: "value20" },
+ xpath: {
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value21",
+ },
+ },
+ {
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value22",
+ xpath: {
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value23",
+ },
+ },
+ {
+ "#input1": "value24",
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value25",
+ id: { input1: "value26" },
+ },
+ {
+ "#input1": "value27",
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value28",
+ id: { input1: "value29" },
+ xpath: {
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value30",
+ },
+ },
+ {
+ "#input1": "value31",
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value32",
+ xpath: {
+ "/xhtml:html/xhtml:body/xhtml:input[@name='input2']": "value33",
+ },
+ },
+ ];
+ let expectedValues = [
+ ["", ""],
+ // old format
+ ["value0", ""],
+ ["value1", "value2"],
+ ["", "value3"],
+ // new format
+ ["value4", ""],
+ ["value5", ""],
+ ["value6", "value7"],
+ ["", "value8"],
+ ["", "value9"],
+ // combinations
+ ["value11", ""],
+ ["value13", "value14"],
+ ["", "value16"],
+ ["value18", ""],
+ ["value20", "value21"],
+ ["", "value23"],
+ ["value26", ""],
+ ["value29", "value30"],
+ ["", "value33"],
+ ];
+
+ let promises = [];
+ for (let i = 0; i < formData.length; i++) {
+ promises.push(testTabRestoreData(formData[i], expectedValues[i]));
+ }
+
+ Promise.all(promises).then(
+ () => finish(),
+ ex => ok(false, ex)
+ );
+}
+
+async function testTabRestoreData(aFormData, aExpectedValue) {
+ let URL = ROOT + "browser_formdata_format_sample.html";
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+
+ aFormData.url = URL;
+ let tabState = {
+ entries: [{ url: URL, triggeringPrincipal_base64 }],
+ formdata: aFormData,
+ };
+
+ await promiseBrowserLoaded(tab.linkedBrowser);
+ await promiseTabState(tab, tabState);
+
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ let restoredTabState = JSON.parse(ss.getTabState(tab));
+ let restoredFormData = restoredTabState.formdata;
+
+ if (restoredFormData) {
+ let doc = tab.linkedBrowser.contentDocument;
+ let input1 = doc.getElementById("input1");
+ let input2 = doc.querySelector("input[name=input2]");
+
+ // test format
+ ok(
+ "id" in restoredFormData || "xpath" in restoredFormData,
+ "FormData format is valid: " + restoredFormData
+ );
+ // validate that there are no old keys
+ for (let key of Object.keys(restoredFormData)) {
+ if (!["id", "xpath", "url"].includes(key)) {
+ ok(false, "FormData format is invalid.");
+ }
+ }
+ // test id
+ is(
+ input1.value,
+ aExpectedValue[0],
+ "FormData by 'id' has been restored correctly"
+ );
+ // test xpath
+ is(
+ input2.value,
+ aExpectedValue[1],
+ "FormData by 'xpath' has been restored correctly"
+ );
+ }
+
+ // clean up
+ gBrowser.removeTab(tab);
+}
diff --git a/browser/components/sessionstore/test/browser_formdata_format_sample.html b/browser/components/sessionstore/test/browser_formdata_format_sample.html
new file mode 100644
index 0000000000..ed71702a1b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_format_sample.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<title>Test formdata format</title>
+
+<!-- input events -->
+<h3>Input fields</h3>
+<input type="text" id="input1">
+<input type="text" name="input2">
diff --git a/browser/components/sessionstore/test/browser_formdata_max_size.js b/browser/components/sessionstore/test/browser_formdata_max_size.js
new file mode 100644
index 0000000000..00b49985e3
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_max_size.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL =
+ "http://mochi.test:8888/browser/browser/components/" +
+ "sessionstore/test/browser_formdata_sample.html";
+
+const SHORT_VALUE = "abc";
+const LONG_VALUE = "abcdef";
+
+add_task(async function test_form_limit() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // "browser.sessionstore.dom_form_limit" limits the length of values in
+ // forms to 5. Here we have that SHORT_VALUE is less than 5 and
+ // LONG_VALUE is greater than 5.
+ ["browser.sessionstore.dom_form_limit", 5],
+ ["browser.sessionstore.debug.no_auto_updates", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser, url: URL }, async browser => {
+ await setPropertyOfFormField(browser, "#txt", "value", SHORT_VALUE);
+ await TabStateFlusher.flush(browser);
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ let state = JSON.parse(ss.getTabState(tab));
+ is(
+ state.formdata.id.txt,
+ SHORT_VALUE,
+ "values shorter than browser.sessionstore.dom_form_limit is ok."
+ );
+
+ await setPropertyOfFormField(browser, "#txt", "value", LONG_VALUE);
+ await TabStateFlusher.flush(browser);
+
+ state = JSON.parse(ss.getTabState(tab));
+ ok(
+ !state?.formdata?.id?.txt,
+ "values shorter than browser.sessionstore.dom_form_limit isn't ok."
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_form_max_limit() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // "browser.sessionstore.dom_form_max_limit" limits the total length
+ // of values AND length id/xpath collected from a form. Here we have
+ // that SHORT_VALUE + 'txt' is less than 7 and LONG_VALUE + 'txt' is
+ // greater than 7.
+ ["browser.sessionstore.dom_form_max_limit", 7],
+ ["browser.sessionstore.debug.no_auto_updates", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser, url: URL }, async browser => {
+ await setPropertyOfFormField(browser, "#txt", "value", SHORT_VALUE);
+ await TabStateFlusher.flush(browser);
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ let state = JSON.parse(ss.getTabState(tab));
+ is(
+ state.formdata.id.txt,
+ SHORT_VALUE,
+ "total length shorter than browser.sessionstore.dom_form_max_limit is ok."
+ );
+
+ await setPropertyOfFormField(browser, "#txt", "value", LONG_VALUE);
+ await TabStateFlusher.flush(browser);
+
+ state = JSON.parse(ss.getTabState(tab));
+ is(
+ state.formdata.id.txt,
+ SHORT_VALUE,
+ "total length shorter than browser.sessionstore.dom_form_max_limit isn't ok."
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_form_max_limit_many_fields() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // "browser.sessionstore.dom_form_max_limit" limits the total length
+ // of values AND length id/xpath collected from a form. Here we have
+ // that SHORT_VALUE * 2 + 'text' + 'txt' is less than 15 and LONG_VALUE
+ // + SHORT_VALUE + 'text' + 'txt' is greater than 15.
+ ["browser.sessionstore.dom_form_max_limit", 15],
+ ["browser.sessionstore.debug.no_auto_updates", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser, url: URL }, async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ let element = content.document.createElement("input");
+ element.id = "text";
+ element.type = "text";
+ content.document.body.appendChild(element);
+ });
+
+ await setPropertyOfFormField(browser, "#txt", "value", SHORT_VALUE);
+ await setPropertyOfFormField(browser, "#text", "value", SHORT_VALUE);
+ await TabStateFlusher.flush(browser);
+
+ let tab = gBrowser.getTabForBrowser(browser);
+ let state = JSON.parse(ss.getTabState(tab));
+ is(
+ state.formdata.id.txt,
+ SHORT_VALUE,
+ "total length shorter than browser.sessionstore.dom_form_max_limit is ok."
+ );
+
+ await setPropertyOfFormField(browser, "#txt", "value", LONG_VALUE);
+ await TabStateFlusher.flush(browser);
+
+ state = JSON.parse(ss.getTabState(tab));
+ is(
+ state.formdata.id.txt,
+ SHORT_VALUE,
+ "total length shorter than browser.sessionstore.dom_form_max_limit isn't ok."
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/sessionstore/test/browser_formdata_password.js b/browser/components/sessionstore/test/browser_formdata_password.js
new file mode 100644
index 0000000000..14abdb89f5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_password.js
@@ -0,0 +1,69 @@
+"use strict";
+
+/**
+ * Ensures that <input>s that are/were type=password are not saved.
+ */
+
+addCoopTask("file_formdata_password.html", test_hasBeenTypePassword, HTTPSROOT);
+
+addNonCoopTask(
+ "file_formdata_password.html",
+ test_hasBeenTypePassword,
+ HTTPROOT
+);
+addNonCoopTask(
+ "file_formdata_password.html",
+ test_hasBeenTypePassword,
+ HTTPSROOT
+);
+
+async function test_hasBeenTypePassword(aURL) {
+ let tab = BrowserTestUtils.addTab(gBrowser, aURL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ await SpecialPowers.spawn(browser, [], async function fillFields() {
+ let doc = content.document;
+
+ doc.getElementById("TextValue").setUserInput("abc");
+
+ doc.getElementById("TextValuePassword").setUserInput("def");
+ doc.getElementById("TextValuePassword").type = "password";
+
+ doc.getElementById("TextPasswordValue").type = "password";
+ doc.getElementById("TextPasswordValue").setUserInput("ghi");
+
+ doc.getElementById("PasswordValueText").setUserInput("jkl");
+ doc.getElementById("PasswordValueText").type = "text";
+
+ doc.getElementById("PasswordTextValue").type = "text";
+ doc.getElementById("PasswordTextValue").setUserInput("mno");
+
+ doc.getElementById("PasswordValue").setUserInput("pqr");
+ });
+
+ // Remove the tab.
+ await promiseRemoveTabAndSessionState(tab);
+
+ let [
+ {
+ state: { formdata },
+ },
+ ] = ss.getClosedTabDataForWindow(window);
+ let expected = [
+ ["TextValue", "abc"],
+ ["TextValuePassword", undefined],
+ ["TextPasswordValue", undefined],
+ ["PasswordValueText", undefined],
+ ["PasswordTextValue", undefined],
+ ["PasswordValue", undefined],
+ ];
+
+ for (let [id, expectedValue] of expected) {
+ is(
+ formdata.id[id],
+ expectedValue,
+ `Value should be ${expectedValue} for ${id}`
+ );
+ }
+}
diff --git a/browser/components/sessionstore/test/browser_formdata_sample.html b/browser/components/sessionstore/test/browser_formdata_sample.html
new file mode 100644
index 0000000000..d469f5751e
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_sample.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_formdata_sample.html</title>
+ </head>
+ <body>
+ <input id="txt" />
+ <iframe id="iframe"></iframe>
+
+ <script type="text/javascript">
+ let isOuter = window == window.top;
+
+ if (isOuter) {
+ let iframe = document.getElementById("iframe");
+ iframe.setAttribute("src", "https://example.com" + location.pathname);
+ }
+ </script>
+ </body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_formdata_xpath.js b/browser/components/sessionstore/test/browser_formdata_xpath.js
new file mode 100644
index 0000000000..eef0ba234a
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_xpath.js
@@ -0,0 +1,242 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = ROOT + "browser_formdata_xpath_sample.html";
+
+/**
+ * Bug 346337 - Generic form data restoration tests.
+ */
+add_setup(function () {
+ // make sure we don't save form data at all (except for tab duplication)
+ Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+ });
+});
+
+const FILE1 = createFilePath("346337_test1.file");
+const FILE2 = createFilePath("346337_test2.file");
+
+const FIELDS = {
+ "//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'][1]": [FILE1],
+ "//input[@type='file'][2]": [FILE1, FILE2],
+};
+
+add_task(async function test_form_data_restoration() {
+ // Load page with some input fields.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Fill in some values.
+ for (let xpath of Object.keys(FIELDS)) {
+ await setFormValue(browser, xpath);
+ }
+
+ // Duplicate the tab.
+ let tab2 = gBrowser.duplicateTab(tab);
+ let browser2 = tab2.linkedBrowser;
+ await promiseTabRestored(tab2);
+
+ // Check that all form values have been duplicated.
+ for (let xpath of Object.keys(FIELDS)) {
+ let expected = JSON.stringify(FIELDS[xpath]);
+ let actual = JSON.stringify(await getFormValue(browser2, xpath));
+ is(
+ actual,
+ expected,
+ 'The value for "' + xpath + '" was correctly restored'
+ );
+ }
+
+ // Remove all tabs.
+ await promiseRemoveTabAndSessionState(tab2);
+ await promiseRemoveTabAndSessionState(tab);
+
+ // Restore one of the tabs again.
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ await promiseTabRestored(tab);
+
+ // Check that none of the form values have been restored due to the privacy
+ // level settings.
+ for (let xpath of Object.keys(FIELDS)) {
+ let expected = FIELDS[xpath];
+ if (expected) {
+ let actual = await getFormValue(browser, xpath, expected);
+ isnot(
+ actual,
+ expected,
+ 'The value for "' + xpath + '" was correctly discarded'
+ );
+ }
+ }
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+});
+
+function getPropertyOfXPath(browserContext, path, propName) {
+ return SpecialPowers.spawn(
+ browserContext,
+ [path, propName],
+ (pathChild, propNameChild) => {
+ let doc = content.document;
+ let xptype = doc.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE;
+ let node = doc.evaluate(
+ pathChild,
+ doc,
+ null,
+ xptype,
+ null
+ ).singleNodeValue;
+ return node[propNameChild];
+ }
+ );
+}
+
+function setPropertyOfXPath(browserContext, path, propName, newValue) {
+ return SpecialPowers.spawn(
+ browserContext,
+ [path, propName, newValue],
+ (pathChild, propNameChild, newValueChild) => {
+ let doc = content.document;
+ let xptype = doc.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE;
+ let node = doc.evaluate(
+ pathChild,
+ doc,
+ null,
+ xptype,
+ null
+ ).singleNodeValue;
+ node[propNameChild] = newValueChild;
+
+ let event = node.ownerDocument.createEvent("UIEvents");
+ event.initUIEvent("input", true, true, node.ownerGlobal, 0);
+ node.dispatchEvent(event);
+ }
+ );
+}
+
+function execUsingXPath(browserContext, path, fnName, arg) {
+ return SpecialPowers.spawn(
+ browserContext,
+ [path, fnName, arg],
+ (pathChild, fnNameChild, argChild) => {
+ let doc = content.document;
+ let xptype = doc.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE;
+ let node = doc.evaluate(
+ pathChild,
+ doc,
+ null,
+ xptype,
+ null
+ ).singleNodeValue;
+
+ switch (fnNameChild) {
+ case "getMultipleSelected":
+ return Array.from(node.options, (opt, idx) => idx).filter(
+ idx => node.options[idx].selected
+ );
+ case "setMultipleSelected":
+ Array.prototype.forEach.call(
+ node.options,
+ (opt, idx) => (opt.selected = argChild.indexOf(idx) > -1)
+ );
+ break;
+ case "getFileNameArray":
+ return node.mozGetFileNameArray();
+ case "setFileNameArray":
+ node.mozSetFileNameArray(argChild, argChild.length);
+ break;
+ }
+
+ let event = node.ownerDocument.createEvent("UIEvents");
+ event.initUIEvent("input", true, true, node.ownerGlobal, 0);
+ node.dispatchEvent(event);
+ return undefined;
+ }
+ );
+}
+
+function createFilePath(leaf) {
+ let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ file.append(leaf);
+ return file.path;
+}
+
+function isArrayOfNumbers(value) {
+ return Array.isArray(value) && value.every(n => typeof n === "number");
+}
+
+function isArrayOfStrings(value) {
+ return Array.isArray(value) && value.every(n => typeof n === "string");
+}
+
+function getFormValue(browser, xpath) {
+ let value = FIELDS[xpath];
+
+ if (typeof value == "string") {
+ return getPropertyOfXPath(browser, xpath, "value");
+ }
+
+ if (typeof value == "boolean") {
+ return getPropertyOfXPath(browser, xpath, "checked");
+ }
+
+ if (typeof value == "number") {
+ return getPropertyOfXPath(browser, xpath, "selectedIndex");
+ }
+
+ if (isArrayOfNumbers(value)) {
+ return execUsingXPath(browser, xpath, "getMultipleSelected");
+ }
+
+ if (isArrayOfStrings(value)) {
+ return execUsingXPath(browser, xpath, "getFileNameArray");
+ }
+
+ throw new Error("unknown input type");
+}
+
+function setFormValue(browser, xpath) {
+ let value = FIELDS[xpath];
+
+ if (typeof value == "string") {
+ return setPropertyOfXPath(browser, xpath, "value", value);
+ }
+
+ if (typeof value == "boolean") {
+ return setPropertyOfXPath(browser, xpath, "checked", value);
+ }
+
+ if (typeof value == "number") {
+ return setPropertyOfXPath(browser, xpath, "selectedIndex", value);
+ }
+
+ if (isArrayOfNumbers(value)) {
+ return execUsingXPath(browser, xpath, "setMultipleSelected", value);
+ }
+
+ if (isArrayOfStrings(value)) {
+ return execUsingXPath(browser, xpath, "setFileNameArray", value);
+ }
+
+ throw new Error("unknown input type");
+}
diff --git a/browser/components/sessionstore/test/browser_formdata_xpath_sample.html b/browser/components/sessionstore/test/browser_formdata_xpath_sample.html
new file mode 100644
index 0000000000..682162d6a3
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_formdata_xpath_sample.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<title>Test for bug 346337</title>
+
+<h3>Text Fields</h3>
+<input type="text" name="input">
+<input type="text" name="spaced 1">
+<input>
+
+<h3>Checkboxes and Radio buttons</h3>
+<input type="checkbox" name="check"> Check 1
+<input type="checkbox" name="uncheck" checked> Check 2
+<p>
+<input type="radio" name="group" value="1"> Radio 1
+<input type="radio" name="group" value="some"> Radio 2
+<input type="radio" name="group" checked> Radio 3
+
+<h3>Selects</h3>
+<select name="any">
+ <option value="1"> Select 1
+ <option value="some"> Select 2
+ <option>Select 3
+</select>
+<select multiple="multiple">
+ <option value=1> Multi-select 1
+ <option value=2> Multi-select 2
+ <option value=3> Multi-select 3
+ <option value=4> Multi-select 4
+</select>
+
+<h3>Text Areas</h3>
+<textarea name="testarea"></textarea>
+<textarea name="sized one" rows="5" cols="25"></textarea>
+<textarea></textarea>
+
+<h3>File Selector</h3>
+<input type="file">
+<input type="file" multiple>
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 @@
+<html>
+ <body>
+ I'm A!
+ </body>
+</html>
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 @@
+<html>
+ <body>
+ I'm B!<br/>
+ <a target="c" href="browser_frame_history_c1.html">click me first</a><br/>
+ <a target="c" href="browser_frame_history_c2.html">then click me</a><br/>
+ Close this tab.<br/>
+ Restore this tab.<br/>
+ Click back.<br/>
+ </body>
+</html>
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 @@
+<html>
+ <body>
+ I'm C!
+ </body>
+</html>
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 @@
+<html>
+ <body>
+ I'm C1!
+ </body>
+</html>
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 @@
+<html>
+ <body>
+ I'm C2!
+ </body>
+</html>
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 @@
+<html>
+ <frameset cols="20%,80%">
+ <frameset rows="30%,70%">
+ <frame src="browser_frame_history_a.html"/>
+ <frame src="browser_frame_history_b.html"/>
+ </frameset>
+ <frame src="browser_frame_history_c.html" name="c"/>
+ </frameset>
+</html>
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 @@
+<html>
+ <iframe src="browser_frame_history_index.html" id="iframe" />
+</html>
diff --git a/browser/components/sessionstore/test/browser_frame_history_index_blank.html b/browser/components/sessionstore/test/browser_frame_history_index_blank.html
new file mode 100644
index 0000000000..4ddd1a7cf7
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frame_history_index_blank.html
@@ -0,0 +1,4 @@
+<html>
+ <body>
+ </body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_frametree.js b/browser/components/sessionstore/test/browser_frametree.js
new file mode 100644
index 0000000000..2171838a0c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frametree.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = HTTPROOT + "browser_frametree_sample.html";
+const URL_FRAMESET = HTTPROOT + "browser_frametree_sample_frameset.html";
+const URL_IFRAMES = HTTPROOT + "browser_frametree_sample_iframes.html";
+
+/**
+ * Check that we correctly enumerate non-dynamic child frames.
+ */
+add_task(async function test_frametree() {
+ // Add an empty tab for a start.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // The page is a single frame with no children.
+ is(await countNonDynamicFrames(browser), 0, "no child frames");
+
+ // Navigate to a frameset.
+ BrowserTestUtils.loadURIString(browser, URL_FRAMESET);
+ await promiseBrowserLoaded(browser);
+
+ // The frameset has two frames.
+ is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
+
+ // Go back in history.
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ true
+ );
+ browser.goBack();
+ await pageShowPromise;
+
+ // We're at page one again.
+ is(await countNonDynamicFrames(browser), 0, "no child frames");
+
+ // Append a dynamic frame.
+ await SpecialPowers.spawn(browser, [URL], async ([url]) => {
+ let frame = content.document.createElement("iframe");
+ frame.setAttribute("src", url);
+ content.document.body.appendChild(frame);
+ await ContentTaskUtils.waitForEvent(frame, "load");
+ });
+
+ // The dynamic frame should be ignored.
+ is(
+ await countNonDynamicFrames(browser),
+ 0,
+ "we still have a single root frame"
+ );
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Check that we correctly enumerate non-dynamic child frames.
+ */
+add_task(async function test_frametree_dynamic() {
+ // Add an empty tab for a start.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL_IFRAMES);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // The page has two iframes.
+ is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
+ is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
+
+ // Insert a dynamic frame.
+ await SpecialPowers.spawn(browser, [URL], async ([url]) => {
+ let frame = content.document.createElement("iframe");
+ frame.setAttribute("src", url);
+ content.document.body.insertBefore(
+ frame,
+ content.document.getElementsByTagName("iframe")[1]
+ );
+ await ContentTaskUtils.waitForEvent(frame, "load");
+ });
+
+ // The page still has two iframes.
+ is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
+ is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
+
+ // Append a dynamic frame.
+ await SpecialPowers.spawn(browser, [URL], async ([url]) => {
+ let frame = content.document.createElement("iframe");
+ frame.setAttribute("src", url);
+ content.document.body.appendChild(frame);
+ await ContentTaskUtils.waitForEvent(frame, "load");
+ });
+
+ // The page still has two iframes.
+ is(await countNonDynamicFrames(browser), 2, "two non-dynamic child frames");
+ is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1");
+
+ // Remopve a non-dynamic iframe.
+ await SpecialPowers.spawn(browser, [URL], async ([url]) => {
+ // Remove the first iframe, which should be a non-dynamic iframe.
+ content.document.body.removeChild(
+ content.document.getElementsByTagName("iframe")[0]
+ );
+ });
+
+ is(await countNonDynamicFrames(browser), 1, "one non-dynamic child frame");
+ is(await enumerateIndexes(browser), "1", "correct index 1");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+});
+
+async function countNonDynamicFrames(browser) {
+ return SpecialPowers.spawn(browser, [], async () => {
+ let count = 0;
+ content.SessionStoreUtils.forEachNonDynamicChildFrame(
+ content,
+ () => count++
+ );
+ return count;
+ });
+}
+
+async function enumerateIndexes(browser) {
+ return SpecialPowers.spawn(browser, [], async () => {
+ let indexes = [];
+ content.SessionStoreUtils.forEachNonDynamicChildFrame(content, (frame, i) =>
+ indexes.push(i)
+ );
+ return indexes.join(",");
+ });
+}
diff --git a/browser/components/sessionstore/test/browser_frametree_sample.html b/browser/components/sessionstore/test/browser_frametree_sample.html
new file mode 100644
index 0000000000..dda129448c
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frametree_sample.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_frametree_sample.html</title>
+ </head>
+ <body style='width: 100000px; height: 100000px;'>top</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_frametree_sample_frameset.html b/browser/components/sessionstore/test/browser_frametree_sample_frameset.html
new file mode 100644
index 0000000000..e1cd087357
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frametree_sample_frameset.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_frametree_sample_frameset.html</title>
+ </head>
+ <frameset id="frames" rows="50%, 50%">
+ <frame src="browser_frametree_sample.html">
+ <frame src="browser_frametree_sample.html">
+ </frameset>
+</html>
diff --git a/browser/components/sessionstore/test/browser_frametree_sample_iframes.html b/browser/components/sessionstore/test/browser_frametree_sample_iframes.html
new file mode 100644
index 0000000000..aaffab8af4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_frametree_sample_iframes.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_frametree_sample_iframes.html</title>
+ </head>
+ <iframe src="browser_frametree_sample.html"></iframe>
+ <iframe src="browser_frametree_sample.html"></iframe>
+</html>
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..b45a2b6779
--- /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.loadURIString(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.loadURIString(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..4230f55f33
--- /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.loadURIString(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.loadURIString(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..9b254c5e77
--- /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 ContentLinkHandler.jsm 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_merge_closed_tabs.js b/browser/components/sessionstore/test/browser_merge_closed_tabs.js
new file mode 100644
index 0000000000..2c6a946cdf
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_merge_closed_tabs.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This test ensures that closed tabs are merged when restoring
+ * a window state without overwriting tabs.
+ */
+add_task(async function () {
+ const initialState = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ ],
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ { ID: 1000, url: "about:blank", triggeringPrincipal_base64 },
+ ],
+ },
+ },
+ {
+ state: {
+ entries: [
+ { ID: 1001, url: "about:blank", triggeringPrincipal_base64 },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ const restoreState = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:robots", triggeringPrincipal_base64 }] },
+ ],
+ _closedTabs: [
+ {
+ state: {
+ entries: [
+ { ID: 1002, url: "about:robots", triggeringPrincipal_base64 },
+ ],
+ },
+ },
+ {
+ state: {
+ entries: [
+ { ID: 1003, url: "about:robots", triggeringPrincipal_base64 },
+ ],
+ },
+ },
+ {
+ state: {
+ entries: [
+ { ID: 1004, url: "about:robots", triggeringPrincipal_base64 },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ const maxTabsUndo = 4;
+ Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", maxTabsUndo);
+
+ // Open a new window and restore it to an initial state.
+ let win = await promiseNewWindowLoaded({ private: false });
+ await setWindowState(win, initialState, true);
+ is(
+ SessionStore.getClosedTabCountForWindow(win),
+ 2,
+ "2 closed tabs after restoring initial state"
+ );
+
+ // Restore the new state but do not overwrite existing tabs (this should
+ // cause the closed tabs to be merged).
+ await setWindowState(win, restoreState);
+
+ // Verify the windows closed tab data is correct.
+ let iClosed = initialState.windows[0]._closedTabs;
+ let rClosed = restoreState.windows[0]._closedTabs;
+ let cData = SessionStore.getClosedTabDataForWindow(win);
+
+ is(
+ cData.length,
+ Math.min(iClosed.length + rClosed.length, maxTabsUndo),
+ "Number of closed tabs is correct"
+ );
+
+ // When the closed tabs are merged the restored tabs are considered to be
+ // closed more recently.
+ for (let i = 0; i < cData.length; i++) {
+ if (i < rClosed.length) {
+ is(
+ cData[i].state.entries[0].ID,
+ rClosed[i].state.entries[0].ID,
+ "Closed tab entry matches"
+ );
+ } else {
+ is(
+ cData[i].state.entries[0].ID,
+ iClosed[i - rClosed.length].state.entries[0].ID,
+ "Closed tab entry matches"
+ );
+ }
+ }
+
+ // Clean up.
+ Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo");
+ await BrowserTestUtils.closeWindow(win);
+});
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..c49a424260
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_multiple_navigateAndRestore.js
@@ -0,0 +1,45 @@
+"use strict";
+
+const PAGE_1 =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const PAGE_2 =
+ "data:text/html,<html><body>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.loadURIString(browser, PAGE_1);
+ browser.stop();
+ BrowserTestUtils.loadURIString(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,
+<select id="select">
+ <option value="1"> 1
+ <option value="2"> 2
+ <option value="3"> 3
+</select>`;
+
+const VALUES = ["1", "3"];
+
+// Tests that a document that changes a <select> element's "multiple" attribute
+// *after* the load event (eg. perhaps in response to some user action) doesn't
+// crash the browser when being restored.
+add_task(async function () {
+ // Create new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ await promiseBrowserLoaded(tab.linkedBrowser);
+
+ // Change the "multiple" attribute of the <select> 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,<title>initial title</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..73529c0b64
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_privatetabs.js
@@ -0,0 +1,33 @@
+/* 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 () {
+ // 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);
+ await TabStateFlusher.flush(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.
+ win.gBrowser.removeTab(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");
+});
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_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..c80a2d710b
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_restore_container_tabs_oa.js
@@ -0,0 +1,231 @@
+/* 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 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 <browser> 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"
+ );
+
+ // 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);
+});
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..33d96fd8da
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_restore_reversed_z_order.js
@@ -0,0 +1,125 @@
+"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.loadURIString(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_session_in_undoCloseTab.js b/browser/components/sessionstore/test/browser_restore_session_in_undoCloseTab.js
new file mode 100644
index 0000000000..a64f432eeb
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_restore_session_in_undoCloseTab.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { _LastSession } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionStore.sys.mjs"
+);
+
+/**
+ * Tests that if the user invokes undoCloseTab in a window for which there are no
+ * tabs to undo closing, that we attempt to restore the previous session if one
+ * exists.
+ */
+add_task(async function test_restore_session_in_undoCloseTab() {
+ forgetClosedTabs(window);
+ registerCleanupFunction(() => {
+ forgetClosedTabs(window);
+ });
+
+ const state = {
+ windows: [
+ {
+ tabs: [
+ {
+ entries: [
+ {
+ url: "https://example.org/",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ url: "https://example.com/",
+ triggeringPrincipal_base64,
+ },
+ ],
+ },
+ ],
+ selected: 2,
+ },
+ ],
+ };
+
+ _LastSession.setState(state);
+
+ let sessionRestored = promiseSessionStoreLoads(2 /* total restored tabs */);
+ let result = undoCloseTab();
+ await sessionRestored;
+ Assert.equal(result, null);
+ Assert.equal(gBrowser.tabs.length, 2);
+
+ gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
+});
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,<iframe srcdoc="${srcdocValue}">`;
+}
+
+async function runTest(srcdocValue) {
+ forgetClosedWindows();
+
+ // Open a new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, makeURL(srcdocValue));
+ await promiseBrowserLoaded(tab.linkedBrowser);
+
+ // Close that tab.
+ await promiseRemoveTabAndSessionState(tab);
+
+ // Restore that tab.
+ tab = ss.undoCloseTab(window, 0);
+ await promiseTabRestored(tab);
+
+ // Verify contents were restored correctly.
+ let iframe = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => content.document.querySelector("iframe").browsingContext
+ );
+ await SpecialPowers.spawn(iframe, [srcdocValue], text => {
+ Assert.equal(content.document.body.innerText, text, "Didn't load neterror");
+ });
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+}
+
+add_task(async function test_non_blank() {
+ await runTest("value");
+});
+
+add_task(async function test_blank() {
+ await runTest("");
+});
diff --git a/browser/components/sessionstore/test/browser_restore_tabless_window.js b/browser/components/sessionstore/test/browser_restore_tabless_window.js
new file mode 100644
index 0000000000..cffb2159f5
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_restore_tabless_window.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * It's possible for us to restore windows without tabs,
+ * when on Windows/Linux the user closes the last tab as
+ * a means of closing the last window. That last tab
+ * would appear as a closed tab in session state for the
+ * window, with no open tabs (so the state would be resumed
+ * as showing the homepage). See bug 490136 for context.
+ * This test checks that in this case, the resulting window
+ * is functional and indeed has the required previously
+ * closed tabs available.
+ */
+add_task(async function test_restoring_tabless_window() {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let newState = {
+ windows: [
+ {
+ tabs: [],
+ _closedTabs: [
+ {
+ state: { entries: [{ url: "about:" }] },
+ title: "About:",
+ },
+ ],
+ },
+ ],
+ };
+
+ await setWindowState(newWin, newState, true);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ newWin.gBrowser,
+ "https://example.com"
+ );
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ let receivedState = SessionStore.getWindowState(newWin);
+ let { tabs, selected } = receivedState.windows[0];
+ is(tabs.length, 2, "Should have two tabs");
+ is(selected, 2, "Should have selected the new tab");
+ ok(
+ tabs[1]?.entries.some(e => e.url == "https://example.com/"),
+ "Should have found the new URL"
+ );
+
+ let closedTabData = SessionStore.getClosedTabDataForWindow(newWin);
+ is(closedTabData.length, 1, "Should have found 1 closed tab");
+ is(
+ closedTabData[0]?.state.entries[0].url,
+ "about:",
+ "Should have found the right URL for the closed tab"
+ );
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/sessionstore/test/browser_restored_window_features.js b/browser/components/sessionstore/test/browser_restored_window_features.js
new file mode 100644
index 0000000000..efb470e385
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_restored_window_features.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BARPROP_NAMES = [
+ "locationbar",
+ "menubar",
+ "personalbar",
+ "scrollbars",
+ "statusbar",
+ "toolbar",
+];
+
+function testFeatures(win, test) {
+ for (let name of BARPROP_NAMES) {
+ is(
+ win[name].visible,
+ !!test.barprops?.[name],
+ name + " should be " + (test.barprops?.[name] ? "visible" : "hidden")
+ );
+ }
+ let toolbar = win.document.getElementById("TabsToolbar");
+ is(
+ toolbar.collapsed,
+ !win.toolbar.visible,
+ win.toolbar.visible
+ ? "tabbar should not be collapsed"
+ : "tabbar should be collapsed"
+ );
+ let chromeFlags = win.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).chromeFlags;
+ is(chromeFlags & test.chromeFlags, test.chromeFlags, "flags should be set");
+ if (test.unsetFlags) {
+ is(chromeFlags & test.unsetFlags, 0, "flags should be unset");
+ }
+}
+
+add_task(async function testRestoredWindowFeatures() {
+ const DUMMY_PAGE = "browser/base/content/test/tabs/dummy_page.html";
+ const ALL_BARPROPS = {
+ locationbar: true,
+ menubar: true,
+ personalbar: true,
+ scrollbars: true,
+ statusbar: true,
+ toolbar: true,
+ };
+ const TESTS = [
+ {
+ url: "http://example.com/browser/" + DUMMY_PAGE,
+ features: "menubar=0,resizable",
+ barprops: { scrollbars: true },
+ chromeFlags: Ci.nsIWebBrowserChrome.CHROME_WINDOW_RESIZE,
+ unsetFlags: Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG,
+ },
+ {
+ url: "data:,", // title should be empty
+ checkContentTitleEmpty: true,
+ features: "location,resizable",
+ barprops: { locationbar: true, scrollbars: true },
+ chromeFlags: Ci.nsIWebBrowserChrome.CHROME_WINDOW_RESIZE,
+ unsetFlags: Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG,
+ },
+ {
+ url: "http://example.com/browser/" + DUMMY_PAGE,
+ features: "dialog,resizable",
+ barprops: { scrollbars: true },
+ chromeFlags:
+ Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG |
+ Ci.nsIWebBrowserChrome.CHROME_WINDOW_RESIZE,
+ },
+ {
+ chrome: true,
+ url: "http://example.com/browser/" + DUMMY_PAGE,
+ features: "chrome,all,dialog=no",
+ barprops: ALL_BARPROPS,
+ chromeFlags: Ci.nsIWebBrowserChrome.CHROME_ALL,
+ unsetFlags: Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG,
+ },
+ {
+ chrome: true,
+ url: "http://example.com/browser/" + DUMMY_PAGE,
+ features: "chrome,all,dialog=no,alwayslowered,centerscreen",
+ barprops: ALL_BARPROPS,
+ chromeFlags:
+ Ci.nsIWebBrowserChrome.CHROME_WINDOW_LOWERED |
+ Ci.nsIWebBrowserChrome.CHROME_CENTER_SCREEN,
+ },
+ {
+ chrome: true,
+ url: "http://example.com/browser/" + DUMMY_PAGE,
+ features: "chrome,all,dialog=no,alwaysraised,dependent",
+ barprops: ALL_BARPROPS,
+ chromeFlags:
+ Ci.nsIWebBrowserChrome.CHROME_WINDOW_RAISED |
+ Ci.nsIWebBrowserChrome.CHROME_DEPENDENT,
+ },
+ ];
+ const TEST_URL_CHROME = "chrome://mochitests/content/browser/" + DUMMY_PAGE;
+
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, TEST_URL_CHROME);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ for (let test of TESTS) {
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: test.url,
+ });
+ let win;
+ if (test.chrome) {
+ win = window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ test.features,
+ test.url
+ );
+ } else {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [test], t => {
+ content.window.open(t.url, "_blank", t.features);
+ });
+ }
+ win = await newWindowPromise;
+
+ let title = win.document.title;
+ if (test.checkContentTitleEmpty) {
+ let contentTitle = await SpecialPowers.spawn(
+ win.gBrowser.selectedBrowser,
+ [],
+ () => content.document.title
+ );
+ is(contentTitle, "", "title should be empty");
+ }
+
+ testFeatures(win, test);
+ let chromeFlags = win.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).chromeFlags;
+
+ await BrowserTestUtils.closeWindow(win);
+
+ newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: test.url,
+ });
+ SessionStore.undoCloseWindow(0);
+ win = await newWindowPromise;
+
+ is(title, win.document.title, "title should be preserved");
+ testFeatures(win, test);
+ is(
+ win.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).chromeFlags,
+ // Use |>>> 0| to force unsigned.
+ (chromeFlags |
+ Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME |
+ Ci.nsIWebBrowserChrome.CHROME_SUPPRESS_ANIMATION) >>>
+ 0,
+ "unexpected chromeFlags"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/browser/components/sessionstore/test/browser_revive_crashed_bg_tabs.js b/browser/components/sessionstore/test/browser_revive_crashed_bg_tabs.js
new file mode 100644
index 0000000000..86f85503d3
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_revive_crashed_bg_tabs.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that even if the user has set their tabs to restore
+ * immediately on session start, that background tabs after a
+ * content process crash restore on demand.
+ */
+
+"use strict";
+
+const PAGE_1 =
+ "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page.";
+const PAGE_2 =
+ "data:text/html,<html><body>Another%20regular,%20everyday,%20normal%20page.";
+
+add_setup(async function () {
+ await pushPrefs(
+ ["dom.ipc.processCount", 1],
+ ["browser.sessionstore.restore_on_demand", false]
+ );
+});
+
+add_task(async function test_revive_bg_tabs_on_demand() {
+ let newTab1 = BrowserTestUtils.addTab(gBrowser, PAGE_1);
+ let browser1 = newTab1.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser1);
+
+ gBrowser.selectedTab = newTab1;
+
+ let newTab2 = BrowserTestUtils.addTab(gBrowser, PAGE_2);
+ let browser2 = newTab2.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser2);
+
+ await TabStateFlusher.flush(browser2);
+
+ // Now crash the selected tab
+ let windowReady = BrowserTestUtils.waitForEvent(window, "SSWindowStateReady");
+ await BrowserTestUtils.crashFrame(browser1);
+
+ ok(newTab1.hasAttribute("crashed"), "Selected tab should be crashed");
+ ok(!newTab2.hasAttribute("crashed"), "Background tab should not be crashed");
+
+ // Wait until we've had a chance to restore all tabs immediately
+ await windowReady;
+
+ // But we should not have restored the background tab
+ ok(newTab2.hasAttribute("pending"), "Background tab should be pending");
+
+ // Now select newTab2 to make sure it restores.
+ let newTab2Restored = promiseTabRestored(newTab2);
+ gBrowser.selectedTab = newTab2;
+ await newTab2Restored;
+
+ ok(browser2.isRemoteBrowser, "Restored browser should be remote");
+
+ BrowserTestUtils.removeTab(newTab1);
+ BrowserTestUtils.removeTab(newTab2);
+});
diff --git a/browser/components/sessionstore/test/browser_scrollPositions.js b/browser/components/sessionstore/test/browser_scrollPositions.js
new file mode 100644
index 0000000000..094aa11325
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositions.js
@@ -0,0 +1,258 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BASE = "http://example.com/browser/browser/components/sessionstore/test/";
+const URL2 = BASE + "browser_scrollPositions_sample2.html";
+const URL_FRAMESET = BASE + "browser_scrollPositions_sample_frameset.html";
+
+// Randomized set of scroll positions we will use in this test.
+const SCROLL_X = Math.round(100 * (1 + Math.random()));
+const SCROLL_Y = Math.round(200 * (1 + Math.random()));
+const SCROLL_STR = SCROLL_X + "," + SCROLL_Y;
+
+const SCROLL2_X = Math.round(300 * (1 + Math.random()));
+const SCROLL2_Y = Math.round(400 * (1 + Math.random()));
+const SCROLL2_STR = SCROLL2_X + "," + SCROLL2_Y;
+
+requestLongerTimeout(10);
+
+add_task(test_scroll_nested);
+
+if (gFissionBrowser) {
+ addCoopTask("browser_scrollPositions_sample.html", test_scroll, HTTPSROOT);
+}
+addNonCoopTask("browser_scrollPositions_sample.html", test_scroll, HTTPROOT);
+addNonCoopTask("browser_scrollPositions_sample.html", test_scroll, HTTPSROOT);
+
+addCoopTask(
+ "browser_scrollPositions_sample.html",
+ test_scroll_background_tabs,
+ HTTPSROOT
+);
+addNonCoopTask(
+ "browser_scrollPositions_sample.html",
+ test_scroll_background_tabs,
+ HTTPROOT
+);
+
+addNonCoopTask(
+ "browser_scrollPositions_sample.html",
+ test_scroll_background_tabs,
+ HTTPSROOT
+);
+
+function getScrollPosition(bc) {
+ return SpecialPowers.spawn(bc, [], () => {
+ let x = {},
+ y = {};
+ content.windowUtils.getVisualViewportOffset(x, y);
+ return { x: x.value, y: y.value };
+ });
+}
+
+/**
+ * This test ensures that we properly serialize and restore scroll positions
+ * for an average page without any frames.
+ */
+async function test_scroll(aURL) {
+ let tab = BrowserTestUtils.addTab(gBrowser, aURL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Scroll down a little.
+ await setScrollPosition(browser, SCROLL_X, SCROLL_Y);
+ await checkScroll(tab, { scroll: SCROLL_STR }, "scroll is fine");
+
+ // Duplicate and check that the scroll position is restored.
+ let tab2 = ss.duplicateTab(window, tab);
+ let browser2 = tab2.linkedBrowser;
+ await promiseTabRestored(tab2);
+
+ let scroll = await getScrollPosition(browser2);
+ is(
+ JSON.stringify(scroll),
+ JSON.stringify({ x: SCROLL_X, y: SCROLL_Y }),
+ "scroll position has been duplicated correctly"
+ );
+
+ // Check that reloading retains the scroll positions.
+ browser2.reload();
+ await promiseBrowserLoaded(browser2);
+ await checkScroll(
+ tab2,
+ { scroll: SCROLL_STR },
+ "reloading retains scroll positions"
+ );
+
+ // Check that a force-reload resets scroll positions.
+ browser2.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ await promiseBrowserLoaded(browser2);
+ await checkScroll(tab2, null, "force-reload resets scroll positions");
+
+ // Scroll back to the top and check that the position has been reset. We
+ // expect the scroll position to be "null" here because there is no data to
+ // be stored if the frame is in its default scroll position.
+ await setScrollPosition(browser, 0, 0);
+ await checkScroll(tab, null, "no scroll stored");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+}
+
+/**
+ * This tests ensures that we properly serialize and restore scroll positions
+ * for multiple frames of pages with framesets.
+ */
+async function test_scroll_nested() {
+ let tab = BrowserTestUtils.addTab(gBrowser, URL_FRAMESET);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Scroll the first child frame down a little.
+ await setScrollPosition(
+ browser.browsingContext.children[0],
+ SCROLL_X,
+ SCROLL_Y
+ );
+ await checkScroll(
+ tab,
+ { children: [{ scroll: SCROLL_STR }] },
+ "scroll is fine"
+ );
+
+ // Scroll the second child frame down a little.
+ await setScrollPosition(
+ browser.browsingContext.children[1],
+ SCROLL2_X,
+ SCROLL2_Y
+ );
+ await checkScroll(
+ tab,
+ { children: [{ scroll: SCROLL_STR }, { scroll: SCROLL2_STR }] },
+ "scroll is fine"
+ );
+
+ // Duplicate and check that the scroll position is restored.
+ let tab2 = ss.duplicateTab(window, tab);
+ let browser2 = tab2.linkedBrowser;
+ await promiseTabRestored(tab2);
+
+ let scroll = await getScrollPosition(browser2.browsingContext.children[0]);
+ is(
+ JSON.stringify(scroll),
+ JSON.stringify({ x: SCROLL_X, y: SCROLL_Y }),
+ "scroll position #1 has been duplicated correctly"
+ );
+
+ scroll = await getScrollPosition(browser2.browsingContext.children[1]);
+ is(
+ JSON.stringify(scroll),
+ JSON.stringify({ x: SCROLL2_X, y: SCROLL2_Y }),
+ "scroll position #2 has been duplicated correctly"
+ );
+
+ // Check that resetting one frame's scroll position removes it from the
+ // serialized value.
+ await setScrollPosition(browser.browsingContext.children[0], 0, 0);
+ await checkScroll(
+ tab,
+ { children: [null, { scroll: SCROLL2_STR }] },
+ "scroll is fine"
+ );
+
+ // Check the resetting all frames' scroll positions nulls the stored value.
+ await setScrollPosition(browser.browsingContext.children[1], 0, 0);
+ await checkScroll(tab, null, "no scroll stored");
+
+ // Cleanup.
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+}
+
+/**
+ * Test that scroll positions persist after restoring background tabs in
+ * a restored window (bug 1228518).
+ * Also test that scroll positions for previous session history entries
+ * are preserved as well (bug 1265818).
+ */
+async function test_scroll_background_tabs(aURL) {
+ await pushPrefs(["browser.sessionstore.restore_on_demand", true]);
+
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let tab = BrowserTestUtils.addTab(newWin.gBrowser, aURL);
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Scroll down a little.
+ await setScrollPosition(browser, SCROLL_X, SCROLL_Y);
+ await checkScroll(
+ tab,
+ { scroll: SCROLL_STR },
+ "scroll on first page is fine"
+ );
+
+ // Navigate to a different page and scroll there as well.
+ let browser2loaded = BrowserTestUtils.browserLoaded(browser, false, URL2);
+ BrowserTestUtils.loadURIString(browser, URL2);
+ await browser2loaded;
+
+ // Scroll down a little.
+ await setScrollPosition(browser, SCROLL2_X, SCROLL2_Y);
+ await checkScroll(
+ tab,
+ { scroll: SCROLL2_STR },
+ "scroll on second page is fine"
+ );
+
+ // Close the window
+ await BrowserTestUtils.closeWindow(newWin);
+
+ await forceSaveState();
+
+ // Now restore the window
+ newWin = ss.undoCloseWindow(0);
+
+ // Make sure to wait for the window to be restored.
+ await BrowserTestUtils.waitForEvent(newWin, "SSWindowStateReady");
+
+ is(newWin.gBrowser.tabs.length, 2, "There should be two tabs");
+
+ // The second tab should be the one we loaded aURL at still
+ tab = newWin.gBrowser.tabs[1];
+
+ ok(tab.hasAttribute("pending"), "Tab should be pending");
+ browser = tab.linkedBrowser;
+
+ // Ensure there are no pending queued messages in the child.
+ await TabStateFlusher.flush(browser);
+
+ // Now check to see if the background tab remembers where it
+ // should be scrolled to.
+ newWin.gBrowser.selectedTab = tab;
+ await promiseTabRestored(tab);
+
+ await checkScroll(
+ tab,
+ { scroll: SCROLL2_STR },
+ "scroll is correct for restored tab"
+ );
+
+ // Now go back in history and check that the scroll position
+ // is restored there as well.
+ is(browser.canGoBack, true, "can go back");
+ browser.goBack();
+
+ await BrowserTestUtils.browserLoaded(browser);
+ await TabStateFlusher.flush(browser);
+
+ await checkScroll(
+ tab,
+ { scroll: SCROLL_STR },
+ "scroll is correct after navigating back within the restored tab"
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+}
diff --git a/browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js b/browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js
new file mode 100644
index 0000000000..c61b37c1a4
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BASE = "http://example.com/browser/browser/components/sessionstore/test/";
+const READER_MODE_URL =
+ "about:reader?url=" +
+ encodeURIComponent(BASE + "browser_scrollPositions_readerModeArticle.html");
+
+// Randomized set of scroll positions we will use in this test.
+const SCROLL_READER_MODE_Y = Math.round(400 * (1 + Math.random()));
+const SCROLL_READER_MODE_STR = "0," + SCROLL_READER_MODE_Y;
+
+requestLongerTimeout(2);
+
+/**
+ * Test that scroll positions of about reader page after restoring background
+ * tabs in a restored window (bug 1153393).
+ */
+add_task(async function test_scroll_background_about_reader_tabs() {
+ await pushPrefs(["browser.sessionstore.restore_on_demand", true]);
+
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let tab = BrowserTestUtils.addTab(newWin.gBrowser, READER_MODE_URL);
+ let browser = tab.linkedBrowser;
+ await Promise.all([
+ BrowserTestUtils.browserLoaded(browser),
+ BrowserTestUtils.waitForContentEvent(browser, "AboutReaderContentReady"),
+ ]);
+
+ // Scroll down a little.
+ await setScrollPosition(browser, 0, SCROLL_READER_MODE_Y);
+ await checkScroll(tab, { scroll: SCROLL_READER_MODE_STR }, "scroll is fine");
+
+ // Close the window
+ await BrowserTestUtils.closeWindow(newWin);
+
+ await forceSaveState();
+
+ // Now restore the window
+ newWin = ss.undoCloseWindow(0);
+
+ // Make sure to wait for the window to be restored.
+ await BrowserTestUtils.waitForEvent(newWin, "SSWindowStateReady");
+
+ is(newWin.gBrowser.tabs.length, 2, "There should be two tabs");
+
+ // The second tab should be the one we loaded URL at still
+ tab = newWin.gBrowser.tabs[1];
+
+ ok(tab.hasAttribute("pending"), "Tab should be pending");
+ browser = tab.linkedBrowser;
+
+ // Ensure there are no pending queued messages in the child.
+ await TabStateFlusher.flush(browser);
+
+ // Now check to see if the background tab remembers where it
+ // should be scrolled to.
+ newWin.gBrowser.selectedTab = tab;
+ await Promise.all([
+ promiseTabRestored(tab),
+ BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "AboutReaderContentReady"
+ ),
+ ]);
+
+ await checkScroll(
+ tab,
+ { scroll: SCROLL_READER_MODE_STR },
+ "scroll is still fine"
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/browser/components/sessionstore/test/browser_scrollPositions_readerModeArticle.html b/browser/components/sessionstore/test/browser_scrollPositions_readerModeArticle.html
new file mode 100644
index 0000000000..55452e0439
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositions_readerModeArticle.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_scrollPositions_sample.html b/browser/components/sessionstore/test/browser_scrollPositions_sample.html
new file mode 100644
index 0000000000..0182783dbb
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositions_sample.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_scrollPositions_sample.html</title>
+ </head>
+ <body style='width: 100000px; height: 100000px;'>top</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_scrollPositions_sample2.html b/browser/components/sessionstore/test/browser_scrollPositions_sample2.html
new file mode 100644
index 0000000000..0182783dbb
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositions_sample2.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_scrollPositions_sample.html</title>
+ </head>
+ <body style='width: 100000px; height: 100000px;'>top</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_scrollPositions_sample_frameset.html b/browser/components/sessionstore/test/browser_scrollPositions_sample_frameset.html
new file mode 100644
index 0000000000..c7e363fa1d
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_scrollPositions_sample_frameset.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_scrollPositions_sample_frameset.html</title>
+ </head>
+ <frameset id="frames" rows="50%, 50%">
+ <frame src="browser_scrollPositions_sample.html">
+ <frame src="browser_scrollPositions_sample.html">
+ </frameset>
+</html>
diff --git a/browser/components/sessionstore/test/browser_send_async_message_oom.js b/browser/components/sessionstore/test/browser_send_async_message_oom.js
new file mode 100644
index 0000000000..7e807f2fbd
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_send_async_message_oom.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const HISTOGRAM_NAME = "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM";
+
+/**
+ * Test that an OOM in sendAsyncMessage in a framescript will be reported
+ * to Telemetry.
+ */
+
+add_setup(async function () {
+ Services.telemetry.canRecordExtended = true;
+});
+
+function frameScript() {
+ // Make send[A]syncMessage("SessionStore:update", ...) simulate OOM.
+ // Other operations are unaffected.
+ let mm = docShell.messageManager;
+
+ let wrap = function (original) {
+ return function (name, ...args) {
+ if (name != "SessionStore:update") {
+ return original(name, ...args);
+ }
+ throw new Components.Exception(
+ "Simulated OOM",
+ Cr.NS_ERROR_OUT_OF_MEMORY
+ );
+ };
+ };
+
+ mm.sendAsyncMessage = wrap(mm.sendAsyncMessage.bind(mm));
+ mm.sendSyncMessage = wrap(mm.sendSyncMessage.bind(mm));
+}
+
+add_task(async function () {
+ // Capture original state.
+ let snapshot = Services.telemetry.getHistogramById(HISTOGRAM_NAME).snapshot();
+
+ // Open a browser, configure it to cause OOM.
+ let newTab = BrowserTestUtils.addTab(gBrowser, "about:robots");
+ let browser = newTab.linkedBrowser;
+ await ContentTask.spawn(browser, null, frameScript);
+
+ let promiseReported = new Promise(resolve => {
+ browser.messageManager.addMessageListener("SessionStore:error", resolve);
+ });
+
+ // Attempt to flush. This should fail.
+ let promiseFlushed = TabStateFlusher.flush(browser);
+ promiseFlushed.then(success => {
+ if (success) {
+ throw new Error("Flush should have failed");
+ }
+ });
+
+ // The frame script should report an error.
+ await promiseReported;
+
+ // Give us some time to handle that error.
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ // By now, Telemetry should have been updated.
+ let snapshot2 = Services.telemetry
+ .getHistogramById(HISTOGRAM_NAME)
+ .snapshot();
+ gBrowser.removeTab(newTab);
+
+ Assert.ok(snapshot2.sum > snapshot.sum);
+});
+
+add_task(async function cleanup() {
+ Services.telemetry.canRecordExtended = false;
+});
diff --git a/browser/components/sessionstore/test/browser_sessionHistory.js b/browser/components/sessionstore/test/browser_sessionHistory.js
new file mode 100644
index 0000000000..ebdf0055e9
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionHistory.js
@@ -0,0 +1,331 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+/**
+ * Ensure that starting a load invalidates shistory.
+ */
+add_task(async function test_load_start() {
+ // Create a new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ const PAGE = "http://example.com/";
+
+ // Load a new URI.
+ let historyReplacePromise = promiseOnHistoryReplaceEntry(browser);
+ BrowserTestUtils.loadURIString(browser, PAGE);
+
+ // Remove the tab before it has finished loading.
+ await historyReplacePromise;
+ await promiseRemoveTabAndSessionState(tab);
+
+ // Undo close the tab.
+ tab = ss.undoCloseTab(window, 0);
+ browser = tab.linkedBrowser;
+ await promiseTabRestored(tab);
+
+ // Check that the correct URL was restored.
+ is(browser.currentURI.spec, PAGE, "url is correct");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that anchor navigation invalidates shistory.
+ */
+add_task(async function test_hashchange() {
+ const PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://example.com/"
+ );
+ const URL = PATH + "file_sessionHistory_hashchange.html";
+ // Create a new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Check that we start with a single shistory entry.
+ await TabStateFlusher.flush(browser);
+ let { entries } = JSON.parse(ss.getTabState(tab));
+ is(entries.length, 1, "there is one shistory entry");
+
+ // Click the link and wait for a hashchange event.
+ let eventPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "hashchange",
+ true
+ );
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.querySelector("#a").click();
+ });
+ info("About to watch for a hash change event");
+ await eventPromise;
+ info("Got a hash change event");
+
+ // Check that we now have two shistory entries.
+ await TabStateFlusher.flush(browser);
+ ({ entries } = JSON.parse(ss.getTabState(tab)));
+ is(entries.length, 2, "there are two shistory entries");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that loading pages from the bfcache invalidates shistory.
+ */
+add_task(async function test_pageshow() {
+ const URL = "data:text/html;charset=utf-8,<h1>first</h1>";
+ const URL2 = "data:text/html;charset=utf-8,<h1>second</h1>";
+
+ // Create a new tab.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL);
+ let browser = tab.linkedBrowser;
+ await promiseBrowserLoaded(browser);
+
+ // Create a second shistory entry.
+ BrowserTestUtils.loadURIString(browser, URL2);
+ await promiseBrowserLoaded(browser);
+
+ // Wait until shistory changes.
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow",
+ true
+ );
+
+ // Go back to the previous url which is loaded from the bfcache.
+ browser.goBack();
+ await pageShowPromise;
+ is(browser.currentURI.spec, URL, "correct url after going back");
+
+ // Check that loading from bfcache did invalidate shistory.
+ await TabStateFlusher.flush(browser);
+ let { index } = JSON.parse(ss.getTabState(tab));
+ is(index, 1, "first history entry is selected");
+
+ // Cleanup.
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Ensure that subframe navigation invalidates shistory.
+ */
+add_task(async function test_subframes() {
+ const URL =
+ "data:text/html;charset=utf-8," +
+ "<iframe src=http%3A//example.com/ name=t></iframe>" +
+ "<a id=a1 href=http%3A//example.com/1 target=t>clickme</a>" +
+ "<a id=a2 href=http%3A//example.com/%23 target=t>clickme</a>";
+
+ // 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.loadURIString(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," +
+ "<frameset cols=50%25,50%25>" +
+ "<frame src='" +
+ SLOW_URL +
+ "'>" +
+ "</frameset>";
+
+ // 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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>browser_sessionStorage.html</title>
+ </head>
+ <body>
+ <iframe id="iframe"></iframe>
+
+ <script type="text/javascript">
+ let isOuter = window == window.top;
+ let args = window.location.search.slice(1).split("&");
+ let rand = args[0];
+
+ if (isOuter) {
+ let iframe = document.getElementById("iframe");
+ let isSecure = args.indexOf("secure") > -1;
+ let scheme = isSecure ? "https" : "http";
+ iframe.setAttribute("src", scheme + "://example.com" + location.pathname + "?" + rand);
+ }
+
+ if (sessionStorage.length === 0) {
+ sessionStorage.test = (isOuter ? "outer" : "inner") + "-value-" + rand;
+ document.title = sessionStorage.test;
+ }
+ </script>
+ </body>
+</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..00d46722e2
--- /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.loadURIString(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..1c2a82305f
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionStoreContainer.js
@@ -0,0 +1,165 @@
+/* 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, data) {
+ if (data == "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_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 @@
+<html>
+<header>
+ <title>Dummy html page to test speculative connect</title>
+</header>
+<body>
+ Hello Speculative Connect
+</body>
+</html>
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..daea961335
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_switch_remoteness.js
@@ -0,0 +1,53 @@
+"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"
+ );
+ ok(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.loadURIString(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.loadURIString(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..5eadca7a24
--- /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,
+<html>
+ <head>
+ <link rel="shortcut icon" href="${FAVICON}">
+ </head>
+ <body>
+ Favicon!
+ </body>
+</html>`;
+
+/**
+ * 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 ContentLinkHandler.jsm 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..924a25c770
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_undoCloseById.js
@@ -0,0 +1,174 @@
+/* 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.loadURIString(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));
+}
+
+add_task(async function test_undoCloseById() {
+ // Clear the lists of closed windows and tabs.
+ forgetClosedWindows();
+ while (SessionStore.getClosedTabCountForWindow(window)) {
+ SessionStore.forgetClosedTab(window, 0);
+ }
+
+ // 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.
+ 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_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,<html><body>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..5c4801718c
--- /dev/null
+++ b/browser/components/sessionstore/test/coopHeaderCommon.sjs
@@ -0,0 +1,31 @@
+function handleRequest(request, response) {
+ Cu.importGlobalProperties(["URLSearchParams"]);
+ let { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+ 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 @@
+<!DOCTYPE html>
+
+<html>
+ <body>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<html>
+ <body>
+ </body>
+</html>
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 @@
+<a href=#>clickme</a>
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 @@
+<a href=#>clickme</a>
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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <!-- Text/Password in the name indicates the type and the position of 'Value'
+ indicates when the value gets set relative to the type changes. -->
+ <input id="TextValue">
+ <input id="TextValuePassword">
+ <input id="TextPasswordValue">
+
+ <input id="PasswordValueText" type="password">
+ <input id="PasswordTextValue" type="password">
+ <input id="PasswordValue" type="password">
+ </body>
+</html>
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 @@
+<a id=a href=#>clickme</a>
diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js
new file mode 100644
index 0000000000..d73c098eea
--- /dev/null
+++ b/browser/components/sessionstore/test/head.js
@@ -0,0 +1,782 @@
+/* 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 ss = SessionStore;
+
+// 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) {
+ 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"
+ );
+ }
+ let windows = [window];
+ 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;
+ info("running " + aSetStateCallback.name);
+ 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) {
+ registerCleanupFunction(function () {
+ if (windowObserving) {
+ Services.ww.unregisterNotification(windowObserver);
+ }
+ });
+ windowObserving = true;
+ Services.ww.registerNotification(windowObserver);
+ }
+
+ 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;
+ gBrowser.tabContainer.addEventListener(
+ "SSTabRestored",
+ onSSTabRestored,
+ true
+ );
+
+ // Ensure setBrowserState() doesn't remove the initial tab.
+ gBrowser.selectedTab = gBrowser.tabs[0];
+
+ // Finally, call setBrowserState
+ ss.setBrowserState(JSON.stringify(aState));
+}
+
+function promiseBrowserState(aState) {
+ return new Promise(resolve => waitForBrowserState(aState, resolve));
+}
+
+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);
+}
+
+async function openAndCloseTab(window, url) {
+ let tab = BrowserTestUtils.addTab(window.gBrowser, url);
+ await promiseBrowserLoaded(tab.linkedBrowser, true, url);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ await promiseRemoveTabAndSessionState(tab);
+}
+
+/**
+ * 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");
+ });
+}
diff --git a/browser/components/sessionstore/test/marionette/manifest.ini b/browser/components/sessionstore/test/marionette/manifest.ini
new file mode 100644
index 0000000000..4664c5912a
--- /dev/null
+++ b/browser/components/sessionstore/test/marionette/manifest.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+tags = local
+
+[test_restore_loading_tab.py]
+[test_restore_manually_with_pinned_tabs.py]
+[test_restore_windows_after_restart_and_quit.py]
+[test_restore_windows_after_windows_shutdown.py]
+skip-if =
+ os != "win"
+ win10_2004 # Bug 1727691
+ win11_2009 # Bug 1727691
+[test_restore_windows_after_close_last_tabs.py]
+skip-if =
+ os == "mac"
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("""<div">Lorem</div>"""),),
+ # Window 2
+ (
+ inline("""<div">ipsum</div>"""),
+ inline("""<div">dolor</div>"""),
+ ),
+ # Window 3
+ (
+ inline("""<div">sit</div>"""),
+ inline("""<div">amet</div>"""),
+ ),
+ ]
+)
+
+
+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("""<div">consectetur</div>"""),
+ inline("""<div">ipsum</div>"""),
+ ),
+ (
+ inline("""<div">adipiscing</div>"""),
+ inline("""<div">consectetur</div>"""),
+ ),
+ ]
+ )
+
+ 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_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("<a id='link' href='{}' target='_blank'>click</a>", True)
+
+ def test_target_other(self):
+ self.do_test("<a id='link' href='{}' target='other'>click</a>", False)
+
+ def test_by_script(self):
+ self.do_test(
+ """
+ <a id='link'>click</a>
+ <script>
+ document.getElementById("link").addEventListener(
+ "click",
+ () => window.open("{}", "_blank");
+ )
+ </script>
+ """,
+ False,
+ )
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("""<div">ipsum</div>"""),
+ inline("""<div">dolor</div>"""),
+ inline("""<div">amet</div>"""),
+ ),
+ ]
+ ),
+ )
+
+ 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..be17f08472
--- /dev/null
+++ b/browser/components/sessionstore/test/marionette/test_restore_windows_after_restart_and_quit.py
@@ -0,0 +1,82 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# add this directory to the path
+import os
+import sys
+
+sys.path.append(os.path.dirname(__file__))
+
+from session_store_test_case import SessionStoreTestCase
+
+
+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
--- /dev/null
+++ b/browser/components/sessionstore/test/restore_redirect_http.html
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 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+<script>
+var newLocation = window.location.toString().replace("restore_redirect_js.html", "restore_redirect_target.html");
+window.location.replace(newLocation);
+</script>
+</head>
+</html>
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 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<html>
+<head>
+<title>Test page</title>
+</head>
+<body>Test page</body>
+</html>
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..503dd71420
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_final_write_cleanup.js
@@ -0,0 +1,118 @@
+"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/#";
+
+Cu.importGlobalProperties(["structuredClone"]);
+
+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.ini b/browser/components/sessionstore/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..b5fadb609d
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/xpcshell.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+head = head.js
+tags = condprof
+firefox-appdir = browser
+skip-if = toolkit == '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_nosession_async.js]
+skip-if = condprof # 1769154
+[test_startup_session_async.js]
+[test_startup_invalid_session.js]
+skip-if = condprof # 1769154