blob: 2c6713d3f2f8c09bdd525db9435151d7436d59b8 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/devtools/chrome_devtools_manager_delegate.h"
#include <memory>
#include <utility>
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/infobars/infobar_features.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/browser_window/test/mock_browser_window_interface.h"
#include "chrome/browser/ui/startup/automation_infobar_delegate.h"
#include "chrome/browser/ui/tabs/tab_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/test/base/testing_profile.h"
#include "components/enterprise/buildflags/buildflags.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(ENTERPRISE_CDP_ATTRIBUTION)
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "chrome/browser/enterprise/connectors/reporting/cdp_actor_session_tracker.h"
#include "chrome/browser/policy/dm_token_utils.h"
#include "components/enterprise/common/proto/synced/browser_events.pb.h"
#include "components/enterprise/connectors/core/features.h"
#include "components/policy/core/common/cloud/dm_token.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/devtools_agent_host_client.h"
#include "content/public/browser/devtools_agent_host_client_channel.h"
#include "third_party/inspector_protocol/crdtp/json.h"
#include "third_party/inspector_protocol/crdtp/span.h"
#endif
namespace {
class ChromeDevToolsManagerDelegateTest : public testing::Test {
protected:
void SetUp() override {
feature_list_.InitAndDisableFeature(infobars::kCentralizedInfoBarFramework);
profile_ = std::make_unique<TestingProfile>();
tab_strip_ =
std::make_unique<TabStripModel>(&tab_strip_delegate_, profile_.get());
tab_strip_delegate_.SetBrowserWindowInterface(&window_);
ON_CALL(window_, GetTabStripModel())
.WillByDefault(testing::Return(tab_strip_.get()));
ON_CALL(std::as_const(window_), GetTabStripModel())
.WillByDefault(testing::Return(tab_strip_.get()));
ON_CALL(window_, GetProfile())
.WillByDefault(testing::Return(profile_.get()));
ON_CALL(std::as_const(window_), GetProfile())
.WillByDefault(testing::Return(profile_.get()));
collection_observer()->OnBrowserCreated(&window_);
ASSERT_FALSE(ChromeDevToolsManagerDelegate::GetInstance());
delegate_ = std::make_unique<ChromeDevToolsManagerDelegate>();
}
void TearDown() override {
delegate_.reset();
tab_strip_->CloseAllTabs();
collection_observer()->OnBrowserClosed(&window_);
tab_strip_delegate_.SetBrowserWindowInterface(nullptr);
tab_strip_.reset();
profile_.reset();
}
infobars::ContentInfoBarManager* AddTab(
bool with_automation_infobar = false) {
auto contents = content::WebContentsTester::CreateTestWebContents(
profile_.get(), nullptr);
infobars::ContentInfoBarManager::CreateForWebContents(contents.get());
auto* manager =
infobars::ContentInfoBarManager::FromWebContents(contents.get());
if (with_automation_infobar) {
// The automation and remote debugging infobars have the same message,
// so the manager rejects the latter as a duplicate.
EXPECT_TRUE(AutomationInfoBarDelegate::Create(manager));
}
tab_strip_->AppendWebContents(std::move(contents), /*foreground=*/true);
return manager;
}
void SetConnections(size_t count) {
static_cast<content::DevToolsManagerDelegate*>(delegate_.get())
->SetActiveWebSocketConnections(count);
}
void ExpectReconnect(infobars::ContentInfoBarManager* manager) {
manager->RemoveAllInfoBars(/*animate=*/false);
SetConnections(1);
ASSERT_EQ(1u, manager->infobars().size());
EXPECT_EQ(
infobars::InfoBarDelegate::DEV_TOOLS_REMOTE_DEBUGGING_INFOBAR_DELEGATE,
manager->infobars()[0]->delegate()->GetIdentifier());
SetConnections(0);
EXPECT_TRUE(manager->infobars().empty());
}
TabStripModel* tab_strip() { return tab_strip_.get(); }
std::unique_ptr<ChromeDevToolsManagerDelegate> delegate_;
private:
BrowserCollectionObserver* collection_observer() {
return GlobalBrowserCollection::GetInstance()->GetPlatformDelegate();
}
content::BrowserTaskEnvironment task_environment_;
ChromeLayoutProvider layout_provider_;
content::RenderViewHostTestEnabler rvh_test_enabler_;
tabs::TabModel::PreventFeatureInitializationForTesting prevent_tab_features_;
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<TestingProfile> profile_;
TestTabStripModelDelegate tab_strip_delegate_;
std::unique_ptr<TabStripModel> tab_strip_;
testing::NiceMock<MockBrowserWindowInterface> window_;
};
TEST_F(ChromeDevToolsManagerDelegateTest, DisconnectAfterInfoBarCreationFails) {
auto* manager = AddTab(/*with_automation_infobar=*/true);
SetConnections(1);
// Let the global infobar delete itself after AddInfoBar() rejects it.
base::RunLoop().RunUntilIdle();
SetConnections(0);
ASSERT_EQ(1u, manager->infobars().size());
EXPECT_EQ(infobars::InfoBarDelegate::AUTOMATION_INFOBAR_DELEGATE,
manager->infobars()[0]->delegate()->GetIdentifier());
ExpectReconnect(manager);
}
TEST_F(ChromeDevToolsManagerDelegateTest, DisconnectAfterInfoBarFailsInNewTab) {
auto* first_manager = AddTab();
SetConnections(1);
ASSERT_EQ(1u, first_manager->infobars().size());
auto* second_manager = AddTab(/*with_automation_infobar=*/true);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(first_manager->infobars().empty());
SetConnections(0);
ASSERT_EQ(1u, second_manager->infobars().size());
ExpectReconnect(second_manager);
EXPECT_TRUE(first_manager->infobars().empty());
}
TEST_F(ChromeDevToolsManagerDelegateTest,
DestroyDelegateAfterInfoBarCreationFails) {
auto* manager = AddTab(/*with_automation_infobar=*/true);
SetConnections(1);
base::RunLoop().RunUntilIdle();
delegate_.reset();
ASSERT_EQ(1u, manager->infobars().size());
EXPECT_EQ(infobars::InfoBarDelegate::AUTOMATION_INFOBAR_DELEGATE,
manager->infobars()[0]->delegate()->GetIdentifier());
}
#if BUILDFLAG(ENTERPRISE_CDP_ATTRIBUTION)
class FakeExternalDevToolsClient : public content::DevToolsAgentHostClient {
public:
explicit FakeExternalDevToolsClient(
content::DevToolsAgentHostClient::PeerMetadata metadata)
: metadata_(std::move(metadata)) {}
~FakeExternalDevToolsClient() override = default;
void DispatchProtocolMessage(content::DevToolsAgentHost*,
base::span<const uint8_t>) override {}
void AgentHostClosed(content::DevToolsAgentHost*) override {}
bool IsTrusted() override { return false; }
std::string GetTypeForMetrics() override { return "RemoteDebugger"; }
PeerMetadata GetPeerMetadata() override { return metadata_; }
private:
PeerMetadata metadata_;
};
class FakeDevToolsClientChannel
: public content::DevToolsAgentHostClientChannel {
public:
FakeDevToolsClientChannel(content::DevToolsAgentHost* agent_host,
content::DevToolsAgentHostClient* client)
: agent_host_(agent_host), client_(client) {}
~FakeDevToolsClientChannel() override = default;
void DispatchProtocolMessageToClient(std::vector<uint8_t>) override {}
content::DevToolsAgentHost* GetAgentHost() override { return agent_host_; }
content::DevToolsAgentHostClient* GetClient() override { return client_; }
private:
raw_ptr<content::DevToolsAgentHost> agent_host_;
raw_ptr<content::DevToolsAgentHostClient> client_;
};
TEST_F(ChromeDevToolsManagerDelegateTest,
CdpActorSessionTrackerCreatedAndWiredWhenFeatureEnabled) {
// Destroy the default `delegate_` created in `SetUp()` first so its singleton
// `g_instance` is cleared before constructing the replacement instance.
delegate_.reset();
policy::SetDMTokenForTesting(
policy::DMToken::CreateValidToken("fake-dm-token"));
base::ScopedClosureRunner reset_dm_token(base::BindOnce([] {
policy::SetDMTokenForTesting(policy::DMToken::CreateEmptyToken());
}));
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
enterprise_connectors::kEventReportingCdpAttribution);
delegate_ = std::make_unique<ChromeDevToolsManagerDelegate>();
auto* manager_delegate =
static_cast<content::DevToolsManagerDelegate*>(delegate_.get());
AddTab();
content::WebContents* web_contents = tab_strip()->GetWebContentsAt(0);
scoped_refptr<content::DevToolsAgentHost> agent_host =
content::DevToolsAgentHost::GetOrCreateForTab(web_contents);
content::DevToolsAgentHostClient::PeerMetadata pipe_meta;
pipe_meta.transport_type =
content::DevToolsAgentHostClient::PeerMetadata::TransportType::kPipe;
FakeExternalDevToolsClient client(pipe_meta);
FakeDevToolsClientChannel channel(agent_host.get(), &client);
manager_delegate->ClientAttached(&channel);
EXPECT_FALSE(
enterprise_connectors::CdpActorSessionTracker::GetEventInitiatorInfo(
web_contents)
.has_value());
std::vector<uint8_t> cbor_command;
ASSERT_TRUE(
crdtp::json::ConvertJSONToCBOR(
crdtp::SpanFrom(R"({"id":1,"method":"Page.reload"})"), &cbor_command)
.ok());
manager_delegate->HandleCommand(&channel, cbor_command, base::DoNothing());
std::optional<chrome::cros::reporting::proto::EventInitiatorInfo> initiator =
enterprise_connectors::CdpActorSessionTracker::GetEventInitiatorInfo(
web_contents);
ASSERT_TRUE(initiator.has_value());
ASSERT_TRUE(initiator->has_external_process_info());
ASSERT_EQ(initiator->external_process_info().external_processes_size(), 1);
EXPECT_EQ(
initiator->external_process_info().external_processes(0).transport_type(),
chrome::cros::reporting::proto::ExternalProcessInfo::TRANSPORT_TYPE_PIPE);
manager_delegate->ClientDetached(&channel);
}
#endif // BUILDFLAG(ENTERPRISE_CDP_ATTRIBUTION)
} // namespace