#include "headless/public/headless_web_contents.h"
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "base/base64.h"
#include "base/check_deref.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "cc/base/switches.h"
#include "cc/test/pixel_test_utils.h"
#include "components/devtools/simple_devtools_protocol_client/simple_devtools_protocol_client.h"
#include "components/viz/common/switches.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "headless/lib/browser/headless_web_contents_impl.h"
#include "headless/public/headless_browser.h"
#include "headless/test/headless_browser_test.h"
#include "headless/test/headless_browser_test_utils.h"
#include "headless/test/headless_devtooled_browsertest.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/switches.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/size_f.h"
#include "url/gurl.h"
using testing::ElementsAre;
using testing::ElementsAreArray;
using testing::Not;
using testing::UnorderedElementsAre;
using testing::UnorderedElementsAreArray;
using simple_devtools_protocol_client::SimpleDevToolsProtocolClient;
namespace headless {
class HeadlessWebContentsTest : public HeadlessBrowserTest {};
IN_PROC_BROWSER_TEST_F(HeadlessWebContentsTest, Navigation) {
EXPECT_TRUE(embedded_test_server()->Start());
HeadlessBrowserContext* browser_context =
browser()->CreateBrowserContextBuilder().Build();
HeadlessWebContents* web_contents =
browser_context->CreateWebContentsBuilder()
.SetInitialURL(embedded_test_server()->GetURL("/hello.html"))
.Build();
EXPECT_TRUE(WaitForLoad(web_contents));
EXPECT_THAT(browser_context->GetAllWebContents(),
UnorderedElementsAre(web_contents));
}
IN_PROC_BROWSER_TEST_F(HeadlessWebContentsTest,
FocusOfHeadlessWebContents_IsIndependent) {
EXPECT_TRUE(embedded_test_server()->Start());
HeadlessBrowserContext* browser_context =
browser()->CreateBrowserContextBuilder().Build();
HeadlessWebContents* web_contents =
browser_context->CreateWebContentsBuilder()
.SetInitialURL(embedded_test_server()->GetURL("/hello.html"))
.Build();
WaitForLoadAndGainFocus(web_contents);
EXPECT_THAT(EvaluateScript(web_contents, "document.hasFocus()"),
DictHasValue("result.result.value", true));
HeadlessWebContents* web_contents2 =
browser_context->CreateWebContentsBuilder()
.SetInitialURL(embedded_test_server()->GetURL("/hello.html"))
.Build();
WaitForLoadAndGainFocus(web_contents2);
EXPECT_THAT(EvaluateScript(web_contents, "document.hasFocus()"),
DictHasValue("result.result.value", true));
EXPECT_THAT(EvaluateScript(web_contents2, "document.hasFocus()"),
DictHasValue("result.result.value", true));
}
IN_PROC_BROWSER_TEST_F(HeadlessWebContentsTest, HandleSSLError) {
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED);
ASSERT_TRUE(https_server.Start());
HeadlessBrowserContext* browser_context =
browser()->CreateBrowserContextBuilder().Build();
HeadlessWebContents* web_contents =
browser_context->CreateWebContentsBuilder()
.SetInitialURL(https_server.GetURL("/hello.html"))
.Build();
EXPECT_FALSE(WaitForLoad(web_contents));
}
class HeadlessWebContentsScreenshotTest
: public HeadlessDevTooledBrowserTest,
public ::testing::WithParamInterface<bool> {
public:
void SetUp() override {
EnablePixelOutput();
if (GetParam()) {
UseSoftwareCompositing();
SetUpWithoutGPU();
} else {
HeadlessDevTooledBrowserTest::SetUp();
}
}
void RunDevTooledTest() override {
devtools_client_.SendCommand(
"Runtime.evaluate",
Param("expression", "document.body.style.background = '#0000ff'"),
base::BindOnce(&HeadlessWebContentsScreenshotTest::OnPageSetupCompleted,
base::Unretained(this)));
}
void OnPageSetupCompleted(base::Value::Dict) {
devtools_client_.SendCommand(
"Page.captureScreenshot",
base::BindOnce(&HeadlessWebContentsScreenshotTest::OnScreenshotCaptured,
base::Unretained(this)));
}
void OnScreenshotCaptured(base::Value::Dict result) {
std::string png_data_base64 = DictString(result, "result.data");
ASSERT_FALSE(png_data_base64.empty());
std::optional<std::vector<uint8_t>> png_data =
base::Base64Decode(png_data_base64);
EXPECT_GT(png_data.value().size(), 0U);
SkBitmap result_bitmap = gfx::PNGCodec::Decode(png_data.value());
EXPECT_FALSE(result_bitmap.isNull());
EXPECT_EQ(800, result_bitmap.width());
EXPECT_EQ(600, result_bitmap.height());
SkColor actual_color = result_bitmap.getColor(400, 300);
SkColor expected_color = SkColorSetRGB(0x00, 0x00, 0xff);
EXPECT_EQ(expected_color, actual_color);
FinishAsynchronousTest();
}
};
HEADLESS_DEVTOOLED_TEST_P(HeadlessWebContentsScreenshotTest);
INSTANTIATE_TEST_SUITE_P(HeadlessWebContentsScreenshotTests,
HeadlessWebContentsScreenshotTest,
::testing::Bool());
class HeadlessWebContentsScreenshotWindowPositionTest
: public HeadlessWebContentsScreenshotTest {
public:
void RunDevTooledTest() override {
base::Value::Dict params;
params.Set("windowId",
HeadlessWebContentsImpl::From(web_contents_)->window_id());
params.SetByDottedPath("bounds.left", 600);
params.SetByDottedPath("bounds.top", 100);
params.SetByDottedPath("bounds.width", 800);
params.SetByDottedPath("bounds.height", 600);
browser_devtools_client_.SendCommand(
"Browser.setWindowBounds", std::move(params),
base::BindOnce(
&HeadlessWebContentsScreenshotWindowPositionTest::OnWindowBoundsSet,
base::Unretained(this)));
}
void OnWindowBoundsSet(base::Value::Dict result) {
EXPECT_NE(result.FindDict("result"), nullptr);
HeadlessWebContentsScreenshotTest::RunDevTooledTest();
}
};
HEADLESS_DEVTOOLED_TEST_P(HeadlessWebContentsScreenshotWindowPositionTest);
INSTANTIATE_TEST_SUITE_P(,
HeadlessWebContentsScreenshotWindowPositionTest,
::testing::Bool());
class HeadlessWebContentsRequestStorageQuotaTest
: public HeadlessDevTooledBrowserTest {
public:
void RunDevTooledTest() override {
EXPECT_TRUE(embedded_test_server()->Start());
devtools_client_.AddEventHandler(
"Runtime.consoleAPICalled",
base::BindRepeating(
&HeadlessWebContentsRequestStorageQuotaTest::OnConsoleAPICalled,
base::Unretained(this)));
SendCommandSync(devtools_client_, "Runtime.enable");
devtools_client_.SendCommand(
"Page.navigate",
Param("url", embedded_test_server()
->GetURL("/request_storage_quota.html")
.spec()));
}
void OnConsoleAPICalled(const base::Value::Dict& params) {
const base::Value::List* args = params.FindListByDottedPath("params.args");
ASSERT_NE(args, nullptr);
ASSERT_GT(args->size(), 0ul);
const base::Value* value = args->front().GetDict().Find("value");
ASSERT_NE(value, nullptr);
EXPECT_EQ(value->GetString(), "success");
FinishAsynchronousTest();
}
};
HEADLESS_DEVTOOLED_TEST_F(HeadlessWebContentsRequestStorageQuotaTest);
IN_PROC_BROWSER_TEST_F(HeadlessWebContentsTest, BrowserTabChangeContent) {
EXPECT_TRUE(embedded_test_server()->Start());
HeadlessBrowserContext* browser_context =
browser()->CreateBrowserContextBuilder().Build();
HeadlessWebContents* web_contents =
browser_context->CreateWebContentsBuilder().Build();
EXPECT_TRUE(WaitForLoad(web_contents));
std::string script = "window.location = '" +
embedded_test_server()->GetURL("/hello.html").spec() +
"';";
EXPECT_THAT(EvaluateScript(web_contents, script),
Not(DictHasKey("exceptionDetails")));
EXPECT_TRUE(WaitForLoad(web_contents));
}
IN_PROC_BROWSER_TEST_F(HeadlessWebContentsTest, BrowserOpenInTab) {
EXPECT_TRUE(embedded_test_server()->Start());
HeadlessBrowserContext* browser_context =
browser()->CreateBrowserContextBuilder().Build();
HeadlessWebContents* web_contents =
browser_context->CreateWebContentsBuilder()
.SetInitialURL(embedded_test_server()->GetURL("/link.html"))
.Build();
EXPECT_TRUE(WaitForLoad(web_contents));
EXPECT_EQ(1u, browser_context->GetAllWebContents().size());
std::string script =
"var event = new MouseEvent('click', {'button': 1});"
"document.getElementsByTagName('a')[0].dispatchEvent(event);";
EXPECT_THAT(EvaluateScript(web_contents, script),
Not(DictHasKey("exceptionDetails")));
EXPECT_EQ(2u, browser_context->GetAllWebContents().size());
}
#if !BUILDFLAG(IS_MAC)
class HeadlessWebContentsBeginFrameControlTest : public HeadlessBrowserTest {
public:
HeadlessWebContentsBeginFrameControlTest() {}
void SetUp() override {
EnablePixelOutput();
HeadlessBrowserTest::SetUp();
}
protected:
virtual std::string GetTestHtmlFile() = 0;
virtual void StartFrames() {}
virtual void OnFrameFinished(base::Value::Dict result) {}
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(::switches::kRunAllCompositorStagesBeforeDraw);
command_line->AppendSwitch(::switches::kDisableNewContentRenderingTimeout);
command_line->AppendSwitch(switches::kDisableCheckerImaging);
command_line->AppendSwitch(switches::kDisableThreadedAnimation);
}
void RunTest() {
browser()->SetDefaultBrowserContext(
browser()->CreateBrowserContextBuilder().Build());
SimpleDevToolsProtocolClient browser_devtools_client;
browser_devtools_client.AttachToBrowser();
EXPECT_TRUE(embedded_test_server()->Start());
base::Value::Dict params;
params.Set("url", "about:blank");
params.Set("width", 200);
params.Set("height", 200);
params.Set("enableBeginFrameControl", true);
browser_devtools_client.SendCommand(
"Target.createTarget", std::move(params),
base::BindOnce(
&HeadlessWebContentsBeginFrameControlTest::OnTargetCreated,
base::Unretained(this)));
RunAsynchronousTest();
browser_devtools_client.DetachClient();
}
void OnTargetCreated(base::Value::Dict result) {
const std::string targetId = DictString(result, "result.targetId");
ASSERT_FALSE(targetId.empty());
web_contents_ = HeadlessWebContentsImpl::From(
content::DevToolsAgentHost::GetForId(targetId)->GetWebContents());
devtools_client_.AttachToWebContents(web_contents_->web_contents());
devtools_client_.SendCommand(
"Page.enable",
base::BindOnce(
&HeadlessWebContentsBeginFrameControlTest::OnPageDomainEnabled,
base::Unretained(this)));
}
void OnPageDomainEnabled(base::Value::Dict) {
devtools_client_.AddEventHandler("Page.loadEventFired",
on_load_event_fired_handler_);
devtools_client_.SendCommand(
"Page.navigate",
Param("url", embedded_test_server()->GetURL(GetTestHtmlFile()).spec()));
}
void OnLoadEventFired(const base::Value::Dict& params) {
TRACE_EVENT0("headless",
"HeadlessWebContentsBeginFrameControlTest::OnLoadEventFired");
devtools_client_.SendCommand("Page.disable");
devtools_client_.RemoveEventHandler("Page.loadEventFired",
on_load_event_fired_handler_);
StartFrames();
}
void BeginFrame(bool screenshot) {
num_begin_frames_++;
base::Value::Dict params;
if (screenshot)
params.Set("screenshot", base::Value::Dict());
devtools_client_.SendCommand(
"HeadlessExperimental.beginFrame", std::move(params),
base::BindOnce(&HeadlessWebContentsBeginFrameControlTest::FrameFinished,
base::Unretained(this)));
}
void FrameFinished(base::Value::Dict result) {
TRACE_EVENT2(
"headless", "HeadlessWebContentsBeginFrameControlTest::FrameFinished",
"has_damage", DictBool(result, "result.hasDamage"),
"has_screenshot_data", DictString(result, "result.screenshotData"));
OnFrameFinished(std::move(result));
}
void PostFinishAsynchronousTest() {
browser()->BrowserMainThread()->PostTask(
FROM_HERE,
base::BindOnce(
&HeadlessWebContentsBeginFrameControlTest::FinishAsynchronousTest,
base::Unretained(this)));
}
raw_ptr<HeadlessWebContentsImpl, AcrossTasksDanglingUntriaged> web_contents_ =
nullptr;
int num_begin_frames_ = 0;
SimpleDevToolsProtocolClient devtools_client_;
SimpleDevToolsProtocolClient::EventCallback on_load_event_fired_handler_ =
base::BindRepeating(
&HeadlessWebContentsBeginFrameControlTest::OnLoadEventFired,
base::Unretained(this));
};
class HeadlessWebContentsBeginFrameControlBasicTest
: public HeadlessWebContentsBeginFrameControlTest {
public:
HeadlessWebContentsBeginFrameControlBasicTest() = default;
protected:
std::string GetTestHtmlFile() override {
return "/blue_page.html";
}
void StartFrames() override { BeginFrame(true); }
void OnFrameFinished(base::Value::Dict result) override {
if (num_begin_frames_ == 1) {
CHECK_EQ(num_retries_, 0);
if (!result.FindStringByDottedPath("result.screenshotData")) {
num_retries_ += 1;
BeginFrame(true);
return;
}
}
int frame_number = num_begin_frames_ - num_retries_;
if (frame_number == 1) {
EXPECT_TRUE(DictBool(result, "result.hasDamage"));
std::string png_data_base64 = DictString(result, "result.screenshotData");
ASSERT_FALSE(png_data_base64.empty());
std::optional<std::vector<uint8_t>> png_data =
base::Base64Decode(png_data_base64);
EXPECT_GT(png_data.value().size(), 0U);
SkBitmap result_bitmap = gfx::PNGCodec::Decode(png_data.value());
EXPECT_FALSE(result_bitmap.isNull());
EXPECT_EQ(200, result_bitmap.width());
EXPECT_EQ(200, result_bitmap.height());
SkColor expected_color = SkColorSetRGB(0x00, 0x00, 0xff);
SkColor actual_color = result_bitmap.getColor(100, 100);
EXPECT_EQ(expected_color, actual_color);
} else {
DCHECK_EQ(2, frame_number);
EXPECT_FALSE(result.FindStringByDottedPath("result.screenshotData"));
}
if (frame_number < 2) {
BeginFrame(false);
} else {
PostFinishAsynchronousTest();
}
}
int num_retries_ = 0;
};
HEADLESS_DEVTOOLED_TEST_F(HeadlessWebContentsBeginFrameControlBasicTest);
class HeadlessWebContentsBeginFrameControlViewportTest
: public HeadlessWebContentsBeginFrameControlTest {
public:
HeadlessWebContentsBeginFrameControlViewportTest() = default;
protected:
std::string GetTestHtmlFile() override {
return "/blue_box.html";
}
void StartFrames() override {
BeginFrame(false);
}
void SetUpViewport() {
base::Value::Dict params;
params.Set("width", 0);
params.Set("height", 0);
params.Set("deviceScaleFactor", 0);
params.Set("mobile", false);
params.SetByDottedPath("viewport.x", 200);
params.SetByDottedPath("viewport.y", 200);
params.SetByDottedPath("viewport.width", 100);
params.SetByDottedPath("viewport.height", 100);
params.SetByDottedPath("viewport.scale", 3);
devtools_client_.SendCommand(
"Emulation.setDeviceMetricsOverride", std::move(params),
base::BindOnce(&HeadlessWebContentsBeginFrameControlViewportTest::
OnSetDeviceMetricsOverrideDone,
base::Unretained(this)));
}
void OnSetDeviceMetricsOverrideDone(base::Value::Dict result) {
EXPECT_THAT(result, DictHasKey("result"));
BeginFrame(true);
}
void OnFrameFinished(base::Value::Dict result) override {
if (num_begin_frames_ == 1) {
SetUpViewport();
return;
}
DCHECK_EQ(2, num_begin_frames_);
EXPECT_TRUE(*result.FindBoolByDottedPath("result.hasDamage"));
std::string png_data_base64 = DictString(result, "result.screenshotData");
ASSERT_FALSE(png_data_base64.empty());
std::optional<std::vector<uint8_t>> png_data =
base::Base64Decode(png_data_base64);
ASSERT_GT(png_data.value().size(), 0ul);
SkBitmap result_bitmap = gfx::PNGCodec::Decode(png_data.value());
EXPECT_FALSE(result_bitmap.isNull());
SkBitmap expected_bitmap;
SkImageInfo info;
expected_bitmap.allocPixels(
SkImageInfo::MakeN32(300, 300, kOpaque_SkAlphaType), 0);
expected_bitmap.eraseColor(SkColorSetRGB(0x00, 0x00, 0xff));
EXPECT_TRUE(cc::MatchesBitmap(result_bitmap, expected_bitmap,
cc::ExactPixelComparator()));
PostFinishAsynchronousTest();
}
};
DISABLED_HEADLESS_DEVTOOLED_TEST_F(
HeadlessWebContentsBeginFrameControlViewportTest);
#endif
class CookiesEnabled : public HeadlessDevTooledBrowserTest {
public:
void RunDevTooledTest() override {
EXPECT_TRUE(embedded_test_server()->Start());
devtools_client_.AddEventHandler(
"Page.loadEventFired",
base::BindRepeating(&CookiesEnabled::OnLoadEventFired,
base::Unretained(this)));
devtools_client_.SendCommand(
"Page.enable", base::BindOnce(&CookiesEnabled::OnPageDomainEnabled,
base::Unretained(this)));
}
void OnPageDomainEnabled(base::Value::Dict) {
devtools_client_.SendCommand(
"Page.navigate",
Param("url", embedded_test_server()->GetURL("/cookie.html").spec()));
}
void OnLoadEventFired(const base::Value::Dict& params) {
devtools_client_.SendCommand(
"Runtime.evaluate", Param("expression", "window.test_result"),
base::BindOnce(&CookiesEnabled::OnEvaluateResult,
base::Unretained(this)));
}
void OnEvaluateResult(base::Value::Dict result) {
EXPECT_EQ(DictString(result, "result.result.value"), "0");
FinishAsynchronousTest();
}
};
HEADLESS_DEVTOOLED_TEST_F(CookiesEnabled);
class BlockDevToolsEmbedding : public HeadlessDevTooledBrowserTest {
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessDevTooledBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kRemoteDebuggingPort,
base::NumberToString(port_));
}
void RunDevTooledTest() override {
std::stringstream url;
url << "data:text/html,<iframe src='http://localhost:" << port_
<< "/json/version'></iframe>";
devtools_client_.AddEventHandler(
"Page.loadEventFired",
base::BindRepeating(&BlockDevToolsEmbedding::OnLoadEventFired,
base::Unretained(this)));
devtools_client_.SendCommand("Page.enable");
devtools_client_.SendCommand("Page.navigate", Param("url", url.str()));
}
void OnLoadEventFired(const base::Value::Dict& params) {
devtools_client_.SendCommand(
"Page.getFrameTree",
base::BindOnce(&BlockDevToolsEmbedding::OnFrameTreeResult,
base::Unretained(this)));
}
void OnFrameTreeResult(base::Value::Dict result) {
const auto& child_frames = CHECK_DEREF(
result.FindListByDottedPath("result.frameTree.childFrames"));
EXPECT_EQ(DictString(child_frames[0].GetDict(), "frame.url"),
"chrome-error://chromewebdata/");
FinishAsynchronousTest();
}
bool ShouldEnableSitePerProcess() override {
return false;
}
private:
int port_ = 10000 + (rand() % 60000);
};
HEADLESS_DEVTOOLED_TEST_F(BlockDevToolsEmbedding);
}