blob: f845f572833aa436e85b3118ff0e010acc1fdd6f [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/devtools/devtools_frontend_host_impl.h"
#include <memory>
#include <string>
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "components/crash/content/browser/error_reporting/javascript_error_report.h" // nogncheck
#include "components/crash/content/browser/error_reporting/js_error_report_processor.h" // nogncheck
#include "content/public/browser/devtools_frontend_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_web_contents.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
namespace content {
namespace {
using testing::Eq;
using testing::Optional;
class FakeJsErrorReportProcessor : public JsErrorReportProcessor {
public:
explicit FakeJsErrorReportProcessor(
scoped_refptr<base::SingleThreadTaskRunner> task_runner)
: task_runner_(std::move(task_runner)) {}
// After calling this, expect all SendErrorReport's to use this
// BrowserContext.
void SetExpectedBrowserContext(BrowserContext* browser_context) {
browser_context_ = browser_context;
}
void SendErrorReport(JavaScriptErrorReport error_report,
base::OnceClosure completion_callback,
BrowserContext* browser_context) override {
CHECK_EQ(browser_context, browser_context_);
last_error_report_ = std::move(error_report);
++error_report_count_;
task_runner_->PostTask(FROM_HERE, std::move(completion_callback));
}
const JavaScriptErrorReport& last_error_report() const {
return last_error_report_;
}
int error_report_count() const { return error_report_count_; }
// Make public for testing.
using JsErrorReportProcessor::SetDefault;
protected:
~FakeJsErrorReportProcessor() override = default;
private:
scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
JavaScriptErrorReport last_error_report_;
int error_report_count_ = 0;
raw_ptr<BrowserContext> browser_context_ = nullptr;
};
} // namespace
class DevToolsFrontendHostImplTest : public RenderViewHostTestHarness {
public:
void SetUp() override {
RenderViewHostTestHarness::SetUp();
site_instance_ = SiteInstance::Create(browser_context());
SetContents(TestWebContents::Create(browser_context(), site_instance_));
// Since we just created the web_contents() pointer with
// TestWebContents::Create, the static_casts are safe.
devtools_frontend_host_impl_ = DevToolsFrontendHostImpl::CreateForTesting(
static_cast<TestWebContents*>(web_contents())->GetPrimaryMainFrame(),
base::RepeatingCallback<void(base::Value::Dict)>());
process()->Init();
previous_processor_ = JsErrorReportProcessor::Get();
processor_ = base::MakeRefCounted<FakeJsErrorReportProcessor>(
task_environment()->GetMainThreadTaskRunner());
FakeJsErrorReportProcessor::SetDefault(processor_);
processor_->SetExpectedBrowserContext(browser_context());
}
void TearDown() override {
FakeJsErrorReportProcessor::SetDefault(previous_processor_);
// We have to destroy the site_instance_ before
// RenderViewHostTestHarness::TearDown() destroys the
// BrowserTaskEnvironment. We've gotten a lot of things that depend directly
// or indirectly on BrowserTaskEnvironment, so just destroy everything we
// can.
previous_processor_.reset();
processor_.reset();
devtools_frontend_host_impl_.reset();
site_instance_.reset();
RenderViewHostTestHarness::TearDown();
}
// Calls the observer's OnDidAddMessageToConsole with the given arguments.
// This is just here so that we don't need to FRIEND_TEST_ALL_PREFIXES for
// each and every test.
void CallOnDidAddMessageToConsole(
RenderFrameHost* source_frame,
blink::mojom::ConsoleMessageLevel log_level,
const std::u16string& message,
int32_t line_no,
const std::u16string& source_id,
const std::optional<std::u16string>& stack_trace) {
devtools_frontend_host_impl_->OnDidAddMessageToConsole(
source_frame, log_level, message, line_no, source_id, stack_trace);
}
protected:
scoped_refptr<SiteInstance> site_instance_;
std::unique_ptr<DevToolsFrontendHostImpl> devtools_frontend_host_impl_;
scoped_refptr<FakeJsErrorReportProcessor> processor_;
scoped_refptr<JsErrorReportProcessor> previous_processor_;
base::test::ScopedFeatureList scoped_feature_list_;
static constexpr char kMessage8[] = "Uncaught exception";
static constexpr char16_t kMessage16[] = u"Uncaught exception";
static constexpr char kSourceURL8[] = "devtools://here.is.error/bad.js";
static constexpr char16_t kSourceURL16[] = u"devtools://here.is.error/bad.js";
static constexpr char kStackTrace8[] =
"at badFunction (devtools://page/my.js:20:30)\n"
"at poorCaller (devtools://page/my.js:50:10)\n";
static constexpr char16_t kStackTrace16[] =
u"at badFunction (devtools://page/my.js:20:30)\n"
u"at poorCaller (devtools://page/my.js:50:10)\n";
};
class DevToolsFrontendHostImplJsErrorReportingEnabledTest
: public DevToolsFrontendHostImplTest {
public:
void SetUp() override {
DevToolsFrontendHostImplTest::SetUp();
scoped_feature_list_.InitAndEnableFeature(
features::kEnableDevToolsJsErrorReporting);
}
};
constexpr char DevToolsFrontendHostImplTest::kMessage8[];
constexpr char16_t DevToolsFrontendHostImplTest::kMessage16[];
constexpr char DevToolsFrontendHostImplTest::kSourceURL8[];
constexpr char16_t DevToolsFrontendHostImplTest::kSourceURL16[];
constexpr char DevToolsFrontendHostImplTest::kStackTrace8[];
constexpr char16_t DevToolsFrontendHostImplTest::kStackTrace16[];
TEST_F(DevToolsFrontendHostImplTest, ErrorNotReportedWhenFeatureIsNotEnabled) {
CallOnDidAddMessageToConsole(main_rfh(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, kSourceURL16, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 0);
}
TEST_F(DevToolsFrontendHostImplJsErrorReportingEnabledTest, Sent) {
CallOnDidAddMessageToConsole(main_rfh(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, kSourceURL16, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 1);
EXPECT_EQ(processor_->last_error_report().message, kMessage8);
EXPECT_EQ(processor_->last_error_report().url, kSourceURL8);
EXPECT_EQ(processor_->last_error_report().source_system,
JavaScriptErrorReport::SourceSystem::kDevToolsObserver);
EXPECT_THAT(processor_->last_error_report().stack_trace,
Optional(Eq(kStackTrace8)));
// DevTools should use default product & version.
EXPECT_EQ(processor_->last_error_report().product, "");
EXPECT_EQ(processor_->last_error_report().version, "");
EXPECT_EQ(*processor_->last_error_report().line_number, 5);
EXPECT_FALSE(processor_->last_error_report().column_number);
EXPECT_FALSE(processor_->last_error_report().app_locale);
EXPECT_TRUE(processor_->last_error_report().send_to_production_servers);
}
TEST_F(DevToolsFrontendHostImplJsErrorReportingEnabledTest,
NotSentForNonErrors) {
CallOnDidAddMessageToConsole(main_rfh(),
blink::mojom::ConsoleMessageLevel::kWarning,
kMessage16, 5, kSourceURL16, kStackTrace16);
CallOnDidAddMessageToConsole(main_rfh(),
blink::mojom::ConsoleMessageLevel::kInfo,
kMessage16, 5, kSourceURL16, kStackTrace16);
CallOnDidAddMessageToConsole(main_rfh(),
blink::mojom::ConsoleMessageLevel::kVerbose,
kMessage16, 5, kSourceURL16, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 0);
}
TEST_F(DevToolsFrontendHostImplJsErrorReportingEnabledTest,
NotSentIfInvalidURL) {
CallOnDidAddMessageToConsole(main_rfh(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, u"invalid URL", kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 0);
}
TEST_F(DevToolsFrontendHostImplJsErrorReportingEnabledTest,
NotSentIfNonDevToolsURL) {
const char16_t* const kNonChromeSourceURLs[] = {
u"chrome-untrusted://media-app",
u"chrome-error://chromewebdata/",
u"chrome-extension://abc123/",
u"chrome://settings",
u"about:blank",
};
for (const auto* url : kNonChromeSourceURLs) {
CallOnDidAddMessageToConsole(main_rfh(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, url, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 0) << url;
}
}
} // namespace content
#endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)