Skip to content

Commit 2571210

Browse files
committed
Support contained tabs session restore and sync.
1 parent 419b6ff commit 2571210

25 files changed

+822
-11
lines changed

browser/containers/containers_browsertest.cc

Lines changed: 244 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ class ContainersBrowserTest : public InProcessBrowserTest {
4444

4545
~ContainersBrowserTest() override = default;
4646

47+
void SetUp() override {
48+
set_open_about_blank_on_browser_launch(false);
49+
InProcessBrowserTest::SetUp();
50+
}
51+
4752
void SetUpCommandLine(base::CommandLine* command_line) override {
4853
InProcessBrowserTest::SetUpCommandLine(command_line);
4954
command_line->AppendSwitchASCII(
@@ -328,6 +333,73 @@ IN_PROC_BROWSER_TEST_F(ContainersBrowserTest, IsolateCookiesAndStorage) {
328333
GetIndexedDBJS("test_key")));
329334
}
330335

336+
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
337+
PRE_StoragePersistenceAcrossSessions) {
338+
const GURL url("https://a.test/simple.html");
339+
340+
// Navigate to the page
341+
NavigateParams params(browser(), url, ui::PAGE_TRANSITION_LINK);
342+
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
343+
params.storage_partition_config = content::StoragePartitionConfig::Create(
344+
browser()->profile(), kContainersStoragePartitionDomain, "container",
345+
browser()->profile()->IsOffTheRecord());
346+
ui_test_utils::NavigateToURL(&params);
347+
348+
content::WebContents* web_contents =
349+
browser()->tab_strip_model()->GetActiveWebContents();
350+
ASSERT_TRUE(web_contents);
351+
352+
// Set persistent storage data
353+
EXPECT_TRUE(content::ExecJs(
354+
web_contents, SetCookieJS("persistent_cookie", "persistent_value")));
355+
EXPECT_TRUE(content::ExecJs(
356+
web_contents, SetLocalStorageJS("persistent_key", "persistent_value")));
357+
358+
EXPECT_TRUE(content::ExecJs(
359+
web_contents, SetIndexedDBJS("persistent_key", "persistent_value")));
360+
361+
// Verify data is set
362+
content::EvalJsResult cookie_result =
363+
content::EvalJs(web_contents, GetCookiesJS());
364+
EXPECT_TRUE(cookie_result.ExtractString().find(
365+
"persistent_cookie=persistent_value") != std::string::npos);
366+
367+
EXPECT_EQ("persistent_value",
368+
content::EvalJs(web_contents, GetLocalStorageJS("persistent_key")));
369+
EXPECT_EQ("persistent_value",
370+
content::EvalJs(web_contents, GetIndexedDBJS("persistent_key")));
371+
}
372+
373+
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
374+
StoragePersistenceAcrossSessions) {
375+
const GURL url("https://a.test/simple.html");
376+
377+
// Navigate to the page
378+
NavigateParams params(browser(), url, ui::PAGE_TRANSITION_LINK);
379+
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
380+
params.storage_partition_config = content::StoragePartitionConfig::Create(
381+
browser()->profile(), kContainersStoragePartitionDomain, "container",
382+
browser()->profile()->IsOffTheRecord());
383+
ui_test_utils::NavigateToURL(&params);
384+
385+
content::WebContents* web_contents_reloaded =
386+
browser()->tab_strip_model()->GetActiveWebContents();
387+
ASSERT_TRUE(web_contents_reloaded);
388+
389+
// Verify persistent data is still available after reload
390+
content::EvalJsResult cookie_result_reloaded =
391+
content::EvalJs(web_contents_reloaded, GetCookiesJS());
392+
EXPECT_TRUE(cookie_result_reloaded.ExtractString().find(
393+
"persistent_cookie=persistent_value") != std::string::npos);
394+
395+
EXPECT_EQ("persistent_value",
396+
content::EvalJs(web_contents_reloaded,
397+
GetLocalStorageJS("persistent_key")));
398+
EXPECT_EQ(
399+
"persistent_value",
400+
content::EvalJs(web_contents_reloaded, GetIndexedDBJS("persistent_key")));
401+
}
402+
331403
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
332404
LinkNavigationInheritsContainerStoragePartition) {
333405
const GURL url("https://a.test/simple.html");
@@ -417,7 +489,8 @@ IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
417489
GetIndexedDBJS("new_tab_key")));
418490
}
419491

420-
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest, IsolateServiceWorkers) {
492+
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
493+
IsolateServiceWorkersBetweenContainers) {
421494
const GURL url("https://a.test/containers/container_test.html");
422495
const GURL worker_url("https://a.test/containers/container_worker.js");
423496
const std::string scope = "https://a.test/containers/";
@@ -851,4 +924,174 @@ IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
851924
// container
852925
}
853926

927+
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
928+
PRE_ServiceWorkerPersistenceAcrossSessions) {
929+
const GURL url("https://a.test/containers/container_test.html");
930+
const GURL worker_url("https://a.test/containers/container_worker.js");
931+
const std::string scope = "https://a.test/containers/";
932+
933+
// Navigate to the page with a container
934+
NavigateParams params(browser(), url, ui::PAGE_TRANSITION_LINK);
935+
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
936+
params.storage_partition_config = content::StoragePartitionConfig::Create(
937+
browser()->profile(), kContainersStoragePartitionDomain,
938+
"persistent-container", browser()->profile()->IsOffTheRecord());
939+
ui_test_utils::NavigateToURL(&params);
940+
941+
content::WebContents* web_contents =
942+
browser()->tab_strip_model()->GetActiveWebContents();
943+
ASSERT_TRUE(web_contents);
944+
945+
// Register service worker
946+
EXPECT_TRUE(content::ExecJs(
947+
web_contents, RegisterServiceWorkerJS(worker_url.spec(), scope)));
948+
949+
// Verify service worker is registered
950+
EXPECT_EQ(
951+
"registered",
952+
content::EvalJs(web_contents, CheckServiceWorkerRegisteredJS(scope)));
953+
954+
// Set some persistent storage data that the service worker might use
955+
EXPECT_TRUE(content::ExecJs(
956+
web_contents, SetLocalStorageJS("sw_data", "persistent_value")));
957+
EXPECT_TRUE(content::ExecJs(web_contents,
958+
SetCookieJS("sw_cookie", "persistent_cookie")));
959+
}
960+
961+
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
962+
ServiceWorkerPersistenceAcrossSessions) {
963+
const GURL url("https://a.test/containers/container_test.html");
964+
const std::string scope = "https://a.test/containers/";
965+
966+
// Navigate to the page with the same container
967+
NavigateParams params(browser(), url, ui::PAGE_TRANSITION_LINK);
968+
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
969+
params.storage_partition_config = content::StoragePartitionConfig::Create(
970+
browser()->profile(), kContainersStoragePartitionDomain,
971+
"persistent-container", browser()->profile()->IsOffTheRecord());
972+
ui_test_utils::NavigateToURL(&params);
973+
974+
content::WebContents* web_contents_reloaded =
975+
browser()->tab_strip_model()->GetActiveWebContents();
976+
ASSERT_TRUE(web_contents_reloaded);
977+
978+
// Verify service worker is still registered after browser restart
979+
EXPECT_EQ("registered",
980+
content::EvalJs(web_contents_reloaded,
981+
CheckServiceWorkerRegisteredJS(scope)));
982+
983+
// Verify persistent storage data is still available
984+
EXPECT_EQ("persistent_value", content::EvalJs(web_contents_reloaded,
985+
GetLocalStorageJS("sw_data")));
986+
987+
content::EvalJsResult cookie_result =
988+
content::EvalJs(web_contents_reloaded, GetCookiesJS());
989+
EXPECT_TRUE(cookie_result.ExtractString().find(
990+
"sw_cookie=persistent_cookie") != std::string::npos);
991+
}
992+
993+
// Test suite to verify behavior when containers feature is disabled after
994+
// a session with container tabs.
995+
class ContainersDisabledAfterRestoreBrowserTest : public ContainersBrowserTest {
996+
public:
997+
ContainersDisabledAfterRestoreBrowserTest() {
998+
const ::testing::TestInfo* test_info =
999+
::testing::UnitTest::GetInstance()->current_test_info();
1000+
std::string test_name = test_info->name();
1001+
1002+
if (!test_name.starts_with("PRE_")) {
1003+
feature_list_override_.InitAndDisableFeature(features::kContainers);
1004+
}
1005+
}
1006+
1007+
protected:
1008+
base::test::ScopedFeatureList feature_list_override_;
1009+
};
1010+
1011+
IN_PROC_BROWSER_TEST_F(ContainersDisabledAfterRestoreBrowserTest,
1012+
PRE_RestoreWithDefaultPartition) {
1013+
const GURL url("https://a.test/simple.html");
1014+
1015+
// Navigate to the page with a container storage partition
1016+
NavigateParams params(browser(), url, ui::PAGE_TRANSITION_LINK);
1017+
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
1018+
params.storage_partition_config = content::StoragePartitionConfig::Create(
1019+
browser()->profile(), kContainersStoragePartitionDomain, "test-container",
1020+
browser()->profile()->IsOffTheRecord());
1021+
ui_test_utils::NavigateToURL(&params);
1022+
1023+
content::WebContents* web_contents =
1024+
browser()->tab_strip_model()->GetActiveWebContents();
1025+
ASSERT_TRUE(web_contents);
1026+
1027+
// Verify we're using a container storage partition
1028+
content::StoragePartition* storage_partition =
1029+
web_contents->GetPrimaryMainFrame()->GetStoragePartition();
1030+
ASSERT_TRUE(storage_partition);
1031+
1032+
EXPECT_EQ(storage_partition->GetConfig().partition_domain(),
1033+
kContainersStoragePartitionDomain);
1034+
EXPECT_EQ(storage_partition->GetConfig().partition_name(), "test-container");
1035+
1036+
// Set some storage data in the container
1037+
EXPECT_TRUE(content::ExecJs(web_contents,
1038+
SetCookieJS("test_cookie", "container_value")));
1039+
EXPECT_TRUE(content::ExecJs(
1040+
web_contents, SetLocalStorageJS("test_key", "container_value")));
1041+
}
1042+
1043+
IN_PROC_BROWSER_TEST_F(ContainersDisabledAfterRestoreBrowserTest,
1044+
RestoreWithDefaultPartition) {
1045+
// At this point, containers feature is disabled, but we have a restored tab
1046+
// that was previously in a container
1047+
1048+
content::WebContents* web_contents =
1049+
browser()->tab_strip_model()->GetActiveWebContents();
1050+
ASSERT_TRUE(web_contents);
1051+
1052+
// Verify the URL scheme has been changed to containers-default+...
1053+
GURL current_url = web_contents->GetLastCommittedURL();
1054+
std::string url_scheme = std::string(current_url.scheme());
1055+
1056+
// The URL should have a scheme like "containers-default+https"
1057+
EXPECT_TRUE(url_scheme.starts_with("containers-default+")) << url_scheme;
1058+
1059+
// Verify the storage partition is now the default one, not a container
1060+
// partition
1061+
content::StoragePartition* storage_partition =
1062+
web_contents->GetPrimaryMainFrame()->GetStoragePartition();
1063+
ASSERT_TRUE(storage_partition);
1064+
1065+
content::StoragePartitionConfig default_config =
1066+
content::StoragePartitionConfig::CreateDefault(browser()->profile());
1067+
1068+
// The storage partition should be the default one
1069+
EXPECT_EQ(default_config, storage_partition->GetConfig());
1070+
EXPECT_TRUE(storage_partition->GetConfig().is_default());
1071+
EXPECT_EQ("", storage_partition->GetConfig().partition_domain());
1072+
EXPECT_EQ("", storage_partition->GetConfig().partition_name());
1073+
1074+
// Verify the restored page behaves like about:blank - it's a valid page
1075+
// but doesn't have access to storage and cookies from the container
1076+
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
1077+
ASSERT_TRUE(main_frame);
1078+
1079+
// The page is not an error document, but it's essentially blank
1080+
EXPECT_FALSE(main_frame->IsErrorDocument());
1081+
1082+
// Verify that JavaScript calls to access storage throw exceptions
1083+
// (the page doesn't have a valid document context for storage APIs)
1084+
content::EvalJsResult cookie_result =
1085+
content::EvalJs(web_contents, GetCookiesJS());
1086+
EXPECT_FALSE(cookie_result.is_ok())
1087+
<< "Expected JS exception when accessing cookies, but got: "
1088+
<< cookie_result;
1089+
1090+
content::EvalJsResult local_storage_result =
1091+
content::EvalJs(web_contents, GetLocalStorageJS("test_key"));
1092+
EXPECT_FALSE(local_storage_result.is_ok())
1093+
<< "Expected JS exception when accessing localStorage, but got: "
1094+
<< local_storage_result;
1095+
}
1096+
8541097
} // namespace containers
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
include_rules = [
22
"+brave/components/containers/buildflags",
3+
"+brave/components/containers/content/browser",
34
]

chromium_src/chrome/browser/ui/browser_tabrestore.cc

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,47 @@
1515
#include "content/public/browser/browser_context.h"
1616

1717
#if BUILDFLAG(ENABLE_CONTAINERS)
18+
#include "brave/components/containers/content/browser/storage_partition_utils.h"
1819
#include "chrome/browser/tab_contents/tab_util.h"
1920

2021
#define AddRestoredTab AddRestoredTab_ChromiumImpl
2122
#define ReplaceRestoredTab ReplaceRestoredTab_ChromiumImpl
2223

23-
#define GetSiteInstanceForNewTab(...) \
24-
GetSiteInstanceForNewTab(__VA_ARGS__, std::nullopt)
24+
#define GetSiteInstanceForNewTab(...) \
25+
GetSiteInstanceForNewTab( \
26+
__VA_ARGS__, GetStoragePartitionConfigToRestore( \
27+
browser->profile(), navigations, selected_navigation))
2528

29+
namespace {
30+
31+
std::optional<content::StoragePartitionConfig>
32+
GetStoragePartitionConfigToRestore(
33+
content::BrowserContext* browser_context,
34+
base::span<const sessions::SerializedNavigationEntry> navigations,
35+
int selected_navigation) {
36+
if (navigations.empty()) {
37+
return std::nullopt;
38+
}
39+
40+
if (selected_navigation < 0 ||
41+
static_cast<size_t>(selected_navigation) >= navigations.size()) {
42+
return std::nullopt;
43+
}
44+
45+
if (const auto& storage_partition_key =
46+
navigations[selected_navigation].storage_partition_key();
47+
storage_partition_key &&
48+
containers::IsContainersStoragePartitionKey(
49+
storage_partition_key->first, storage_partition_key->second)) {
50+
return content::StoragePartitionConfig::Create(
51+
browser_context, storage_partition_key->first,
52+
storage_partition_key->second, browser_context->IsOffTheRecord());
53+
}
54+
55+
return std::nullopt;
56+
}
57+
58+
} // namespace
2659
#endif // BUILDFLAG(ENABLE_CONTAINERS)
2760

2861
#include <chrome/browser/ui/browser_tabrestore.cc>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include_rules = [
2+
"+brave/components/containers/buildflags",
3+
"+brave/components/containers/content/browser",
4+
"+brave/components/containers/core/common",
5+
]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
4+
* You can obtain one at https://mozilla.org/MPL/2.0/. */
5+
6+
#include "components/sessions/content/content_serialized_navigation_builder.h"
7+
8+
#include <string>
9+
10+
#include "brave/components/containers/buildflags/buildflags.h"
11+
12+
#if BUILDFLAG(ENABLE_CONTAINERS)
13+
#include "base/containers/map_util.h"
14+
#include "brave/components/containers/content/browser/session_utils.h"
15+
#include "brave/components/containers/core/common/features.h"
16+
#endif // BUILDFLAG(ENABLE_CONTAINERS)
17+
18+
#define FromNavigationEntry FromNavigationEntry_ChromiumImpl
19+
20+
#define BRAVE_CONTENT_SERIALIZED_NAVIGATION_BUILDER_TO_NAVIGATION_ENTRY \
21+
if (base::FeatureList::IsEnabled(containers::features::kContainers) && \
22+
navigation->storage_partition_key().has_value()) { \
23+
entry->SetStoragePartitionKeyToRestore( \
24+
navigation->storage_partition_key().value()); \
25+
}
26+
27+
#include <components/sessions/content/content_serialized_navigation_builder.cc>
28+
29+
#undef FromNavigationEntry
30+
#undef BRAVE_CONTENT_SERIALIZED_NAVIGATION_BUILDER_TO_NAVIGATION_ENTRY
31+
32+
namespace sessions {
33+
34+
// static
35+
SerializedNavigationEntry
36+
ContentSerializedNavigationBuilder::FromNavigationEntry(
37+
int index,
38+
content::NavigationEntry* entry,
39+
SerializationOptions serialization_options) {
40+
SerializedNavigationEntry navigation =
41+
FromNavigationEntry_ChromiumImpl(index, entry, serialization_options);
42+
43+
#if BUILDFLAG(ENABLE_CONTAINERS)
44+
if (base::FeatureList::IsEnabled(containers::features::kContainers)) {
45+
if (auto storage_partition_key_to_restore =
46+
entry->GetStoragePartitionKeyToRestore()) {
47+
if (auto virtual_url_prefix = containers::GetVirtualUrlPrefix(
48+
*storage_partition_key_to_restore)) {
49+
navigation.set_virtual_url_prefix(*virtual_url_prefix);
50+
navigation.set_storage_partition_key(*storage_partition_key_to_restore);
51+
}
52+
}
53+
}
54+
#endif // BUILDFLAG(ENABLE_CONTAINERS)
55+
return navigation;
56+
}
57+
58+
} // namespace sessions

0 commit comments

Comments
 (0)