[go: nahoru, domu]

blob: abf0c834eeadd9749b899df3d1daa29999c745e1 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cmath>
#include <string>
#include "base/command_line.h"
#include "base/containers/cxx20_erase.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/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.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 "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test.h"
#include "headless/lib/browser/headless_browser_context_impl.h"
#include "headless/lib/browser/headless_browser_impl.h"
#include "headless/lib/browser/headless_web_contents_impl.h"
#include "headless/public/headless_browser.h"
#include "headless/public/headless_browser_context.h"
#include "headless/public/headless_web_contents.h"
#include "headless/public/switches.h"
#include "headless/test/headless_browser_test.h"
#include "headless/test/headless_browser_test_utils.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "pdf/buildflags.h"
#include "printing/buildflags/buildflags.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.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 "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_POSIX)
#include <signal.h>
#include <unistd.h>
#endif
#if BUILDFLAG(IS_WIN)
#include <Windows.h>
#endif
#if BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(ENABLE_PDF)
#include "components/headless/test/pdf_utils.h"
#endif
namespace headless {
namespace {
bool DecodePNG(const std::string& png_data, SkBitmap* bitmap) {
return gfx::PNGCodec::Decode(
reinterpret_cast<const unsigned char*>(png_data.data()), png_data.size(),
bitmap);
}
} // namespace
class HeadlessCommandBrowserTest : public HeadlessBrowserTest,
public content::WebContentsObserver {
public:
HeadlessCommandBrowserTest() = default;
void RunTest() {
ASSERT_TRUE(embedded_test_server()->Start());
HeadlessBrowserContext::Builder context_builder =
browser()->CreateBrowserContextBuilder();
HeadlessBrowserContext* browser_context = context_builder.Build();
browser()->SetDefaultBrowserContext(browser_context);
GURL handler_url = HeadlessCommandHandler::GetHandlerUrl();
HeadlessWebContents::Builder builder(
browser_context->CreateWebContentsBuilder());
HeadlessWebContents* web_contents =
builder.SetInitialURL(handler_url).Build();
content::WebContents* content_web_contents =
HeadlessWebContentsImpl::From(web_contents)->web_contents();
content::WebContentsObserver::Observe(content_web_contents);
HeadlessCommandHandler::ProcessCommands(
content_web_contents, GetTargetUrl(),
base::BindOnce(&HeadlessCommandBrowserTest::FinishTest,
base::Unretained(this)),
base::SingleThreadTaskRunner::GetCurrentDefault());
RunAsynchronousTest();
if (aborted_) {
return;
}
content::WebContentsObserver::Observe(nullptr);
web_contents->Close();
browser_context->Close();
base::RunLoop().RunUntilIdle();
}
void set_aborted() { aborted_ = true; }
absl::optional<HeadlessCommandHandler::Result> result() const {
return result_;
}
private:
virtual GURL GetTargetUrl() = 0;
void FinishTest(HeadlessCommandHandler::Result result) {
result_ = result;
FinishAsynchronousTest();
}
bool aborted_ = false;
absl::optional<HeadlessCommandHandler::Result> result_;
};
class HeadlessFileCommandBrowserTest : public HeadlessCommandBrowserTest {
public:
HeadlessFileCommandBrowserTest() = default;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir()));
HeadlessCommandBrowserTest::SetUp();
}
void TearDown() override {
HeadlessCommandBrowserTest::TearDown();
ASSERT_TRUE(temp_dir_.Delete());
}
const base::FilePath& temp_dir() const { return temp_dir_.GetPath(); }
base::ScopedTempDir temp_dir_;
};
// DumpDom command tests ----------------------------------------------
class HeadlessDumpDomCommandBrowserTest : public HeadlessCommandBrowserTest {
public:
HeadlessDumpDomCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kDumpDom);
}
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/hello.html");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessDumpDomCommandBrowserTest, DumpDom) {
base::ScopedAllowBlockingForTesting allow_blocking;
CaptureStdOut capture_stdout;
capture_stdout.StartCapture();
RunTest();
capture_stdout.StopCapture();
ASSERT_THAT(result(), testing::Eq(HeadlessCommandHandler::Result::kSuccess));
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 HeadlessDumpDomVirtualTimeBudgetCommandBrowserTest
: public HeadlessDumpDomCommandBrowserTest {
public:
HeadlessDumpDomVirtualTimeBudgetCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessDumpDomCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kVirtualTimeBudget, "5500");
}
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/stepper_page.html");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessDumpDomVirtualTimeBudgetCommandBrowserTest,
DumpDomVirtualTimeBudget) {
base::ScopedAllowBlockingForTesting allow_blocking;
CaptureStdOut capture_stdout;
capture_stdout.StartCapture();
RunTest();
capture_stdout.StopCapture();
ASSERT_THAT(result(), testing::Eq(HeadlessCommandHandler::Result::kSuccess));
std::vector<std::string> captured_lines =
base::SplitString(capture_stdout.TakeCapturedData(), "\n",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
EXPECT_THAT(captured_lines, testing::Contains(R"(<div id="box">5</div>)"));
}
class HeadlessDumpDomTimeoutCommandBrowserTestBase
: public HeadlessDumpDomCommandBrowserTest {
public:
HeadlessDumpDomTimeoutCommandBrowserTestBase() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessDumpDomCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kTimeout,
base::ToString(timeout().InMilliseconds()));
}
base::TimeDelta timeout() { return TestTimeouts::action_timeout(); }
};
class HeadlessDumpDomTimeoutCommandBrowserTest
: public HeadlessDumpDomTimeoutCommandBrowserTestBase {
public:
HeadlessDumpDomTimeoutCommandBrowserTest() = default;
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/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(HeadlessDumpDomTimeoutCommandBrowserTest,
DumpDomTimeout) {
base::ScopedAllowBlockingForTesting allow_blocking;
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&HeadlessDumpDomTimeoutCommandBrowserTest::RequestHandler,
base::Unretained(this)));
CaptureStdOut capture_stdout;
capture_stdout.StartCapture();
RunTest();
capture_stdout.StopCapture();
// Main page timeout should be reported.
EXPECT_THAT(result(),
testing::Eq(HeadlessCommandHandler::Result::kPageLoadTimeout));
std::vector<std::string> captured_lines =
base::SplitString(capture_stdout.TakeCapturedData(), "\n",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
// Expect about:blank page DOM.
EXPECT_THAT(captured_lines,
testing::Contains(R"(<html><head></head><body></body></html>)"));
}
class HeadlessDumpDomSubResourceTimeoutCommandBrowserTest
: public HeadlessDumpDomTimeoutCommandBrowserTestBase,
public testing::WithParamInterface<bool> {
public:
HeadlessDumpDomSubResourceTimeoutCommandBrowserTest() = default;
bool delay_response() { return GetParam(); }
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/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,
HeadlessDumpDomSubResourceTimeoutCommandBrowserTest,
testing::Values(false, true));
IN_PROC_BROWSER_TEST_P(HeadlessDumpDomSubResourceTimeoutCommandBrowserTest,
DumpDomSubResourceTimeout) {
base::ScopedAllowBlockingForTesting allow_blocking;
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&HeadlessDumpDomSubResourceTimeoutCommandBrowserTest::RequestHandler,
base::Unretained(this)));
CaptureStdOut capture_stdout;
capture_stdout.StartCapture();
RunTest();
capture_stdout.StopCapture();
std::string captured_stdout_data = capture_stdout.TakeCapturedData();
base::EraseIf(captured_stdout_data, isspace);
if (delay_response()) {
EXPECT_THAT(result(),
testing::Eq(HeadlessCommandHandler::Result::kPageLoadTimeout));
EXPECT_THAT(captured_stdout_data,
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_data,
testing::HasSubstr(
"<html><head></head><body><divid=\"thediv\">REPLACED</"
"div><scriptsrc=\"./script.js\"></script></body></html>"));
}
}
// Screenshot command tests -------------------------------------------
class HeadlessScreenshotCommandBrowserTest
: public HeadlessFileCommandBrowserTest {
public:
HeadlessScreenshotCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessFileCommandBrowserTest::SetUpCommandLine(command_line);
screenshot_filename_ =
temp_dir().Append(FILE_PATH_LITERAL("screenshot.png"));
command_line->AppendSwitchPath(switches::kScreenshot, screenshot_filename_);
command_line->AppendSwitchASCII(::switches::kForceDeviceScaleFactor, "1");
}
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/centered_blue_box.html");
}
base::FilePath screenshot_filename_;
};
IN_PROC_BROWSER_TEST_F(HeadlessScreenshotCommandBrowserTest, Screenshot) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
ASSERT_THAT(result(), testing::Eq(HeadlessCommandHandler::Result::kSuccess));
ASSERT_TRUE(base::PathExists(screenshot_filename_)) << screenshot_filename_;
std::string png_data;
ASSERT_TRUE(base::ReadFileToString(screenshot_filename_, &png_data))
<< screenshot_filename_;
SkBitmap bitmap;
ASSERT_TRUE(DecodePNG(png_data, &bitmap));
ASSERT_EQ(800, bitmap.width());
ASSERT_EQ(600, bitmap.height());
// Expect a centered blue rectangle on white background.
EXPECT_TRUE(CheckColoredRect(bitmap, SkColorSetRGB(0x00, 0x00, 0xff),
SkColorSetRGB(0xff, 0xff, 0xff)));
}
class HeadlessScreenshotWithBackgroundCommandBrowserTest
: public HeadlessScreenshotCommandBrowserTest {
public:
HeadlessScreenshotWithBackgroundCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessScreenshotCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kDefaultBackgroundColor,
"ff0000");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessScreenshotWithBackgroundCommandBrowserTest,
ScreenshotBackground) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
ASSERT_THAT(result(), testing::Eq(HeadlessCommandHandler::Result::kSuccess));
ASSERT_TRUE(base::PathExists(screenshot_filename_)) << screenshot_filename_;
std::string png_data;
ASSERT_TRUE(base::ReadFileToString(screenshot_filename_, &png_data))
<< screenshot_filename_;
SkBitmap bitmap;
ASSERT_TRUE(DecodePNG(png_data, &bitmap));
ASSERT_EQ(800, bitmap.width());
ASSERT_EQ(600, bitmap.height());
// Expect a centered blue rectangle on red background.
EXPECT_TRUE(CheckColoredRect(bitmap, SkColorSetRGB(0x00, 0x00, 0xff),
SkColorSetRGB(0xff, 0x00, 0x00)));
}
class HeadlessScreenshotCommandBrowserTestWithWindowSize
: public HeadlessScreenshotCommandBrowserTest {
public:
static constexpr gfx::Size kWindowSize = {4096, 2160};
HeadlessScreenshotCommandBrowserTestWithWindowSize() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessScreenshotCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(
switches::kWindowSize,
base::StringPrintf("%u,%u", kWindowSize.width(), kWindowSize.height()));
}
};
IN_PROC_BROWSER_TEST_F(HeadlessScreenshotCommandBrowserTestWithWindowSize,
ScreenshotWindowSize) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
ASSERT_THAT(result(), testing::Eq(HeadlessCommandHandler::Result::kSuccess));
ASSERT_TRUE(base::PathExists(screenshot_filename_)) << screenshot_filename_;
std::string png_data;
ASSERT_TRUE(base::ReadFileToString(screenshot_filename_, &png_data))
<< screenshot_filename_;
SkBitmap bitmap;
ASSERT_TRUE(DecodePNG(png_data, &bitmap));
EXPECT_EQ(bitmap.width(), kWindowSize.width());
EXPECT_EQ(bitmap.height(), kWindowSize.height());
}
// PrintToPDF command tests -------------------------------------------
#if BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(ENABLE_PDF)
class HeadlessPrintToPdfCommandBrowserTest
: public HeadlessFileCommandBrowserTest {
public:
static constexpr float kPageMarginsInInches =
0.393701; // See Page.PrintToPDF specs.
HeadlessPrintToPdfCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessFileCommandBrowserTest::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);
}
GURL GetTargetUrl() override {
return embedded_test_server()->GetURL("/centered_blue_box.html");
}
base::FilePath print_to_pdf_filename_;
};
IN_PROC_BROWSER_TEST_F(HeadlessPrintToPdfCommandBrowserTest, PrintToPdf) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
ASSERT_THAT(result(), testing::Eq(HeadlessCommandHandler::Result::kSuccess));
ASSERT_TRUE(base::PathExists(print_to_pdf_filename_))
<< print_to_pdf_filename_;
std::string pdf_data;
ASSERT_TRUE(base::ReadFileToString(print_to_pdf_filename_, &pdf_data))
<< print_to_pdf_filename_;
PDFPageBitmap page_bitmap;
ASSERT_TRUE(page_bitmap.Render(pdf_data, 0));
// Expect blue rectangle on white background.
EXPECT_TRUE(page_bitmap.CheckColoredRect(0xff0000ff, 0xffffffff));
}
class HeadlessPrintToPdfWithBackgroundCommandBrowserTest
: public HeadlessPrintToPdfCommandBrowserTest {
public:
HeadlessPrintToPdfWithBackgroundCommandBrowserTest() = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessPrintToPdfCommandBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kDefaultBackgroundColor,
"ff0000");
}
};
IN_PROC_BROWSER_TEST_F(HeadlessPrintToPdfWithBackgroundCommandBrowserTest,
PrintToPdfBackground) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
ASSERT_THAT(result(), testing::Eq(HeadlessCommandHandler::Result::kSuccess));
ASSERT_TRUE(base::PathExists(print_to_pdf_filename_))
<< print_to_pdf_filename_;
std::string pdf_data;
ASSERT_TRUE(base::ReadFileToString(print_to_pdf_filename_, &pdf_data))
<< print_to_pdf_filename_;
PDFPageBitmap page_bitmap;
ASSERT_TRUE(page_bitmap.Render(pdf_data, 0));
// Expect blue rectangle on red background sans margin.
EXPECT_TRUE(page_bitmap.CheckColoredRect(0xff0000ff, 0xffff0000, 120));
}
#endif // BUILDFLAG(ENABLE_PRINTING) && BUILDFLAG(ENABLE_PDF)
// Graceful signal handling is currently available only on Linux, Mac and
// Windows.
#if BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_WIN)
class HeadlessCommandSignalBrowserTest
: public HeadlessDumpDomCommandBrowserTest {
public:
HeadlessCommandSignalBrowserTest() = default;
// content::WebContentsObserver implementation:
void DocumentOnLoadCompletedInPrimaryMainFrame() override {
set_aborted();
#if BUILDFLAG(IS_POSIX)
set_expected_exit_code(128 + SIGINT);
raise(SIGINT);
#elif BUILDFLAG(IS_WIN)
::GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
#endif
}
};
IN_PROC_BROWSER_TEST_F(HeadlessCommandSignalBrowserTest, SendCtrlCSignal) {
base::ScopedAllowBlockingForTesting allow_blocking;
RunTest();
// Command execution is expected to be Ctrl+C'ed gracefully.
}
#endif // #if BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_WIN)
} // namespace headless