// Copyright 2021 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/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/unguessable_token.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/apps/browser_instance/browser_app_instance.h"
#include "chrome/browser/apps/browser_instance/browser_app_instance_observer.h"
#include "chrome/browser/apps/browser_instance/browser_app_instance_tracker.h"
#include "chrome/browser/ash/system_web_apps/apps/crosh_system_web_app_info.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/devtools/devtools_window_testing.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_model.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/common/constants.h"
// default implementation of RunTestOnMainThread() and TestBody()
#include "content/public/test/browser_test.h"
namespace {
constexpr char kTitle_A[] = "a.example.org";
// Generated from start URL "https://a.example.org/".
// See |web_app::GenerateAppId|.
constexpr char kAppId_A[] = "dhehpanpcmiafdmbldplnfenbijejdfe";
constexpr char kTitle_B[] = "b.example.org";
// Generated from start URL "https://b.example.org/".
constexpr char kAppId_B[] = "abhkhfladdfdlfmhaokoglcllbamaili";
uint64_t ToUint64(base::UnguessableToken id) {
// test IDs have only low part set
DCHECK(!id.GetHighForSerialization());
return id.GetLowForSerialization();
}
base::UnguessableToken TestId(uint64_t id) {
return base::UnguessableToken::CreateForTesting(0, id);
}
// Make test sequence easier to scan
constexpr bool kActive = true;
constexpr bool kInactive = false;
constexpr char kAppTab[] = "tab";
constexpr char kAppWindow[] = "window";
constexpr char kChromeWindow[] = "chrome";
struct TestInstance {
static TestInstance Create(const std::string& name,
const apps::BrowserAppInstance& instance) {
return {
name,
ToUint64(instance.id),
instance.type == apps::BrowserAppInstance::Type::kAppTab ? kAppTab
: kAppWindow,
instance.app_id,
instance.window,
instance.title,
instance.is_web_contents_active,
};
}
static TestInstance Create(const std::string name,
const apps::BrowserWindowInstance& instance) {
return {
name,
ToUint64(instance.id),
kChromeWindow,
/* app_id= */ "",
instance.window,
/* title= */ "",
/* is_web_contents_active= */ false,
};
}
static TestInstance Create(const apps::BrowserAppInstance* instance) {
if (instance) {
return Create("snapshot", *instance);
}
return {};
}
static TestInstance Create(const apps::BrowserWindowInstance* instance) {
if (instance) {
return Create("snapshot", *instance);
}
return {};
}
std::string name;
uint64_t id;
std::string type;
std::string app_id;
raw_ptr<aura::Window> window;
std::string title;
bool is_web_contents_active;
};
bool operator==(const TestInstance& e1, const TestInstance& e2) {
return e1.name == e2.name && e1.id == e2.id && e1.type == e2.type &&
e1.app_id == e2.app_id && e1.window == e2.window &&
e1.title == e2.title &&
e1.is_web_contents_active == e2.is_web_contents_active;
}
bool operator<(const TestInstance& e1, const TestInstance& e2) {
return std::tie(e1.name, e1.id, e1.type, e1.app_id, e1.window, e1.title,
e1.is_web_contents_active) <
std::tie(e2.name, e2.id, e2.type, e2.app_id, e2.window, e2.title,
e2.is_web_contents_active);
}
std::ostream& operator<<(std::ostream& os, const TestInstance& e) {
if (e.name == "") {
return os << "none";
}
return os << e.name << "(id=" << e.id << ",type=" << e.type
<< ",app_id=" << e.app_id << ", title='" << e.title << "'"
<< ", window=" << e.window
<< ", tab=" << (e.is_web_contents_active ? "active" : "inactive")
<< ")";
}
class Tracker : public apps::BrowserAppInstanceTracker {
public:
Tracker(Profile* profile, apps::AppRegistryCache& app_registry_cache)
: apps::BrowserAppInstanceTracker(profile, app_registry_cache) {}
private:
base::UnguessableToken GenerateId() const override {
return TestId(++last_id_);
}
mutable uint64_t last_id_{0};
};
class Recorder : public apps::BrowserAppInstanceObserver {
public:
explicit Recorder(apps::BrowserAppInstanceTracker& tracker)
: tracker_(tracker) {
tracker_->AddObserver(this);
}
~Recorder() override { tracker_->RemoveObserver(this); }
void OnBrowserWindowAdded(
const apps::BrowserWindowInstance& instance) override {
calls_.push_back(TestInstance::Create("added", instance));
}
void OnBrowserWindowUpdated(
const apps::BrowserWindowInstance& instance) override {
calls_.push_back(TestInstance::Create("updated", instance));
}
void OnBrowserWindowRemoved(
const apps::BrowserWindowInstance& instance) override {
calls_.push_back(TestInstance::Create("removed", instance));
}
void OnBrowserAppAdded(const apps::BrowserAppInstance& instance) override {
calls_.push_back(TestInstance::Create("added", instance));
}
void OnBrowserAppUpdated(const apps::BrowserAppInstance& instance) override {
calls_.push_back(TestInstance::Create("updated", instance));
}
void OnBrowserAppRemoved(const apps::BrowserAppInstance& instance) override {
calls_.push_back(TestInstance::Create("removed", instance));
}
void Verify(const std::vector<TestInstance>& expected_calls) {
EXPECT_EQ(calls_.size(), expected_calls.size());
for (size_t i = 0; i < std::max(calls_.size(), expected_calls.size());
++i) {
EXPECT_EQ(Get(calls_, i), Get(expected_calls, i)) << "call #" << i;
}
}
void VerifyIgnoreOrder(const std::vector<TestInstance>& expected_calls) {
EXPECT_EQ(calls_.size(), expected_calls.size());
std::vector<TestInstance> expected_calls_copy(expected_calls);
std::vector<TestInstance> calls_copy(calls_);
std::sort(expected_calls_copy.begin(), expected_calls_copy.end());
std::sort(calls_copy.begin(), calls_copy.end());
for (size_t i = 0;
i < std::max(calls_copy.size(), expected_calls_copy.size()); ++i) {
EXPECT_EQ(Get(calls_copy, i), Get(expected_calls_copy, i))
<< "call #" << i;
}
}
private:
static const TestInstance Get(const std::vector<TestInstance>& calls,
size_t i) {
if (i < calls.size()) {
return calls[i];
}
return {};
}
const raw_ref<apps::BrowserAppInstanceTracker> tracker_;
std::vector<TestInstance> calls_;
};
} // namespace
class BrowserAppInstanceTrackerTest : public InProcessBrowserTest {
protected:
Browser* CreateBrowser() {
Profile* profile = ProfileManager::GetPrimaryUserProfile();
Browser::CreateParams params(profile, true /* user_gesture */);
Browser* browser = Browser::Create(params);
browser->window()->Show();
return browser;
}
Browser* CreatePopupBrowser() {
Profile* profile = ProfileManager::GetPrimaryUserProfile();
Browser::CreateParams params(profile, true /* user_gesture */);
params.type = Browser::TYPE_POPUP;
Browser* browser = Browser::Create(params);
browser->window()->Show();
return browser;
}
Browser* CreateAppBrowser(const std::string& app_id) {
Profile* profile = ProfileManager::GetPrimaryUserProfile();
auto params = Browser::CreateParams::CreateForApp(
"_crx_" + app_id, true /* trusted_source */,
gfx::Rect(), /* window_bounts */
profile, true /* user_gesture */);
Browser* browser = Browser::Create(params);
browser->window()->Show();
return browser;
}
content::WebContents* NavigateAndWait(Browser* browser,
const std::string& url,
WindowOpenDisposition disposition) {
NavigateParams params(browser, GURL(url),
ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
params.disposition = disposition;
Navigate(¶ms);
auto* contents = params.navigated_or_inserted_contents.get();
DCHECK_EQ(chrome::FindBrowserWithTab(params.navigated_or_inserted_contents),
browser);
content::TestNavigationObserver observer(contents);
observer.Wait();
return contents;
}
void NavigateActiveTab(Browser* browser, const std::string& url) {
NavigateAndWait(browser, url, WindowOpenDisposition::CURRENT_TAB);
}
content::WebContents* InsertBackgroundTab(Browser* browser,
const std::string& url) {
return NavigateAndWait(browser, url,
WindowOpenDisposition::NEW_BACKGROUND_TAB);
}
content::WebContents* InsertForegroundTab(Browser* browser,
const std::string& url) {
return NavigateAndWait(browser, url,
WindowOpenDisposition::NEW_FOREGROUND_TAB);
}
webapps::AppId InstallWebApp(
const std::string& start_url,
web_app::mojom::UserDisplayMode user_display_mode) {
auto info = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
GURL(start_url));
info->user_display_mode = user_display_mode;
Profile* profile = ProfileManager::GetPrimaryUserProfile();
auto app_id = web_app::test::InstallWebApp(profile, std::move(info));
return app_id;
}
webapps::AppId InstallWebAppOpeningAsTab(const std::string& start_url) {
return InstallWebApp(start_url, web_app::mojom::UserDisplayMode::kBrowser);
}
webapps::AppId InstallWebAppOpeningAsWindow(const std::string& start_url) {
return InstallWebApp(start_url,
web_app::mojom::UserDisplayMode::kStandalone);
}
void UninstallWebApp(const webapps::AppId& app_id) {
Profile* profile = ProfileManager::GetPrimaryUserProfile();
web_app::test::UninstallWebApp(profile, app_id);
}
uint64_t GetId(content::WebContents* contents) {
const auto* instance = tracker_->GetAppInstance(contents);
return instance ? ToUint64(instance->id) : 0;
}
uint64_t GetId(Browser* browser) {
if (const auto* instance = tracker_->GetAppInstance(browser)) {
return ToUint64(instance->id);
}
const auto* instance = tracker_->GetBrowserWindowInstance(browser);
return instance ? ToUint64(instance->id) : 0;
}
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
Profile* profile = ProfileManager::GetPrimaryUserProfile();
tracker_ = std::make_unique<Tracker>(
profile, apps::AppServiceProxyFactory::GetForProfile(profile)
->AppRegistryCache());
ASSERT_EQ(kAppId_A, InstallWebAppOpeningAsTab("https://a.example.org"));
ASSERT_EQ(kAppId_B, InstallWebAppOpeningAsTab("https://b.example.org"));
}
void TearDownOnMainThread() override {
InProcessBrowserTest::TearDownOnMainThread();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
InProcessBrowserTest ::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kNoStartupWindow);
}
protected:
std::unique_ptr<Tracker> tracker_;
const base::ProcessId pid_ = base::Process::Current().Pid();
};
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, InsertAndCloseTabs) {
Browser* browser = nullptr;
aura::Window* window = nullptr;
content::WebContents* tab_app1 = nullptr;
content::WebContents* tab_app2 = nullptr;
// Open a foreground tab with a website.
{
SCOPED_TRACE("insert an initial foreground tab");
Recorder recorder(*tracker_);
browser = CreateBrowser();
window = browser->window()->GetNativeWindow();
tab_app1 = InsertForegroundTab(browser, "https://a.example.org");
EXPECT_EQ(GetId(browser), 1u);
EXPECT_EQ(GetId(tab_app1), 2u);
recorder.Verify({
{"added", 1, kChromeWindow, "", window, "", false},
{"added", 2, kAppTab, kAppId_A, window, "", kActive},
{"updated", 2, kAppTab, kAppId_A, window, kTitle_A, kActive},
});
}
// Open a second tab in foreground.
{
SCOPED_TRACE("insert a second foreground tab");
Recorder recorder(*tracker_);
tab_app2 = InsertForegroundTab(browser, "https://b.example.org");
EXPECT_EQ(GetId(tab_app2), 3u);
recorder.Verify({
{"updated", 2, kAppTab, kAppId_A, window, kTitle_A, kInactive},
{"added", 3, kAppTab, kAppId_B, window, "", kActive},
{"updated", 3, kAppTab, kAppId_B, window, kTitle_B, kActive},
});
}
// Open a third tab in foreground with no app.
{
SCOPED_TRACE("insert a third foreground tab without app");
Recorder recorder(*tracker_);
InsertForegroundTab(browser, "https://c.example.org");
recorder.Verify({
{"updated", 3, kAppTab, kAppId_B, window, kTitle_B, kInactive},
});
}
// Open two more tabs in foreground and close them.
{
SCOPED_TRACE("insert and close two more tabs");
Recorder recorder(*tracker_);
auto* tab_app3 = InsertForegroundTab(browser, "https://a.example.org");
EXPECT_EQ(GetId(tab_app3), 4u);
auto* tab_app4 = InsertForegroundTab(browser, "https://b.example.org");
EXPECT_EQ(GetId(tab_app4), 5u);
// Close in reverse order.
int i = browser->tab_strip_model()->GetIndexOfWebContents(tab_app4);
browser->tab_strip_model()->CloseWebContentsAt(
i, TabCloseTypes::CLOSE_USER_GESTURE);
i = browser->tab_strip_model()->GetIndexOfWebContents(tab_app3);
browser->tab_strip_model()->CloseWebContentsAt(
i, TabCloseTypes::CLOSE_USER_GESTURE);
recorder.Verify({
// tab 4 opened: no events for tab 3 as it has no app
{"added", 4, kAppTab, kAppId_A, window, "", kActive},
{"updated", 4, kAppTab, kAppId_A, window, kTitle_A, kActive},
// tab 5 opened: tab 4 deactivates
{"updated", 4, kAppTab, kAppId_A, window, kTitle_A, kInactive},
{"added", 5, kAppTab, kAppId_B, window, "", kActive},
{"updated", 5, kAppTab, kAppId_B, window, kTitle_B, kActive},
// tab 5 closed: tab 4 reactivates
{"removed", 5, kAppTab, kAppId_B, window, kTitle_B, kActive},
{"updated", 4, kAppTab, kAppId_A, window, kTitle_A, kActive},
// tab closed: no events for tab 3 as it has no app
{"removed", 4, kAppTab, kAppId_A, window, kTitle_A, kActive},
});
}
// Close the browser.
{
SCOPED_TRACE("close browser");
Recorder recorder(*tracker_);
browser->tab_strip_model()->CloseAllTabs();
recorder.Verify({
{"removed", 3, kAppTab, kAppId_B, window, kTitle_B, kInactive},
{"removed", 2, kAppTab, kAppId_A, window, kTitle_A, kInactive},
{"removed", 1, kChromeWindow, "", window, "", false},
});
}
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, PopupBrowserWindow) {
Browser* browser = nullptr;
aura::Window* window = nullptr;
{
SCOPED_TRACE("open popup browser window");
Recorder recorder(*tracker_);
browser = CreatePopupBrowser();
window = browser->window()->GetNativeWindow();
InsertForegroundTab(browser, "https://c.example.org");
recorder.Verify({
{"added", 1, kChromeWindow, "", window, "", false},
});
}
{
SCOPED_TRACE("close popup browser window");
Recorder recorder(*tracker_);
browser->tab_strip_model()->CloseAllTabs();
recorder.Verify({
{"removed", 1, kChromeWindow, "", window, "", false},
});
}
{
// Happens when an app running in a browser tab opens a separate popup
// window: it's not of type Browser::TYPE_APP_POPUP, but the window contains
// an instance of this app.
SCOPED_TRACE("open popup browser window with app tab");
Recorder recorder(*tracker_);
browser = CreatePopupBrowser();
window = browser->window()->GetNativeWindow();
InsertForegroundTab(browser, "https://a.example.org");
recorder.Verify({
{"added", 2, kChromeWindow, "", window, "", false},
{"added", 3, kAppTab, kAppId_A, window, "", kActive},
{"updated", 3, kAppTab, kAppId_A, window, kTitle_A, kActive},
});
}
{
SCOPED_TRACE("close popup browser window with app tab");
Recorder recorder(*tracker_);
browser->tab_strip_model()->CloseAllTabs();
recorder.Verify({
{"removed", 3, kAppTab, kAppId_A, window, kTitle_A, kActive},
{"removed", 2, kChromeWindow, "", window, "", false},
});
}
}
// Broken on ChromeOS <https://crbug.com/1493240>
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_DevtoolsWindow DISABLED_DevtoolsWindow
#else
#define MAYBE_DevtoolsWindow DevtoolsWindow
#endif
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, MAYBE_DevtoolsWindow) {
Browser* browser = CreateBrowser();
InsertForegroundTab(browser, "https://c.example.org");
aura::Window* window1 = browser->window()->GetNativeWindow();
{
SCOPED_TRACE("docked dev tools window");
Recorder recorder(*tracker_);
DevToolsWindow* dev_tools_window =
DevToolsWindowTesting::OpenDevToolsWindowSync(browser,
/*is_docked=*/true);
DevToolsWindowTesting::CloseDevToolsWindowSync(dev_tools_window);
recorder.Verify({});
}
{
SCOPED_TRACE("undocked dev tools window");
Recorder recorder(*tracker_);
DevToolsWindow* dev_tools_window =
DevToolsWindowTesting::OpenDevToolsWindowSync(browser,
/*is_docked=*/false);
aura::Window* window2 = DevToolsWindowTesting::Get(dev_tools_window)
->browser()
->window()
->GetNativeWindow();
DevToolsWindowTesting::CloseDevToolsWindowSync(dev_tools_window);
recorder.Verify({
// dev tools window opened
{"added", 2, kChromeWindow, "", window2, "", false},
{"updated", 1, kChromeWindow, "", window1, "", false},
{"updated", 2, kChromeWindow, "", window2, "", false},
// dev tools window closed
{"updated", 2, kChromeWindow, "", window2, "", false},
{"updated", 1, kChromeWindow, "", window1, "", false},
{"removed", 2, kChromeWindow, "", window2, "", false},
});
}
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, ForegroundTabNavigate) {
// Setup: one foreground tab with no app.
auto* browser = CreateBrowser();
auto* tab = InsertForegroundTab(browser, "https://c.example.org");
auto* window = browser->window()->GetNativeWindow();
EXPECT_EQ(GetId(browser), 1u);
EXPECT_EQ(GetId(tab), 0u);
// Navigate the foreground tab to app A.
{
SCOPED_TRACE("navigate tab to app A");
Recorder recorder(*tracker_);
NavigateActiveTab(browser, "https://a.example.org");
EXPECT_EQ(GetId(tab), 2u);
recorder.Verify({
{"added", 2, kAppTab, kAppId_A, window, kTitle_A, kActive},
});
}
// Navigate the foreground tab to app B.
{
SCOPED_TRACE("navigate tab to app B");
Recorder recorder(*tracker_);
NavigateActiveTab(browser, "https://b.example.org");
EXPECT_EQ(GetId(tab), 3u);
recorder.Verify({
{"removed", 2, kAppTab, kAppId_A, window, kTitle_A, kActive},
{"added", 3, kAppTab, kAppId_B, window, kTitle_B, kActive},
});
}
// Navigate the foreground tab to a different subdomain with no app.
{
SCOPED_TRACE("navigate tab from app B to a non-app subdomain");
Recorder recorder(*tracker_);
NavigateActiveTab(browser, "https://c.example.org");
EXPECT_EQ(GetId(tab), 0u);
recorder.Verify({
{"removed", 3, kAppTab, kAppId_B, window, kTitle_B, kActive},
});
}
// Navigate the foreground tab from a non-app subdomain to app B.
{
SCOPED_TRACE("navigate tab from a non-app subdomain to app B");
Recorder recorder(*tracker_);
NavigateActiveTab(browser, "https://b.example.org");
EXPECT_EQ(GetId(tab), 4u);
recorder.Verify({
{"added", 4, kAppTab, kAppId_B, window, kTitle_B, kActive},
});
}
// Navigate the foreground tab to a different domain with no app.
{
SCOPED_TRACE("navigate tab from app B to a non-app domain");
Recorder recorder(*tracker_);
NavigateActiveTab(browser, "https://example.com");
EXPECT_EQ(GetId(tab), 0u);
recorder.Verify({
{"removed", 4, kAppTab, kAppId_B, window, kTitle_B, kActive},
});
}
// Navigate the foreground tab from a non-app domain to app B.
{
SCOPED_TRACE("navigate tab from a non-app domain to app B");
Recorder recorder(*tracker_);
NavigateActiveTab(browser, "https://b.example.org");
EXPECT_EQ(GetId(tab), 5u);
recorder.Verify({
{"added", 5, kAppTab, kAppId_B, window, kTitle_B, kActive},
});
}
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, WindowedWebApp) {
std::string app_id = InstallWebAppOpeningAsWindow("https://d.example.org");
Browser* browser = nullptr;
content::WebContents* tab = nullptr;
aura::Window* window = nullptr;
// Open app D in a window (configured to open in a window).
{
SCOPED_TRACE("create a windowed app in a window");
Recorder recorder(*tracker_);
browser = CreateAppBrowser(app_id);
tab = InsertForegroundTab(browser, "https://d.example.org");
EXPECT_EQ(GetId(browser), 1u);
EXPECT_EQ(GetId(tab), 1u);
window = browser->window()->GetNativeWindow();
recorder.Verify({
{"added", 1, kAppWindow, app_id, window, "", kActive},
{"updated", 1, kAppWindow, app_id, window, "d.example.org", kActive},
});
}
// Close the browser.
{
SCOPED_TRACE("close browser");
Recorder recorder(*tracker_);
browser->tab_strip_model()->CloseAllTabs();
recorder.Verify({
{"removed", 1, kAppWindow, app_id, window, "d.example.org", kActive},
});
}
// Open app A in a window (configured to open in a tab).
{
SCOPED_TRACE("create a tabbed app in a window");
Recorder recorder(*tracker_);
browser = CreateAppBrowser(kAppId_A);
tab = InsertForegroundTab(browser, "https://a.example.org");
EXPECT_EQ(GetId(browser), 2u);
EXPECT_EQ(GetId(tab), 2u);
window = browser->window()->GetNativeWindow();
// When open in a window it's still an app, even if configured to open in a
// tab.
recorder.Verify({
{"added", 2, kAppWindow, kAppId_A, window, "", kActive},
{"updated", 2, kAppWindow, kAppId_A, window, kTitle_A, kActive},
});
}
// Close the browser.
{
SCOPED_TRACE("close browser");
Recorder recorder(*tracker_);
browser->tab_strip_model()->CloseAllTabs();
recorder.Verify({
{"removed", 2, kAppWindow, kAppId_A, window, kTitle_A, kActive},
});
}
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, TabbedSystemWebApp) {
// Make sure we can use crosh.
Profile* profile = ProfileManager::GetPrimaryUserProfile();
DCHECK(ash::SystemWebAppManager::Get(profile));
ash::SystemWebAppManager::Get(profile)->InstallSystemAppsForTesting();
Browser* browser = nullptr;
aura::Window* window = nullptr;
{
SCOPED_TRACE("create an app window");
Recorder recorder(*tracker_);
// Open an app window (crosh) and insert a tab.
browser = CreateAppBrowser(web_app::kCroshAppId);
chrome::NewTab(browser);
content::WebContents* tab = browser->tab_strip_model()->GetWebContentsAt(0);
NavigateActiveTab(browser, chrome::kChromeUIUntrustedCroshURL);
tab->UpdateTitleForEntry(tab->GetController().GetLastCommittedEntry(),
u"crosh1");
// A window is added, both the window and the tab map to the same app
// instance.
EXPECT_EQ(GetId(browser), 1u);
EXPECT_EQ(GetId(tab), 1u);
window = browser->window()->GetNativeWindow();
recorder.Verify({
{"added", 1, kAppWindow, web_app::kCroshAppId, window, "", kActive},
{"updated", 1, kAppWindow, web_app::kCroshAppId, window, "crosh1",
kActive},
});
}
{
SCOPED_TRACE("add a second tab");
Recorder recorder(*tracker_);
// Add a second WebContents to the same app window.
chrome::NewTab(browser);
content::WebContents* tab = browser->tab_strip_model()->GetWebContentsAt(1);
NavigateActiveTab(browser, chrome::kChromeUIUntrustedCroshURL);
tab->UpdateTitleForEntry(tab->GetController().GetLastCommittedEntry(),
u"crosh2");
// Only title of the existing app instance should be updated.
EXPECT_EQ(GetId(tab), 1u);
recorder.Verify({
{"updated", 1, kAppWindow, web_app::kCroshAppId, window, "", kActive},
{"updated", 1, kAppWindow, web_app::kCroshAppId, window, "crosh2",
kActive},
});
}
{
SCOPED_TRACE("close browser");
Recorder recorder(*tracker_);
browser->tab_strip_model()->CloseAllTabs();
// The app instance disappars with the window.
recorder.Verify({
{"removed", 1, kAppWindow, web_app::kCroshAppId, window, "crosh2",
kActive},
});
}
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, SwitchTabs) {
// Setup: one foreground tab and one background tab.
auto* browser = CreateBrowser();
auto* window = browser->window()->GetNativeWindow();
auto* tab0 = InsertForegroundTab(browser, "https://a.example.org");
EXPECT_EQ(GetId(browser), 1u);
EXPECT_EQ(GetId(tab0), 2u);
auto* tab1 = InsertForegroundTab(browser, "https://b.example.org");
EXPECT_EQ(GetId(tab1), 3u);
InsertForegroundTab(browser, "https://c.example.org");
// Switch tabs: no app -> app A
{
SCOPED_TRACE("switch tabs to app A");
Recorder recorder(*tracker_);
browser->tab_strip_model()->ActivateTabAt(0);
recorder.Verify({
{"updated", 2, kAppTab, kAppId_A, window, kTitle_A, kActive},
});
}
// Switch tabs: app A -> app B
{
SCOPED_TRACE("switch tabs to app B");
Recorder recorder(*tracker_);
browser->tab_strip_model()->ActivateTabAt(1);
recorder.Verify({
{"updated", 3, kAppTab, kAppId_B, window, kTitle_B, kActive},
{"updated", 2, kAppTab, kAppId_A, window, kTitle_A, kInactive},
});
}
// Switch tabs: app B -> no app
{
SCOPED_TRACE("switch tabs to no app");
Recorder recorder(*tracker_);
browser->tab_strip_model()->ActivateTabAt(2);
recorder.Verify({
{"updated", 3, kAppTab, kAppId_B, window, kTitle_B, kInactive},
});
}
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, TabDrag) {
// Setup: two browsers: one with two, another with three tabs.
auto* browser1 = CreateBrowser();
auto* window1 = browser1->window()->GetNativeWindow();
auto* b1_tab1 = InsertForegroundTab(browser1, "https://a.example.org");
auto* b1_tab2 = InsertForegroundTab(browser1, "https://b.example.org");
EXPECT_EQ(GetId(browser1), 1u);
EXPECT_EQ(GetId(b1_tab1), 2u);
EXPECT_EQ(GetId(b1_tab2), 3u);
auto* browser2 = CreateBrowser();
auto* window2 = browser2->window()->GetNativeWindow();
auto* b2_tab1 = InsertForegroundTab(browser2, "https://a.example.org");
EXPECT_EQ(GetId(browser2), 4u);
EXPECT_EQ(GetId(b2_tab1), 5u);
auto* b2_tab2 = InsertForegroundTab(browser2, "https://a.example.org");
EXPECT_EQ(GetId(b2_tab2), 6u);
auto* b2_tab3 = InsertForegroundTab(browser2, "https://b.example.org");
EXPECT_EQ(GetId(b2_tab3), 7u);
ASSERT_FALSE(browser1->window()->IsActive());
ASSERT_TRUE(browser2->window()->IsActive());
// Drag the active tab of browser 2 and rop it into the last position in
// browser 1.
SCOPED_TRACE("tab drag and drop");
Recorder recorder(*tracker_);
// We skip a step where a detached tab gets inserted into a temporary browser
// but the sequence there is identical.
// Detach.
int src_index = browser2->tab_strip_model()->GetIndexOfWebContents(b2_tab3);
std::unique_ptr<tabs::TabModel> detached_tab =
browser2->tab_strip_model()->DetachTabAtForInsertion(src_index);
// Target browser window goes into foreground right before drop.
browser1->window()->Activate();
// Attach.
int dst_index = browser1->tab_strip_model()->count();
browser1->tab_strip_model()->InsertDetachedTabAt(
dst_index, std::move(detached_tab), AddTabTypes::ADD_ACTIVE);
recorder.Verify({
// background tab in the dragged-from browser gets activated when the
// active tab is detached
{"updated", 6, kAppTab, kAppId_A, window2, kTitle_A, kActive},
// previously foreground tab in the dragged-into browser goes into
// background when the dragged tab is attached to the new browser
{"updated", 3, kAppTab, kAppId_B, window1, kTitle_B, kInactive},
// dragged tab gets reparented and becomes active in the new browser
{"updated", 7, kAppTab, kAppId_B, window1, kTitle_B, kActive},
});
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, MoveTabToAppWindow) {
// Setup: a browser with two tabs. One tab opens a website that matches the
// app configured to open in a window.
std::string app_id = InstallWebAppOpeningAsWindow("https://d.example.org");
auto* browser1 = CreateBrowser();
InsertForegroundTab(browser1, "https://c.example.org");
auto* tab = InsertForegroundTab(browser1, "https://d.example.org");
EXPECT_EQ(GetId(browser1), 1u);
EXPECT_EQ(GetId(tab), 0u);
ASSERT_TRUE(browser1->window()->IsActive());
// Move the tab from the browser to the newly created app browser. This
// simulates "open in window".
SCOPED_TRACE("open in window");
Recorder recorder(*tracker_);
auto* browser2 = CreateAppBrowser(app_id);
EXPECT_EQ(GetId(browser2), 0u);
auto* window2 = browser2->window()->GetNativeWindow();
// Target app browser goes into foreground.
browser2->window()->Activate();
// Detach.
int src_index = browser1->tab_strip_model()->GetIndexOfWebContents(tab);
std::unique_ptr<tabs::TabModel> detached_tab =
browser1->tab_strip_model()->DetachTabAtForInsertion(src_index);
// Attach.
int dst_index = browser2->tab_strip_model()->count();
browser2->tab_strip_model()->InsertDetachedTabAt(
dst_index, std::move(detached_tab), AddTabTypes::ADD_ACTIVE);
recorder.Verify({
// moved tab gets reparented and becomes an app in the new browser
{"added", 2, kAppWindow, app_id, window2, "d.example.org", kActive},
});
}
// TODO(crbug.com/40772830): test tab replace (portals)
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, Accessors) {
// Setup: two regular browsers, and one app window browser.
auto* browser1 = CreateBrowser();
auto* window1 = browser1->window()->GetNativeWindow();
auto* b1_tab1 = InsertForegroundTab(browser1, "https://a.example.org");
EXPECT_EQ(GetId(browser1), 1u);
EXPECT_EQ(GetId(b1_tab1), 2u);
auto* b1_tab2 = InsertForegroundTab(browser1, "https://c.example.org");
auto* b1_tab3 = InsertForegroundTab(browser1, "https://b.example.org");
EXPECT_EQ(GetId(b1_tab3), 3u);
auto* browser2 = CreateBrowser();
auto* window2 = browser2->window()->GetNativeWindow();
auto* b2_tab1 = InsertForegroundTab(browser2, "https://c.example.org");
auto* b2_tab2 = InsertForegroundTab(browser2, "https://b.example.org");
EXPECT_EQ(GetId(browser2), 4u);
EXPECT_EQ(GetId(b2_tab2), 5u);
auto* browser3 = CreateAppBrowser(kAppId_B);
auto* window3 = browser3->window()->GetNativeWindow();
auto* b3_tab1 = InsertForegroundTab(browser3, "https://b.example.org");
EXPECT_EQ(GetId(b3_tab1), 6u);
ASSERT_FALSE(browser1->window()->IsActive());
ASSERT_FALSE(browser2->window()->IsActive());
ASSERT_TRUE(browser3->window()->IsActive());
auto* b1_app = tracker_->GetBrowserWindowInstance(browser1);
auto* b1_tab1_app = tracker_->GetAppInstance(b1_tab1);
auto* b1_tab2_app = tracker_->GetAppInstance(b1_tab2);
auto* b1_tab3_app = tracker_->GetAppInstance(b1_tab3);
auto* b2_app = tracker_->GetBrowserWindowInstance(browser2);
auto* b2_tab1_app = tracker_->GetAppInstance(b2_tab1);
auto* b2_tab2_app = tracker_->GetAppInstance(b2_tab2);
auto* b3_app = tracker_->GetAppInstance(browser3);
auto* b3_tab1_app = tracker_->GetAppInstance(b3_tab1);
EXPECT_EQ(
TestInstance::Create(b1_app),
(TestInstance{"snapshot", 1, kChromeWindow, "", window1, "", false}));
EXPECT_EQ(TestInstance::Create(b1_tab1_app),
(TestInstance{"snapshot", 2, kAppTab, kAppId_A, window1, kTitle_A,
kInactive}));
EXPECT_EQ(TestInstance::Create(b1_tab2_app), TestInstance{});
EXPECT_EQ(TestInstance::Create(b1_tab3_app),
(TestInstance{"snapshot", 3, kAppTab, kAppId_B, window1, kTitle_B,
kActive}));
EXPECT_EQ(
TestInstance::Create(b2_app),
(TestInstance{"snapshot", 4, kChromeWindow, "", window2, "", false}));
EXPECT_EQ(TestInstance::Create(b2_tab1_app), TestInstance{});
EXPECT_EQ(TestInstance::Create(b2_tab2_app),
(TestInstance{"snapshot", 5, kAppTab, kAppId_B, window2, kTitle_B,
kActive}));
// browser3 does not map to any browser window instance, but it maps to the
// same app instance as the tab.
EXPECT_EQ(TestInstance::Create(tracker_->GetBrowserWindowInstance(browser3)),
TestInstance{});
EXPECT_EQ(b3_app, b3_tab1_app);
EXPECT_EQ(TestInstance::Create(b3_tab1_app),
(TestInstance{"snapshot", 6, kAppWindow, kAppId_B, window3,
kTitle_B, kActive}));
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, AppInstall) {
auto* browser1 = CreateBrowser();
auto* window1 = browser1->window()->GetNativeWindow();
auto* tab1 = InsertForegroundTab(browser1, "https://c.example.org");
InsertForegroundTab(browser1, "https://d.example.org");
auto* tab3 = InsertForegroundTab(browser1, "https://c.example.org");
std::string app_id;
std::string title = "c.example.org";
{
SCOPED_TRACE("install app opening in a tab");
Recorder recorder(*tracker_);
EXPECT_EQ(GetId(tab1), 0u);
EXPECT_EQ(GetId(tab3), 0u);
app_id = InstallWebAppOpeningAsTab("https://c.example.org");
EXPECT_EQ(GetId(tab1), 2u);
EXPECT_EQ(GetId(tab3), 3u);
recorder.Verify({
{"added", 2, kAppTab, app_id, window1, title, kInactive},
{"added", 3, kAppTab, app_id, window1, title, kActive},
});
}
{
SCOPED_TRACE("uninstall app");
Recorder recorder(*tracker_);
UninstallWebApp(app_id);
EXPECT_EQ(GetId(tab1), 0u);
EXPECT_EQ(GetId(tab3), 0u);
recorder.Verify({
{"removed", 2, kAppTab, app_id, window1, title, kInactive},
{"removed", 3, kAppTab, app_id, window1, title, kActive},
});
}
{
SCOPED_TRACE("install app opening in a window");
Recorder recorder(*tracker_);
EXPECT_EQ(GetId(tab1), 0u);
EXPECT_EQ(GetId(tab3), 0u);
app_id = InstallWebAppOpeningAsWindow("https://c.example.org");
// This has no effect: apps configured to open in a window aren't counted as
// apps when opened in a tab.
EXPECT_EQ(GetId(tab1), 0u);
EXPECT_EQ(GetId(tab3), 0u);
recorder.Verify({});
}
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, ActivateTabInstance) {
// Setup: a browser with 2 tabs (app A and a non-app tab).
Browser* browser = nullptr;
content::WebContents* web_contents_a;
content::WebContents* web_contents_c;
// Open app A in a tab.
browser = CreateBrowser();
web_contents_a = InsertForegroundTab(browser, "https://a.example.org");
// Open a second tab with no app.
web_contents_c = InsertForegroundTab(browser, "https://c.example.org");
EXPECT_EQ(browser->tab_strip_model()->GetActiveWebContents(), web_contents_c);
tracker_->ActivateTabInstance(tracker_->GetAppInstance(web_contents_a)->id);
EXPECT_EQ(browser->tab_strip_model()->GetActiveWebContents(), web_contents_a);
}
IN_PROC_BROWSER_TEST_F(BrowserAppInstanceTrackerTest, StopInstancesOfApp) {
// Setup: a browser with 4 tabs and an app window for app D. The browser
// contains 2 tabs of app A, one tab of app B and one tab not associated with
// an app.
Browser* browser1 = nullptr;
aura::Window* window1 = nullptr;
Browser* browser2 = nullptr;
aura::Window* window2 = nullptr;
// Open two tabbed instances of app A and 1 instance of app B.
browser1 = CreateBrowser();
window1 = browser1->window()->GetNativeWindow();
content::WebContents* tab;
tab = InsertForegroundTab(browser1, "https://a.example.org");
EXPECT_EQ(GetId(tab), 2u);
tab = InsertForegroundTab(browser1, "https://a.example.org");
EXPECT_EQ(GetId(tab), 3u);
tab = InsertForegroundTab(browser1, "https://b.example.org");
EXPECT_EQ(GetId(tab), 4u);
// Open a fourth tab with no app.
InsertForegroundTab(browser1, "https://c.example.org");
// Open a windowed instance of app D.
std::string app_d_id = InstallWebAppOpeningAsWindow("https://d.example.org");
browser2 = CreateAppBrowser(app_d_id);
window2 = browser2->window()->GetNativeWindow();
tab = InsertForegroundTab(browser2, "https://d.example.org");
EXPECT_EQ(GetId(tab), 5u);
// Stop app A.
{
SCOPED_TRACE("close app A");
Recorder recorder(*tracker_);
tracker_->StopInstancesOfApp(kAppId_A);
recorder.VerifyIgnoreOrder({
{"removed", 3, kAppTab, kAppId_A, window1, kTitle_A, kInactive},
{"removed", 2, kAppTab, kAppId_A, window1, kTitle_A, kInactive},
});
}
// Stop app D.
{
SCOPED_TRACE("close app D");
Recorder recorder(*tracker_);
tracker_->StopInstancesOfApp(app_d_id);
recorder.Verify({
{"removed", 5, kAppWindow, app_d_id, window2, "d.example.org", kActive},
});
}
// Stop app B.
{
SCOPED_TRACE("close app B");
Recorder recorder(*tracker_);
tracker_->StopInstancesOfApp(kAppId_B);
recorder.Verify({
{"removed", 4, kAppTab, kAppId_B, window1, kTitle_B, kInactive},
});
}
}