blob: 533f24085b4ecd996d0118fe53826ea4e869f1d2 [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 <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/to_string.h"
#include "base/test/test_timeouts.h"
#include "base/test/values_test_util.h"
#include "base/threading/platform_thread.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/headless/headless_mode_browsertest.h"
#include "chrome/common/chrome_switches.h"
#include "components/headless/command_handler/headless_command_handler.h"
#include "components/headless/command_handler/headless_command_switches.h"
#include "components/headless/test/bitmap_utils.h"
#include "components/headless/test/capture_std_stream.h"
#include "components/headless/test/pdf_utils.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "pdf/pdf.h"
#include "printing/buildflags/buildflags.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/display/display_switches.h"
#include "ui/gfx/codec/png_codec.h"
#include "url/gurl.h"
namespace headless {
class HeadlessModeCommandBrowserTest : public HeadlessModeBrowserTest {
public:
HeadlessModeCommandBrowserTest() = default;
void SetUp() override {
ASSERT_TRUE(embedded_test_server()->Start());
HeadlessCommandHandler::SetDoneCallbackForTesting(base::BindOnce(
&HeadlessModeCommandBrowserTest::FinishTest, base::Unretained(this)));
HeadlessModeBrowserTest::SetUp();
}
protected:
// Override this to provide the test specific target page.
virtual std::string GetTargetPage() { return "/hello.html"; }
GURL GetTargetUrl(const std::string& url) {
return embedded_test_server()->GetURL(url);
}
std::optional<HeadlessCommandHandler::Result> ProcessCommands() {
if (!test_complete_) {
run_loop_ = std::make_unique<base::RunLoop>();
run_loop_->Run();
run_loop_.reset();
}
return result_;
}
private:
void FinishTest(HeadlessCommandHandler::Result result) {
result_ = result;
test_complete_ = true;
if (run_loop_) {
run_loop_->Quit();
}
}
std::unique_ptr<base::RunLoop> run_loop_;
bool test_complete_ = false;
std::optional<HeadlessCommandHandler::Result> result_;
};
#define HEADLESS_MODE_COMMAND_BROWSER_TEST_WITH_TARGET_URL( \
test_fixture, test_name, target_url) \
class HeadlessModeCommandBrowserTest_##test_name : public test_fixture { \
public: \
HeadlessModeCommandBrowserTest_##test_name() = default; \
std::string GetTargetPage() override { \
return target_url; \
} \
}; \
IN_PROC_BROWSER_TEST_F(HeadlessModeCommandBrowserTest_##test_name, test_name)
class HeadlessModeCommandBrowserTestWithTempDir
: public HeadlessModeCommandBrowserTest {
public:
HeadlessModeCommandBrowserTestWithTempDir() = default;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir()));
HeadlessModeCommandBrowserTest::SetUp();
}
void TearDown() override {
HeadlessModeCommandBrowserTest::TearDown();
ASSERT_TRUE(temp_dir_.Delete());
}
const base::FilePath& temp_dir() const { return temp_dir_.GetPath(); }
private:
base::ScopedTempDir temp_dir_;
};
// DumpDom command tests ----------------------------------------------
class HeadlessModeDumpDomCommandBrowserTestBase
: public HeadlessModeCommandBrowserTest {
public:
HeadlessModeDumpDomCommandBrowserTestBase() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessModeCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kDumpDom);
command_line->AppendArg(GetTargetUrl(GetTargetPage()).spec());
capture_stdout_.StartCapture();
}
protected:
CaptureStdOut capture_stdout_;
};
class HeadlessModeDumpDomCommandBrowserTest
: public HeadlessModeDumpDomCommandBrowserTestBase,
public testing::WithParamInterface<bool> {
public:
HeadlessModeDumpDomCommandBrowserTest() = default;
private:
bool IsIncognito() override { return GetParam(); }
};
INSTANTIATE_TEST_SUITE_P(/* no prefix */,
HeadlessModeDumpDomCommandBrowserTest,
::testing::Bool());
// TODO(crbug.com/40266323): Reenable once deflaked.
#if BUILDFLAG(IS_MAC)
#define MAYBE_HeadlessDumpDom DISABLED_HeadlessDumpDom
#else
#define MAYBE_HeadlessDumpDom HeadlessDumpDom
#endif
IN_PROC_BROWSER_TEST_P(HeadlessModeDumpDomCommandBrowserTest,
MAYBE_HeadlessDumpDom) {
ASSERT_THAT(ProcessCommands(),
testing::Eq(HeadlessCommandHandler::Result::kSuccess));
capture_stdout_.StopCapture();
std::string captured_stdout = capture_stdout_.TakeCapturedData();
static const char kDomDump[] =
"<!DOCTYPE html>\n"
"<html><head></head><body><h1>Hello headless world!</h1>\n"
"</body></html>\n";
EXPECT_THAT(captured_stdout, testing::HasSubstr(kDomDump));
}
class HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase
: public HeadlessModeDumpDomCommandBrowserTestBase {
public:
HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessModeDumpDomCommandBrowserTestBase::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kTimeout,
base::ToString(timeout().InMilliseconds()));
}
base::TimeDelta timeout() { return TestTimeouts::action_timeout(); }
};
class HeadlessModeDumpDomCommandBrowserTestWithTimeout
: public HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase {
public:
HeadlessModeDumpDomCommandBrowserTestWithTimeout() = default;
void SetUp() override {
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&HeadlessModeDumpDomCommandBrowserTestWithTimeout::RequestHandler,
base::Unretained(this)));
HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase::SetUp();
}
private:
std::string GetTargetPage() override { return "/page.html"; }
std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
const net::test_server::HttpRequest& request) {
if (request.relative_url == "/page.html") {
auto response = std::make_unique<net::test_server::DelayedHttpResponse>(
timeout() * 2);
response->set_code(net::HTTP_OK);
response->set_content_type("text/html");
response->set_content(R"(<div>Hi, I'm headless!</div>)");
return response;
}
return nullptr;
}
};
IN_PROC_BROWSER_TEST_F(HeadlessModeDumpDomCommandBrowserTestWithTimeout,
HeadlessDumpDomWithTimeout) {
// Main page timeout should be reported.
EXPECT_THAT(ProcessCommands(),
testing::Eq(HeadlessCommandHandler::Result::kPageLoadTimeout));
capture_stdout_.StopCapture();
std::string captured_stdout = capture_stdout_.TakeCapturedData();
std::erase_if(captured_stdout, isspace);
// Expect about:blank page DOM.
EXPECT_THAT(captured_stdout,
testing::HasSubstr("<html><head></head><body></body></html>"));
}
class HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout
: public HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase,
public testing::WithParamInterface<bool> {
public:
HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout() = default;
bool delay_response() { return GetParam(); }
void SetUp() override {
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout::
RequestHandler,
base::Unretained(this)));
HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase::SetUp();
}
private:
std::string GetTargetPage() override { return "/page.html"; }
std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
const net::test_server::HttpRequest& request) {
if (request.relative_url == "/page.html") {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("text/html");
response->set_content(R"(
<html><body>
<div id="thediv">INITIAL</div>
<script src="./script.js"></script>
</body></html>
)");
return response;
}
if (request.relative_url == "/script.js") {
std::unique_ptr<net::test_server::BasicHttpResponse> response;
if (delay_response()) {
response = std::make_unique<net::test_server::DelayedHttpResponse>(
timeout() * 2);
} else {
response = std::make_unique<net::test_server::BasicHttpResponse>();
}
response->set_code(net::HTTP_OK);
response->set_content_type("text/javascript");
response->set_content(R"(
document.getElementById("thediv").innerText="REPLACED";
)");
return response;
}
return nullptr;
}
};
INSTANTIATE_TEST_SUITE_P(
All,
HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout,
testing::Bool());
IN_PROC_BROWSER_TEST_P(
HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout,
HeadlessDumpDomWithSubResourceTimeout) {
std::optional<HeadlessCommandHandler::Result> result = ProcessCommands();
capture_stdout_.StopCapture();
std::string captured_stdout = capture_stdout_.TakeCapturedData();
std::erase_if(captured_stdout, isspace);
if (delay_response()) {
EXPECT_THAT(result,
testing::Eq(HeadlessCommandHandler::Result::kPageLoadTimeout));
EXPECT_THAT(captured_stdout,
testing::HasSubstr(
"<html><head></head><body><divid=\"thediv\">INITIAL</"
"div><scriptsrc=\"./script.js\"></script></body></html>"));
} else {
EXPECT_THAT(result, testing::Eq(HeadlessCommandHandler::Result::kSuccess));
EXPECT_THAT(captured_stdout,
testing::HasSubstr(
"<html><head></head><body><divid=\"thediv\">REPLACED</"
"div><scriptsrc=\"./script.js\"></script></body></html>"));
}
}
HEADLESS_MODE_COMMAND_BROWSER_TEST_WITH_TARGET_URL(
HeadlessModeDumpDomCommandBrowserTestBase,
DumpDomWithBeforeUnloadPreventDefault,
"/before_unload_prevent_default.html") {
// Make sure that 'beforeunload' that prevents default action does not stall
// the command processing. The "Leave site" popup should not appear because
// command target was not user activated.
EXPECT_THAT(ProcessCommands(),
testing::Eq(HeadlessCommandHandler::Result::kSuccess));
}
// Screenshot command tests -------------------------------------------
class HeadlessModeScreenshotCommandBrowserTest
: public HeadlessModeCommandBrowserTestWithTempDir {
public:
HeadlessModeScreenshotCommandBrowserTest() = default;
#if BUILDFLAG(IS_WIN)
void SetUp() override {
// Use software compositing instead of GL which causes blank screenshots on
// Windows, especially under ASAN. See https://crbug.com/1442606 and
// https://crbug.com/328195816.
UseSoftwareCompositing();
HeadlessModeCommandBrowserTestWithTempDir::SetUp();
}
#endif
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessModeCommandBrowserTestWithTempDir::SetUpCommandLine(command_line);
screenshot_filename_ =
temp_dir().Append(FILE_PATH_LITERAL("screenshot.png"));
command_line->AppendSwitchPath(switches::kScreenshot, screenshot_filename_);
command_line->AppendArg(GetTargetUrl("/centered_blue_box.html").spec());
}
protected:
base::FilePath screenshot_filename_;
};
IN_PROC_BROWSER_TEST_F(HeadlessModeScreenshotCommandBrowserTest,
HeadlessScreenshot) {
ASSERT_THAT(ProcessCommands(),
testing::Eq(HeadlessCommandHandler::Result::kSuccess));
base::ScopedAllowBlockingForTesting allow_blocking;
std::optional<std::vector<uint8_t>> png_data =
base::ReadFileToBytes(screenshot_filename_);
SkBitmap bitmap = gfx::PNGCodec::Decode(png_data.value());
ASSERT_FALSE(bitmap.isNull());
// Expect a centered blue rectangle on white background.
EXPECT_TRUE(CheckColoredRect(bitmap, SkColorSetRGB(0x00, 0x00, 0xff),
SkColorSetRGB(0xff, 0xff, 0xff)));
}
class HeadlessModeScreenshotCommandWithWindowSizeBrowserTest
: public HeadlessModeScreenshotCommandBrowserTest {
public:
HeadlessModeScreenshotCommandWithWindowSizeBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessModeScreenshotCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(::switches::kWindowSize, "2345,1234");
command_line->AppendSwitchASCII(::switches::kForceDeviceScaleFactor, "1");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessModeScreenshotCommandWithWindowSizeBrowserTest,
HeadlessScreenshotWithWindowSize) {
ASSERT_THAT(ProcessCommands(),
testing::Eq(HeadlessCommandHandler::Result::kSuccess));
base::ScopedAllowBlockingForTesting allow_blocking;
std::optional<std::vector<uint8_t>> png_data =
base::ReadFileToBytes(screenshot_filename_);
SkBitmap bitmap = gfx::PNGCodec::Decode(png_data.value());
ASSERT_FALSE(bitmap.isNull());
EXPECT_EQ(bitmap.width(), 2345);
EXPECT_EQ(bitmap.height(), 1234);
// Expect a centered blue rectangle on white background.
EXPECT_TRUE(CheckColoredRect(bitmap, SkColorSetRGB(0x00, 0x00, 0xff),
SkColorSetRGB(0xff, 0xff, 0xff)));
}
class HeadlessModeScreenshotCommandWithBackgroundBrowserTest
: public HeadlessModeScreenshotCommandBrowserTest {
public:
HeadlessModeScreenshotCommandWithBackgroundBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessModeScreenshotCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kDefaultBackgroundColor,
"ff0000");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessModeScreenshotCommandWithBackgroundBrowserTest,
HeadlessScreenshotWithBackground) {
ASSERT_THAT(ProcessCommands(),
testing::Eq(HeadlessCommandHandler::Result::kSuccess));
base::ScopedAllowBlockingForTesting allow_blocking;
std::optional<std::vector<uint8_t>> png_data =
base::ReadFileToBytes(screenshot_filename_);
SkBitmap bitmap = gfx::PNGCodec::Decode(png_data.value());
ASSERT_FALSE(bitmap.isNull());
// Expect a centered blue rectangle on red background.
EXPECT_TRUE(CheckColoredRect(bitmap, SkColorSetRGB(0x00, 0x00, 0xff),
SkColorSetRGB(0xff, 0x00, 0x00)));
}
// PrintToPDF command tests -------------------------------------------
class HeadlessModePrintToPdfCommandBrowserTestBase
: public HeadlessModeCommandBrowserTestWithTempDir {
public:
HeadlessModePrintToPdfCommandBrowserTestBase() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessModeCommandBrowserTestWithTempDir::SetUpCommandLine(command_line);
print_to_pdf_filename_ =
temp_dir().Append(FILE_PATH_LITERAL("print-to.pdf"));
command_line->AppendSwitchPath(switches::kPrintToPDF,
print_to_pdf_filename_);
command_line->AppendSwitch(switches::kNoPDFHeaderFooter);
command_line->AppendArg(GetTargetUrl(GetTargetPage()).spec());
}
protected:
base::FilePath print_to_pdf_filename_;
};
class HeadlessModePrintToPdfCommandBrowserTest
: public HeadlessModePrintToPdfCommandBrowserTestBase {
public:
HeadlessModePrintToPdfCommandBrowserTest() = default;
std::string GetTargetPage() override { return "/centered_blue_box.html"; }
};
// TODO(crbug.com/40266323): Reenable once deflaked.
#if BUILDFLAG(IS_MAC)
#define MAYBE_HeadlessPrintToPdf DISABLED_HeadlessPrintToPdf
#else
#define MAYBE_HeadlessPrintToPdf HeadlessPrintToPdf
#endif
IN_PROC_BROWSER_TEST_F(HeadlessModePrintToPdfCommandBrowserTest,
MAYBE_HeadlessPrintToPdf) {
ASSERT_THAT(ProcessCommands(),
testing::Eq(HeadlessCommandHandler::Result::kSuccess));
base::ScopedAllowBlockingForTesting allow_blocking;
std::optional<std::vector<uint8_t>> pdf_data =
base::ReadFileToBytes(print_to_pdf_filename_);
ASSERT_TRUE(pdf_data.has_value()) << print_to_pdf_filename_;
PDFPageBitmap page_bitmap;
ASSERT_TRUE(page_bitmap.Render(pdf_data.value(), /*page_index=*/0));
// Expect blue rectangle on white background.
EXPECT_TRUE(page_bitmap.CheckColoredRect(SkColorSetRGB(0x00, 0x00, 0xff),
SkColorSetRGB(0xff, 0xff, 0xff)));
}
HEADLESS_MODE_COMMAND_BROWSER_TEST_WITH_TARGET_URL(
HeadlessModePrintToPdfCommandBrowserTestBase,
PrintToPdfWithLazyLoading,
"/page_with_lazy_image.html") {
ASSERT_THAT(ProcessCommands(),
testing::Eq(HeadlessCommandHandler::Result::kSuccess));
base::ScopedAllowBlockingForTesting allow_blocking;
std::optional<std::vector<uint8_t>> pdf_data =
base::ReadFileToBytes(print_to_pdf_filename_);
ASSERT_TRUE(pdf_data.has_value()) << print_to_pdf_filename_;
PDFPageBitmap page_bitmap;
ASSERT_TRUE(page_bitmap.Render(pdf_data.value(), /*page_index=*/4));
// Expect green rectangle on white background.
EXPECT_TRUE(page_bitmap.CheckColoredRect(SkColorSetRGB(0x00, 0x64, 0x00),
SkColorSetRGB(0xff, 0xff, 0xff)));
}
class HeadlessModeTaggedPrintToPdfCommandBrowserTest
: public HeadlessModePrintToPdfCommandBrowserTestBase,
public ::testing::WithParamInterface<bool> {
public:
HeadlessModeTaggedPrintToPdfCommandBrowserTest() = default;
bool generate_tagged_pdf() { return GetParam(); }
std::string GetTargetPage() override { return "/hello.html"; }
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessModePrintToPdfCommandBrowserTestBase::SetUpCommandLine(
command_line);
if (!generate_tagged_pdf()) {
command_line->AppendSwitch(switches::kDisablePDFTagging);
}
}
};
const char kExpectedStructTreeJSON[] = R"({
"lang": "en-US",
"type": "Document",
"~children": [ {
"type": "H1",
"~children": [ {
"type": "NonStruct"
} ]
} ]
}
)";
INSTANTIATE_TEST_SUITE_P(/* no prefix */,
HeadlessModeTaggedPrintToPdfCommandBrowserTest,
::testing::Bool());
IN_PROC_BROWSER_TEST_P(HeadlessModeTaggedPrintToPdfCommandBrowserTest,
HeadlessTaggedPrintToPdf) {
ASSERT_THAT(ProcessCommands(),
testing::Eq(HeadlessCommandHandler::Result::kSuccess));
base::ScopedAllowBlockingForTesting allow_blocking;
std::optional<std::vector<uint8_t>> pdf_data =
base::ReadFileToBytes(print_to_pdf_filename_);
ASSERT_TRUE(pdf_data.has_value()) << print_to_pdf_filename_;
auto pdf_span = base::as_byte_span(pdf_data.value());
int num_pages;
ASSERT_TRUE(chrome_pdf::GetPDFDocInfo(pdf_span, &num_pages,
/*max_page_width=*/nullptr));
EXPECT_THAT(num_pages, testing::Eq(1));
ASSERT_THAT(chrome_pdf::IsPDFDocTagged(pdf_span),
testing::Optional(generate_tagged_pdf()));
if (generate_tagged_pdf()) {
base::Value struct_tree =
chrome_pdf::GetPDFStructTreeForPage(pdf_span, /*page_index=*/0);
EXPECT_THAT(kExpectedStructTreeJSON, base::test::IsJson((struct_tree)));
}
}
} // namespace headless