|
5 | 5 | #include "base/files/file_path.h" |
6 | 6 | #include "base/files/file_util.h" |
7 | 7 | #include "base/files/scoped_temp_dir.h" |
| 8 | +#include "base/run_loop.h" |
8 | 9 | #include "base/strings/strcat.h" |
9 | 10 | #include "base/strings/string_util.h" |
10 | 11 | #include "base/strings/utf_string_conversions.h" |
|
31 | 32 | #include "content/public/browser/web_contents.h" |
32 | 33 | #include "content/public/test/browser_test.h" |
33 | 34 | #include "content/public/test/browser_test_utils.h" |
| 35 | +#include "content/public/test/test_navigation_observer.h" |
34 | 36 | #include "ui/base/hit_test.h" |
35 | 37 | #include "dao/browser/agent/dao_agent_memory_service.h" |
36 | 38 | #include "dao/browser/agent/dao_agent_memory_service_factory.h" |
@@ -2249,5 +2251,213 @@ IN_PROC_BROWSER_TEST_F(DaoAgentMemoryServiceConversationTest, StatsAvailable) { |
2249 | 2251 | EXPECT_GE(stats.Get().episode_count, 0); |
2250 | 2252 | } |
2251 | 2253 |
|
| 2254 | +// ============================================================================= |
| 2255 | +// DaoBackToOpenerBrowserTest |
| 2256 | +// ============================================================================= |
| 2257 | +// |
| 2258 | +// These tests pin down the back-to-opener semantics that Dao's address-bar |
| 2259 | +// Back button is expected to honor once it routes through chrome::GoBack / |
| 2260 | +// chrome::CanGoBack. The feature flag (tabs::kBackToOpener) is enabled by |
| 2261 | +// default in our patched build, so we exercise the public command surface |
| 2262 | +// without re-enabling it here. |
| 2263 | + |
| 2264 | +class DaoBackToOpenerBrowserTest : public InProcessBrowserTest { |
| 2265 | + public: |
| 2266 | + DaoBackToOpenerBrowserTest() = default; |
| 2267 | + |
| 2268 | + void SetUpOnMainThread() override { |
| 2269 | + InProcessBrowserTest::SetUpOnMainThread(); |
| 2270 | + host_resolver()->AddRule("*", "127.0.0.1"); |
| 2271 | + embedded_test_server()->ServeFilesFromSourceDirectory("chrome/test/data"); |
| 2272 | + ASSERT_TRUE(embedded_test_server()->Start()); |
| 2273 | + } |
| 2274 | + |
| 2275 | + // Navigate the active tab to the opener fixture and click the link to spawn |
| 2276 | + // a child tab in the same browser. Returns the destination WebContents. |
| 2277 | + content::WebContents* OpenChildFromOpener(GURL* opener_url_out = nullptr) { |
| 2278 | + GURL opener_url = |
| 2279 | + embedded_test_server()->GetURL("/back_to_opener_opener.html"); |
| 2280 | + if (opener_url_out) { |
| 2281 | + *opener_url_out = opener_url; |
| 2282 | + } |
| 2283 | + EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), opener_url)); |
| 2284 | + |
| 2285 | + content::WebContents* opener_contents = |
| 2286 | + browser()->tab_strip_model()->GetActiveWebContents(); |
| 2287 | + EXPECT_TRUE(content::WaitForLoadStop(opener_contents)); |
| 2288 | + |
| 2289 | + ui_test_utils::TabAddedWaiter tab_waiter(browser()); |
| 2290 | + EXPECT_TRUE(content::ExecJs(opener_contents, |
| 2291 | + "document.getElementById('link').click();")); |
| 2292 | + content::WebContents* dest_contents = tab_waiter.Wait(); |
| 2293 | + EXPECT_NE(nullptr, dest_contents); |
| 2294 | + EXPECT_NE(dest_contents, opener_contents); |
| 2295 | + EXPECT_TRUE(content::WaitForLoadStop(dest_contents)); |
| 2296 | + return dest_contents; |
| 2297 | + } |
| 2298 | +}; |
| 2299 | + |
| 2300 | +// 1. Clicking back in a child tab whose in-tab history is empty should close |
| 2301 | +// the child tab and re-activate the opener tab. |
| 2302 | +IN_PROC_BROWSER_TEST_F(DaoBackToOpenerBrowserTest, |
| 2303 | + BackClosesChildAndActivatesParent) { |
| 2304 | + TabStripModel* tab_strip = browser()->tab_strip_model(); |
| 2305 | + content::WebContents* opener_contents = tab_strip->GetActiveWebContents(); |
| 2306 | + const int opener_index = tab_strip->GetIndexOfWebContents(opener_contents); |
| 2307 | + |
| 2308 | + content::WebContents* dest_contents = OpenChildFromOpener(); |
| 2309 | + ASSERT_NE(nullptr, dest_contents); |
| 2310 | + ASSERT_EQ(2, tab_strip->count()); |
| 2311 | + ASSERT_EQ(dest_contents, tab_strip->GetActiveWebContents()); |
| 2312 | + |
| 2313 | + // Back-to-opener should be available even though the child tab itself has |
| 2314 | + // no in-tab back history. |
| 2315 | + EXPECT_FALSE(dest_contents->GetController().CanGoBack()); |
| 2316 | + EXPECT_TRUE(chrome::CanGoBack(browser())); |
| 2317 | + |
| 2318 | + content::WebContentsDestroyedWatcher close_watcher(dest_contents); |
| 2319 | + chrome::GoBack(browser(), WindowOpenDisposition::CURRENT_TAB); |
| 2320 | + close_watcher.Wait(); |
| 2321 | + |
| 2322 | + // Child closed, opener focused. |
| 2323 | + EXPECT_EQ(1, tab_strip->count()); |
| 2324 | + EXPECT_EQ(opener_index, |
| 2325 | + tab_strip->GetIndexOfWebContents(opener_contents)); |
| 2326 | + EXPECT_EQ(opener_contents, tab_strip->GetActiveWebContents()); |
| 2327 | +} |
| 2328 | + |
| 2329 | +// 2. If the opener tab navigates away after spawning the child, back-to-opener |
| 2330 | +// should no longer be available from the child (chrome::CanGoBack returns |
| 2331 | +// false when the child has no in-tab history). |
| 2332 | +IN_PROC_BROWSER_TEST_F(DaoBackToOpenerBrowserTest, |
| 2333 | + ParentNavigatedAwayDisablesBack) { |
| 2334 | + TabStripModel* tab_strip = browser()->tab_strip_model(); |
| 2335 | + content::WebContents* opener_contents = tab_strip->GetActiveWebContents(); |
| 2336 | + const int opener_index = tab_strip->GetIndexOfWebContents(opener_contents); |
| 2337 | + |
| 2338 | + content::WebContents* dest_contents = OpenChildFromOpener(); |
| 2339 | + ASSERT_NE(nullptr, dest_contents); |
| 2340 | + ASSERT_EQ(2, tab_strip->count()); |
| 2341 | + |
| 2342 | + // Switch back to the opener and navigate it elsewhere. |
| 2343 | + tab_strip->ActivateTabAt(opener_index); |
| 2344 | + ASSERT_EQ(opener_contents, tab_strip->GetActiveWebContents()); |
| 2345 | + GURL other_url = embedded_test_server()->GetURL("/title2.html"); |
| 2346 | + { |
| 2347 | + content::TestNavigationObserver nav_observer(opener_contents); |
| 2348 | + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), other_url)); |
| 2349 | + nav_observer.Wait(); |
| 2350 | + } |
| 2351 | + |
| 2352 | + // Re-focus the child tab. Its own history is empty, and the opener has |
| 2353 | + // moved on, so chrome::CanGoBack should now report false. |
| 2354 | + const int dest_index = tab_strip->GetIndexOfWebContents(dest_contents); |
| 2355 | + ASSERT_NE(TabStripModel::kNoTab, dest_index); |
| 2356 | + tab_strip->ActivateTabAt(dest_index); |
| 2357 | + ASSERT_EQ(dest_contents, tab_strip->GetActiveWebContents()); |
| 2358 | + |
| 2359 | + EXPECT_FALSE(dest_contents->GetController().CanGoBack()); |
| 2360 | + EXPECT_FALSE(chrome::CanGoBack(browser())); |
| 2361 | +} |
| 2362 | + |
| 2363 | +// 3. Closing the opener tab should also disable back-to-opener for the |
| 2364 | +// orphaned child tab. |
| 2365 | +IN_PROC_BROWSER_TEST_F(DaoBackToOpenerBrowserTest, |
| 2366 | + ParentClosedDisablesBack) { |
| 2367 | + TabStripModel* tab_strip = browser()->tab_strip_model(); |
| 2368 | + content::WebContents* opener_contents = tab_strip->GetActiveWebContents(); |
| 2369 | + const int opener_index = tab_strip->GetIndexOfWebContents(opener_contents); |
| 2370 | + |
| 2371 | + content::WebContents* dest_contents = OpenChildFromOpener(); |
| 2372 | + ASSERT_NE(nullptr, dest_contents); |
| 2373 | + ASSERT_EQ(2, tab_strip->count()); |
| 2374 | + |
| 2375 | + // Close the opener. |
| 2376 | + { |
| 2377 | + content::WebContentsDestroyedWatcher destroyed_watcher(opener_contents); |
| 2378 | + tab_strip->CloseWebContentsAt(opener_index, |
| 2379 | + TabCloseTypes::CLOSE_USER_GESTURE); |
| 2380 | + destroyed_watcher.Wait(); |
| 2381 | + } |
| 2382 | + ASSERT_EQ(1, tab_strip->count()); |
| 2383 | + ASSERT_EQ(dest_contents, tab_strip->GetActiveWebContents()); |
| 2384 | + |
| 2385 | + // With the opener gone and no in-tab history, back must be disabled. |
| 2386 | + EXPECT_FALSE(dest_contents->GetController().CanGoBack()); |
| 2387 | + EXPECT_FALSE(chrome::CanGoBack(browser())); |
| 2388 | +} |
| 2389 | + |
| 2390 | +// 4. When a child tab has its own in-tab navigation history, the regular |
| 2391 | +// in-tab Back semantics should take precedence: clicking back navigates the |
| 2392 | +// child instead of closing it. |
| 2393 | +IN_PROC_BROWSER_TEST_F(DaoBackToOpenerBrowserTest, |
| 2394 | + InTabHistoryTakesPrecedence) { |
| 2395 | + TabStripModel* tab_strip = browser()->tab_strip_model(); |
| 2396 | + |
| 2397 | + content::WebContents* dest_contents = OpenChildFromOpener(); |
| 2398 | + ASSERT_NE(nullptr, dest_contents); |
| 2399 | + ASSERT_EQ(2, tab_strip->count()); |
| 2400 | + const int dest_index = tab_strip->GetIndexOfWebContents(dest_contents); |
| 2401 | + |
| 2402 | + // Drive an in-tab navigation in the child to grow its history. |
| 2403 | + GURL second_url = embedded_test_server()->GetURL("/title2.html"); |
| 2404 | + { |
| 2405 | + content::TestNavigationObserver nav_observer(dest_contents); |
| 2406 | + ASSERT_TRUE(content::ExecJs( |
| 2407 | + dest_contents, |
| 2408 | + content::JsReplace("window.location.href = $1;", second_url))); |
| 2409 | + nav_observer.Wait(); |
| 2410 | + } |
| 2411 | + EXPECT_TRUE(dest_contents->GetController().CanGoBack()); |
| 2412 | + EXPECT_TRUE(chrome::CanGoBack(browser())); |
| 2413 | + |
| 2414 | + // chrome::GoBack should navigate the child, not close it. |
| 2415 | + { |
| 2416 | + content::TestNavigationObserver nav_observer(dest_contents); |
| 2417 | + chrome::GoBack(browser(), WindowOpenDisposition::CURRENT_TAB); |
| 2418 | + nav_observer.Wait(); |
| 2419 | + } |
| 2420 | + |
| 2421 | + // Tab is still open and is still the active tab. |
| 2422 | + EXPECT_EQ(2, tab_strip->count()); |
| 2423 | + EXPECT_EQ(dest_index, tab_strip->GetIndexOfWebContents(dest_contents)); |
| 2424 | + EXPECT_EQ(dest_contents, tab_strip->GetActiveWebContents()); |
| 2425 | +} |
| 2426 | + |
| 2427 | +// 5. Pinned tabs should not participate in back-to-opener: a pinned child |
| 2428 | +// tab with no in-tab history should report CanGoBack == false. |
| 2429 | +IN_PROC_BROWSER_TEST_F(DaoBackToOpenerBrowserTest, |
| 2430 | + PinnedChildDoesNotGoBack) { |
| 2431 | + TabStripModel* tab_strip = browser()->tab_strip_model(); |
| 2432 | + |
| 2433 | + content::WebContents* dest_contents = OpenChildFromOpener(); |
| 2434 | + ASSERT_NE(nullptr, dest_contents); |
| 2435 | + ASSERT_EQ(2, tab_strip->count()); |
| 2436 | + |
| 2437 | + int dest_index = tab_strip->GetIndexOfWebContents(dest_contents); |
| 2438 | + ASSERT_NE(TabStripModel::kNoTab, dest_index); |
| 2439 | + |
| 2440 | + // Pin the destination tab. Pinning may reorder; refresh the index and |
| 2441 | + // re-activate it. |
| 2442 | + dest_index = tab_strip->SetTabPinned(dest_index, true); |
| 2443 | + ASSERT_NE(TabStripModel::kNoTab, dest_index); |
| 2444 | + tab_strip->ActivateTabAt(dest_index); |
| 2445 | + ASSERT_EQ(dest_contents, tab_strip->GetActiveWebContents()); |
| 2446 | + ASSERT_TRUE(tab_strip->IsTabPinned(dest_index)); |
| 2447 | + |
| 2448 | + // No in-tab history and pinned: chrome::CanGoBack must be false. Clicking |
| 2449 | + // back must NOT close the pinned tab. |
| 2450 | + EXPECT_FALSE(dest_contents->GetController().CanGoBack()); |
| 2451 | + EXPECT_FALSE(chrome::CanGoBack(browser())); |
| 2452 | + |
| 2453 | + // Even if invoked anyway, the tab count must stay at 2 (defense in depth). |
| 2454 | + const int tab_count_before = tab_strip->count(); |
| 2455 | + if (chrome::CanGoBack(browser())) { |
| 2456 | + chrome::GoBack(browser(), WindowOpenDisposition::CURRENT_TAB); |
| 2457 | + base::RunLoop().RunUntilIdle(); |
| 2458 | + } |
| 2459 | + EXPECT_EQ(tab_count_before, tab_strip->count()); |
| 2460 | +} |
| 2461 | + |
2252 | 2462 | } // namespace |
2253 | 2463 | } // namespace dao |
0 commit comments