// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "fuchsia_web/common/test/test_navigation_listener.h"
#include <string>
#include <string_view>
#include <utility>
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "url/gurl.h"
namespace {
const char* PageTypeToString(fuchsia::web::PageType type) {
return type == fuchsia::web::PageType::NORMAL ? "NORMAL" : "ERROR";
}
} // namespace
TestNavigationListener::TestNavigationListener() {
// Set up the default acknowledgement handling behavior.
SetBeforeAckHook({});
}
TestNavigationListener::~TestNavigationListener() = default;
void TestNavigationListener::RunUntilNavigationStateMatches(
const fuchsia::web::NavigationState& expected_state) {
DCHECK(before_ack_);
DCHECK(!expected_state_);
expected_state_ = &expected_state;
if (!AllFieldsMatch(nullptr)) {
// Spin the runloop until the expected conditions are met.
base::RunLoop run_loop;
base::AutoReset<BeforeAckCallback> callback_setter(
&before_ack_,
base::BindRepeating(&TestNavigationListener::QuitLoopIfAllFieldsMatch,
base::Unretained(this), run_loop.QuitClosure(),
before_ack_));
run_loop.Run();
}
expected_state_ = nullptr;
}
void TestNavigationListener::RunUntilLoaded() {
fuchsia::web::NavigationState state;
state.set_page_type(fuchsia::web::PageType::NORMAL);
state.set_is_main_document_loaded(true);
RunUntilNavigationStateMatches(state);
}
void TestNavigationListener::RunUntilUrlEquals(const GURL& expected_url) {
fuchsia::web::NavigationState state;
state.set_url(expected_url.spec());
state.set_page_type(fuchsia::web::PageType::NORMAL);
state.set_is_main_document_loaded(true);
RunUntilNavigationStateMatches(state);
}
void TestNavigationListener::RunUntilTitleEquals(
const std::string_view expected_title) {
fuchsia::web::NavigationState state;
state.set_title(std::string(expected_title));
state.set_page_type(fuchsia::web::PageType::NORMAL);
RunUntilNavigationStateMatches(state);
}
void TestNavigationListener::RunUntilUrlAndTitleEquals(
const GURL& expected_url,
const std::string_view expected_title) {
fuchsia::web::NavigationState state;
state.set_url(expected_url.spec());
state.set_title(std::string(expected_title));
state.set_page_type(fuchsia::web::PageType::NORMAL);
RunUntilNavigationStateMatches(state);
}
void TestNavigationListener::RunUntilErrorPageIsLoadedAndTitleEquals(
std::string_view expected_title) {
fuchsia::web::NavigationState state;
state.set_title(std::string(expected_title));
state.set_page_type(fuchsia::web::PageType::ERROR);
state.set_is_main_document_loaded(true);
RunUntilNavigationStateMatches(state);
}
void TestNavigationListener::RunUntilUrlTitleBackForwardEquals(
const GURL& expected_url,
std::string_view expected_title,
bool expected_can_go_back,
bool expected_can_go_forward) {
fuchsia::web::NavigationState state;
state.set_url(expected_url.spec());
state.set_title(std::string(expected_title));
state.set_page_type(fuchsia::web::PageType::NORMAL);
state.set_can_go_back(expected_can_go_back);
state.set_can_go_forward(expected_can_go_forward);
RunUntilNavigationStateMatches(state);
}
void TestNavigationListener::SetBeforeAckHook(BeforeAckCallback send_ack_cb) {
if (send_ack_cb) {
before_ack_ = send_ack_cb;
} else {
before_ack_ = base::BindRepeating(
[](const fuchsia::web::NavigationState&,
OnNavigationStateChangedCallback callback) { callback(); });
}
}
void TestNavigationListener::OnNavigationStateChanged(
fuchsia::web::NavigationState changes,
OnNavigationStateChangedCallback callback) {
DCHECK(before_ack_);
// Update our local cache of the Frame's current state.
if (changes.has_url())
current_state_.set_url(changes.url());
if (changes.has_title())
current_state_.set_title(changes.title());
if (changes.has_can_go_back())
current_state_.set_can_go_back(changes.can_go_back());
if (changes.has_can_go_forward())
current_state_.set_can_go_forward(changes.can_go_forward());
if (changes.has_page_type())
current_state_.set_page_type(changes.page_type());
if (changes.has_is_main_document_loaded()) {
current_state_.set_is_main_document_loaded(
changes.is_main_document_loaded());
}
if (changes.has_favicon()) {
current_state_.set_favicon(std::move(*changes.mutable_favicon()));
}
if (VLOG_IS_ON(1)) {
std::string state_string;
state_string.reserve(100);
if (current_state_.has_url()) {
state_string.append(
base::StringPrintf(" url=%s ", current_state_.url().c_str()));
}
if (current_state_.has_title()) {
state_string.append(
base::StringPrintf(" title='%s' ", current_state_.title().c_str()));
}
if (current_state_.has_can_go_back()) {
state_string.append(
base::StringPrintf(" can_go_back=%d ", current_state_.can_go_back()));
}
if (current_state_.has_can_go_forward()) {
state_string.append(base::StringPrintf(" can_go_forward=%d ",
current_state_.can_go_forward()));
}
if (current_state_.has_page_type()) {
state_string.append(base::StringPrintf(
" page_type=%s ", PageTypeToString(current_state_.page_type())));
}
if (current_state_.has_is_main_document_loaded()) {
state_string.append(
base::StringPrintf(" is_main_document_loaded=%d ",
current_state_.is_main_document_loaded()));
}
VLOG(1) << "Navigation state changed: " << state_string;
}
last_changes_ = std::move(changes);
// Signal readiness for the next navigation event.
before_ack_.Run(last_changes_, std::move(callback));
}
bool TestNavigationListener::AllFieldsMatch(
std::vector<FailureReason>* failure_reasons) {
bool success = true;
if (expected_state_->has_url() &&
(!current_state_.has_url() ||
expected_state_->url() != current_state_.url())) {
if (failure_reasons) {
failure_reasons->push_back({"url", expected_state_->url()});
}
success = false;
}
if (expected_state_->has_title() &&
(!current_state_.has_title() ||
expected_state_->title() != current_state_.title())) {
if (failure_reasons) {
failure_reasons->push_back({"title", expected_state_->title()});
}
success = false;
}
if (expected_state_->has_can_go_forward() &&
(!current_state_.has_can_go_forward() ||
expected_state_->can_go_forward() != current_state_.can_go_forward())) {
if (failure_reasons) {
failure_reasons->push_back(
{"can_go_forward",
base::NumberToString(expected_state_->can_go_forward())});
}
success = false;
}
if (expected_state_->has_can_go_back() &&
(!current_state_.has_can_go_back() ||
expected_state_->can_go_back() != current_state_.can_go_back())) {
if (failure_reasons) {
failure_reasons->push_back(
{"can_go_back",
base::NumberToString(expected_state_->can_go_back())});
}
success = false;
}
if (expected_state_->has_page_type() &&
(!current_state_.has_page_type() ||
expected_state_->page_type() != current_state_.page_type())) {
if (failure_reasons) {
failure_reasons->push_back(
{"page_type", PageTypeToString(expected_state_->page_type())});
}
success = false;
}
if (expected_state_->has_is_main_document_loaded() &&
(!current_state_.has_is_main_document_loaded() ||
expected_state_->is_main_document_loaded() !=
current_state_.is_main_document_loaded())) {
if (failure_reasons) {
failure_reasons->push_back(
{"is_main_document_loaded",
base::NumberToString(expected_state_->is_main_document_loaded())});
}
success = false;
}
return success;
}
void TestNavigationListener::QuitLoopIfAllFieldsMatch(
base::RepeatingClosure quit_run_loop_closure,
TestNavigationListener::BeforeAckCallback before_ack_callback,
const fuchsia::web::NavigationState& change,
fuchsia::web::NavigationEventListener::OnNavigationStateChangedCallback
ack_callback) {
std::vector<FailureReason> failure_reasons;
if (AllFieldsMatch(VLOG_IS_ON(1) ? &failure_reasons : nullptr)) {
VLOG(1) << "All navigation expectations satisfied, continuing...";
quit_run_loop_closure.Run();
} else if (VLOG_IS_ON(1)) {
std::vector<std::string> formatted_reasons;
for (const auto& reason : failure_reasons) {
formatted_reasons.push_back(base::StringPrintf("%s=%s", reason.field_name,
reason.expected.c_str()));
}
VLOG(1) << "Still waiting on the following unmet expectations: "
<< base::JoinString(formatted_reasons, ", ");
}
before_ack_callback.Run(change, std::move(ack_callback));
}