blob: 8fc55217f87aeef7c8674a483345782511bfe35e [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/ui/web_applications/test/web_app_navigation_browsertest.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/test/web_app_test_observers.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/embedder_support/switches.h"
#include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h"
using content::RenderFrameHost;
using content::WebContents;
using content::test::PrerenderHostObserver;
using content::test::PrerenderHostRegistryObserver;
using content::test::PrerenderTestHelper;
using ui_test_utils::BrowserChangeObserver;
namespace web_app {
using ClientMode = LaunchHandler::ClientMode;
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Tests that links are captured correctly into an installed WebApp using the
// 'tabbed' display mode, which allows the webapp window to have multiple tabs.
class WebAppLinkCapturingBrowserTest : public WebAppNavigationBrowserTest {
public:
WebAppLinkCapturingBrowserTest() {
feature_list_.InitAndEnableFeature(
blink::features::kWebAppEnableLaunchHandler);
}
~WebAppLinkCapturingBrowserTest() override = default;
void SetUpOnMainThread() override {
WebAppNavigationBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(https_server().Start());
ASSERT_TRUE(embedded_test_server()->Start());
out_of_scope_ = https_server().GetURL("/");
}
void InstallTestApp(const char* path) {
start_url_ = embedded_test_server()->GetURL(path);
in_scope_1_ = start_url_.Resolve("page1.html");
in_scope_2_ = start_url_.Resolve("page2.html");
scope_ = start_url_.GetWithoutFilename();
app_id_ = InstallWebAppFromPageAndCloseAppBrowser(browser(), start_url_);
}
WebAppProvider& provider() {
auto* provider = WebAppProvider::GetForTest(profile());
DCHECK(provider);
return *provider;
}
void AddTab(Browser* browser, const GURL& url) {
content::TestNavigationObserver observer(url);
observer.StartWatchingNewWebContents();
chrome::AddTabAt(browser, url, /*index=*/-1, /*foreground=*/true);
observer.Wait();
}
void Navigate(Browser* browser,
const GURL& url,
LinkTarget link_target = LinkTarget::SELF) {
ClickLinkAndWait(browser->tab_strip_model()->GetActiveWebContents(), url,
link_target, "");
}
Browser* GetNewBrowserFromNavigation(Browser* browser,
const GURL& url,
bool preserve_about_blank = true) {
if (preserve_about_blank && browser->tab_strip_model()
->GetActiveWebContents()
->GetVisibleURL()
.IsAboutBlank()) {
// Create a new tab to link capture in because about:blank tabs are
// destroyed after link capturing, see:
// CommonAppsNavigationThrottle::ShouldCancelNavigation()
AddTab(browser, about_blank_);
}
BrowserChangeObserver observer(nullptr,
BrowserChangeObserver::ChangeType::kAdded);
Navigate(browser, url);
return observer.Wait();
}
void ExpectTabs(Browser* test_browser, std::vector<GURL> urls) {
std::string debug_info = "\nOpen browsers:\n";
for (Browser* open_browser : *BrowserList::GetInstance()) {
debug_info += " ";
if (open_browser == browser())
debug_info += "Main browser";
else if (open_browser->app_controller())
debug_info += "App browser";
else
debug_info += "Browser";
debug_info += ":\n";
for (int i = 0; i < open_browser->tab_strip_model()->count(); ++i) {
debug_info += " - " +
open_browser->tab_strip_model()
->GetWebContentsAt(i)
->GetVisibleURL()
.spec() +
"\n";
}
}
SCOPED_TRACE(debug_info);
TabStripModel& tab_strip = *test_browser->tab_strip_model();
ASSERT_EQ(static_cast<size_t>(tab_strip.count()), urls.size());
for (int i = 0; i < tab_strip.count(); ++i) {
SCOPED_TRACE(base::StringPrintf("is app browser: %d, tab index: %d",
bool(test_browser->app_controller()), i));
EXPECT_EQ(
test_browser->tab_strip_model()->GetWebContentsAt(i)->GetVisibleURL(),
urls[i]);
}
}
void TurnOnLinkCapturing() {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
proxy->SetSupportedLinksPreference(app_id_);
}
absl::optional<LaunchHandler> GetLaunchHandler(const AppId& app_id) {
return provider().registrar_unsafe().GetAppById(app_id)->launch_handler();
}
protected:
AppId app_id_;
GURL start_url_;
GURL in_scope_1_;
GURL in_scope_2_;
GURL scope_;
GURL out_of_scope_;
const GURL about_blank_{"about:blank"};
base::test::ScopedFeatureList feature_list_;
OsIntegrationManager::ScopedSuppressForTesting os_hooks_supress_;
};
// Link capturing with navigate_existing_client: always should navigate existing
// app windows.
IN_PROC_BROWSER_TEST_F(WebAppLinkCapturingBrowserTest,
NavigateExistingClientFromBrowser) {
InstallTestApp(
"/web_apps/get_manifest.html?"
"launch_handler_client_mode_navigate_existing.json");
EXPECT_EQ(GetLaunchHandler(app_id_),
(LaunchHandler{ClientMode::kNavigateExisting}));
TurnOnLinkCapturing();
// Start browser at an out of scope page.
Navigate(browser(), out_of_scope_);
// In scope navigation should open app window.
Browser* app_browser = GetNewBrowserFromNavigation(browser(), in_scope_1_);
EXPECT_TRUE(AppBrowserController::IsForWebApp(app_browser, app_id_));
ExpectTabs(browser(), {out_of_scope_});
ExpectTabs(app_browser, {in_scope_1_});
// Navigate the app window out of scope to ensure the captured link triggers a
// navigation.
Navigate(app_browser, out_of_scope_);
ExpectTabs(app_browser, {out_of_scope_});
// Click a link in the browser in to scope. Ensure that no additional tabs get
// opened in the browser.
Navigate(browser(), in_scope_1_);
ExpectTabs(browser(), {out_of_scope_});
ExpectTabs(app_browser, {in_scope_1_});
}
// Link captures from about:blank cleans up the about:blank page.
IN_PROC_BROWSER_TEST_F(WebAppLinkCapturingBrowserTest,
AboutBlankNavigationCleanUp) {
InstallTestApp("/web_apps/basic.html");
TurnOnLinkCapturing();
ExpectTabs(browser(), {about_blank_});
BrowserChangeObserver removed_observer(
browser(), BrowserChangeObserver::ChangeType::kRemoved);
// Navigate an about:blank page.
Browser* app_browser = GetNewBrowserFromNavigation(
browser(), in_scope_1_, /*preserve_about_blank=*/false);
EXPECT_TRUE(AppBrowserController::IsForWebApp(app_browser, app_id_));
ExpectTabs(app_browser, {in_scope_1_});
// Old about:blank page cleaned up.
removed_observer.Wait();
}
// JavaScript initiated link captures from about:blank cleans up the about:blank
// page.
IN_PROC_BROWSER_TEST_F(WebAppLinkCapturingBrowserTest,
JavascriptAboutBlankNavigationCleanUp) {
InstallTestApp("/web_apps/basic.html");
TurnOnLinkCapturing();
ExpectTabs(browser(), {about_blank_});
BrowserChangeObserver removed_observer(
browser(), BrowserChangeObserver::ChangeType::kRemoved);
// Navigate an about:blank page using JavaScript.
BrowserChangeObserver added_observer(
nullptr, BrowserChangeObserver::ChangeType::kAdded);
ASSERT_TRUE(content::ExecJs(
browser()->tab_strip_model()->GetActiveWebContents(),
base::StringPrintf("location = '%s';", in_scope_1_.spec().c_str())));
Browser* app_browser = added_observer.Wait();
ExpectTabs(app_browser, {in_scope_1_});
// Old about:blank page cleaned up.
removed_observer.Wait();
}
// TODO: Run these tests on Chrome OS with both Ash and Lacros processes active.
class WebAppTabStripLinkCapturingBrowserTest
: public WebAppLinkCapturingBrowserTest {
public:
WebAppTabStripLinkCapturingBrowserTest() {
features_.InitWithFeatures({features::kDesktopPWAsTabStrip,
features::kDesktopPWAsTabStripSettings},
{});
}
void InstallTestTabbedApp() {
WebAppLinkCapturingBrowserTest::InstallTestApp("/web_apps/basic.html");
provider().sync_bridge_unsafe().SetAppUserDisplayMode(
app_id_, mojom::UserDisplayMode::kTabbed, /*is_user_action=*/false);
}
private:
base::test::ScopedFeatureList features_;
};
// First in scope navigation from out of scope gets captured and reparented into
// the app window.
IN_PROC_BROWSER_TEST_F(WebAppTabStripLinkCapturingBrowserTest,
InScopeNavigationsCaptured) {
InstallTestTabbedApp();
TurnOnLinkCapturing();
// Start browser at an out of scope page.
Navigate(browser(), out_of_scope_);
// In scope navigation should open app window.
Browser* app_browser = GetNewBrowserFromNavigation(browser(), in_scope_1_);
EXPECT_TRUE(AppBrowserController::IsForWebApp(app_browser, app_id_));
ExpectTabs(browser(), {out_of_scope_});
ExpectTabs(app_browser, {in_scope_1_});
// Another in scope navigation should open a new tab in the same app window.
Navigate(browser(), in_scope_2_);
ExpectTabs(browser(), {out_of_scope_});
ExpectTabs(app_browser, {in_scope_1_, in_scope_2_});
// Whole origin should count as in scope.
Navigate(browser(), scope_);
ExpectTabs(browser(), {out_of_scope_});
ExpectTabs(app_browser, {in_scope_1_, in_scope_2_, scope_});
// Middle clicking links should not be captured.
ClickLinkWithModifiersAndWaitForURL(
browser()->tab_strip_model()->GetActiveWebContents(), scope_, scope_,
LinkTarget::SELF, "", blink::WebInputEvent::Modifiers::kNoModifiers,
blink::WebMouseEvent::Button::kMiddle);
ExpectTabs(browser(), {out_of_scope_, scope_});
ExpectTabs(app_browser, {in_scope_1_, in_scope_2_, scope_});
// Out of scope should behave as usual.
Navigate(browser(), out_of_scope_);
ExpectTabs(browser(), {out_of_scope_, scope_});
ExpectTabs(app_browser, {in_scope_1_, in_scope_2_, scope_});
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
} // namespace web_app