#ifndef CHROME_BROWSER_GLIC_TEST_SUPPORT_GLIC_API_TEST_H_
#define CHROME_BROWSER_GLIC_TEST_SUPPORT_GLIC_API_TEST_H_
#include <type_traits>
#include "base/json/json_writer.h"
#include "base/test/run_until.h"
#include "base/test/test_timeouts.h"
#include "base/values.h"
#include "chrome/browser/glic/test_support/interactive_glic_test.h"
#include "chrome/browser/glic/test_support/non_interactive_glic_test.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace glic {
struct ExecuteTestOptions {
base::Value params;
bool expect_guest_frame_destroyed = false;
bool wait_for_guest = true;
bool should_fail = false;
std::string_view should_fail_with_error;
};
class WebUIStateListener : public Host::Observer {
public:
explicit WebUIStateListener(Host* host);
~WebUIStateListener() override;
void WebUiStateChanged(mojom::WebUiState state) override;
void WaitForWebUiState(mojom::WebUiState state);
private:
base::WeakPtr<Host> host_;
std::deque<mojom::WebUiState> states_;
};
class CurrentViewListener : public Host::Observer {
public:
explicit CurrentViewListener(Host* host);
~CurrentViewListener() override;
void OnViewChanged(mojom::CurrentView view) override;
void WaitForCurrentView(mojom::CurrentView view);
private:
raw_ptr<Host> host_;
std::deque<mojom::CurrentView> views_;
};
template <typename T>
requires std::is_base_of<
test::InteractiveGlicTestMixin<InteractiveBrowserTest>,
T>::value
class GlicApiTestBase : public T {
public:
explicit GlicApiTestBase(std::string_view js_source_path) {
T::embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&GlicApiTestBase::SorryPageRequestHandler, base::Unretained(this)));
T::embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&GlicApiTestBase::FakeRpcRequestHandler, base::Unretained(this)));
T::embedded_test_server()->RegisterRequestMonitor(
base::BindRepeating(&GlicApiTestBase::OnEmbeddedTestServerHttpRequest,
base::Unretained(this)));
T::add_mock_glic_query_param(
"test",
::testing::UnitTest::GetInstance()->current_test_info()->name());
features_.InitWithFeaturesAndParameters(
{
{features::kGlic,
{
{"glic-default-hotkey", "Ctrl+G"},
{features::kGlicPreLoadingTimeMs.name, "20"},
{features::kGlicMinLoadingTimeMs.name, "40"},
}},
},
{
features::kGlicWarming,
});
base::CommandLine::ForCurrentProcess()->AppendSwitch(
::switches::kGlicHostLogging);
T::SetGlicPagePath("/glic/browser_tests/test.html");
T::add_mock_glic_query_param("testsrc", js_source_path);
}
~GlicApiTestBase() override = default;
void SetUpOnMainThread() override {
T::host_resolver()->AddRule("a.com", "127.0.0.1");
T::host_resolver()->AddRule("b.com", "127.0.0.1");
T::DisableWarming();
NonInteractiveGlicTest::SetUpOnMainThread();
}
void TearDownOnMainThread() override {
if (!next_step_required_.empty()) {
FAIL() << "Test not finished: call ContinueJsTest()";
}
NonInteractiveGlicTest::TearDownOnMainThread();
}
GlicKeyedService* GetService() {
Profile* profile = T::browser()->profile();
return GlicKeyedServiceFactory::GetGlicKeyedService(profile);
}
Host* GetHost() {
GlicInstance* instance = T::GetGlicInstance();
return instance ? &instance->host() : nullptr;
}
void ExecuteJsTest(ExecuteTestOptions options = {}) {
if (options.wait_for_guest) {
WaitForGuest();
}
content::RenderFrameHost* glic_guest_frame = T::FindGlicGuestMainFrame();
ASSERT_TRUE(glic_guest_frame);
std::string param_json = base::WriteJson(options.params).value_or("");
ProcessTestResult(
glic_guest_frame->GetGlobalId(), options,
content::EvalJs(
glic_guest_frame,
base::StrCat(
{"runApiTest(",
base::NumberToString((TestTimeouts::action_max_timeout() * 0.9)
.InMilliseconds()),
",", param_json, ")"})));
}
void ContinueJsTest(ExecuteTestOptions options = {}) {
content::RenderFrameHost* glic_guest_frame = T::FindGlicGuestMainFrame();
ASSERT_TRUE(glic_guest_frame);
ASSERT_TRUE(next_step_required_.contains(glic_guest_frame->GetGlobalId()));
next_step_required_.erase(glic_guest_frame->GetGlobalId());
std::string param_json = base::WriteJson(options.params).value_or("");
ProcessTestResult(
glic_guest_frame->GetGlobalId(), options,
content::EvalJs(glic_guest_frame,
base::StrCat({"continueApiTest(", param_json, ")"})));
}
void WaitForGuest() {
auto end_time = base::TimeTicks::Now() + base::Seconds(10);
content::RenderFrameHost* frame = nullptr;
while (base::TimeTicks::Now() < end_time) {
frame = T::FindGlicGuestMainFrame();
if (frame) {
auto result =
content::EvalJs(frame, {"typeof runApiTest !== 'undefined'"});
if (result.is_ok() && result.ExtractBool()) {
return;
}
}
sleepWithRunLoop(base::Milliseconds(200));
}
FAIL() << "Timed out waiting for guest frame. Guest frame: "
<< (frame ? frame->GetLastCommittedURL().spec() : "not found");
}
void WaitForWebUiState(mojom::WebUiState state) {
WebUIStateListener listener(T::GetHost());
listener.WaitForWebUiState(state);
}
const std::optional<base::Value>& step_data() const { return step_data_; }
protected:
std::unique_ptr<net::test_server::HttpResponse> SorryPageRequestHandler(
const net::test_server::HttpRequest& request) {
if (request.method != net::test_server::METHOD_GET ||
!base::StartsWith(request.relative_url, "/sorry/index.html")) {
return nullptr;
}
auto result = std::make_unique<net::test_server::BasicHttpResponse>();
result->set_code(net::HttpStatusCode::HTTP_OK);
result->set_content_type("text/html");
result->set_content("Sorry!");
return result;
}
std::unique_ptr<net::test_server::HttpResponse> FakeRpcRequestHandler(
const net::test_server::HttpRequest& request) {
if (request.method != net::test_server::METHOD_GET ||
!base::StartsWith(request.relative_url, "/fake-rpc")) {
return nullptr;
}
auto result = std::make_unique<net::test_server::BasicHttpResponse>();
result->set_code(net::HttpStatusCode::HTTP_OK);
result->set_content_type("application/json");
result->set_content("{\"status\": \"ok\"}");
if (request.relative_url.find("/cors") != std::string::npos) {
result->AddCustomHeader("Access-Control-Allow-Origin", "*");
}
return result;
}
void ProcessTestResult(content::GlobalRenderFrameHostId frame_id,
const ExecuteTestOptions& options,
const content::EvalJsResult& result) {
if (options.expect_guest_frame_destroyed) {
ASSERT_THAT(result, content::EvalJsResult::ErrorIs(
testing::HasSubstr("RenderFrame deleted.")));
return;
}
ASSERT_THAT(result, content::EvalJsResult::IsOk());
if (result.is_dict()) {
const base::Value::Dict& dict = result.ExtractDict();
auto* id = dict.Find("id");
if (id && id->is_string() && id->GetString() == "next-step") {
step_data_ = dict.Find("payload")->Clone();
}
next_step_required_.insert(frame_id);
return;
}
if (!options.should_fail) {
ASSERT_EQ(result, "pass");
} else if (options.should_fail_with_error.empty()) {
ASSERT_NE(result, "pass")
<< "JS step should have failed, but it succeeded";
} else {
ASSERT_EQ(result, options.should_fail_with_error)
<< "JS step should have failed, but it succeeded";
}
}
void AssertAllTestsRegistered(
std::vector<std::string> gunit_test_suite_names) {
#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || \
defined(MEMORY_SANITIZER)
GTEST_SKIP() << "AssertAllTestsRegistered not processed for slow binaries.";
#else
T::RunTestSequence(T::OpenGlicWindow(T::GlicWindowMode::kDetached,
T::GlicInstrumentMode::kNone));
ExecuteJsTest();
ASSERT_TRUE(step_data()->is_list());
::testing::UnitTest* unit_test = ::testing::UnitTest::GetInstance();
std::set<std::string> test_suites;
std::set<std::string> js_test_names, cc_test_names;
for (const auto& test_name : step_data()->GetList()) {
js_test_names.insert(test_name.GetString());
}
for (int i = 0; i < unit_test->total_test_suite_count(); ++i) {
const auto* test_suite = unit_test->GetTestSuite(i);
if (!base::Contains(gunit_test_suite_names,
std::string(test_suite->name()))) {
continue;
}
for (int j = 0; j < test_suite->total_test_count(); ++j) {
std::string name = test_suite->GetTestInfo(j)->name();
name = name.substr(0, name.find_last_of('/'));
if (name.starts_with("DISABLED_")) {
cc_test_names.insert(name.substr(9));
} else {
cc_test_names.insert(name);
}
}
}
ASSERT_THAT(js_test_names, testing::IsSubsetOf(cc_test_names))
<< "Test cases in js, but not cc";
ContinueJsTest();
#endif
}
void OnEmbeddedTestServerHttpRequest(
const net::test_server::HttpRequest& request) {
embedded_test_server_requests_.push_back(request);
}
void sleepWithRunLoop(base::TimeDelta sleepDuration) {
base::RunLoop run_loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), sleepDuration);
run_loop.Run();
}
std::vector<net::test_server::HttpRequest> embedded_test_server_requests_;
std::set<content::GlobalRenderFrameHostId> next_step_required_;
std::optional<base::Value> step_data_;
base::test::ScopedFeatureList features_;
};
using NonInteractiveGlicApiTest = GlicApiTestBase<NonInteractiveGlicTest>;
using InteractiveGlicApiTest = GlicApiTestBase<test::InteractiveGlicTest>;
}
#endif