Commit 274b93d0 authored by Peter Kvitek's avatar Peter Kvitek Committed by Commit Bot

Removed obsolete headless C++ tests that where replaced with JS equivalents.

Change-Id: I56bb2d9930203b0a1668b6f00b9fdcd89fcf1e3e
Reviewed-on: https://chromium-review.googlesource.com/1155745Reviewed-by: default avatarPavel Feldman <pfeldman@chromium.org>
Commit-Queue: Peter Kvitek <kvitekp@chromium.org>
Cr-Commit-Position: refs/heads/master@{#579197}
parent 07ffa1bd
...@@ -346,14 +346,10 @@ component("headless") { ...@@ -346,14 +346,10 @@ component("headless") {
"public/internal/headless_devtools_client_impl.h", "public/internal/headless_devtools_client_impl.h",
"public/internal/message_dispatcher.h", "public/internal/message_dispatcher.h",
"public/internal/value_conversions.h", "public/internal/value_conversions.h",
"public/util/compositor_controller.cc",
"public/util/compositor_controller.h",
"public/util/error_reporter.cc", "public/util/error_reporter.cc",
"public/util/error_reporter.h", "public/util/error_reporter.h",
"public/util/user_agent.cc", "public/util/user_agent.cc",
"public/util/user_agent.h", "public/util/user_agent.h",
"public/util/virtual_time_controller.cc",
"public/util/virtual_time_controller.h",
] ]
if (!is_fuchsia) { if (!is_fuchsia) {
...@@ -557,9 +553,7 @@ group("headless_tests") { ...@@ -557,9 +553,7 @@ group("headless_tests") {
test("headless_unittests") { test("headless_unittests") {
sources = [ sources = [
"public/domains/types_unittest.cc", "public/domains/types_unittest.cc",
"public/util/compositor_controller_unittest.cc",
"public/util/error_reporter_unittest.cc", "public/util/error_reporter_unittest.cc",
"public/util/virtual_time_controller_test.cc",
] ]
if (!is_component_build) { if (!is_component_build) {
...@@ -686,7 +680,6 @@ test("headless_browsertests") { ...@@ -686,7 +680,6 @@ test("headless_browsertests") {
"lib/headless_browser_context_browsertest.cc", "lib/headless_browser_context_browsertest.cc",
"lib/headless_devtools_client_browsertest.cc", "lib/headless_devtools_client_browsertest.cc",
"lib/headless_web_contents_browsertest.cc", "lib/headless_web_contents_browsertest.cc",
"public/util/compositor_controller_browsertest.cc",
"test/headless_browser_test.cc", "test/headless_browser_test.cc",
"test/headless_browser_test.h", "test/headless_browser_test.h",
"test/headless_client_browsertest.cc", "test/headless_client_browsertest.cc",
...@@ -745,12 +738,7 @@ test("headless_browsertests") { ...@@ -745,12 +738,7 @@ test("headless_browsertests") {
if (is_linux) { if (is_linux) {
# Only include this if we built the js_binary # Only include this if we built the js_binary
data += [ "$root_out_dir/headless_browser_tests.pak" ] data += [ "$root_out_dir/headless_browser_tests.pak" ]
sources += [ sources += [ "test/headless_js_bindings_browsertest.cc" ]
"test/headless_js_bindings_browsertest.cc",
"test/headless_render_browsertest.cc",
"test/headless_render_test.cc",
"test/headless_render_test.h",
]
deps += [ deps += [
":headless_browser_tests_pak", ":headless_browser_tests_pak",
"//ui/gfx:geometry_skia", "//ui/gfx:geometry_skia",
......
specific_include_rules = { specific_include_rules = {
"compositor_controller_browsertest.cc": [
"+cc/base/switches.h",
"+components/viz/common/features.h",
"+components/viz/common/switches.h",
"+third_party/skia/include",
]
} }
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/public/util/compositor_controller.h"
#include <memory>
#include "base/base64.h"
#include "base/bind.h"
#include "base/cancelable_callback.h"
#include "base/logging.h"
#include "base/single_thread_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "headless/public/util/virtual_time_controller.h"
namespace headless {
// Sends BeginFrames to advance animations while virtual time advances in
// intervals.
class CompositorController::AnimationBeginFrameTask
: public VirtualTimeController::RepeatingTask,
public VirtualTimeController::ResumeDeferrer {
public:
explicit AnimationBeginFrameTask(CompositorController* compositor_controller)
: RepeatingTask(StartPolicy::START_IMMEDIATELY, -1),
compositor_controller_(compositor_controller),
weak_ptr_factory_(this) {}
// VirtualTimeController::RepeatingTask implementation:
void IntervalElapsed(
base::TimeDelta virtual_time_offset,
base::OnceCallback<void(ContinuePolicy)> continue_callback) override {
needs_begin_frame_on_virtual_time_resume_ = true;
std::move(continue_callback).Run(ContinuePolicy::NOT_REQUIRED);
}
// VirtualTimeController::ResumeDeferrer implementation:
void DeferResume(base::OnceClosure continue_callback) override {
// Run a BeginFrame if we scheduled one in the last interval and no other
// BeginFrame was sent while virtual time was paused.
if (needs_begin_frame_on_virtual_time_resume_) {
continue_callback_ = std::move(continue_callback);
IssueAnimationBeginFrame();
return;
}
std::move(continue_callback).Run();
}
void CompositorControllerIssuingScreenshotBeginFrame() {
TRACE_EVENT0("headless",
"CompositorController::AnimationBeginFrameTask::"
"CompositorControllerIssuingScreenshotBeginFrame");
// The screenshotting BeginFrame will replace our animation-only BeginFrame.
// We cancel any pending animation BeginFrame to avoid sending two
// BeginFrames within the same virtual time pause.
needs_begin_frame_on_virtual_time_resume_ = false;
}
private:
void IssueAnimationBeginFrame() {
TRACE_EVENT0("headless",
"CompositorController::AnimationBeginFrameTask::"
"IssueAnimationBeginFrame");
needs_begin_frame_on_virtual_time_resume_ = false;
bool update_display =
compositor_controller_->update_display_for_animations_;
// Display needs to be updated for first BeginFrame. Otherwise, the
// RenderWidget's surface may not be created and the root surface may block
// waiting for it forever.
update_display |=
compositor_controller_->last_begin_frame_time_ == base::TimeTicks();
compositor_controller_->PostBeginFrame(
base::BindOnce(&AnimationBeginFrameTask::BeginFrameComplete,
weak_ptr_factory_.GetWeakPtr()),
!update_display);
}
void BeginFrameComplete(std::unique_ptr<BeginFrameResult>) {
TRACE_EVENT0(
"headless",
"CompositorController::AnimationBeginFrameTask::BeginFrameComplete");
DCHECK(continue_callback_);
std::move(continue_callback_).Run();
}
CompositorController* compositor_controller_; // NOT OWNED
bool needs_begin_frame_on_virtual_time_resume_ = true;
base::CancelableClosure begin_frame_task_;
base::OnceClosure continue_callback_;
base::WeakPtrFactory<AnimationBeginFrameTask> weak_ptr_factory_;
};
CompositorController::CompositorController(
scoped_refptr<base::SequencedTaskRunner> task_runner,
HeadlessDevToolsClient* devtools_client,
VirtualTimeController* virtual_time_controller,
base::TimeDelta animation_begin_frame_interval,
bool update_display_for_animations)
: task_runner_(std::move(task_runner)),
devtools_client_(devtools_client),
virtual_time_controller_(virtual_time_controller),
animation_task_(std::make_unique<AnimationBeginFrameTask>(this)),
animation_begin_frame_interval_(animation_begin_frame_interval),
update_display_for_animations_(update_display_for_animations),
weak_ptr_factory_(this) {
devtools_client_->GetHeadlessExperimental()->GetExperimental()->AddObserver(
this);
// No need to wait for completion of this, since we are waiting for the
// setNeedsBeginFramesChanged event instead, which will be sent at some point
// after enabling the domain.
devtools_client_->GetHeadlessExperimental()->GetExperimental()->Enable(
headless_experimental::EnableParams::Builder().Build());
virtual_time_controller_->ScheduleRepeatingTask(
animation_task_.get(), animation_begin_frame_interval_);
virtual_time_controller_->SetResumeDeferrer(animation_task_.get());
}
CompositorController::~CompositorController() {
virtual_time_controller_->CancelRepeatingTask(animation_task_.get());
virtual_time_controller_->SetResumeDeferrer(nullptr);
devtools_client_->GetHeadlessExperimental()
->GetExperimental()
->RemoveObserver(this);
}
void CompositorController::PostBeginFrame(
base::OnceCallback<void(std::unique_ptr<BeginFrameResult>)>
begin_frame_complete_callback,
bool no_display_updates,
std::unique_ptr<ScreenshotParams> screenshot) {
// In certain nesting situations, we should not issue a BeginFrame immediately
// - for example, issuing a new BeginFrame within a BeginFrameCompleted or
// NeedsBeginFramesChanged event can upset the compositor. We avoid these
// situations by issuing our BeginFrames from a separately posted task.
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&CompositorController::BeginFrame,
weak_ptr_factory_.GetWeakPtr(),
std::move(begin_frame_complete_callback),
no_display_updates, std::move(screenshot)));
}
void CompositorController::BeginFrame(
base::OnceCallback<void(std::unique_ptr<BeginFrameResult>)>
begin_frame_complete_callback,
bool no_display_updates,
std::unique_ptr<ScreenshotParams> screenshot) {
DCHECK(!begin_frame_complete_callback_);
begin_frame_complete_callback_ = std::move(begin_frame_complete_callback);
if (needs_begin_frames_ || screenshot) {
auto params_builder = headless_experimental::BeginFrameParams::Builder();
// Use virtual time for frame time, so that rendering of animations etc. is
// aligned with virtual time progression.
base::TimeTicks frame_time =
virtual_time_controller_->GetCurrentVirtualTime();
if (frame_time <= last_begin_frame_time_) {
// Frame time cannot go backwards or stop, so we issue another BeginFrame
// with a small time offset from the last BeginFrame's time instead.
frame_time =
last_begin_frame_time_ + base::TimeDelta::FromMicroseconds(1);
}
params_builder.SetFrameTimeTicks(
(frame_time - base::TimeTicks()).InMillisecondsF());
DCHECK_GT(frame_time, last_begin_frame_time_);
last_begin_frame_time_ = frame_time;
params_builder.SetInterval(
animation_begin_frame_interval_.InMillisecondsF());
params_builder.SetNoDisplayUpdates(no_display_updates);
if (screenshot)
params_builder.SetScreenshot(std::move(screenshot));
devtools_client_->GetHeadlessExperimental()->GetExperimental()->BeginFrame(
params_builder.Build(),
base::BindOnce(&CompositorController::BeginFrameComplete,
weak_ptr_factory_.GetWeakPtr()));
} else {
BeginFrameComplete(nullptr);
}
}
void CompositorController::BeginFrameComplete(
std::unique_ptr<BeginFrameResult> result) {
std::move(begin_frame_complete_callback_).Run(std::move(result));
if (idle_callback_)
std::move(idle_callback_).Run();
}
void CompositorController::OnNeedsBeginFramesChanged(
const NeedsBeginFramesChangedParams& params) {
needs_begin_frames_ = params.GetNeedsBeginFrames();
}
void CompositorController::WaitUntilIdle(base::OnceClosure idle_callback) {
TRACE_EVENT_INSTANT1("headless", "CompositorController::WaitUntilIdle",
TRACE_EVENT_SCOPE_THREAD, "begin_frame_in_flight",
!!begin_frame_complete_callback_);
DCHECK(!idle_callback_);
if (!begin_frame_complete_callback_) {
std::move(idle_callback).Run();
return;
}
idle_callback_ = std::move(idle_callback);
}
void CompositorController::CaptureScreenshot(
ScreenshotParamsFormat format,
int quality,
base::OnceCallback<void(const std::string&)> screenshot_captured_callback) {
TRACE_EVENT0("headless", "CompositorController::CaptureScreenshot");
DCHECK(!begin_frame_complete_callback_);
DCHECK(!screenshot_captured_callback_);
screenshot_captured_callback_ = std::move(screenshot_captured_callback);
// Let AnimationBeginFrameTask know that it doesn't need to issue an
// animation BeginFrame for the current virtual time pause.
animation_task_->CompositorControllerIssuingScreenshotBeginFrame();
const bool no_display_updates = false;
PostBeginFrame(
base::BindOnce(&CompositorController::CaptureScreenshotBeginFrameComplete,
weak_ptr_factory_.GetWeakPtr()),
no_display_updates,
ScreenshotParams::Builder()
.SetFormat(format)
.SetQuality(quality)
.Build());
}
void CompositorController::CaptureScreenshotBeginFrameComplete(
std::unique_ptr<BeginFrameResult> result) {
TRACE_EVENT1(
"headless", "CompositorController::CaptureScreenshotBeginFrameComplete",
"hasScreenshotData",
result ? std::to_string(result->HasScreenshotData()) : "invalid");
DCHECK(screenshot_captured_callback_);
if (result && result->HasScreenshotData()) {
// TODO(eseckler): Look into returning binary screenshot data via DevTools.
std::string decoded_data;
base::Base64Decode(result->GetScreenshotData(), &decoded_data);
std::move(screenshot_captured_callback_).Run(decoded_data);
} else {
LOG(ERROR) << "Screenshotting failed, BeginFrameResult has no data and "
"hasDamage is "
<< (result ? std::to_string(result->HasScreenshotData())
: "invalid");
std::move(screenshot_captured_callback_).Run(std::string());
}
}
} // namespace headless
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef HEADLESS_PUBLIC_UTIL_COMPOSITOR_CONTROLLER_H_
#define HEADLESS_PUBLIC_UTIL_COMPOSITOR_CONTROLLER_H_
#include "base/callback.h"
#include "base/cancelable_callback.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/sequenced_task_runner.h"
#include "base/time/time.h"
#include "headless/public/devtools/domains/headless_experimental.h"
#include "headless/public/headless_devtools_client.h"
namespace headless {
class VirtualTimeController;
// Issues BeginFrames (Chromium's vsync signal) while virtual time advances and
// and takes screenshots.
class HEADLESS_EXPORT CompositorController
: public headless_experimental::ExperimentalObserver {
public:
using BeginFrameResult = headless_experimental::BeginFrameResult;
using NeedsBeginFramesChangedParams =
headless_experimental::NeedsBeginFramesChangedParams;
using ScreenshotParams = headless_experimental::ScreenshotParams;
using ScreenshotParamsFormat = headless_experimental::ScreenshotParamsFormat;
// |animation_begin_frame_interval| specifies the virtual time between
// individual BeginFrames while virtual time advances.
// If |update_display_for_animations| is false, animation BeginFrames will not
// commit or draw visual updates to the display. This can be used to reduce
// the overhead of such BeginFrames in the common case that screenshots will
// be taken from separate BeginFrames.
CompositorController(
scoped_refptr<base::SequencedTaskRunner> task_runner,
HeadlessDevToolsClient* devtools_client,
VirtualTimeController* virtual_time_controller,
base::TimeDelta animation_begin_frame_interval,
bool update_display_for_animations = true);
~CompositorController() override;
// Executes |idle_callback| when no BeginFrames are in flight.
void WaitUntilIdle(base::OnceClosure idle_callback);
// Captures a screenshot by issuing a BeginFrame. |quality| is only valid for
// jpeg format screenshots, in range 0..100. Should not be called again until
// |screenshot_captured_callback| was run. Should only be called while no
// other BeginFrame is in flight and after the compositor is ready.
void CaptureScreenshot(ScreenshotParamsFormat format,
int quality,
base::OnceCallback<void(const std::string&)>
screenshot_captured_callback);
private:
class AnimationBeginFrameTask;
// headless_experimental_::Observer implementation:
void OnNeedsBeginFramesChanged(
const NeedsBeginFramesChangedParams& params) override;
// Posts a BeginFrame as a new task to avoid nesting it inside the current
// callstack, which can upset the compositor.
void PostBeginFrame(
base::OnceCallback<void(std::unique_ptr<BeginFrameResult>)>
begin_frame_complete_callback,
bool no_display_updates = false,
std::unique_ptr<ScreenshotParams> screenshot = nullptr);
// Issues a BeginFrame synchronously and runs |begin_frame_complete_callback|
// when done. Should not be called again until |begin_frame_complete_callback|
// was run.
void BeginFrame(base::OnceCallback<void(std::unique_ptr<BeginFrameResult>)>
begin_frame_complete_callback,
bool no_display_updates = false,
std::unique_ptr<ScreenshotParams> screenshot = nullptr);
// Runs the |begin_frame_complete_callback_| and the |idle_callback_| if set.
void BeginFrameComplete(std::unique_ptr<BeginFrameResult>);
void CaptureScreenshotBeginFrameComplete(
std::unique_ptr<BeginFrameResult> result);
scoped_refptr<base::SequencedTaskRunner> task_runner_;
HeadlessDevToolsClient* devtools_client_; // NOT OWNED
VirtualTimeController* virtual_time_controller_; // NOT OWNED
std::unique_ptr<AnimationBeginFrameTask> animation_task_;
base::OnceClosure idle_callback_;
base::OnceCallback<void(const std::string&)> screenshot_captured_callback_;
base::OnceCallback<void(std::unique_ptr<BeginFrameResult>)>
begin_frame_complete_callback_;
base::TimeDelta animation_begin_frame_interval_;
bool update_display_for_animations_;
bool needs_begin_frames_ = false;
base::TimeTicks last_begin_frame_time_;
base::WeakPtrFactory<CompositorController> weak_ptr_factory_;
};
} // namespace headless
#endif // HEADLESS_PUBLIC_UTIL_COMPOSITOR_CONTROLLER_H_
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/public/util/compositor_controller.h"
#include <memory>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "cc/base/switches.h"
#include "components/viz/common/features.h"
#include "components/viz/common/switches.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "headless/lib/browser/headless_web_contents_impl.h"
#include "headless/public/devtools/domains/emulation.h"
#include "headless/public/devtools/domains/runtime.h"
#include "headless/public/headless_browser.h"
#include "headless/public/headless_devtools_client.h"
#include "headless/public/util/virtual_time_controller.h"
#include "headless/test/headless_browser_test.h"
#include "testing/gmock/include/gmock/gmock.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/gfx/codec/png_codec.h"
using testing::ElementsAre;
#define EXPECT_SCOPED(statements) \
{ \
SCOPED_TRACE(""); \
statements; \
}
namespace headless {
// BeginFrameControl is not supported on Mac.
#if !defined(OS_MACOSX)
namespace {
class BeginFrameCounter : HeadlessDevToolsClient::RawProtocolListener {
public:
BeginFrameCounter(HeadlessDevToolsClient* client) : client_(client) {
client_->SetRawProtocolListener(this);
}
~BeginFrameCounter() override { client_->SetRawProtocolListener(nullptr); }
bool OnProtocolMessage(const std::string& json_message,
const base::DictionaryValue& parsed_message) override {
const base::Value* id_value = parsed_message.FindKey("id");
if (!id_value)
return false;
const base::DictionaryValue* result_dict;
if (parsed_message.GetDictionary("result", &result_dict)) {
bool has_damage;
if (result_dict->GetBoolean("hasDamage", &has_damage))
++begin_frame_count_;
}
return false;
}
int begin_frame_count() const { return begin_frame_count_; }
private:
HeadlessDevToolsClient* client_; // NOT OWNED.
int begin_frame_count_ = 0;
};
bool DecodePNG(std::string png_data, SkBitmap* bitmap) {
return gfx::PNGCodec::Decode(
reinterpret_cast<unsigned const char*>(png_data.data()), png_data.size(),
bitmap);
}
} // namespace
class CompositorControllerBrowserTest
: public HeadlessAsyncDevTooledBrowserTest,
public ::testing::WithParamInterface<bool> {
public:
class AdditionalVirtualTimeBudget
: public VirtualTimeController::RepeatingTask,
public VirtualTimeController::Observer {
public:
AdditionalVirtualTimeBudget(
VirtualTimeController* virtual_time_controller,
StartPolicy start_policy,
base::TimeDelta budget,
base::OnceClosure budget_expired_callback,
base::OnceClosure virtual_time_started_callback = base::OnceClosure())
: RepeatingTask(start_policy, 0),
virtual_time_controller_(virtual_time_controller),
budget_expired_callback_(std::move(budget_expired_callback)),
virtual_time_started_callback_(
std::move(virtual_time_started_callback)) {
virtual_time_controller_->ScheduleRepeatingTask(this, budget);
virtual_time_controller_->AddObserver(this);
virtual_time_controller_->StartVirtualTime();
}
~AdditionalVirtualTimeBudget() override {
virtual_time_controller_->RemoveObserver(this);
virtual_time_controller_->CancelRepeatingTask(this);
}
// headless::VirtualTimeController::RepeatingTask implementation:
void IntervalElapsed(
base::TimeDelta virtual_time,
base::OnceCallback<void(ContinuePolicy)> continue_callback) override {
std::move(continue_callback).Run(ContinuePolicy::NOT_REQUIRED);
}
// headless::VirtualTimeController::Observer:
void VirtualTimeStarted(base::TimeDelta virtual_time_offset) override {
if (virtual_time_started_callback_)
std::move(virtual_time_started_callback_).Run();
}
void VirtualTimeStopped(base::TimeDelta virtual_time_offset) override {
std::move(budget_expired_callback_).Run();
delete this;
}
private:
headless::VirtualTimeController* const virtual_time_controller_;
base::OnceClosure budget_expired_callback_;
base::OnceClosure virtual_time_started_callback_;
};
void SetUp() override {
EnablePixelOutput();
if (GetParam()) {
UseSoftwareCompositing();
SetUpWithoutGPU();
} else {
HeadlessAsyncDevTooledBrowserTest::SetUp();
}
}
void SetUpCommandLine(base::CommandLine* command_line) override {
HeadlessAsyncDevTooledBrowserTest::SetUpCommandLine(command_line);
// See bit.ly/headless-rendering for why we use these flags.
command_line->AppendSwitch(switches::kRunAllCompositorStagesBeforeDraw);
command_line->AppendSwitch(switches::kDisableNewContentRenderingTimeout);
command_line->AppendSwitch(cc::switches::kDisableCheckerImaging);
command_line->AppendSwitch(cc::switches::kDisableThreadedAnimation);
command_line->AppendSwitch(switches::kDisableImageAnimationResync);
command_line->AppendSwitch(switches::kDisableThreadedScrolling);
scoped_feature_list_.InitAndEnableFeature(
features::kEnableSurfaceSynchronization);
}
bool GetEnableBeginFrameControl() override { return true; }
void RunDevTooledTest() override {
EXPECT_TRUE(embedded_test_server()->Start());
virtual_time_controller_ =
std::make_unique<VirtualTimeController>(devtools_client_.get());
const bool update_display_for_animations = false;
compositor_controller_ = std::make_unique<CompositorController>(
browser()->BrowserMainThread(), devtools_client_.get(),
virtual_time_controller_.get(), GetAnimationFrameInterval(),
update_display_for_animations);
// Initially pause virtual time.
devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy(
emulation::SetVirtualTimePolicyParams::Builder()
.SetPolicy(emulation::VirtualTimePolicy::PAUSE)
.SetInitialVirtualTime(100)
.Build(),
base::BindRepeating(
&CompositorControllerBrowserTest::SetVirtualTimePolicyDone,
base::Unretained(this)));
}
protected:
virtual base::TimeDelta GetAnimationFrameInterval() const {
return base::TimeDelta::FromMilliseconds(16);
}
virtual std::string GetTestFile() const { return "/blank.html"; }
void SetVirtualTimePolicyDone(
std::unique_ptr<emulation::SetVirtualTimePolicyResult>) {
// Run a first BeginFrame to initialize surface. Wait a while before doing
// so, since it takes a while before the compositor is ready for a
// RenderFrameSubmissionObserver.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CompositorControllerBrowserTest::RunFirstBeginFrame,
base::Unretained(this)),
base::TimeDelta::FromSeconds(1));
}
void RunFirstBeginFrame() {
begin_frame_counter_ =
std::make_unique<BeginFrameCounter>(devtools_client_.get());
render_frame_submission_observer_ =
std::make_unique<content::RenderFrameSubmissionObserver>(
HeadlessWebContentsImpl::From(web_contents_)->web_contents());
// AdditionalVirtualTimeBudget will self delete.
new AdditionalVirtualTimeBudget(
virtual_time_controller_.get(),
AdditionalVirtualTimeBudget::StartPolicy::WAIT_FOR_NAVIGATION,
GetAnimationFrameInterval(),
base::BindOnce(
&CompositorControllerBrowserTest::OnFirstBeginFrameComplete,
base::Unretained(this)),
base::BindOnce(&CompositorControllerBrowserTest::Navigate,
base::Unretained(this)));
}
void Navigate() {
// Navigate (after the first BeginFrame) to start virtual time.
devtools_client_->GetPage()->Navigate(
embedded_test_server()->GetURL(GetTestFile()).spec());
}
virtual void OnFirstBeginFrameComplete() {
// With surface sync enabled, we should have waited for the renderer's
// CompositorFrame in the first BeginFrame.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(1, 1));
}
void FinishCompositorControllerTest() {
render_frame_submission_observer_.reset();
FinishAsynchronousTest();
}
void ExpectAdditionalFrameCounts(int additional_begin_frame_count,
int additional_render_frame_count) {
expected_begin_frame_count_ += additional_begin_frame_count;
expected_render_frame_count_ += additional_render_frame_count;
EXPECT_EQ(expected_begin_frame_count_,
begin_frame_counter_->begin_frame_count());
EXPECT_EQ(expected_render_frame_count_,
render_frame_submission_observer_->render_frame_count());
}
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<VirtualTimeController> virtual_time_controller_;
std::unique_ptr<CompositorController> compositor_controller_;
std::unique_ptr<BeginFrameCounter> begin_frame_counter_;
std::unique_ptr<content::RenderFrameSubmissionObserver>
render_frame_submission_observer_;
int expected_begin_frame_count_ = 0;
int expected_render_frame_count_ = 0;
};
// Runs requestAnimationFrame three times without updating display for
// animations and takes a screenshot.
class CompositorControllerRafBrowserTest
: public CompositorControllerBrowserTest,
public runtime::Observer {
private:
void OnFirstBeginFrameComplete() override {
CompositorControllerBrowserTest::OnFirstBeginFrameComplete();
devtools_client_->GetRuntime()->AddObserver(this);
devtools_client_->GetRuntime()->Enable(
base::BindRepeating(&CompositorControllerRafBrowserTest::RuntimeEnabled,
base::Unretained(this)));
}
void RuntimeEnabled() {
// Request animation frames in the main frame. Each frame changes the body
// background color.
devtools_client_->GetRuntime()->Evaluate(
"window.rafCount = 0;"
"function onRaf(timestamp) {"
" console.log('rAF timestamp ' + timestamp + 'ms'); "
" window.rafCount++;"
" document.body.style.backgroundColor = '#' + window.rafCount * 100;"
" window.requestAnimationFrame(onRaf);"
"};"
"window.requestAnimationFrame(onRaf);",
base::BindRepeating(&CompositorControllerRafBrowserTest::OnRafReady,
base::Unretained(this)));
}
// runtime::Observer implementation:
void OnConsoleAPICalled(
const runtime::ConsoleAPICalledParams& params) override {
// We expect the arguments always to be a single string.
const std::vector<std::unique_ptr<runtime::RemoteObject>>& args =
*params.GetArgs();
if (args.size() == 1u && args[0]->HasValue())
log_.push_back(args[0]->GetValue()->GetString());
}
void OnRafReady(std::unique_ptr<runtime::EvaluateResult> result) {
EXPECT_NE(nullptr, result);
EXPECT_FALSE(result->HasExceptionDetails());
// AdditionalVirtualTimeBudget will self delete.
new AdditionalVirtualTimeBudget(
virtual_time_controller_.get(),
AdditionalVirtualTimeBudget::StartPolicy::START_IMMEDIATELY,
kNumFrames * GetAnimationFrameInterval(),
base::BindOnce(&CompositorControllerRafBrowserTest::OnRafBudgetExpired,
base::Unretained(this)));
}
void OnRafBudgetExpired() {
// Even though the rAF made a change to the frame's background color, no
// further CompositorFrames should have been produced for animations,
// because update_display_for_animations is false.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(kNumFrames, 0));
// Get animation frame count.
devtools_client_->GetRuntime()->Evaluate(
"window.rafCount",
base::BindRepeating(&CompositorControllerRafBrowserTest::OnGetRafCount,
base::Unretained(this)));
}
void OnGetRafCount(std::unique_ptr<runtime::EvaluateResult> result) {
EXPECT_NE(nullptr, result);
EXPECT_FALSE(result->HasExceptionDetails());
EXPECT_EQ(kNumFrames, result->GetResult()->GetValue()->GetInt());
compositor_controller_->CaptureScreenshot(
headless_experimental::ScreenshotParamsFormat::PNG, 100,
base::BindRepeating(&CompositorControllerRafBrowserTest::OnScreenshot,
base::Unretained(this)));
}
void OnScreenshot(const std::string& screenshot_data) {
// Screenshot should have incurred a new CompositorFrame from renderer.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(1, 1));
EXPECT_LT(0U, screenshot_data.length());
if (screenshot_data.length()) {
SkBitmap result_bitmap;
EXPECT_TRUE(DecodePNG(screenshot_data, &result_bitmap));
EXPECT_EQ(800, result_bitmap.width());
EXPECT_EQ(600, result_bitmap.height());
SkColor actual_color = result_bitmap.getColor(200, 200);
// Screenshot was the forth frame, so background color should be #400.
SkColor expected_color = SkColorSetRGB(0x44, 0x00, 0x00);
EXPECT_EQ(expected_color, actual_color);
}
EXPECT_THAT(log_, ElementsAre("rAF timestamp 16ms", "rAF timestamp 32ms",
"rAF timestamp 48ms", "rAF timestamp 64ms"));
FinishCompositorControllerTest();
}
static constexpr int kNumFrames = 3;
std::vector<std::string> log_;
};
/* static */
constexpr int CompositorControllerRafBrowserTest::kNumFrames;
HEADLESS_ASYNC_DEVTOOLED_TEST_P(CompositorControllerRafBrowserTest);
// Instantiate test case for both software and gpu compositing modes.
INSTANTIATE_TEST_CASE_P(CompositorControllerRafBrowserTests,
CompositorControllerRafBrowserTest,
::testing::Bool());
// Loads an animated GIF and verifies that:
// - animate_only BeginFrames don't produce CompositorFrames,
// - first screenshot starts the GIF animation,
// - animation is advanced according to virtual time.
// - the animation is not resynced after the first iteration.
class CompositorControllerImageAnimationBrowserTest
: public CompositorControllerBrowserTest {
private:
base::TimeDelta GetAnimationFrameInterval() const override {
return base::TimeDelta::FromMilliseconds(500);
}
std::string GetTestFile() const override {
// GIF: 1 second blue, 1 second red, 1 second yellow (100x100px).
return "/animated_gif.html";
}
void OnFirstBeginFrameComplete() override {
CompositorControllerBrowserTest::OnFirstBeginFrameComplete();
// Post a task to grant more virtual time as we can't do this synchronously
// from within VirtualTimeStopped().
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&CompositorControllerImageAnimationBrowserTest::
GrantFirstIterationBudget,
base::Unretained(this)));
}
void GrantFirstIterationBudget() {
// AdditionalVirtualTimeBudget will self delete.
new AdditionalVirtualTimeBudget(
virtual_time_controller_.get(),
AdditionalVirtualTimeBudget::StartPolicy::START_IMMEDIATELY,
kNumFramesFirstIteration * GetAnimationFrameInterval(),
base::BindOnce(&CompositorControllerImageAnimationBrowserTest::
OnFirstBudgetExpired,
base::Unretained(this)));
}
void OnFirstBudgetExpired() {
// The GIF should not have started animating yet, even though we advanced
// virtual time. It only starts animating when first painted, i.e. when the
// first screenshot is taken.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(kNumFramesFirstIteration, 0));
compositor_controller_->CaptureScreenshot(
headless_experimental::ScreenshotParamsFormat::PNG, 100,
base::BindRepeating(&CompositorControllerImageAnimationBrowserTest::
OnScreenshotAfterFirstIteration,
base::Unretained(this)));
}
void OnScreenshotAfterFirstIteration(const std::string& screenshot_data) {
// Screenshot should have incurred a new CompositorFrame from renderer.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(1, 1));
EXPECT_LT(0U, screenshot_data.length());
if (screenshot_data.length()) {
SkBitmap result_bitmap;
EXPECT_TRUE(DecodePNG(screenshot_data, &result_bitmap));
EXPECT_EQ(800, result_bitmap.width());
EXPECT_EQ(600, result_bitmap.height());
SkColor actual_color = result_bitmap.getColor(50, 50);
// Animation starts when first screenshot is taken, so should be blue.
SkColor expected_color = SkColorSetRGB(0x00, 0x00, 0xff);
EXPECT_EQ(expected_color, actual_color);
}
// Advance another iteration and check again that no CompositorFrames are
// produced. AdditionalVirtualTimeBudget will self delete.
new AdditionalVirtualTimeBudget(
virtual_time_controller_.get(),
AdditionalVirtualTimeBudget::StartPolicy::START_IMMEDIATELY,
kNumFramesSecondIteration * GetAnimationFrameInterval(),
base::BindOnce(&CompositorControllerImageAnimationBrowserTest::
OnSecondBudgetExpired,
base::Unretained(this)));
}
void OnSecondBudgetExpired() {
// Even though the GIF animated, no further CompositorFrames should have
// been produced, because update_display_for_animations is false. The second
// iteration only produces kNumFramesSecondIteration - 1 BeginFrames since
// the first animation frame is skipped because of the prior screenshot.
EXPECT_SCOPED(
ExpectAdditionalFrameCounts(kNumFramesSecondIteration - 1, 0));
compositor_controller_->CaptureScreenshot(
headless_experimental::ScreenshotParamsFormat::PNG, 100,
base::BindRepeating(&CompositorControllerImageAnimationBrowserTest::
OnScreenshotAfterSecondIteration,
base::Unretained(this)));
}
void OnScreenshotAfterSecondIteration(const std::string& screenshot_data) {
// Screenshot should have incurred a new CompositorFrame from renderer.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(1, 1));
EXPECT_LT(0U, screenshot_data.length());
if (screenshot_data.length()) {
SkBitmap result_bitmap;
EXPECT_TRUE(DecodePNG(screenshot_data, &result_bitmap));
EXPECT_EQ(800, result_bitmap.width());
EXPECT_EQ(600, result_bitmap.height());
SkColor actual_color = result_bitmap.getColor(50, 50);
// We advanced two animation frames, so animation should now be yellow.
SkColor expected_color = SkColorSetRGB(0xff, 0xff, 0x00);
EXPECT_EQ(expected_color, actual_color);
}
// Advance a full animation iteration and check that animation doesn't reset
// to the beginning, because of kDisableImageAnimationResync.
new AdditionalVirtualTimeBudget(
virtual_time_controller_.get(),
AdditionalVirtualTimeBudget::StartPolicy::START_IMMEDIATELY,
kNumFramesThirdIteration * GetAnimationFrameInterval(),
base::BindOnce(&CompositorControllerImageAnimationBrowserTest::
OnThirdBudgetExpired,
base::Unretained(this)));
}
void OnThirdBudgetExpired() {
// Even though the GIF animated, no further CompositorFrames should have
// been produced, because update_display_for_animations is false. The third
// iteration only produces kNumFramesThirdIteration - 1 BeginFrames since
// the first animation frame is skipped because of the prior screenshot.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(kNumFramesThirdIteration - 1, 0));
compositor_controller_->CaptureScreenshot(
headless_experimental::ScreenshotParamsFormat::PNG, 100,
base::BindRepeating(&CompositorControllerImageAnimationBrowserTest::
OnScreenshotAfterThirdIteration,
base::Unretained(this)));
}
void OnScreenshotAfterThirdIteration(const std::string& screenshot_data) {
// Screenshot should have incurred no new CompositorFrame from renderer
// since animation frame didn't change, but a new BeginFrame.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(1, 0));
EXPECT_LT(0U, screenshot_data.length());
if (screenshot_data.length()) {
SkBitmap result_bitmap;
EXPECT_TRUE(DecodePNG(screenshot_data, &result_bitmap));
EXPECT_EQ(800, result_bitmap.width());
EXPECT_EQ(600, result_bitmap.height());
SkColor actual_color = result_bitmap.getColor(50, 50);
// We advanced a full iteration, so animation should be yellow again.
SkColor expected_color = SkColorSetRGB(0xff, 0xff, 0x00);
EXPECT_EQ(expected_color, actual_color);
}
FinishCompositorControllerTest();
}
// Enough to cover a full animation iteration.
static constexpr int kNumFramesFirstIteration = 7;
// Advances two animation frames only.
static constexpr int kNumFramesSecondIteration = 5;
// Advances a full animation iteration.
static constexpr int kNumFramesThirdIteration = 6;
};
/* static */
constexpr int
CompositorControllerImageAnimationBrowserTest::kNumFramesFirstIteration;
/* static */
constexpr int
CompositorControllerImageAnimationBrowserTest::kNumFramesSecondIteration;
/* static */
constexpr int
CompositorControllerImageAnimationBrowserTest::kNumFramesThirdIteration;
HEADLESS_ASYNC_DEVTOOLED_TEST_P(CompositorControllerImageAnimationBrowserTest);
// Instantiate test case for both software and gpu compositing modes.
INSTANTIATE_TEST_CASE_P(CompositorControllerImageAnimationBrowserTests,
CompositorControllerImageAnimationBrowserTest,
::testing::Bool());
// Loads a CSS animation and verifies that:
// - animate_only BeginFrames don't produce CompositorFrames,
// - animate_only BeginFrames advance animations and trigger itersection events,
// - animation is advanced according to virtual time.
class CompositorControllerCssAnimationBrowserTest
: public CompositorControllerBrowserTest,
public runtime::Observer {
private:
base::TimeDelta GetAnimationFrameInterval() const override {
return base::TimeDelta::FromMilliseconds(500);
}
std::string GetTestFile() const override {
// Animates opacity of a blue 100px square on red blackground over 4
// seconds (100% -> 0% -> 100% four times). Logs events to console.
//
// Timeline:
// 0 ms: --- animation starts at 500ms ---
// 500 ms: 100% opacity -> blue background.
// 1000 ms: 0% opacity -> red background.
// 1500 ms: 100% opacity -> blue background.
// 2000 ms: 0% opacity -> red background.
// 2500 ms: 100% opacity -> blue background.
// 3000 ms: 0% opacity -> red background.
// 3500 ms: 100% opacity -> blue background.
// 4000 ms: 0% opacity -> red background.
// 4500 ms: 100% opacity -> blue background.
//
// The animation will start with the first BeginFrame after load.
return "/css_animation.html";
}
void OnFirstBeginFrameComplete() override {
CompositorControllerBrowserTest::OnFirstBeginFrameComplete();
// First frame advanced one BeginFrame interval.
elapsed_time_ += GetAnimationFrameInterval();
// First BeginFrame advanced by one interval.
devtools_client_->GetRuntime()->AddObserver(this);
devtools_client_->GetRuntime()->Enable(base::BindRepeating(
&CompositorControllerCssAnimationBrowserTest::RuntimeEnabled,
base::Unretained(this)));
}
void RuntimeEnabled() {
// Animation starts with the first BeginFrame of this budget. Advance five
// frames to reach 3000ms, at which point the background should be red.
GrantBudget(GetAnimationFrameInterval() * 5);
}
void GrantBudget(base::TimeDelta budget) {
// Grant the budget in two halves, with screenshots at the end of each.
// AdditionalVirtualTimeBudget will self delete.
new AdditionalVirtualTimeBudget(
virtual_time_controller_.get(),
AdditionalVirtualTimeBudget::StartPolicy::START_IMMEDIATELY, budget,
base::BindOnce(
&CompositorControllerCssAnimationBrowserTest::OnBudgetExpired,
base::Unretained(this), budget));
}
void OnBudgetExpired(base::TimeDelta budget) {
elapsed_time_ += budget;
EXPECT_THAT(
elapsed_time_,
testing::AnyOf(testing::Eq(base::TimeDelta::FromMilliseconds(3000)),
testing::Eq(base::TimeDelta::FromMilliseconds(4500))));
if (elapsed_time_ == base::TimeDelta::FromMilliseconds(3000)) {
// We should have advanced five BeginFrames. No CompositorFrames from
// renderer because update_display_for_animations is false.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(5, 0));
} else {
// We should have advanced two more BeginFrames since the second budget
// was preceded by a screenshot. No CompositorFrames from renderer
// because update_display_for_animations is false.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(2, 0));
}
compositor_controller_->CaptureScreenshot(
headless_experimental::ScreenshotParamsFormat::PNG, 100,
base::BindRepeating(
&CompositorControllerCssAnimationBrowserTest::OnScreenshot,
base::Unretained(this)));
}
void OnScreenshot(const std::string& screenshot_data) {
// Screenshot should have incurred a new CompositorFrame from renderer.
EXPECT_SCOPED(ExpectAdditionalFrameCounts(1, 1));
EXPECT_LT(0U, screenshot_data.length());
if (screenshot_data.length()) {
SkBitmap result_bitmap;
EXPECT_TRUE(DecodePNG(screenshot_data, &result_bitmap));
EXPECT_EQ(800, result_bitmap.width());
EXPECT_EQ(600, result_bitmap.height());
SkColor actual_color = result_bitmap.getColor(50, 50);
// First screenshot should be red, because box is not visible.
SkColor expected_color = SkColorSetRGB(0xff, 0x00, 0x00);
if (elapsed_time_ == base::TimeDelta::FromMilliseconds(4500)) {
// Box is visible in second screenshot, so it should be blue.
expected_color = SkColorSetRGB(0x00, 0x00, 0xff);
}
EXPECT_EQ(expected_color, actual_color);
}
if (elapsed_time_ == base::TimeDelta::FromMilliseconds(3000)) {
// Advance to the end of the animation.
GrantBudget(base::TimeDelta::FromMilliseconds(1500));
} else {
EXPECT_THAT(log_, testing::ElementsAre(
// Animation actually started at 500ms, but the
// event is executed a BeginFrame later.
"event animationstart at 101000ms",
"event animationiteration at 101500ms",
"event animationiteration at 102500ms",
"event animationiteration at 103500ms",
"event animationend at 104500ms"));
FinishCompositorControllerTest();
}
}
// runtime::Observer implementation:
void OnConsoleAPICalled(
const runtime::ConsoleAPICalledParams& params) override {
// We expect the arguments always to be a single string.
const std::vector<std::unique_ptr<runtime::RemoteObject>>& args =
*params.GetArgs();
if (args.size() == 1u && args[0]->HasValue())
log_.push_back(args[0]->GetValue()->GetString());
}
base::TimeDelta elapsed_time_;
std::vector<std::string> log_;
};
HEADLESS_ASYNC_DEVTOOLED_TEST_P(CompositorControllerCssAnimationBrowserTest);
// Instantiate test case for both software and gpu compositing modes.
INSTANTIATE_TEST_CASE_P(CompositorControllerCssAnimationBrowserTests,
CompositorControllerCssAnimationBrowserTest,
::testing::Bool());
#endif // !defined(OS_MACOSX)
} // namespace headless
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/public/util/compositor_controller.h"
#include <memory>
#include "base/base64.h"
#include "base/bind.h"
#include "base/json/json_writer.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/test_simple_task_runner.h"
#include "headless/public/internal/headless_devtools_client_impl.h"
#include "headless/public/util/virtual_time_controller.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace headless {
namespace {
static constexpr base::TimeDelta kAnimationFrameInterval =
base::TimeDelta::FromMilliseconds(16);
class MockChannel : public HeadlessDevToolsChannel {
public:
MockChannel() {}
~MockChannel() override {}
MOCK_METHOD1(SetClient, void(HeadlessDevToolsChannel::Client*));
MOCK_METHOD1(SendProtocolMessage, void(const std::string&));
};
} // namespace
using testing::_;
using testing::Return;
class TestVirtualTimeController : public VirtualTimeController {
public:
TestVirtualTimeController(HeadlessDevToolsClient* devtools_client)
: VirtualTimeController(devtools_client) {}
~TestVirtualTimeController() override = default;
MOCK_METHOD0(StartVirtualTime, void());
MOCK_METHOD2(ScheduleRepeatingTask,
void(RepeatingTask* task, base::TimeDelta interval));
MOCK_METHOD1(CancelRepeatingTask, void(RepeatingTask* task));
MOCK_METHOD1(AddObserver, void(Observer* observer));
MOCK_METHOD1(RemoveObserver, void(Observer* observer));
MOCK_METHOD1(SetResumeDeferrer, void(ResumeDeferrer* deferrer));
MOCK_CONST_METHOD0(GetVirtualTimeBase, base::TimeTicks());
MOCK_CONST_METHOD0(GetCurrentVirtualTimeOffset, base::TimeDelta());
};
class CompositorControllerTest : public ::testing::Test {
protected:
CompositorControllerTest(bool update_display_for_animations = true) {
task_runner_ = base::MakeRefCounted<base::TestSimpleTaskRunner>();
auto channel = std::make_unique<MockChannel>();
mock_channel_ = channel.get();
client_.AttachToChannel(std::move(channel));
client_.SetTaskRunnerForTests(task_runner_);
virtual_time_controller_ =
std::make_unique<TestVirtualTimeController>(&client_);
EXPECT_CALL(*virtual_time_controller_,
ScheduleRepeatingTask(_, kAnimationFrameInterval))
.WillOnce(testing::SaveArg<0>(&task_));
EXPECT_CALL(*virtual_time_controller_, SetResumeDeferrer(_))
.WillOnce(testing::SaveArg<0>(&deferrer_));
ExpectHeadlessExperimentalEnable();
controller_ = std::make_unique<CompositorController>(
task_runner_, &client_, virtual_time_controller_.get(),
kAnimationFrameInterval, update_display_for_animations);
EXPECT_NE(nullptr, task_);
}
~CompositorControllerTest() override {
EXPECT_CALL(*virtual_time_controller_, CancelRepeatingTask(_));
EXPECT_CALL(*virtual_time_controller_, SetResumeDeferrer(_));
}
void ExpectHeadlessExperimentalEnable() {
last_command_id_ += 2;
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(base::StringPrintf(
"{\"id\":%d,\"method\":\"HeadlessExperimental.enable\","
"\"params\":{}}",
last_command_id_)));
}
void ExpectVirtualTime(double base, double offset) {
auto base_time_ticks =
base::TimeTicks() + base::TimeDelta::FromMillisecondsD(base);
auto offset_delta = base::TimeDelta::FromMilliseconds(offset);
EXPECT_CALL(*virtual_time_controller_, GetVirtualTimeBase())
.WillOnce(Return(base_time_ticks));
EXPECT_CALL(*virtual_time_controller_, GetCurrentVirtualTimeOffset())
.WillOnce(Return(offset_delta));
// Next BeginFrame's time should be the virtual time provided it has
// progressed.
base::TimeTicks virtual_time = base_time_ticks + offset_delta;
if (virtual_time > next_begin_frame_time_)
next_begin_frame_time_ = virtual_time;
}
void ExpectBeginFrame(bool no_display_updates = false,
std::unique_ptr<headless_experimental::ScreenshotParams>
screenshot_params = nullptr) {
last_command_id_ += 2;
base::DictionaryValue params;
auto builder = std::move(
headless_experimental::BeginFrameParams::Builder()
.SetFrameTimeTicks(
(next_begin_frame_time_ - base::TimeTicks()).InMillisecondsF())
.SetInterval(kAnimationFrameInterval.InMillisecondsF())
.SetNoDisplayUpdates(no_display_updates));
if (screenshot_params)
builder.SetScreenshot(std::move(screenshot_params));
// Subsequent BeginFrames should have a later timestamp.
next_begin_frame_time_ += base::TimeDelta::FromMicroseconds(1);
std::string params_json;
auto params_value = builder.Build()->Serialize();
base::JSONWriter::Write(*params_value, &params_json);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(base::StringPrintf(
"{\"id\":%d,\"method\":\"HeadlessExperimental.beginFrame\","
"\"params\":%s}",
last_command_id_, params_json.c_str())));
}
void SendBeginFrameReply(bool has_damage,
const std::string& screenshot_data) {
auto result = headless_experimental::BeginFrameResult::Builder()
.SetHasDamage(has_damage)
.Build();
if (screenshot_data.length())
result->SetScreenshotData(screenshot_data);
std::string result_json;
auto result_value = result->Serialize();
base::JSONWriter::Write(*result_value, &result_json);
client_.ReceiveProtocolMessage(base::StringPrintf(
"{\"id\":%d,\"result\":%s}", last_command_id_, result_json.c_str()));
task_runner_->RunPendingTasks();
}
void SendNeedsBeginFramesEvent(bool needs_begin_frames) {
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"method\":\"HeadlessExperimental."
"needsBeginFramesChanged\",\"params\":{"
"\"needsBeginFrames\":%s}}",
needs_begin_frames ? "true" : "false"));
// Events are dispatched asynchronously.
task_runner_->RunPendingTasks();
}
scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
HeadlessDevToolsClientImpl client_;
MockChannel* mock_channel_ = nullptr;
std::unique_ptr<TestVirtualTimeController> virtual_time_controller_;
std::unique_ptr<CompositorController> controller_;
int last_command_id_ = -2;
TestVirtualTimeController::RepeatingTask* task_ = nullptr;
TestVirtualTimeController::Observer* observer_ = nullptr;
TestVirtualTimeController::ResumeDeferrer* deferrer_ = nullptr;
base::TimeTicks next_begin_frame_time_ =
base::TimeTicks() + base::TimeDelta::FromMicroseconds(1);
};
TEST_F(CompositorControllerTest, CaptureScreenshot) {
bool done = false;
controller_->CaptureScreenshot(
headless_experimental::ScreenshotParamsFormat::PNG, 100,
base::BindRepeating(
[](bool* done, const std::string& screenshot_data) {
*done = true;
EXPECT_EQ("test", screenshot_data);
},
&done));
EXPECT_TRUE(task_runner_->HasPendingTask());
ExpectVirtualTime(0, 0);
ExpectBeginFrame(
false, headless_experimental::ScreenshotParams::Builder()
.SetFormat(headless_experimental::ScreenshotParamsFormat::PNG)
.SetQuality(100)
.Build());
task_runner_->RunPendingTasks();
std::string base64;
base::Base64Encode("test", &base64);
SendBeginFrameReply(true, base64);
EXPECT_FALSE(task_runner_->HasPendingTask());
EXPECT_TRUE(done);
}
TEST_F(CompositorControllerTest, SendsAnimationFrames) {
base::Optional<VirtualTimeController::RepeatingTask::ContinuePolicy>
continue_policy;
auto continue_callback = base::BindRepeating(
[](base::Optional<VirtualTimeController::RepeatingTask::ContinuePolicy>*
continue_policy,
VirtualTimeController::RepeatingTask::ContinuePolicy policy) {
*continue_policy = policy;
},
&continue_policy);
// Doesn't send BeginFrames before virtual time started.
SendNeedsBeginFramesEvent(true);
EXPECT_FALSE(task_runner_->HasPendingTask());
bool can_continue = false;
auto defer_callback = base::BindRepeating(
[](bool* can_continue) { *can_continue = true; }, &can_continue);
// Sends a BeginFrame at start of interval.
deferrer_->DeferResume(defer_callback);
EXPECT_TRUE(task_runner_->HasPendingTask());
ExpectVirtualTime(1000, 0);
ExpectBeginFrame();
task_runner_->RunPendingTasks();
EXPECT_FALSE(can_continue);
// Lets virtual time continue after BeginFrame was completed.
SendBeginFrameReply(false, std::string());
EXPECT_FALSE(task_runner_->HasPendingTask());
EXPECT_TRUE(can_continue);
can_continue = false;
// Sends a BeginFrame after interval elapsed, but only just before virtual
// time resumes.
task_->IntervalElapsed(kAnimationFrameInterval, continue_callback);
EXPECT_EQ(VirtualTimeController::RepeatingTask::ContinuePolicy::NOT_REQUIRED,
*continue_policy);
continue_policy = base::nullopt;
EXPECT_FALSE(task_runner_->HasPendingTask());
deferrer_->DeferResume(defer_callback);
EXPECT_TRUE(task_runner_->HasPendingTask());
ExpectVirtualTime(1000, kAnimationFrameInterval.InMillisecondsF());
ExpectBeginFrame();
task_runner_->RunPendingTasks();
EXPECT_FALSE(can_continue);
// Lets virtual time continue after BeginFrame was completed.
SendBeginFrameReply(false, std::string());
EXPECT_FALSE(task_runner_->HasPendingTask());
EXPECT_TRUE(can_continue);
can_continue = false;
// Doesn't send a BeginFrame if another task pauses and resumes virtual time.
deferrer_->DeferResume(defer_callback);
EXPECT_FALSE(task_runner_->HasPendingTask());
EXPECT_TRUE(can_continue);
can_continue = false;
// Sends another BeginFrame after next animation interval elapsed.
task_->IntervalElapsed(kAnimationFrameInterval, continue_callback);
EXPECT_EQ(VirtualTimeController::RepeatingTask::ContinuePolicy::NOT_REQUIRED,
*continue_policy);
continue_policy = base::nullopt;
EXPECT_FALSE(task_runner_->HasPendingTask());
deferrer_->DeferResume(defer_callback);
EXPECT_TRUE(task_runner_->HasPendingTask());
ExpectVirtualTime(1000, kAnimationFrameInterval.InMillisecondsF() * 2);
ExpectBeginFrame();
task_runner_->RunPendingTasks();
EXPECT_FALSE(can_continue);
// Lets virtual time continue after BeginFrame was completed.
SendBeginFrameReply(false, std::string());
EXPECT_FALSE(task_runner_->HasPendingTask());
EXPECT_TRUE(can_continue);
can_continue = false;
}
TEST_F(CompositorControllerTest, WaitUntilIdle) {
bool idle = false;
auto idle_callback =
base::BindRepeating([](bool* idle) { *idle = true; }, &idle);
SendNeedsBeginFramesEvent(true);
EXPECT_FALSE(task_runner_->HasPendingTask());
// WaitUntilIdle executes callback immediately if no BeginFrame is active.
controller_->WaitUntilIdle(idle_callback);
EXPECT_TRUE(idle);
idle = false;
// Send a BeginFrame.
task_->IntervalElapsed(
kAnimationFrameInterval,
base::BindRepeating(
[](VirtualTimeController::RepeatingTask::ContinuePolicy) {}));
EXPECT_FALSE(task_runner_->HasPendingTask());
bool can_continue = false;
auto defer_callback = base::BindRepeating(
[](bool* can_continue) { *can_continue = true; }, &can_continue);
deferrer_->DeferResume(defer_callback);
EXPECT_TRUE(task_runner_->HasPendingTask());
ExpectVirtualTime(1000, kAnimationFrameInterval.InMillisecondsF());
ExpectBeginFrame();
task_runner_->RunPendingTasks();
EXPECT_FALSE(can_continue);
// WaitUntilIdle only executes callback after BeginFrame was completed.
controller_->WaitUntilIdle(idle_callback);
EXPECT_FALSE(idle);
SendBeginFrameReply(false, std::string());
EXPECT_FALSE(task_runner_->HasPendingTask());
EXPECT_TRUE(idle);
idle = false;
EXPECT_TRUE(can_continue);
can_continue = false;
}
class CompositorControllerNoDisplayUpdateTest
: public CompositorControllerTest {
protected:
CompositorControllerNoDisplayUpdateTest() : CompositorControllerTest(false) {}
};
TEST_F(CompositorControllerNoDisplayUpdateTest,
SkipsDisplayUpdateOnlyForAnimationFrames) {
base::Optional<VirtualTimeController::RepeatingTask::ContinuePolicy>
continue_policy;
auto continue_callback = base::BindRepeating(
[](base::Optional<VirtualTimeController::RepeatingTask::ContinuePolicy>*
continue_policy,
VirtualTimeController::RepeatingTask::ContinuePolicy policy) {
*continue_policy = policy;
},
&continue_policy);
SendNeedsBeginFramesEvent(true);
EXPECT_FALSE(task_runner_->HasPendingTask());
bool can_continue = false;
auto defer_callback = base::BindRepeating(
[](bool* can_continue) { *can_continue = true; }, &can_continue);
// Initial animation-BeginFrame always updates display (see comment in
// compositor_controller.cc).
deferrer_->DeferResume(defer_callback);
EXPECT_TRUE(task_runner_->HasPendingTask());
ExpectVirtualTime(1000, 0);
ExpectBeginFrame();
task_runner_->RunPendingTasks();
EXPECT_FALSE(can_continue);
// Lets virtual time continue after BeginFrame was completed.
SendBeginFrameReply(false, std::string());
EXPECT_FALSE(task_runner_->HasPendingTask());
EXPECT_TRUE(can_continue);
can_continue = false;
// Sends an animation BeginFrame without display update after interval
// elapsed.
task_->IntervalElapsed(kAnimationFrameInterval, continue_callback);
EXPECT_EQ(VirtualTimeController::RepeatingTask::ContinuePolicy::NOT_REQUIRED,
*continue_policy);
continue_policy = base::nullopt;
EXPECT_FALSE(task_runner_->HasPendingTask());
deferrer_->DeferResume(defer_callback);
EXPECT_TRUE(task_runner_->HasPendingTask());
ExpectVirtualTime(1000, kAnimationFrameInterval.InMillisecondsF());
ExpectBeginFrame(true);
task_runner_->RunPendingTasks();
EXPECT_FALSE(can_continue);
// Lets virtual time continue after BeginFrame was completed.
SendBeginFrameReply(false, std::string());
EXPECT_FALSE(task_runner_->HasPendingTask());
EXPECT_TRUE(can_continue);
can_continue = false;
// Screenshots update display.
task_->IntervalElapsed(kAnimationFrameInterval, continue_callback);
EXPECT_EQ(VirtualTimeController::RepeatingTask::ContinuePolicy::NOT_REQUIRED,
*continue_policy);
continue_policy = base::nullopt;
EXPECT_FALSE(task_runner_->HasPendingTask());
controller_->CaptureScreenshot(
headless_experimental::ScreenshotParamsFormat::PNG, 100,
base::BindRepeating([](const std::string&) {}));
EXPECT_TRUE(task_runner_->HasPendingTask());
ExpectVirtualTime(1000, kAnimationFrameInterval.InMillisecondsF() * 2);
ExpectBeginFrame(
false, headless_experimental::ScreenshotParams::Builder()
.SetFormat(headless_experimental::ScreenshotParamsFormat::PNG)
.SetQuality(100)
.Build());
task_runner_->RunPendingTasks();
}
} // namespace headless
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/public/util/virtual_time_controller.h"
#include "base/auto_reset.h"
#include "base/bind.h"
#include "base/logging.h"
namespace headless {
using base::TimeDelta;
VirtualTimeController::VirtualTimeController(
HeadlessDevToolsClient* devtools_client,
int max_task_starvation_count)
: devtools_client_(devtools_client),
max_task_starvation_count_(max_task_starvation_count),
weak_ptr_factory_(this) {
devtools_client_->GetEmulation()->GetExperimental()->AddObserver(this);
}
VirtualTimeController::~VirtualTimeController() {
devtools_client_->GetEmulation()->GetExperimental()->RemoveObserver(this);
}
void VirtualTimeController::StartVirtualTime() {
if (virtual_time_started_)
return;
TimeDelta next_budget;
bool wait_for_navigation = false;
for (auto& entry_pair : tasks_) {
entry_pair.second.ready_to_advance = true;
if (entry_pair.first->start_policy() ==
RepeatingTask::StartPolicy::WAIT_FOR_NAVIGATION) {
wait_for_navigation = true;
}
if (next_budget.is_zero()) {
next_budget =
entry_pair.second.next_execution_time - total_elapsed_time_offset_;
} else {
next_budget =
std::min(next_budget, entry_pair.second.next_execution_time -
total_elapsed_time_offset_);
}
}
// If there's no budget, then don't do anything!
if (next_budget.is_zero())
return;
virtual_time_started_ = true;
should_send_start_notification_ = true;
if (resume_deferrer_) {
resume_deferrer_->DeferResume(base::BindOnce(
&VirtualTimeController::SetVirtualTimePolicy,
weak_ptr_factory_.GetWeakPtr(), next_budget, wait_for_navigation));
} else {
SetVirtualTimePolicy(next_budget, wait_for_navigation);
}
}
void VirtualTimeController::NotifyTasksAndAdvance() {
// The task may call its continue callback synchronously. Prevent re-entrance.
if (in_notify_tasks_and_advance_)
return;
base::AutoReset<bool> reset(&in_notify_tasks_and_advance_, true);
for (auto iter = tasks_.begin(); iter != tasks_.end();) {
auto entry_pair = iter++;
if (entry_pair->second.next_execution_time <= total_elapsed_time_offset_) {
entry_pair->second.ready_to_advance = false;
entry_pair->second.next_execution_time =
total_elapsed_time_offset_ + entry_pair->second.interval;
// This may delete itself.
entry_pair->first->IntervalElapsed(
total_elapsed_time_offset_,
base::BindOnce(&VirtualTimeController::TaskReadyToAdvance,
weak_ptr_factory_.GetWeakPtr(),
base::Unretained(&entry_pair->second)));
}
}
// Give at most as much virtual time as available until the next callback.
bool advance_virtual_time = false;
bool stop_virtual_time = false;
bool ready_to_advance = true;
TimeDelta next_budget;
for (const auto& entry_pair : tasks_) {
ready_to_advance &= entry_pair.second.ready_to_advance;
if (next_budget.is_zero()) {
next_budget =
entry_pair.second.next_execution_time - total_elapsed_time_offset_;
} else {
next_budget =
std::min(next_budget, entry_pair.second.next_execution_time -
total_elapsed_time_offset_);
}
if (entry_pair.second.continue_policy ==
RepeatingTask::ContinuePolicy::CONTINUE_MORE_TIME_NEEDED) {
advance_virtual_time = true;
} else if (entry_pair.second.continue_policy ==
RepeatingTask::ContinuePolicy::STOP) {
stop_virtual_time = true;
}
}
if (!ready_to_advance)
return;
if (!advance_virtual_time || stop_virtual_time) {
for (auto& entry_pair : tasks_) {
entry_pair.second.ready_to_advance = false;
}
for (auto iter = observers_.begin(); iter != observers_.end();) {
Observer* observer = *iter++;
// |observer| may delete itself.
observer->VirtualTimeStopped(total_elapsed_time_offset_);
}
virtual_time_started_ = false;
return;
}
DCHECK(!next_budget.is_zero());
if (resume_deferrer_) {
resume_deferrer_->DeferResume(
base::BindOnce(&VirtualTimeController::SetVirtualTimePolicy,
weak_ptr_factory_.GetWeakPtr(), next_budget,
false /* wait_for_navigation */));
} else {
SetVirtualTimePolicy(next_budget, false /* wait_for_navigation */);
}
}
void VirtualTimeController::TaskReadyToAdvance(
TaskEntry* entry,
RepeatingTask::ContinuePolicy continue_policy) {
entry->ready_to_advance = true;
entry->continue_policy = continue_policy;
NotifyTasksAndAdvance();
}
void VirtualTimeController::SetVirtualTimePolicy(base::TimeDelta next_budget,
bool wait_for_navigation) {
last_budget_ = next_budget;
devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy(
emulation::SetVirtualTimePolicyParams::Builder()
.SetPolicy(
emulation::VirtualTimePolicy::PAUSE_IF_NETWORK_FETCHES_PENDING)
.SetBudget(next_budget.InMillisecondsF())
.SetMaxVirtualTimeTaskStarvationCount(max_task_starvation_count_)
.SetWaitForNavigation(wait_for_navigation)
.Build(),
base::BindOnce(&VirtualTimeController::SetVirtualTimePolicyDone,
weak_ptr_factory_.GetWeakPtr()));
}
void VirtualTimeController::SetVirtualTimePolicyDone(
std::unique_ptr<emulation::SetVirtualTimePolicyResult> result) {
if (result) {
virtual_time_base_ =
base::TimeTicks() +
base::TimeDelta::FromMillisecondsD(result->GetVirtualTimeTicksBase());
} else {
LOG(WARNING) << "SetVirtualTimePolicy did not succeed";
}
if (should_send_start_notification_) {
should_send_start_notification_ = false;
for (auto iter = observers_.begin(); iter != observers_.end();) {
Observer* observer = *iter++;
// |observer| may delete itself.
observer->VirtualTimeStarted(total_elapsed_time_offset_);
}
}
}
void VirtualTimeController::OnVirtualTimeBudgetExpired(
const emulation::VirtualTimeBudgetExpiredParams& params) {
total_elapsed_time_offset_ += last_budget_;
virtual_time_paused_ = true;
NotifyTasksAndAdvance();
}
void VirtualTimeController::ScheduleRepeatingTask(RepeatingTask* task,
base::TimeDelta interval) {
if (!virtual_time_paused_) {
// We cannot accurately modify any previously granted virtual time budget.
LOG(WARNING) << "VirtualTimeController tasks should be added while "
"virtual time is paused.";
}
TaskEntry entry;
entry.interval = interval;
entry.next_execution_time = total_elapsed_time_offset_ + entry.interval;
tasks_.insert(std::make_pair(task, entry));
}
void VirtualTimeController::CancelRepeatingTask(RepeatingTask* task) {
tasks_.erase(task);
}
void VirtualTimeController::AddObserver(Observer* observer) {
observers_.insert(observer);
}
void VirtualTimeController::RemoveObserver(Observer* observer) {
observers_.erase(observer);
}
base::TimeTicks VirtualTimeController::GetVirtualTimeBase() const {
return virtual_time_base_;
}
base::TimeDelta VirtualTimeController::GetCurrentVirtualTimeOffset() const {
return total_elapsed_time_offset_;
}
void VirtualTimeController::SetResumeDeferrer(ResumeDeferrer* resume_deferrer) {
resume_deferrer_ = resume_deferrer;
}
} // namespace headless
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef HEADLESS_PUBLIC_UTIL_VIRTUAL_TIME_CONTROLLER_H_
#define HEADLESS_PUBLIC_UTIL_VIRTUAL_TIME_CONTROLLER_H_
#include "base/callback.h"
#include "base/time/time.h"
#include "headless/public/devtools/domains/emulation.h"
#include "headless/public/headless_devtools_client.h"
#include "headless/public/headless_export.h"
namespace headless {
// Controls how virtual time progresses. RepeatingTasks can register their
// interest to be periodically notified about changes to the current virtual
// time.
class HEADLESS_EXPORT VirtualTimeController
: public emulation::ExperimentalObserver {
public:
VirtualTimeController(HeadlessDevToolsClient* devtools_client,
int max_task_starvation_count = 0);
~VirtualTimeController() override;
// Signals that virtual time should start advancing. If virtual time is
// already running, this does nothing. When virtual time is ready to start
// the observers will be notified.
virtual void StartVirtualTime();
class RepeatingTask {
public:
// This policy controls whether or not StartVirtualTime() should wait for a
// navigation first.
enum StartPolicy {
WAIT_FOR_NAVIGATION,
START_IMMEDIATELY,
};
explicit RepeatingTask(StartPolicy start_policy, int priority)
: start_policy_(start_policy), priority_(priority) {}
virtual ~RepeatingTask() {}
enum class ContinuePolicy {
CONTINUE_MORE_TIME_NEEDED,
NOT_REQUIRED,
STOP, // Note STOP trumps CONTINUE_MORE_TIME_NEEDED.
};
// Called when the tasks's requested virtual time interval has elapsed.
// |virtual_time_offset| is the virtual time duration that has advanced
// since the page started loading (millisecond granularity). When the task
// has completed it's perioodic work it should call |continue_callback|
// with CONTINUE_MORE_TIME_NEEDED if it wants virtual time to continue
// advancing, or NOT_REQUIRED otherwise. Virtual time will continue to
// advance until all RepeatingTasks want it to stop.
virtual void IntervalElapsed(
base::TimeDelta virtual_time_offset,
base::OnceCallback<void(ContinuePolicy policy)> continue_callback) = 0;
StartPolicy start_policy() const { return start_policy_; }
int priority() const { return priority_; }
private:
const StartPolicy start_policy_;
// If more than one RepeatingTask is scheduled to run at any instant they
// are run in order of ascending |priority_|.
const int priority_;
};
// An API used by the CompositorController to defer the start and resumption
// of virtual time until it's ready.
class ResumeDeferrer {
public:
virtual ~ResumeDeferrer() {}
// Called before virtual time progression resumes after it was stopped or
// paused to execute repeating tasks.
virtual void DeferResume(base::OnceClosure ready_callback) = 0;
};
class Observer {
public:
virtual ~Observer() {}
// Called when StartVirtualTime was called. May be delayed by a
// StartDeferrer.
virtual void VirtualTimeStarted(base::TimeDelta virtual_time_offset) = 0;
// Called when all RepeatingTasks have either voted for virtual time to stop
// advancing, or all have been removed.
virtual void VirtualTimeStopped(base::TimeDelta virtual_time_offset) = 0;
};
// Interleaves execution of the provided |task| with progression of virtual
// time. The task will be notified whenever another |interval| of virtual time
// have elapsed, as well as when the last granted budget has been used up.
//
// To ensure that the task is notified of elapsed intervals accurately, it
// should be added while virtual time is paused.
virtual void ScheduleRepeatingTask(RepeatingTask* task,
base::TimeDelta interval);
virtual void CancelRepeatingTask(RepeatingTask* task);
// Adds an observer which is notified when virtual time starts and stops.
virtual void AddObserver(Observer* observer);
virtual void RemoveObserver(Observer* observer);
// Returns the time that virtual time offsets are relative to.
virtual base::TimeTicks GetVirtualTimeBase() const;
// Returns the current virtual time offset. Only accurate while virtual time
// is paused.
virtual base::TimeDelta GetCurrentVirtualTimeOffset() const;
// Returns the current virtual time stamp. Only accurate while virtual time
// is paused.
base::TimeTicks GetCurrentVirtualTime() const {
return GetVirtualTimeBase() + GetCurrentVirtualTimeOffset();
}
virtual void SetResumeDeferrer(ResumeDeferrer* resume_deferrer);
private:
struct TaskEntry {
base::TimeDelta interval;
base::TimeDelta next_execution_time;
bool ready_to_advance = true;
RepeatingTask::ContinuePolicy continue_policy =
RepeatingTask::ContinuePolicy::CONTINUE_MORE_TIME_NEEDED;
};
void ObserverReadyToStart();
// emulation::Observer implementation:
void OnVirtualTimeBudgetExpired(
const emulation::VirtualTimeBudgetExpiredParams& params) override;
void NotifyTasksAndAdvance();
void NotifyTaskIntervalElapsed(TaskEntry* entry);
void NotifyTaskVirtualTimeStarted(TaskEntry* entry);
void TaskReadyToAdvance(TaskEntry* entry,
RepeatingTask::ContinuePolicy continue_policy);
void SetVirtualTimePolicy(base::TimeDelta next_budget,
bool wait_for_navigation);
void SetVirtualTimePolicyDone(
std::unique_ptr<emulation::SetVirtualTimePolicyResult>);
HeadlessDevToolsClient* const devtools_client_; // NOT OWNED
ResumeDeferrer* resume_deferrer_ = nullptr; // NOT OWNED
const int max_task_starvation_count_;
base::TimeDelta total_elapsed_time_offset_;
base::TimeDelta last_budget_;
// Initial virtual time that virtual time offsets are relative to.
base::TimeTicks virtual_time_base_;
struct RepeatingTaskOrdering {
bool operator()(RepeatingTask* a, RepeatingTask* b) const {
if (a->priority() == b->priority())
return a < b;
return a->priority() < b->priority();
};
};
std::map<RepeatingTask*, TaskEntry, RepeatingTaskOrdering> tasks_;
std::set<Observer*> observers_;
bool in_notify_tasks_and_advance_ = false;
bool virtual_time_started_ = false;
bool virtual_time_paused_ = true;
bool should_send_start_notification_ = false;
base::WeakPtrFactory<VirtualTimeController> weak_ptr_factory_;
};
} // namespace headless
#endif // HEADLESS_PUBLIC_UTIL_VIRTUAL_TIME_CONTROLLER_H_
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/public/util/virtual_time_controller.h"
#include <memory>
#include <tuple>
#include "base/bind.h"
#include "base/memory/ref_counted.h"
#include "base/strings/stringprintf.h"
#include "base/test/test_simple_task_runner.h"
#include "headless/public/internal/headless_devtools_client_impl.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace headless {
using testing::ElementsAre;
using testing::Mock;
using testing::Return;
using testing::_;
namespace {
class MockChannel : public HeadlessDevToolsChannel {
public:
MockChannel() {}
~MockChannel() override {}
MOCK_METHOD1(SetClient, void(HeadlessDevToolsChannel::Client*));
MOCK_METHOD1(SendProtocolMessage, void(const std::string&));
};
} // namespace
class VirtualTimeControllerTest : public ::testing::Test {
protected:
VirtualTimeControllerTest() {
task_runner_ = base::MakeRefCounted<base::TestSimpleTaskRunner>();
auto channel = std::make_unique<MockChannel>();
mock_channel_ = channel.get();
client_.AttachToChannel(std::move(channel));
client_.SetTaskRunnerForTests(task_runner_);
controller_ = std::make_unique<VirtualTimeController>(&client_, 0);
}
~VirtualTimeControllerTest() override = default;
// TODO(alexclarke): This is a common pattern add a helper.
class AdditionalVirtualTimeBudget
: public VirtualTimeController::RepeatingTask,
public VirtualTimeController::Observer {
public:
AdditionalVirtualTimeBudget(VirtualTimeController* virtual_time_controller,
VirtualTimeControllerTest* test,
base::TimeDelta budget)
: RepeatingTask(StartPolicy::START_IMMEDIATELY, 0),
virtual_time_controller_(virtual_time_controller),
test_(test) {
virtual_time_controller_->ScheduleRepeatingTask(this, budget);
virtual_time_controller_->AddObserver(this);
virtual_time_controller_->StartVirtualTime();
}
~AdditionalVirtualTimeBudget() override {
virtual_time_controller_->RemoveObserver(this);
virtual_time_controller_->CancelRepeatingTask(this);
}
// headless::VirtualTimeController::RepeatingTask implementation:
void IntervalElapsed(
base::TimeDelta virtual_time,
base::OnceCallback<void(ContinuePolicy)> continue_callback) override {
std::move(continue_callback).Run(ContinuePolicy::NOT_REQUIRED);
}
// headless::VirtualTimeController::Observer:
void VirtualTimeStarted(base::TimeDelta virtual_time_offset) override {
EXPECT_FALSE(test_->set_up_complete_);
test_->set_up_complete_ = true;
}
void VirtualTimeStopped(base::TimeDelta virtual_time_offset) override {
EXPECT_FALSE(test_->budget_expired_);
test_->budget_expired_ = true;
delete this;
}
private:
headless::VirtualTimeController* const virtual_time_controller_;
VirtualTimeControllerTest* test_;
};
void GrantVirtualTimeBudget(int budget_ms) {
ASSERT_FALSE(set_up_complete_);
ASSERT_FALSE(budget_expired_);
// AdditionalVirtualTimeBudget will self delete
new AdditionalVirtualTimeBudget(
controller_.get(), this, base::TimeDelta::FromMilliseconds(budget_ms));
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
}
void SendVirtualTimeBudgetExpiredEvent() {
client_.ReceiveProtocolMessage(
"{\"method\":\"Emulation.virtualTimeBudgetExpired\",\"params\":{}}");
// Events are dispatched asynchronously.
task_runner_->RunPendingTasks();
}
scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
HeadlessDevToolsClientImpl client_;
MockChannel* mock_channel_ = nullptr;
std::unique_ptr<VirtualTimeController> controller_;
bool set_up_complete_ = false;
bool budget_expired_ = false;
};
TEST_F(VirtualTimeControllerTest, DoesNotAdvanceTimeWithoutTasks) {
controller_ = std::make_unique<VirtualTimeController>(&client_, 1000);
EXPECT_CALL(*mock_channel_, SendProtocolMessage(_)).Times(0);
controller_->StartVirtualTime();
}
TEST_F(VirtualTimeControllerTest, MaxVirtualTimeTaskStarvationCount) {
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":5000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
GrantVirtualTimeBudget(5000);
client_.ReceiveProtocolMessage(
"{\"id\":0,\"result\":{\"virtualTimeBase\":1."
"0,\"virtualTimeTicksBase\":1.0}}");
task_runner_->RunPendingTasks();
EXPECT_TRUE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
SendVirtualTimeBudgetExpiredEvent();
EXPECT_TRUE(budget_expired_);
}
namespace {
class MockTask : public VirtualTimeController::RepeatingTask {
public:
MockTask() : RepeatingTask(StartPolicy::START_IMMEDIATELY, 0) {}
MockTask(StartPolicy start_policy, int priority)
: RepeatingTask(start_policy, priority) {}
~MockTask() override { EXPECT_TRUE(!expected_virtual_time_offset_); }
// GMock doesn't support move only types
void IntervalElapsed(
base::TimeDelta virtual_time_offset,
base::OnceCallback<void(ContinuePolicy)> continue_callback) override {
EXPECT_EQ(*expected_virtual_time_offset_, virtual_time_offset);
expected_virtual_time_offset_ = base::nullopt;
std::move(continue_callback).Run(policy_);
interval_elapsed_ = true;
}
void ExpectCallOnceWithOffsetAndReturn(base::TimeDelta virtual_time_offset,
ContinuePolicy policy) {
expected_virtual_time_offset_ = virtual_time_offset;
policy_ = policy;
}
private:
base::Optional<base::TimeDelta> expected_virtual_time_offset_;
ContinuePolicy policy_ = ContinuePolicy::NOT_REQUIRED;
bool interval_elapsed_ = false;
};
class MockDeferrer : public VirtualTimeController::ResumeDeferrer {
public:
// GMock doesn't support move only types
void DeferResume(base::OnceCallback<void()> continue_callback) override {
continue_callback_ = std::move(continue_callback);
}
base::OnceCallback<void()> continue_callback_;
};
class MockObserver : public VirtualTimeController::Observer {
public:
MOCK_METHOD1(VirtualTimeStarted, void(base::TimeDelta virtual_time_offset));
MOCK_METHOD1(VirtualTimeStopped, void(base::TimeDelta virtual_time_offset));
};
ACTION_TEMPLATE(RunClosure,
HAS_1_TEMPLATE_PARAMS(int, k),
AND_0_VALUE_PARAMS()) {
std::get<k>(args).Run();
}
ACTION_P(RunClosure, closure) {
closure.Run();
};
} // namespace
TEST_F(VirtualTimeControllerTest, InterleavesTasksWithVirtualTime) {
MockTask task;
MockObserver observer;
controller_->AddObserver(&observer);
controller_->ScheduleRepeatingTask(&task,
base::TimeDelta::FromMilliseconds(1000));
EXPECT_CALL(observer,
VirtualTimeStarted(base::TimeDelta::FromMilliseconds(0)));
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
GrantVirtualTimeBudget(3000);
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
client_.ReceiveProtocolMessage(
"{\"id\":0,\"result\":{\"virtualTimeBase\":1."
"0,\"virtualTimeTicksBase\":1.0}}");
task_runner_->RunPendingTasks();
EXPECT_TRUE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
// We check that set_up_complete_callback is only run once, so reset it here.
set_up_complete_ = false;
for (int i = 1; i < 3; i++) {
task.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(1000 * i),
MockTask::ContinuePolicy::NOT_REQUIRED);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(base::StringPrintf(
"{\"id\":%d,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}",
i * 2)));
SendVirtualTimeBudgetExpiredEvent();
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":%d,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}",
i * 2));
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
}
task.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(3000),
MockTask::ContinuePolicy::NOT_REQUIRED);
EXPECT_CALL(observer,
VirtualTimeStopped(base::TimeDelta::FromMilliseconds(3000)));
SendVirtualTimeBudgetExpiredEvent();
EXPECT_FALSE(set_up_complete_);
EXPECT_TRUE(budget_expired_);
}
TEST_F(VirtualTimeControllerTest, CanceledTask) {
MockTask task;
controller_->ScheduleRepeatingTask(&task,
base::TimeDelta::FromMilliseconds(1000));
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
GrantVirtualTimeBudget(5000);
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
client_.ReceiveProtocolMessage(
"{\"id\":0,\"result\":{\"virtualTimeBase\":1."
"0,\"virtualTimeTicksBase\":1.0}}");
task_runner_->RunPendingTasks();
EXPECT_TRUE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
// We check that set_up_complete_callback is only run once, so reset it here.
set_up_complete_ = false;
task.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(1000),
MockTask::ContinuePolicy::NOT_REQUIRED);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":2,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
SendVirtualTimeBudgetExpiredEvent();
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":2,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}"));
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
controller_->CancelRepeatingTask(&task);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":4,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":3000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
SendVirtualTimeBudgetExpiredEvent();
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":4,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}"));
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
SendVirtualTimeBudgetExpiredEvent();
EXPECT_FALSE(set_up_complete_);
EXPECT_TRUE(budget_expired_);
}
TEST_F(VirtualTimeControllerTest, MultipleTasks) {
MockTask task1;
MockTask task2;
controller_->ScheduleRepeatingTask(&task1,
base::TimeDelta::FromMilliseconds(1000));
controller_->ScheduleRepeatingTask(&task2,
base::TimeDelta::FromMilliseconds(1000));
// We should only get one call to Emulation.setVirtualTimePolicy despite
// having two tasks.
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
GrantVirtualTimeBudget(2000);
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":0,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}"));
task_runner_->RunPendingTasks();
EXPECT_TRUE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
}
TEST_F(VirtualTimeControllerTest, StartPolicy) {
MockTask task1(MockTask::StartPolicy::START_IMMEDIATELY, 0);
MockTask task2(MockTask::StartPolicy::START_IMMEDIATELY, 0);
MockTask task3(MockTask::StartPolicy::WAIT_FOR_NAVIGATION, 0);
controller_->ScheduleRepeatingTask(&task1,
base::TimeDelta::FromMilliseconds(1000));
controller_->ScheduleRepeatingTask(&task2,
base::TimeDelta::FromMilliseconds(1000));
controller_->ScheduleRepeatingTask(&task3,
base::TimeDelta::FromMilliseconds(1000));
// Despite only one task asking for it we should get waitForNavigation:true
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":true}}"));
GrantVirtualTimeBudget(2000);
}
TEST_F(VirtualTimeControllerTest, DeferStartAndResume) {
MockDeferrer deferrer;
controller_->SetResumeDeferrer(&deferrer);
MockTask task1(MockTask::StartPolicy::START_IMMEDIATELY, 0);
controller_->ScheduleRepeatingTask(&task1,
base::TimeDelta::FromMilliseconds(1000));
EXPECT_FALSE(deferrer.continue_callback_);
// Shouldn't see the devtools command until the deferrer's callback has run.
EXPECT_CALL(*mock_channel_, SendProtocolMessage(_)).Times(0);
GrantVirtualTimeBudget(2000);
EXPECT_TRUE(deferrer.continue_callback_);
Mock::VerifyAndClearExpectations(mock_channel_);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
std::move(deferrer.continue_callback_).Run();
client_.ReceiveProtocolMessage(
"{\"id\":0,\"result\":{\"virtualTimeBase\":1."
"0,\"virtualTimeTicksBase\":1.0}}");
EXPECT_FALSE(deferrer.continue_callback_);
task1.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(1000),
MockTask::ContinuePolicy::NOT_REQUIRED);
// Even after executing task1, virtual time shouldn't resume until the
// deferrer's callback has run.
EXPECT_CALL(*mock_channel_, SendProtocolMessage(_)).Times(0);
SendVirtualTimeBudgetExpiredEvent();
EXPECT_TRUE(deferrer.continue_callback_);
Mock::VerifyAndClearExpectations(mock_channel_);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":2,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
std::move(deferrer.continue_callback_).Run();
}
class VirtualTimeTask : public VirtualTimeController::RepeatingTask,
public VirtualTimeController::Observer {
public:
using Task = base::RepeatingCallback<void(base::TimeDelta virtual_time)>;
VirtualTimeTask(VirtualTimeController* controller,
Task virtual_time_started_task,
Task interval_elapsed_task,
Task virtual_time_stopped_task,
int priority = 0)
: RepeatingTask(StartPolicy::START_IMMEDIATELY, priority),
controller_(controller),
virtual_time_started_task_(virtual_time_started_task),
interval_elapsed_task_(interval_elapsed_task),
virtual_time_stopped_task_(virtual_time_stopped_task) {
controller_->AddObserver(this);
}
~VirtualTimeTask() override { controller_->RemoveObserver(this); }
// VirtualTimeController::RepeatingTask:
void IntervalElapsed(
base::TimeDelta virtual_time,
base::OnceCallback<void(ContinuePolicy)> continue_callback) override {
std::move(continue_callback).Run(ContinuePolicy::NOT_REQUIRED);
interval_elapsed_task_.Run(virtual_time);
}
// VirtualTimeController::Observer:
void VirtualTimeStarted(base::TimeDelta virtual_time) override {
virtual_time_started_task_.Run(virtual_time);
}
void VirtualTimeStopped(base::TimeDelta virtual_time) override {
virtual_time_stopped_task_.Run(virtual_time);
};
VirtualTimeController* controller_; // NOT OWNED
Task virtual_time_started_task_;
Task interval_elapsed_task_;
Task virtual_time_stopped_task_;
};
TEST_F(VirtualTimeControllerTest, ReentrantTask) {
#if defined(__clang__)
std::vector<std::string> log;
VirtualTimeTask task_b(
controller_.get(),
base::BindRepeating([](base::TimeDelta virtual_time) {}),
base::BindRepeating(
[](std::vector<std::string>* log, VirtualTimeController* controller,
VirtualTimeTask* task_b, base::TimeDelta virtual_time) {
log->push_back(base::StringPrintf(
"B: interval elapsed @ %d",
static_cast<int>(virtual_time.InMilliseconds())));
controller->CancelRepeatingTask(task_b);
},
&log, controller_.get(), &task_b),
base::BindRepeating(
[](std::vector<std::string>* log, base::TimeDelta virtual_time) {
log->push_back(base::StringPrintf(
"B: virtual time stopped @ %d",
static_cast<int>(virtual_time.InMilliseconds())));
},
&log));
VirtualTimeTask task_a(
controller_.get(),
base::BindRepeating(
[](std::vector<std::string>* log, base::TimeDelta virtual_time) {
log->push_back(base::StringPrintf(
"Virtual time started @ %d",
static_cast<int>(virtual_time.InMilliseconds())));
},
&log),
base::BindRepeating(
[](std::vector<std::string>* log, VirtualTimeController* controller,
VirtualTimeTask* task_a, VirtualTimeTask* task_b,
base::TimeDelta virtual_time) {
log->push_back(base::StringPrintf(
"A: interval elapsed @ %d",
static_cast<int>(virtual_time.InMilliseconds())));
controller->CancelRepeatingTask(task_a);
controller->ScheduleRepeatingTask(
task_b, base::TimeDelta::FromMilliseconds(1500));
},
&log, controller_.get(), &task_a, &task_b),
base::BindRepeating(
[](std::vector<std::string>* log, base::TimeDelta virtual_time) {
log->push_back(base::StringPrintf(
"A: virtual time stopped @ %d",
static_cast<int>(virtual_time.InMilliseconds())));
},
&log));
controller_->ScheduleRepeatingTask(&task_a,
base::TimeDelta::FromMilliseconds(1000));
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
GrantVirtualTimeBudget(6000);
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":0,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}"));
Mock::VerifyAndClearExpectations(mock_channel_);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":2,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1500.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
SendVirtualTimeBudgetExpiredEvent();
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":2,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}"));
Mock::VerifyAndClearExpectations(mock_channel_);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":4,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":3500.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
SendVirtualTimeBudgetExpiredEvent();
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":4,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}"));
EXPECT_THAT(
log, ElementsAre("Virtual time started @ 0", "A: interval elapsed @ 1000",
"B: interval elapsed @ 2500"));
#endif
}
TEST_F(VirtualTimeControllerTest, Priority) {
std::vector<std::string> log;
VirtualTimeTask task_a(
controller_.get(),
base::BindRepeating([](base::TimeDelta virtual_time) {}),
base::BindRepeating(
[](std::vector<std::string>* log, base::TimeDelta virtual_time) {
log->push_back(base::StringPrintf(
"A: interval elapsed @ %d",
static_cast<int>(virtual_time.InMilliseconds())));
},
&log),
base::BindRepeating([](base::TimeDelta virtual_time) {}), 30);
VirtualTimeTask task_b(
controller_.get(),
base::BindRepeating([](base::TimeDelta virtual_time) {}),
base::BindRepeating(
[](std::vector<std::string>* log, base::TimeDelta virtual_time) {
log->push_back(base::StringPrintf(
"B: interval elapsed @ %d",
static_cast<int>(virtual_time.InMilliseconds())));
},
&log),
base::BindRepeating([](base::TimeDelta virtual_time) {}), 20);
VirtualTimeTask task_c(
controller_.get(),
base::BindRepeating([](base::TimeDelta virtual_time) {}),
base::BindRepeating(
[](std::vector<std::string>* log, base::TimeDelta virtual_time) {
log->push_back(base::StringPrintf(
"C: interval elapsed @ %d",
static_cast<int>(virtual_time.InMilliseconds())));
},
&log),
base::BindRepeating([](base::TimeDelta virtual_time) {}), 10);
controller_->ScheduleRepeatingTask(&task_a,
base::TimeDelta::FromMilliseconds(1000));
controller_->ScheduleRepeatingTask(&task_b,
base::TimeDelta::FromMilliseconds(1000));
controller_->ScheduleRepeatingTask(&task_c,
base::TimeDelta::FromMilliseconds(1000));
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
GrantVirtualTimeBudget(2000);
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":0,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}"));
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":2,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
SendVirtualTimeBudgetExpiredEvent();
EXPECT_THAT(log, ElementsAre("C: interval elapsed @ 1000",
"B: interval elapsed @ 1000",
"A: interval elapsed @ 1000"));
}
TEST_F(VirtualTimeControllerTest, ContinuePolicyContinueMoreTimeNeeded) {
MockTask task;
MockObserver observer;
controller_->AddObserver(&observer);
controller_->ScheduleRepeatingTask(&task,
base::TimeDelta::FromMilliseconds(1000));
EXPECT_CALL(observer,
VirtualTimeStarted(base::TimeDelta::FromMilliseconds(0)));
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
controller_->StartVirtualTime();
client_.ReceiveProtocolMessage(
"{\"id\":0,\"result\":{\"virtualTimeBase\":1."
"0,\"virtualTimeTicksBase\":1.0}}");
for (int i = 1; i < 4; i++) {
task.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(1000 * i),
MockTask::ContinuePolicy::CONTINUE_MORE_TIME_NEEDED);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(base::StringPrintf(
"{\"id\":%d,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}",
i * 2)));
SendVirtualTimeBudgetExpiredEvent();
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":%d,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}",
i * 2));
}
task.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(4000),
MockTask::ContinuePolicy::NOT_REQUIRED);
EXPECT_CALL(observer,
VirtualTimeStopped(base::TimeDelta::FromMilliseconds(4000)));
SendVirtualTimeBudgetExpiredEvent();
}
TEST_F(VirtualTimeControllerTest, ContinuePolicyStopAndRestart) {
MockTask task1;
MockTask task2;
MockTask task3;
MockObserver observer;
controller_->AddObserver(&observer);
controller_->ScheduleRepeatingTask(&task1,
base::TimeDelta::FromMilliseconds(1000));
controller_->ScheduleRepeatingTask(&task2,
base::TimeDelta::FromMilliseconds(1000));
controller_->ScheduleRepeatingTask(&task3,
base::TimeDelta::FromMilliseconds(4000));
EXPECT_CALL(observer,
VirtualTimeStarted(base::TimeDelta::FromMilliseconds(0)));
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
controller_->StartVirtualTime();
client_.ReceiveProtocolMessage(
"{\"id\":0,\"result\":{\"virtualTimeBase\":1."
"0,\"virtualTimeTicksBase\":1.0}}");
for (int i = 1; i < 4; i++) {
task1.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(1000 * i),
MockTask::ContinuePolicy::CONTINUE_MORE_TIME_NEEDED);
task2.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(1000 * i),
MockTask::ContinuePolicy::CONTINUE_MORE_TIME_NEEDED);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(base::StringPrintf(
"{\"id\":%d,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}",
i * 2)));
SendVirtualTimeBudgetExpiredEvent();
client_.ReceiveProtocolMessage(
base::StringPrintf("{\"id\":%d,\"result\":{\"virtualTimeBase\":1.0,"
"\"virtualTimeTicksBase\":1.0}}",
i * 2));
}
task1.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(4000),
MockTask::ContinuePolicy::CONTINUE_MORE_TIME_NEEDED);
// STOP should take precedence over CONTINUE_MORE_TIME_NEEDED.
task2.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(4000), MockTask::ContinuePolicy::STOP);
task3.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(4000),
MockTask::ContinuePolicy::CONTINUE_MORE_TIME_NEEDED);
EXPECT_CALL(observer,
VirtualTimeStopped(base::TimeDelta::FromMilliseconds(4000)));
SendVirtualTimeBudgetExpiredEvent();
// If we start again, no task should block initially.
EXPECT_CALL(observer,
VirtualTimeStarted(base::TimeDelta::FromMilliseconds(4000)));
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":8,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
controller_->StartVirtualTime();
client_.ReceiveProtocolMessage(
"{\"id\":8,\"result\":{\"virtualTimeBase\":1."
"0,\"virtualTimeTicksBase\":1.0}}");
task1.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(5000),
MockTask::ContinuePolicy::CONTINUE_MORE_TIME_NEEDED);
task2.ExpectCallOnceWithOffsetAndReturn(
base::TimeDelta::FromMilliseconds(5000),
MockTask::ContinuePolicy::CONTINUE_MORE_TIME_NEEDED);
EXPECT_CALL(*mock_channel_,
SendProtocolMessage(
"{\"id\":10,\"method\":\"Emulation.setVirtualTimePolicy\","
"\"params\":{\"budget\":1000.0,"
"\"maxVirtualTimeTaskStarvationCount\":0,\"policy\":"
"\"pauseIfNetworkFetchesPending\","
"\"waitForNavigation\":false}}"));
SendVirtualTimeBudgetExpiredEvent();
}
} // namespace headless
specific_include_rules = { specific_include_rules = {
"headless_render_test.cc": [
"+cc/base/switches.h",
"+components/viz/common/features.h",
"+components/viz/common/switches.h",
"+third_party/skia/include",
],
"headless_protocol_browsertest.cc": [ "headless_protocol_browsertest.cc": [
"+cc/base/switches.h", "+cc/base/switches.h",
"+components/viz/common/features.h", "+components/viz/common/features.h",
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <functional>
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "content/public/test/browser_test.h"
#include "headless/public/devtools/domains/dom_snapshot.h"
#include "headless/public/devtools/domains/page.h"
#include "headless/public/devtools/domains/runtime.h"
#include "headless/public/headless_devtools_client.h"
#include "headless/test/headless_render_test.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#define HEADLESS_RENDER_BROWSERTEST(clazz) \
class HeadlessRenderBrowserTest##clazz : public clazz {}; \
HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessRenderBrowserTest##clazz)
#define DISABLED_HEADLESS_RENDER_BROWSERTEST(clazz) \
class HeadlessRenderBrowserTest##clazz : public clazz {}; \
DISABLED_HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessRenderBrowserTest##clazz)
// TODO(dats): For some reason we are missing all HTTP redirects.
// crbug.com/789298
#define DISABLE_HTTP_REDIRECTS_CHECKS
namespace headless {
namespace {
constexpr char kSomeUrl[] = "http://example.com/foobar";
constexpr char kTextHtml[] = "text/html";
constexpr char kApplicationOctetStream[] = "application/octet-stream";
constexpr char kImagePng[] = "image/png";
constexpr char kImageSvgXml[] = "image/svg+xml";
using dom_snapshot::GetSnapshotResult;
using dom_snapshot::DOMNode;
using dom_snapshot::LayoutTreeNode;
using net::test_server::HttpRequest;
using net::test_server::HttpResponse;
using net::test_server::BasicHttpResponse;
using net::test_server::RawHttpResponse;
using page::FrameScheduledNavigationReason;
using testing::ElementsAre;
using testing::UnorderedElementsAre;
using testing::Eq;
using testing::Ne;
using testing::StartsWith;
template <typename T, typename V>
std::vector<T> ElementsView(const std::vector<std::unique_ptr<V>>& elements,
std::function<bool(const V&)> filter,
std::function<T(const V&)> transform) {
std::vector<T> result;
for (const auto& element : elements) {
if (filter(*element))
result.push_back(transform(*element));
}
return result;
}
bool HasType(int type, const DOMNode& node) {
return node.GetNodeType() == type;
}
bool HasName(const char* name, const DOMNode& node) {
return node.GetNodeName() == name;
}
bool IsTag(const DOMNode& node) {
return HasType(1, node);
}
bool IsText(const DOMNode& node) {
return HasType(3, node);
}
std::vector<std::string> TextLayout(const GetSnapshotResult* snapshot) {
return ElementsView<std::string, LayoutTreeNode>(
*snapshot->GetLayoutTreeNodes(),
[](const auto& node) { return node.HasLayoutText(); },
[](const auto& node) { return node.GetLayoutText(); });
}
std::vector<const DOMNode*> FilterDOM(
const GetSnapshotResult* snapshot,
std::function<bool(const DOMNode&)> filter) {
return ElementsView<const DOMNode*, DOMNode>(
*snapshot->GetDomNodes(), filter, [](const auto& n) { return &n; });
}
std::vector<const DOMNode*> FindTags(const GetSnapshotResult* snapshot,
const char* name = nullptr) {
return FilterDOM(snapshot, [name](const auto& n) {
return IsTag(n) && (!name || HasName(name, n));
});
}
size_t IndexInDOM(const GetSnapshotResult* snapshot, const DOMNode* node) {
for (size_t i = 0; i < snapshot->GetDomNodes()->size(); ++i) {
if (snapshot->GetDomNodes()->at(i).get() == node)
return i;
}
CHECK(false);
return static_cast<size_t>(-1);
}
const DOMNode* GetAt(const GetSnapshotResult* snapshot, size_t index) {
CHECK_LE(index, snapshot->GetDomNodes()->size());
return snapshot->GetDomNodes()->at(index).get();
}
const DOMNode* NextNode(const GetSnapshotResult* snapshot,
const DOMNode* node) {
return GetAt(snapshot, IndexInDOM(snapshot, node) + 1);
}
MATCHER_P(NodeName, expected, "") {
return arg->GetNodeName() == expected;
}
MATCHER_P(NodeValue, expected, "") {
return arg->GetNodeValue() == expected;
}
MATCHER_P(NodeType, expected, 0) {
return arg->GetNodeType() == expected;
}
MATCHER_P(RemoteString, expected, "") {
return arg->GetType() == runtime::RemoteObjectType::STRING &&
arg->GetValue()->GetString() == expected;
}
MATCHER_P(RequestPath, expected, "") {
return arg.relative_url == expected;
}
MATCHER_P(Reason, expected, "") {
return arg.reason == expected;
}
MATCHER_P(CookieValue, expected, "") {
return arg->GetValue() == expected;
}
const DOMNode* FindTag(const GetSnapshotResult* snapshot, const char* name) {
auto tags = FindTags(snapshot, name);
if (tags.empty())
return nullptr;
EXPECT_THAT(tags, ElementsAre(NodeName(name)));
return tags[0];
}
TestNetworkInterceptor::Response HttpRedirect(
int code,
const std::string& url,
const std::string& status = "Moved") {
CHECK(code >= 300 && code < 400);
std::stringstream str;
str << "HTTP/1.1 " << code << " " << status << "\r\nLocation: " << url
<< "\r\n\r\n";
return TestNetworkInterceptor::Response(str.str());
}
TestNetworkInterceptor::Response HttpOk(
const std::string& html,
const std::string& mime_type = kTextHtml) {
return TestNetworkInterceptor::Response(html, mime_type);
}
TestNetworkInterceptor::Response ResponseFromFile(
const std::string& file_name,
const std::string& mime_type) {
static const base::FilePath kTestDataDirectory(
FILE_PATH_LITERAL("headless/test/data"));
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath src_dir;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
base::FilePath file_path =
src_dir.Append(kTestDataDirectory).Append(file_name);
std::string contents;
CHECK(base::ReadFileToString(file_path, &contents));
return TestNetworkInterceptor::Response(contents, mime_type);
}
} // namespace
class HelloWorldTest : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse(kSomeUrl, HttpOk(R"|(<!doctype html>
<h1>Hello headless world!</h1>
)|"));
return GURL(kSomeUrl);
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(FindTags(dom_snapshot),
ElementsAre(NodeName("HTML"), NodeName("HEAD"),
NodeName("BODY"), NodeName("H1")));
EXPECT_THAT(
FilterDOM(dom_snapshot, IsText),
ElementsAre(NodeValue("Hello headless world!"), NodeValue("\n")));
EXPECT_THAT(TextLayout(dom_snapshot), ElementsAre("Hello headless world!"));
EXPECT_THAT(interceptor_->urls_requested(), ElementsAre(kSomeUrl));
EXPECT_FALSE(main_frame_.empty());
EXPECT_TRUE(scheduled_navigations_.empty());
EXPECT_THAT(frames_[main_frame_].size(), Eq(1u));
const auto& frame = frames_[main_frame_][0];
EXPECT_THAT(frame->GetUrl(), Eq(kSomeUrl));
}
};
HEADLESS_RENDER_BROWSERTEST(HelloWorldTest);
class TimeoutTest : public HelloWorldTest {
private:
void OnPageRenderCompleted() override {
// Never complete.
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
FAIL() << "Should not reach here";
}
void OnTimeout() override { SetTestCompleted(); }
};
HEADLESS_RENDER_BROWSERTEST(TimeoutTest);
class JavaScriptOverrideTitle_JsEnabled : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse(kSomeUrl, HttpOk(R"|(
<html>
<head>
<title>JavaScript is off</title>
<script language="JavaScript">
<!-- Begin
document.title = 'JavaScript is on';
// End -->
</script>
</head>
<body onload="settitle()">
Hello, World!
</body>
</html>
)|"));
return GURL(kSomeUrl);
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
const DOMNode* value =
NextNode(dom_snapshot, FindTag(dom_snapshot, "TITLE"));
EXPECT_THAT(value, NodeValue("JavaScript is on"));
}
};
HEADLESS_RENDER_BROWSERTEST(JavaScriptOverrideTitle_JsEnabled);
class JavaScriptOverrideTitle_JsDisabled
: public JavaScriptOverrideTitle_JsEnabled {
private:
void OverrideWebPreferences(WebPreferences* preferences) override {
JavaScriptOverrideTitle_JsEnabled::OverrideWebPreferences(preferences);
preferences->javascript_enabled = false;
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
const DOMNode* value =
NextNode(dom_snapshot, FindTag(dom_snapshot, "TITLE"));
EXPECT_THAT(value, NodeValue("JavaScript is off"));
}
};
HEADLESS_RENDER_BROWSERTEST(JavaScriptOverrideTitle_JsDisabled);
class JavaScriptConsoleErrors : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse(kSomeUrl, HttpOk(R"|(
<html>
<head>
<script language="JavaScript">
<![CDATA[
function image() {
window.open('<xsl:value-of select="/IMAGE/@href" />');
}
]]>
</script>
</head>
<body onload="func3()">
<script type="text/javascript">
func1()
</script>
<script type="text/javascript">
func2();
</script>
<script type="text/javascript">
console.log("Hello, Script!");
</script>
</body>
</html>
)|"));
return GURL(kSomeUrl);
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(console_log_, ElementsAre("L Hello, Script!"));
EXPECT_THAT(js_exceptions_,
ElementsAre(StartsWith("Uncaught SyntaxError:"),
StartsWith("Uncaught ReferenceError: func1"),
StartsWith("Uncaught ReferenceError: func2"),
StartsWith("Uncaught ReferenceError: func3")));
}
};
HEADLESS_RENDER_BROWSERTEST(JavaScriptConsoleErrors);
class DelayedCompletion : public HeadlessRenderTest {
private:
base::TimeTicks start_;
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse(kSomeUrl, HttpOk(R"|(
<html>
<body>
<script type="text/javascript">
setTimeout(() => {
var div = document.getElementById('content');
var p = document.createElement('p');
p.textContent = 'delayed text';
div.appendChild(p);
}, 3000);
</script>
<div id="content"/>
</body>
</html>
)|"));
start_ = base::TimeTicks::Now();
return GURL(kSomeUrl);
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
base::TimeTicks end = base::TimeTicks::Now();
EXPECT_THAT(
FindTags(dom_snapshot),
ElementsAre(NodeName("HTML"), NodeName("HEAD"), NodeName("BODY"),
NodeName("SCRIPT"), NodeName("DIV"), NodeName("P")));
const DOMNode* value = NextNode(dom_snapshot, FindTag(dom_snapshot, "P"));
EXPECT_THAT(value, NodeValue("delayed text"));
// The page delays output for 3 seconds. Due to virtual time this should
// take significantly less actual time.
base::TimeDelta passed = end - start_;
EXPECT_THAT(passed.InSecondsF(), testing::Le(2.9f));
}
};
HEADLESS_RENDER_BROWSERTEST(DelayedCompletion);
class ClientRedirectChain : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<head>
<meta http-equiv="refresh" content="0; url=http://www.example.com/1"/>
<title>Hello, World 0</title>
</head>
<body>http://www.example.com/</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/1", HttpOk(R"|(
<html>
<head>
<title>Hello, World 1</title>
<script>
document.location='http://www.example.com/2';
</script>
</head>
<body>http://www.example.com/1</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/2", HttpOk(R"|(
<html>
<head>
<title>Hello, World 2</title>
<script>
setTimeout("document.location='http://www.example.com/3'", 1000);
</script>
</head>
<body>http://www.example.com/2</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/3", HttpOk(R"|(
<html>
<head>
<title>Pass</title>
</head>
<body>
http://www.example.com/3
<img src="pass">
</body>
</html>
)|"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1",
"http://www.example.com/2", "http://www.example.com/3",
"http://www.example.com/pass"));
const DOMNode* value =
NextNode(dom_snapshot, FindTag(dom_snapshot, "TITLE"));
EXPECT_THAT(value, NodeValue("Pass"));
EXPECT_THAT(
scheduled_navigations_[main_frame_],
ElementsAre(Reason(FrameScheduledNavigationReason::META_TAG_REFRESH),
Reason(FrameScheduledNavigationReason::SCRIPT_INITIATED),
Reason(FrameScheduledNavigationReason::SCRIPT_INITIATED)));
EXPECT_THAT(frames_[main_frame_].size(), Eq(4u));
}
};
HEADLESS_RENDER_BROWSERTEST(ClientRedirectChain);
class ClientRedirectChain_NoJs : public ClientRedirectChain {
private:
void OverrideWebPreferences(WebPreferences* preferences) override {
ClientRedirectChain::OverrideWebPreferences(preferences);
preferences->javascript_enabled = false;
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1"));
const DOMNode* value =
NextNode(dom_snapshot, FindTag(dom_snapshot, "TITLE"));
EXPECT_THAT(value, NodeValue("Hello, World 1"));
EXPECT_THAT(
scheduled_navigations_[main_frame_],
ElementsAre(Reason(FrameScheduledNavigationReason::META_TAG_REFRESH)));
EXPECT_THAT(frames_[main_frame_].size(), Eq(2u));
}
};
HEADLESS_RENDER_BROWSERTEST(ClientRedirectChain_NoJs);
class ServerRedirectChain : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/",
HttpRedirect(302, "http://www.example.com/1"));
interceptor_->InsertResponse("http://www.example.com/1",
HttpRedirect(301, "http://www.example.com/2"));
interceptor_->InsertResponse("http://www.example.com/2",
HttpRedirect(302, "http://www.example.com/3"));
interceptor_->InsertResponse("http://www.example.com/3",
HttpOk("<p>Pass</p>"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1",
"http://www.example.com/2", "http://www.example.com/3"));
const DOMNode* value = NextNode(dom_snapshot, FindTag(dom_snapshot, "P"));
EXPECT_THAT(value, NodeValue("Pass"));
#ifndef DISABLE_HTTP_REDIRECTS_CHECKS
EXPECT_THAT(
scheduled_navigations_[main_frame_],
ElementsAre(
Reason(FrameScheduledNavigationReason::HTTP_HEADER_REFRESH),
Reason(FrameScheduledNavigationReason::HTTP_HEADER_REFRESH),
Reason(FrameScheduledNavigationReason::HTTP_HEADER_REFRESH)));
EXPECT_THAT(frames_[main_frame_].size(), Eq(4u));
#endif // #ifndef DISABLE_HTTP_REDIRECTS_CHECKS
}
};
HEADLESS_RENDER_BROWSERTEST(ServerRedirectChain);
class ServerRedirectToFailure : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/",
HttpRedirect(302, "http://www.example.com/1"));
interceptor_->InsertResponse(
"http://www.example.com/1",
HttpRedirect(301, "http://www.example.com/FAIL"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1",
"http://www.example.com/FAIL"));
}
};
// TODO(crbug.com/861548): re-implement as DevTools protocol test.
DISABLED_HEADLESS_RENDER_BROWSERTEST(ServerRedirectToFailure);
class ServerRedirectRelativeChain : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/",
HttpRedirect(302, "http://www.mysite.com/1"));
interceptor_->InsertResponse("http://www.mysite.com/1",
HttpRedirect(301, "/2"));
interceptor_->InsertResponse("http://www.mysite.com/2",
HttpOk("<p>Pass</p>"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.mysite.com/1",
"http://www.mysite.com/2"));
const DOMNode* value = NextNode(dom_snapshot, FindTag(dom_snapshot, "P"));
EXPECT_THAT(value, NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(ServerRedirectRelativeChain);
class MixedRedirectChain : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<head>
<meta http-equiv="refresh" content="0; url=http://www.example.com/1"/>
<title>Hello, World 0</title>
</head>
<body>http://www.example.com/</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/1", HttpOk(R"|(
<html>
<head>
<title>Hello, World 1</title>
<script>
document.location='http://www.example.com/2';
</script>
</head>
<body>http://www.example.com/1</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/2",
HttpRedirect(302, "3"));
interceptor_->InsertResponse("http://www.example.com/3",
HttpRedirect(301, "http://www.example.com/4"));
interceptor_->InsertResponse("http://www.example.com/4",
HttpOk("<p>Pass</p>"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1",
"http://www.example.com/2", "http://www.example.com/3",
"http://www.example.com/4"));
const DOMNode* value = NextNode(dom_snapshot, FindTag(dom_snapshot, "P"));
EXPECT_THAT(value, NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(MixedRedirectChain);
class FramesRedirectChain : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/",
HttpRedirect(302, "http://www.example.com/1"));
interceptor_->InsertResponse("http://www.example.com/1", HttpOk(R"|(
<html>
<frameset>
<frame src="http://www.example.com/frameA/">
<frame src="http://www.example.com/frameB/">
</frameset>
</html>
)|"));
// Frame A
interceptor_->InsertResponse("http://www.example.com/frameA/", HttpOk(R"|(
<html>
<head>
<script>document.location='http://www.example.com/frameA/1'</script>
</head>
<body>HELLO WORLD 1</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/frameA/1",
HttpRedirect(301, "/frameA/2"));
interceptor_->InsertResponse("http://www.example.com/frameA/2",
HttpOk("<p>FRAME A</p>"));
// Frame B
interceptor_->InsertResponse("http://www.example.com/frameB/", HttpOk(R"|(
<html>
<head><title>HELLO WORLD 2</title></head>
<body>
<iframe src="http://www.example.com/iframe/"></iframe>
</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/iframe/", HttpOk(R"|(
<html>
<head>
<script>document.location='http://www.example.com/iframe/1'</script>
</head>
<body>HELLO WORLD 1</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/iframe/1",
HttpRedirect(302, "/iframe/2"));
interceptor_->InsertResponse("http://www.example.com/iframe/2",
HttpRedirect(301, "3"));
interceptor_->InsertResponse("http://www.example.com/iframe/3",
HttpOk("<p>IFRAME B</p>"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
UnorderedElementsAre(
"http://www.example.com/", "http://www.example.com/1",
"http://www.example.com/frameA/", "http://www.example.com/frameA/1",
"http://www.example.com/frameA/2", "http://www.example.com/frameB/",
"http://www.example.com/iframe/", "http://www.example.com/iframe/1",
"http://www.example.com/iframe/2",
"http://www.example.com/iframe/3"));
auto dom = FindTags(dom_snapshot, "P");
EXPECT_THAT(dom, ElementsAre(NodeName("P"), NodeName("P")));
EXPECT_THAT(NextNode(dom_snapshot, dom[0]), NodeValue("FRAME A"));
EXPECT_THAT(NextNode(dom_snapshot, dom[1]), NodeValue("IFRAME B"));
const page::Frame* main_frame = nullptr;
const page::Frame* a_frame = nullptr;
const page::Frame* b_frame = nullptr;
const page::Frame* i_frame = nullptr;
EXPECT_THAT(frames_.size(), Eq(4u));
for (const auto& it : frames_) {
if (it.second.back()->GetUrl() == "http://www.example.com/1")
main_frame = it.second.back().get();
else if (it.second.back()->GetUrl() == "http://www.example.com/frameA/2")
a_frame = it.second.back().get();
else if (it.second.back()->GetUrl() == "http://www.example.com/frameB/")
b_frame = it.second.back().get();
else if (it.second.back()->GetUrl() == "http://www.example.com/iframe/3")
i_frame = it.second.back().get();
else
ADD_FAILURE() << "Unexpected frame URL: " << it.second.back()->GetUrl();
}
#ifndef DISABLE_HTTP_REDIRECTS_CHECKS
EXPECT_THAT(frames_[main_frame->GetId()].size(), Eq(2u));
EXPECT_THAT(frames_[a_frame->GetId()].size(), Eq(3u));
EXPECT_THAT(frames_[b_frame->GetId()].size(), Eq(1u));
EXPECT_THAT(frames_[i_frame->GetId()].size(), Eq(4u));
EXPECT_THAT(scheduled_navigations_[main_frame->GetId()],
ElementsAre(Reason(
FrameScheduledNavigationReason::HTTP_HEADER_REFRESH)));
EXPECT_THAT(
scheduled_navigations_[a_frame->GetId()],
ElementsAre(
Reason(FrameScheduledNavigationReason::SCRIPT_INITIATED),
Reason(FrameScheduledNavigationReason::HTTP_HEADER_REFRESH)));
EXPECT_THAT(
scheduled_navigations_[i_frame->GetId()],
ElementsAre(
Reason(FrameScheduledNavigationReason::SCRIPT_INITIATED),
Reason(FrameScheduledNavigationReason::HTTP_HEADER_REFRESH),
Reason(FrameScheduledNavigationReason::HTTP_HEADER_REFRESH)));
#endif // #ifndef DISABLE_HTTP_REDIRECTS_CHECKS
}
};
HEADLESS_RENDER_BROWSERTEST(FramesRedirectChain);
class DoubleRedirect : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<head>
<title>Hello, World 1</title>
<script>
document.location='http://www.example.com/1';
document.location='http://www.example.com/2';
</script>
</head>
<body>http://www.example.com/1</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/2",
HttpOk("<p>Pass</p>"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
// Two navigations have been scheduled while the document was loading...
EXPECT_THAT(
scheduled_navigations_[main_frame_],
ElementsAre(Reason(FrameScheduledNavigationReason::SCRIPT_INITIATED),
Reason(FrameScheduledNavigationReason::SCRIPT_INITIATED)));
// ..., but only the second one was started. It canceled the first one.
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/2"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
EXPECT_THAT(frames_[main_frame_].size(), Eq(2u));
}
};
HEADLESS_RENDER_BROWSERTEST(DoubleRedirect);
class RedirectAfterCompletion : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<head>
<meta http-equiv='refresh' content='120; url=http://www.example.com/1'>
</head>
<body><p>Pass</p></body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/1",
HttpOk("<p>Fail</p>"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
// While the document was loading, one navigation has been scheduled...
EXPECT_THAT(
scheduled_navigations_[main_frame_],
ElementsAre(Reason(FrameScheduledNavigationReason::META_TAG_REFRESH)));
// ..., but because of the timeout, it has not been started yet.
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://www.example.com/"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
EXPECT_THAT(frames_[main_frame_].size(), Eq(1u));
}
};
HEADLESS_RENDER_BROWSERTEST(RedirectAfterCompletion);
class Redirect307PostMethod : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<body onload='document.forms[0].submit();'>
<form action='1' method='post'>
<input name='foo' value='bar'>
</form>
</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/1",
HttpRedirect(307, "/2"));
interceptor_->InsertResponse("http://www.example.com/2",
HttpOk("<p>Pass</p>"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1",
"http://www.example.com/2"));
EXPECT_THAT(interceptor_->methods_requested(),
ElementsAre("GET", "POST", "POST"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(Redirect307PostMethod);
class RedirectPostChain : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<body onload='document.forms[0].submit();'>
<form action='1' method='post'>
<input name='foo' value='bar'>
</form>
</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/1",
HttpRedirect(307, "/2"));
interceptor_->InsertResponse("http://www.example.com/2", HttpOk(R"|(
<html>
<body onload='document.forms[0].submit();'>
<form action='3' method='post'>
</form>
</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/3",
HttpRedirect(307, "/4"));
interceptor_->InsertResponse("http://www.example.com/4",
HttpOk("<p>Pass</p>"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1",
"http://www.example.com/2", "http://www.example.com/3",
"http://www.example.com/4"));
EXPECT_THAT(interceptor_->methods_requested(),
ElementsAre("GET", "POST", "POST", "POST", "POST"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(RedirectPostChain);
class Redirect307PutMethod : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<head>
<script>
function doPut() {
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'http://www.example.com/1');
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.addEventListener('load', function() {
document.getElementById('content').textContent = this.responseText;
});
xhr.send('some data');
}
</script>
</head>
<body onload='doPut();'>
<p id="content"></p>
</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/1",
HttpRedirect(307, "/2"));
interceptor_->InsertResponse("http://www.example.com/2",
{"Pass", "text/plain"});
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1",
"http://www.example.com/2"));
EXPECT_THAT(interceptor_->methods_requested(),
ElementsAre("GET", "PUT", "PUT"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(Redirect307PutMethod);
class Redirect303PutGet : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<head>
<script>
function doPut() {
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'http://www.example.com/1');
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.addEventListener('load', function() {
document.getElementById('content').textContent = this.responseText;
});
xhr.send('some data');
}
</script>
</head>
<body onload='doPut();'>
<p id="content"></p>
</body>
</html>
)|"));
interceptor_->InsertResponse("http://www.example.com/1",
HttpRedirect(303, "/2"));
interceptor_->InsertResponse("http://www.example.com/2",
{"Pass", "text/plain"});
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1",
"http://www.example.com/2"));
EXPECT_THAT(interceptor_->methods_requested(),
ElementsAre("GET", "PUT", "GET"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(Redirect303PutGet);
class RedirectBaseUrl : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://foo.com/",
HttpRedirect(302, "http://bar.com/"));
interceptor_->InsertResponse("http://bar.com/",
HttpOk("<img src=\"pass\">"));
return GURL("http://foo.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://foo.com/", "http://bar.com/",
"http://bar.com/pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(RedirectBaseUrl);
class RedirectNonAsciiUrl : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
// "中文" is 0xE4 0xB8 0xAD, 0xE6 0x96 0x87
interceptor_->InsertResponse(
"http://www.example.com/",
HttpRedirect(302, "http://www.example.com/中文"));
interceptor_->InsertResponse(
"http://www.example.com/%E4%B8%AD%E6%96%87",
HttpRedirect(303, "http://www.example.com/pass#中文"));
interceptor_->InsertResponse(
"http://www.example.com/pass#%E4%B8%AD%E6%96%87",
HttpOk("<p>Pass</p>"));
interceptor_->InsertResponse(
"http://www.example.com/%C3%A4%C2%B8%C2%AD%C3%A6%C2%96%C2%87",
{"HTTP/1.1 500 Bad Response\r\nContent-Type: text/html\r\n\r\nFail"});
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://www.example.com/",
"http://www.example.com/%E4%B8%AD%E6%96%87",
"http://www.example.com/pass#%E4%B8%AD%E6%96%87"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(RedirectNonAsciiUrl);
class RedirectEmptyUrl : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse(
"http://www.example.com/",
{"HTTP/1.1 302 Found\r\nLocation: \r\nContent-Type: "
"text/html\r\n\r\n<!DOCTYPE html><p>Pass</p>"});
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://www.example.com/"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(RedirectEmptyUrl);
class RedirectInvalidUrl : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse(
"http://www.example.com/",
{"HTTP/1.1 302 Found\r\nLocation: http://\r\n\r\n"
"<!DOCTYPE html><p>Pass</p>"});
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://www.example.com/"));
}
};
// TODO(crbug.com/861548): re-implement as DevTools protocol test.
DISABLED_HEADLESS_RENDER_BROWSERTEST(RedirectInvalidUrl);
class RedirectKeepsFragment : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/#foo",
HttpRedirect(302, "/1"));
interceptor_->InsertResponse("http://www.example.com/1#foo",
HttpRedirect(302, "/2"));
interceptor_->InsertResponse("http://www.example.com/2#foo", HttpOk(R"|(
<body>
<p id="content"></p>
<script>
document.getElementById('content').textContent = window.location.href;
</script>
</body>
)|"));
return GURL("http://www.example.com/#foo");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://www.example.com/#foo",
"http://www.example.com/1#foo",
"http://www.example.com/2#foo"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("http://www.example.com/2#foo"));
}
};
HEADLESS_RENDER_BROWSERTEST(RedirectKeepsFragment);
class RedirectReplacesFragment : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/#foo",
HttpRedirect(302, "/1#bar"));
interceptor_->InsertResponse("http://www.example.com/1#bar",
HttpRedirect(302, "/2"));
interceptor_->InsertResponse("http://www.example.com/2#bar", HttpOk(R"|(
<body>
<p id="content"></p>
<script>
document.getElementById('content').textContent = window.location.href;
</script>
</body>
)|"));
return GURL("http://www.example.com/#foo");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://www.example.com/#foo",
"http://www.example.com/1#bar",
"http://www.example.com/2#bar"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("http://www.example.com/2#bar"));
}
};
// TODO(crbug.com/861548): re-implement as DevTools protocol test.
DISABLED_HEADLESS_RENDER_BROWSERTEST(RedirectReplacesFragment);
class RedirectNewFragment : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/",
HttpRedirect(302, "/1#foo"));
interceptor_->InsertResponse("http://www.example.com/1#foo",
HttpRedirect(302, "/2"));
interceptor_->InsertResponse("http://www.example.com/2#foo", HttpOk(R"|(
<body>
<p id="content"></p>
<script>
document.getElementById('content').textContent = window.location.href;
</script>
</body>
)|"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(
interceptor_->urls_requested(),
ElementsAre("http://www.example.com/", "http://www.example.com/1#foo",
"http://www.example.com/2#foo"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("http://www.example.com/2#foo"));
}
};
// TODO(https://crbug.com/839747): Re-implement as DevTools protocol test.
DISABLED_HEADLESS_RENDER_BROWSERTEST(RedirectNewFragment);
class WindowLocationFragments : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/#fragment1",
HttpOk(R"|(
<script>
if (window.location.hash == '#fragment1') {
document.write('<iframe src="iframe#fragment2"></iframe>');
}
</script>)|"));
interceptor_->InsertResponse("http://www.example.com/iframe#fragment2",
HttpOk(R"|(
<script>
if (window.location.hash == '#fragment2') {
document.location = 'http://www.example.com/pass';
}
</script>)|"));
interceptor_->InsertResponse("http://www.example.com/pass",
HttpOk("<p>Pass</p>"));
return GURL("http://www.example.com/#fragment1");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://www.example.com/#fragment1",
"http://www.example.com/iframe#fragment2",
"http://www.example.com/pass"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(WindowLocationFragments);
class CookieSetFromJs : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html><head><script>
document.cookie = 'SessionID=123';
n = document.cookie.indexOf('SessionID');
if (n < 0) {
top.location = '/epicfail';
}
</script></head><body>Pass</body></html>)|"));
return GURL("http://www.example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://www.example.com/"));
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "BODY")),
NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(CookieSetFromJs);
class CookieSetFromJs_NoCookies : public CookieSetFromJs {
private:
void OverrideWebPreferences(WebPreferences* preferences) override {
HeadlessRenderTest::OverrideWebPreferences(preferences);
preferences->cookie_enabled = false;
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(interceptor_->urls_requested(),
ElementsAre("http://www.example.com/",
"http://www.example.com/epicfail"));
}
};
// Flaky on Linux. https://crbug.com/839747
#if defined(OS_LINUX)
DISABLED_HEADLESS_RENDER_BROWSERTEST(CookieSetFromJs_NoCookies);
#else
HEADLESS_RENDER_BROWSERTEST(CookieSetFromJs_NoCookies);
#endif
class CookieUpdatedFromJs : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
client->GetNetwork()->SetCookie(network::SetCookieParams::Builder()
.SetUrl("http://www.example.com/")
.SetName("foo")
.SetValue("bar")
.Build());
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html><head><script>
var x = document.cookie;
document.cookie = x + 'baz';
</script></head><body>Pass</body></html>)|"));
return GURL("http://www.example.com/");
}
void OnPageRenderCompleted() override {
devtools_client_->GetNetwork()->GetCookies(
network::GetCookiesParams::Builder()
.SetUrls({"http://www.example.com/"})
.Build(),
base::BindOnce(&CookieUpdatedFromJs::OnGetCookies,
base::Unretained(this)));
}
void OnGetCookies(std::unique_ptr<network::GetCookiesResult> result) {
const auto& cookies = *result->GetCookies();
EXPECT_THAT(cookies, ElementsAre(CookieValue("barbaz")));
HeadlessRenderTest::OnPageRenderCompleted();
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "BODY")),
NodeValue("Pass"));
}
};
HEADLESS_RENDER_BROWSERTEST(CookieUpdatedFromJs);
class InCrossOriginObject : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://foo.com/", HttpOk(R"|(
<html><body>
<iframe id='myframe' src='http://bar.com/'></iframe>
<script>
window.onload = function() {
try {
var a = 0 in document.getElementById('myframe').contentWindow;
} catch (e) {
console.log(e.message);
}
};
</script><p>Pass</p></body></html>)|"));
interceptor_->InsertResponse("http://bar.com/",
HttpOk(R"|(<html></html>)|"));
return GURL("http://foo.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(NextNode(dom_snapshot, FindTag(dom_snapshot, "P")),
NodeValue("Pass"));
EXPECT_THAT(console_log_,
ElementsAre(StartsWith("L Blocked a frame with origin "
"\"http://foo.com\" from accessing")));
}
};
HEADLESS_RENDER_BROWSERTEST(InCrossOriginObject);
class ContentSecurityPolicy : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
// Only first 3 scripts of 4 on the page are whitelisted for execution.
// Therefore only 3 lines in the log are expected.
interceptor_->InsertResponse(
"http://example.com/",
{"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Security-Policy: script-src"
" 'sha256-INSsCHXoo4K3+jDRF8FSvl13GP22I9vcqcJjkq35Y20='"
" 'sha384-77lSn5Q6V979pJ8W2TXc6Lrj98LughR0ofkFwa+"
"qOEtlcofEdLPkOPtpJF8QQMev'"
" 'sha512-"
"2cS3KZwfnxFo6lvBvAl113f5N3QCRgtRJBbtFaQHKOhk36sdYYKFvhCqGTvbN7pBKUfsj"
"fCQgFF4MSbCQuvT8A=='\r\n\r\n"
"<!DOCTYPE html>\n"
"<script>console.log('pass256');</script>\n"
"<script>console.log('pass384');</script>\n"
"<script>console.log('pass512');</script>\n"
"<script>console.log('fail');</script>"});
// For example, regenerate sha256 hash with:
// echo -n "console.log('pass256');" \
// | openssl sha256 -binary \
// | openssl base64
return GURL("http://example.com/");
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
EXPECT_THAT(console_log_,
ElementsAre("L pass256", "L pass384", "L pass512"));
}
};
HEADLESS_RENDER_BROWSERTEST(ContentSecurityPolicy);
class FrameLoadEvents : public HeadlessRenderTest {
private:
std::map<std::string, std::string> frame_navigated_;
std::map<std::string, std::string> frame_scheduled_;
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://example.com/",
HttpRedirect(302, "http://example.com/1"));
interceptor_->InsertResponse("http://example.com/1", HttpOk(R"|(
<html><frameset>
<frame src="http://example.com/frameA/" id="frameA">
<frame src="http://example.com/frameB/" id="frameB">
</frameset></html>
)|"));
interceptor_->InsertResponse("http://example.com/frameA/", HttpOk(R"|(
<html><head><script>
document.location="http://example.com/frameA/1"
</script></head></html>
)|"));
interceptor_->InsertResponse("http://example.com/frameB/", HttpOk(R"|(
<html><head><script>
document.location="http://example.com/frameB/1"
</script></head></html>
)|"));
interceptor_->InsertResponse("http://example.com/frameA/1",
HttpOk("<html><body>FRAME A 1</body></html>"));
interceptor_->InsertResponse("http://example.com/frameB/1", HttpOk(R"|(
<html><body>FRAME B 1
<iframe src="http://example.com/frameB/1/iframe/" id="iframe"></iframe>
</body></html>
)|"));
interceptor_->InsertResponse("http://example.com/frameB/1/iframe/",
HttpOk(R"|(
<html><head><script>
document.location="http://example.com/frameB/1/iframe/1"
</script></head></html>
)|"));
interceptor_->InsertResponse("http://example.com/frameB/1/iframe/1",
HttpOk("<html><body>IFRAME 1</body><html>"));
return GURL("http://example.com/");
}
void OnFrameNavigated(const page::FrameNavigatedParams& params) override {
frame_navigated_.insert(std::make_pair(params.GetFrame()->GetId(),
params.GetFrame()->GetUrl()));
HeadlessRenderTest::OnFrameNavigated(params);
}
void OnFrameScheduledNavigation(
const page::FrameScheduledNavigationParams& params) override {
frame_scheduled_.insert(
std::make_pair(params.GetFrameId(), params.GetUrl()));
HeadlessRenderTest::OnFrameScheduledNavigation(params);
}
void VerifyDom(GetSnapshotResult* dom_snapshot) override {
std::vector<std::string> urls;
for (const auto& kv : frame_navigated_) {
urls.push_back(kv.second);
}
EXPECT_THAT(urls, UnorderedElementsAre(
"http://example.com/1", "http://example.com/frameA/",
"http://example.com/frameB/",
"http://example.com/frameB/1/iframe/"));
urls.clear();
for (const auto& kv : frame_scheduled_) {
urls.push_back(kv.second);
}
EXPECT_THAT(urls,
UnorderedElementsAre("http://example.com/frameA/1",
"http://example.com/frameB/1",
"http://example.com/frameB/1/iframe/1"));
}
};
HEADLESS_RENDER_BROWSERTEST(FrameLoadEvents);
class CustomFont : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<head>
<style>
@font-face {
font-family: testfont;
src: url("font.ttf");
}
span.test {
font-family: testfont;
font-size: 200px;
}
</style>
</head>
<body>
<span class="test">Hello</span>
</body>
</html>
)|"));
interceptor_->InsertResponse(
"http://www.example.com/font.ttf",
ResponseFromFile("font.ttf", kApplicationOctetStream));
return GURL("http://www.example.com/");
}
base::Optional<ScreenshotOptions> GetScreenshotOptions() override {
return ScreenshotOptions("custom_font.png", 0, 0, 500, 250, 1);
}
};
HEADLESS_RENDER_BROWSERTEST(CustomFont);
// Ensures that "filter: url(...)" does not get into an infinite style update
// loop.
class CssUrlFilter : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
// The image from circle.svg will be drawn with the blur from blur.svg.
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<!DOCTYPE html>
<style>
body { margin: 0; }
img {
-webkit-filter: url(blur.svg#blur);
filter: url(blur.svg#blur);
}
</style>
<img src="circle.svg">
)|"));
// Just a normal image.
interceptor_->InsertResponse("http://www.example.com/circle.svg",
HttpOk(R"|(
<svg width="100" height="100" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<circle cx="50" cy="50" r="50" fill="green" />
</svg>
)|",
kImageSvgXml));
// A blur filter stored inside an svg file.
interceptor_->InsertResponse("http://www.example.com/blur.svg#blur",
HttpOk(R"|(
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="blur">
<feGaussianBlur in="SourceGraphic" stdDeviation="5"/>
</filter>
</svg>
)|",
kImageSvgXml));
return GURL("http://www.example.com/");
}
base::Optional<ScreenshotOptions> GetScreenshotOptions() override {
return ScreenshotOptions("css_url_filter.png", 0, 0, 100, 100, 1);
}
};
HEADLESS_RENDER_BROWSERTEST(CssUrlFilter);
// Ensures that a number of SVGs features render correctly.
class SvgExamples : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse(
"http://www.example.com/",
ResponseFromFile("svg_examples.svg", kImageSvgXml));
interceptor_->InsertResponse(
"http://www.example.com/svg_example_image.png",
ResponseFromFile("svg_example_image.png", kImagePng));
return GURL("http://www.example.com/");
}
base::Optional<ScreenshotOptions> GetScreenshotOptions() override {
return ScreenshotOptions("svg_examples.png", 0, 0, 400, 600, 1);
}
};
#if defined(OS_LINUX) && defined(ARCH_CPU_X86) && !defined(NDEBUG)
// https://crbug.com/859325
DISABLED_HEADLESS_RENDER_BROWSERTEST(SvgExamples);
#else
HEADLESS_RENDER_BROWSERTEST(SvgExamples);
#endif
// Ensures that basic <canvas> painting is supported.
class Canvas : public HeadlessRenderTest {
private:
GURL GetPageUrl(HeadlessDevToolsClient* client) override {
interceptor_->InsertResponse("http://www.example.com/", HttpOk(R"|(
<html>
<body>
<canvas id="test_canvas" width="200" height="200"
style="position:absolute;left:0px;top:0px">
Oops! Canvas not supported!
</canvas>
<script>
var context = document.getElementById("test_canvas").
getContext("2d");
context.fillStyle = "rgb(255,0,0)";
context.fillRect(30, 30, 50, 50);
</script>
</body>
</html>
)|"));
return GURL("http://www.example.com/");
}
base::Optional<ScreenshotOptions> GetScreenshotOptions() override {
return ScreenshotOptions("canvas.png", 0, 0, 200, 200, 1);
}
};
HEADLESS_RENDER_BROWSERTEST(Canvas);
} // namespace headless
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "headless/test/headless_render_test.h"
#include "base/base_paths.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/threading/thread_restrictions.h"
#include "cc/base/switches.h"
#include "components/viz/common/features.h"
#include "components/viz/common/switches.h"
#include "content/public/common/content_switches.h"
#include "headless/public/devtools/domains/dom_snapshot.h"
#include "headless/public/headless_devtools_client.h"
#include "headless/public/util/compositor_controller.h"
#include "headless/public/util/virtual_time_controller.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/gfx/codec/png_codec.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/skia_util.h"
namespace headless {
namespace {
static constexpr int kAnimationIntervalMs = 100;
static constexpr bool kUpdateDisplayForAnimations = false;
static const char kUpdateGoldens[] = "update-goldens";
void SetVirtualTimePolicyDoneCallback(
base::RunLoop* run_loop,
std::unique_ptr<emulation::SetVirtualTimePolicyResult>) {
run_loop->Quit();
}
bool DecodePNG(const std::string& data, SkBitmap* bitmap) {
return gfx::PNGCodec::Decode(
reinterpret_cast<unsigned const char*>(data.data()), data.size(), bitmap);
}
bool ColorsMatchWithinLimit(SkColor color1, SkColor color2, int error_limit) {
auto a_diff = static_cast<int>(SkColorGetA(color1)) -
static_cast<int>(SkColorGetA(color2));
auto r_diff = static_cast<int>(SkColorGetR(color1)) -
static_cast<int>(SkColorGetR(color2));
auto g_diff = static_cast<int>(SkColorGetG(color1)) -
static_cast<int>(SkColorGetG(color2));
auto b_diff = static_cast<int>(SkColorGetB(color1)) -
static_cast<int>(SkColorGetB(color2));
return a_diff * a_diff + r_diff * r_diff + g_diff * g_diff +
b_diff * b_diff <=
error_limit * error_limit;
}
bool MatchesBitmap(const SkBitmap& expected_bmp,
const SkBitmap& actual_bmp,
int error_limit) {
// Number of pixels with an error
int error_pixels_count = 0;
gfx::Rect error_bounding_rect = gfx::Rect();
// Check that bitmaps have identical dimensions.
EXPECT_EQ(expected_bmp.width(), actual_bmp.width());
EXPECT_EQ(expected_bmp.height(), actual_bmp.height());
if (expected_bmp.width() != actual_bmp.width() ||
expected_bmp.height() != actual_bmp.height()) {
LOG(ERROR) << "To update goldens, use --update-goldens.";
return false;
}
for (int y = 0; y < actual_bmp.height(); ++y) {
for (int x = 0; x < actual_bmp.width(); ++x) {
SkColor actual_color = actual_bmp.getColor(x, y);
SkColor expected_color = expected_bmp.getColor(x, y);
if (!ColorsMatchWithinLimit(actual_color, expected_color, error_limit)) {
if (error_pixels_count < 10) {
LOG(ERROR) << "Pixel (" << x << "," << y << "): expected " << std::hex
<< expected_color << " actual " << actual_color;
}
error_pixels_count++;
error_bounding_rect.Union(gfx::Rect(x, y, 1, 1));
}
}
}
if (error_pixels_count != 0) {
LOG(ERROR) << "Number of pixel with an error: " << error_pixels_count;
LOG(ERROR) << "Error Bounding Box : " << error_bounding_rect.ToString();
LOG(ERROR) << "To update goldens, use --update-goldens.";
return false;
}
return true;
}
bool WriteStringToFile(const base::FilePath& file_path,
const std::string& content) {
int result = base::WriteFile(file_path, content.data(),
static_cast<int>(content.size()));
return content.size() == static_cast<size_t>(result);
}
bool ScreenshotMatchesGolden(const std::string& screenshot_data,
const std::string& golden_file_name) {
static const base::FilePath kGoldenDirectory(
FILE_PATH_LITERAL("headless/test/data/golden"));
SkBitmap actual_bitmap;
EXPECT_TRUE(DecodePNG(screenshot_data, &actual_bitmap));
if (actual_bitmap.empty())
return false;
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath src_dir;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
base::FilePath golden_path =
src_dir.Append(kGoldenDirectory).Append(golden_file_name);
if (base::CommandLine::ForCurrentProcess()->HasSwitch(kUpdateGoldens)) {
LOG(INFO) << "Updating golden file at " << golden_path;
CHECK(WriteStringToFile(golden_path, screenshot_data));
}
std::string golden_data;
CHECK(base::ReadFileToString(golden_path, &golden_data));
SkBitmap expected_bitmap;
EXPECT_TRUE(DecodePNG(golden_data, &expected_bitmap));
if (expected_bitmap.empty())
return false;
return MatchesBitmap(expected_bitmap, actual_bitmap, 5);
}
} // namespace
HeadlessRenderTest::HeadlessRenderTest() : weak_ptr_factory_(this) {}
HeadlessRenderTest::~HeadlessRenderTest() = default;
void HeadlessRenderTest::PostRunAsynchronousTest() {
// Make sure the test did complete.
EXPECT_EQ(FINISHED, state_) << "The test did not finish.";
}
class HeadlessRenderTest::AdditionalVirtualTimeBudget
: public VirtualTimeController::RepeatingTask,
public VirtualTimeController::Observer {
public:
AdditionalVirtualTimeBudget(VirtualTimeController* virtual_time_controller,
HeadlessRenderTest* test,
base::RunLoop* run_loop,
int budget_ms)
: RepeatingTask(StartPolicy::WAIT_FOR_NAVIGATION, 0),
virtual_time_controller_(virtual_time_controller),
test_(test),
run_loop_(run_loop) {
virtual_time_controller_->ScheduleRepeatingTask(
this, base::TimeDelta::FromMilliseconds(budget_ms));
virtual_time_controller_->AddObserver(this);
virtual_time_controller_->StartVirtualTime();
}
~AdditionalVirtualTimeBudget() override {
virtual_time_controller_->RemoveObserver(this);
virtual_time_controller_->CancelRepeatingTask(this);
}
// headless::VirtualTimeController::RepeatingTask implementation:
void IntervalElapsed(
base::TimeDelta virtual_time,
base::OnceCallback<void(ContinuePolicy)> continue_callback) override {
std::move(continue_callback).Run(ContinuePolicy::NOT_REQUIRED);
}
// headless::VirtualTimeController::Observer:
void VirtualTimeStarted(base::TimeDelta virtual_time_offset) override {
run_loop_->Quit();
}
void VirtualTimeStopped(base::TimeDelta virtual_time_offset) override {
test_->HandleVirtualTimeExhausted();
delete this;
}
private:
headless::VirtualTimeController* const virtual_time_controller_;
HeadlessRenderTest* test_;
base::RunLoop* run_loop_;
};
void HeadlessRenderTest::RunDevTooledTest() {
virtual_time_controller_ =
std::make_unique<VirtualTimeController>(devtools_client_.get());
SetDeviceMetricsOverride(headless::page::Viewport::Builder()
.SetX(0)
.SetY(0)
.SetWidth(1)
.SetHeight(1)
.SetScale(1)
.Build());
compositor_controller_ = std::make_unique<CompositorController>(
browser()->BrowserMainThread(), devtools_client_.get(),
virtual_time_controller_.get(),
base::TimeDelta::FromMilliseconds(kAnimationIntervalMs),
kUpdateDisplayForAnimations);
devtools_client_->GetPage()->GetExperimental()->AddObserver(this);
devtools_client_->GetPage()->Enable(Sync());
devtools_client_->GetRuntime()->GetExperimental()->AddObserver(this);
devtools_client_->GetRuntime()->Enable(Sync());
GURL url = GetPageUrl(devtools_client_.get());
// Pause virtual time until we actually start loading content.
{
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy(
emulation::SetVirtualTimePolicyParams::Builder()
.SetPolicy(emulation::VirtualTimePolicy::PAUSE)
.Build());
devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy(
emulation::SetVirtualTimePolicyParams::Builder()
.SetPolicy(
emulation::VirtualTimePolicy::PAUSE_IF_NETWORK_FETCHES_PENDING)
.SetBudget(4001)
.SetWaitForNavigation(true)
.Build(),
base::BindOnce(&SetVirtualTimePolicyDoneCallback, &run_loop));
run_loop.Run();
}
{
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
// Note AdditionalVirtualTimeBudget will self delete.
new AdditionalVirtualTimeBudget(virtual_time_controller_.get(), this,
&run_loop, 5000);
run_loop.Run();
}
state_ = STARTING;
devtools_client_->GetPage()->Navigate(url.spec());
browser()->BrowserMainThread()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&HeadlessRenderTest::HandleTimeout,
weak_ptr_factory_.GetWeakPtr()),
base::TimeDelta::FromSeconds(10));
// The caller will loop until FinishAsynchronousTest() is called either
// from OnGetDomSnapshotDone() or from HandleTimeout().
}
void HeadlessRenderTest::SetDeviceMetricsOverride(
std::unique_ptr<headless::page::Viewport> viewport) {
gfx::Size size = GetEmulatedWindowSize();
devtools_client_->GetEmulation()->GetExperimental()->SetDeviceMetricsOverride(
headless::emulation::SetDeviceMetricsOverrideParams::Builder()
.SetDeviceScaleFactor(0)
.SetMobile(false)
.SetWidth(size.width())
.SetHeight(size.height())
.SetScreenWidth(size.width())
.SetScreenHeight(size.height())
.SetViewport(std::move(viewport))
.Build(),
Sync());
}
void HeadlessRenderTest::OnTimeout() {
ADD_FAILURE() << "Rendering timed out!";
}
void HeadlessRenderTest::SetUpCommandLine(base::CommandLine* command_line) {
HeadlessAsyncDevTooledBrowserTest::SetUpCommandLine(command_line);
// See bit.ly/headless-rendering for why we use these flags.
command_line->AppendSwitch(switches::kRunAllCompositorStagesBeforeDraw);
command_line->AppendSwitch(switches::kDisableNewContentRenderingTimeout);
command_line->AppendSwitch(cc::switches::kDisableCheckerImaging);
command_line->AppendSwitch(cc::switches::kDisableThreadedAnimation);
command_line->AppendSwitch(switches::kDisableImageAnimationResync);
command_line->AppendSwitch(switches::kDisableThreadedScrolling);
scoped_feature_list_.InitAndEnableFeature(
features::kEnableSurfaceSynchronization);
}
void HeadlessRenderTest::SetUp() {
EnablePixelOutput();
HeadlessAsyncDevTooledBrowserTest::SetUp();
}
void HeadlessRenderTest::CustomizeHeadlessBrowserContext(
HeadlessBrowserContext::Builder& builder) {
builder.SetOverrideWebPreferencesCallback(
base::Bind(&HeadlessRenderTest::OverrideWebPreferences,
weak_ptr_factory_.GetWeakPtr()));
}
bool HeadlessRenderTest::GetEnableBeginFrameControl() {
return true;
}
void HeadlessRenderTest::OverrideWebPreferences(WebPreferences* preferences) {
preferences->hide_scrollbars = true;
preferences->javascript_enabled = true;
preferences->autoplay_policy = content::AutoplayPolicy::kUserGestureRequired;
}
base::Optional<HeadlessRenderTest::ScreenshotOptions>
HeadlessRenderTest::GetScreenshotOptions() {
return base::nullopt;
}
gfx::Size HeadlessRenderTest::GetEmulatedWindowSize() {
return gfx::Size(800, 600);
}
void HeadlessRenderTest::OnLoadEventFired(const page::LoadEventFiredParams&) {
CHECK_NE(INIT, state_);
if (state_ == LOADING || state_ == STARTING) {
state_ = RENDERING;
}
}
void HeadlessRenderTest::OnFrameStartedLoading(
const page::FrameStartedLoadingParams& params) {
CHECK_NE(INIT, state_);
if (state_ == STARTING) {
state_ = LOADING;
main_frame_ = params.GetFrameId();
}
}
void HeadlessRenderTest::OnFrameScheduledNavigation(
const page::FrameScheduledNavigationParams& params) {
scheduled_navigations_[params.GetFrameId()].emplace_back(
Navigation{params.GetUrl(), params.GetReason()});
}
void HeadlessRenderTest::OnFrameNavigated(
const page::FrameNavigatedParams& params) {
frames_[params.GetFrame()->GetId()].push_back(params.GetFrame()->Clone());
}
void HeadlessRenderTest::OnConsoleAPICalled(
const runtime::ConsoleAPICalledParams& params) {
std::stringstream str;
switch (params.GetType()) {
case runtime::ConsoleAPICalledType::WARNING:
str << "W";
break;
case runtime::ConsoleAPICalledType::ASSERT:
case runtime::ConsoleAPICalledType::ERR:
str << "E";
break;
case runtime::ConsoleAPICalledType::DEBUG:
str << "D";
break;
case runtime::ConsoleAPICalledType::INFO:
str << "I";
break;
default:
str << "L";
break;
}
const auto& args = *params.GetArgs();
for (const auto& arg : args) {
str << " ";
if (arg->HasDescription()) {
str << arg->GetDescription();
} else if (arg->GetType() == runtime::RemoteObjectType::UNDEFINED) {
str << "undefined";
} else if (arg->HasValue()) {
const base::Value* v = arg->GetValue();
switch (v->type()) {
case base::Value::Type::NONE:
str << "null";
break;
case base::Value::Type::BOOLEAN:
str << v->GetBool();
break;
case base::Value::Type::INTEGER:
str << v->GetInt();
break;
case base::Value::Type::DOUBLE:
str << v->GetDouble();
break;
case base::Value::Type::STRING:
str << v->GetString();
break;
default:
DCHECK(false);
break;
}
} else {
DCHECK(false);
}
}
console_log_.push_back(str.str());
}
void HeadlessRenderTest::OnExceptionThrown(
const runtime::ExceptionThrownParams& params) {
const runtime::ExceptionDetails* details = params.GetExceptionDetails();
js_exceptions_.push_back(details->GetText() + " " +
details->GetException()->GetDescription());
}
void HeadlessRenderTest::VerifyDom(
dom_snapshot::GetSnapshotResult* dom_snapshot) {}
void HeadlessRenderTest::OnPageRenderCompleted() {
CHECK_GE(state_, LOADING);
if (state_ >= DONE)
return;
state_ = DONE;
devtools_client_->GetDOMSnapshot()->GetExperimental()->GetSnapshot(
dom_snapshot::GetSnapshotParams::Builder()
.SetComputedStyleWhitelist(std::vector<std::string>())
.Build(),
base::BindOnce(&HeadlessRenderTest::OnGetDomSnapshotDone,
weak_ptr_factory_.GetWeakPtr()));
}
void HeadlessRenderTest::HandleVirtualTimeExhausted() {
if (state_ < DONE) {
OnPageRenderCompleted();
}
}
void HeadlessRenderTest::OnGetDomSnapshotDone(
std::unique_ptr<dom_snapshot::GetSnapshotResult> result) {
CHECK_EQ(DONE, state_);
VerifyDom(result.get());
base::Optional<ScreenshotOptions> screenshot_options = GetScreenshotOptions();
if (screenshot_options) {
state_ = SCREENSHOT;
CaptureScreenshot(*screenshot_options);
return;
}
RenderComplete();
}
void HeadlessRenderTest::CaptureScreenshot(const ScreenshotOptions& options) {
// Set up emulation according to options.
auto clip = headless::page::Viewport::Builder()
.SetX(options.x)
.SetY(options.y)
.SetWidth(options.width)
.SetHeight(options.height)
.SetScale(options.scale)
.Build();
SetDeviceMetricsOverride(std::move(clip));
compositor_controller_->CaptureScreenshot(
CompositorController::ScreenshotParamsFormat::PNG, 100,
base::BindRepeating(&HeadlessRenderTest::ScreenshotCaptured,
base::Unretained(this), options));
}
void HeadlessRenderTest::ScreenshotCaptured(const ScreenshotOptions& options,
const std::string& data) {
EXPECT_TRUE(ScreenshotMatchesGolden(data, options.golden_file_name));
RenderComplete();
}
void HeadlessRenderTest::RenderComplete() {
state_ = FINISHED;
CleanUp();
FinishAsynchronousTest();
}
void HeadlessRenderTest::HandleTimeout() {
if (state_ != FINISHED) {
CleanUp();
FinishAsynchronousTest();
OnTimeout();
}
}
void HeadlessRenderTest::CleanUp() {
devtools_client_->GetRuntime()->Disable(Sync());
devtools_client_->GetRuntime()->GetExperimental()->RemoveObserver(this);
devtools_client_->GetPage()->Disable(Sync());
devtools_client_->GetPage()->GetExperimental()->RemoveObserver(this);
}
HeadlessRenderTest::ScreenshotOptions::ScreenshotOptions(
const std::string& golden_file_name,
int x,
int y,
int width,
int height,
double scale)
: golden_file_name(golden_file_name),
x(x),
y(y),
width(width),
height(height),
scale(scale) {}
} // namespace headless
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef HEADLESS_TEST_HEADLESS_RENDER_TEST_H_
#define HEADLESS_TEST_HEADLESS_RENDER_TEST_H_
#include <memory>
#include <string>
#include "base/macros.h"
#include "base/message_loop/message_loop_current.h"
#include "base/optional.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "headless/public/devtools/domains/emulation.h"
#include "headless/public/devtools/domains/page.h"
#include "headless/public/devtools/domains/runtime.h"
#include "headless/public/headless_browser.h"
#include "headless/public/headless_browser_context.h"
#include "headless/test/headless_browser_test.h"
#include "ui/gfx/geometry/size.h"
#include "url/gurl.h"
namespace headless {
class CompositorController;
class HeadlessDevToolsClient;
class VirtualTimeController;
namespace dom_snapshot {
class GetSnapshotResult;
} // namespace dom_snapshot
// Base class for tests that render a particular page and verify the output.
class HeadlessRenderTest : public HeadlessAsyncDevTooledBrowserTest,
public page::ExperimentalObserver,
public runtime::ExperimentalObserver {
public:
struct Navigation {
std::string url;
page::FrameScheduledNavigationReason reason;
};
void RunDevTooledTest() override;
protected:
// Automatically waits in destructor until callback is called.
class Sync {
public:
Sync() {}
~Sync() {
base::MessageLoopCurrent::ScopedNestableTaskAllower nest_loop;
run_loop.Run();
}
operator base::OnceClosure() { return run_loop.QuitClosure(); }
private:
base::RunLoop run_loop;
DISALLOW_COPY_AND_ASSIGN(Sync);
};
struct ScreenshotOptions {
ScreenshotOptions(const std::string& golden_file_name,
int x,
int y,
int width,
int height,
double scale);
std::string golden_file_name;
int x;
int y;
int width;
int height;
double scale;
};
HeadlessRenderTest();
~HeadlessRenderTest() override;
// Marks that the test case reached the final conclusion.
void SetTestCompleted() { state_ = FINISHED; }
// Do necessary preparations and return a URL to render.
virtual GURL GetPageUrl(HeadlessDevToolsClient* client) = 0;
// Check if the DOM snapshot is as expected.
virtual void VerifyDom(dom_snapshot::GetSnapshotResult* dom_snapshot);
// Called when all steps needed to load and present page are done.
virtual void OnPageRenderCompleted();
// Called if page rendering wasn't completed within reasonable time.
virtual void OnTimeout();
// Override to set specific options for requests.
virtual void OverrideWebPreferences(WebPreferences* preferences);
// Determines whether a screenshot will be taken or not. If one is taken,
// ScreenshotOptions specifies the area to capture as well as a golden data
// file to compare the result to. By default, no screenshot is taken.
virtual base::Optional<ScreenshotOptions> GetScreenshotOptions();
// Returns the emulated width/height of the window. Defaults to 800x600.
virtual gfx::Size GetEmulatedWindowSize();
// Setting up the browsertest.
void SetUpCommandLine(base::CommandLine* command_line) override;
void SetUp() override;
void CustomizeHeadlessBrowserContext(
HeadlessBrowserContext::Builder& builder) override;
bool GetEnableBeginFrameControl() override;
void PostRunAsynchronousTest() override;
// page::ExperimentalObserver implementation:
void OnLoadEventFired(const page::LoadEventFiredParams& params) override;
void OnFrameStartedLoading(
const page::FrameStartedLoadingParams& params) override;
void OnFrameScheduledNavigation(
const page::FrameScheduledNavigationParams& params) override;
void OnFrameNavigated(const page::FrameNavigatedParams& params) override;
// runtime::ExperimentalObserver implementation:
void OnConsoleAPICalled(
const runtime::ConsoleAPICalledParams& params) override;
void OnExceptionThrown(const runtime::ExceptionThrownParams& params) override;
// For each frame, keep track of scheduled navigations.
// FYI: It doesn't track every navigations. For instance, it doesn't include
// the first navigation. It doesn't include HTTP redirect, but it includes
// client-side redirect.
std::map<std::string, std::vector<Navigation>> scheduled_navigations_;
std::map<std::string, std::vector<std::unique_ptr<page::Frame>>> frames_;
std::string main_frame_;
std::vector<std::string> console_log_;
std::vector<std::string> js_exceptions_;
private:
class AdditionalVirtualTimeBudget;
void SetDeviceMetricsOverride(
std::unique_ptr<headless::page::Viewport> viewport);
void HandleVirtualTimeExhausted();
void OnGetDomSnapshotDone(
std::unique_ptr<dom_snapshot::GetSnapshotResult> result);
void CaptureScreenshot(const ScreenshotOptions& options);
void ScreenshotCaptured(const ScreenshotOptions& options,
const std::string& data);
void RenderComplete();
void HandleTimeout();
void CleanUp();
enum State {
INIT, // Setting up the client, no navigation performed yet.
STARTING, // Navigation request issued but URL not being loaded yet.
LOADING, // URL was requested but resources are not fully loaded yet.
RENDERING, // Main resources were loaded but page is still being rendered.
DONE, // Page considered to be rendered, DOM snapshot is being taken.
SCREENSHOT, // DOM snapshot has completed, screenshot is being taken.
FINISHED, // Test has finished.
};
State state_ = INIT;
std::unique_ptr<VirtualTimeController> virtual_time_controller_;
std::unique_ptr<CompositorController> compositor_controller_;
base::test::ScopedFeatureList scoped_feature_list_;
base::WeakPtrFactory<HeadlessRenderTest> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(HeadlessRenderTest);
};
} // namespace headless
#endif // HEADLESS_TEST_HEADLESS_RENDER_TEST_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment