blob: dff168ff95ebe78f936f584fe2fecb1a013d2d29 [file] [log] [blame] [edit]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <math.h>
#include <algorithm>
#include <deque>
#include <memory>
#include <ranges>
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "base/values.h"
#include "base/version_info/version_info.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/contextual_cueing/contextual_cueing_features.h"
#include "chrome/browser/contextual_cueing/contextual_cueing_service.h"
#include "chrome/browser/contextual_cueing/contextual_cueing_service_factory.h"
#include "chrome/browser/contextual_cueing/mock_contextual_cueing_service.h"
#include "chrome/browser/enterprise/browser_management/management_service_factory.h"
#include "chrome/browser/glic/glic_keyed_service.h"
#include "chrome/browser/glic/glic_keyed_service_factory.h"
#include "chrome/browser/glic/glic_metrics.h"
#include "chrome/browser/glic/glic_profile_manager.h"
#include "chrome/browser/glic/host/glic.mojom.h"
#include "chrome/browser/glic/host/glic_page_handler.h"
#include "chrome/browser/glic/host/host.h"
#include "chrome/browser/glic/test_support/glic_test_util.h"
#include "chrome/browser/glic/test_support/interactive_test_util.h"
#include "chrome/browser/glic/test_support/non_interactive_glic_test.h"
#include "chrome/browser/glic/widget/glic_window_controller.h"
#include "chrome/browser/media/audio_ducker.h"
#include "chrome/browser/permissions/system/mock_platform_handle.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/profiles/profile_picker.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/metrics/metrics_service.h"
#include "components/policy/core/common/management/management_service.h"
#include "components/policy/core/common/management/scoped_management_service_override_for_testing.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/variations/synthetic_trial_registry.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "pdf/buildflags.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/display/screen.h"
// This file runs the respective JS tests from
// chrome/test/data/webui/glic/api_test.ts.
#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || \
defined(MEMORY_SANITIZER)
#define SLOW_BINARY
#endif
namespace glic {
namespace {
using testing::_;
using testing::Contains;
using testing::Pair;
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kFirstTab);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kSecondTab);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kSettingsTab);
std::vector<std::string> GetTestSuiteNames() {
return {
"GlicApiTest",
"GlicApiTestWithOneTab",
"GlicApiTestWithFastTimeout",
"GlicApiTestSystemSettingsTest",
"GlicApiTestWithOneTabAndContextualCueing",
"GlicApiTestWithOneTabAndPreloading",
"GlicApiTestUserStatusCheckTest",
"GlicApiTestWithOneTabMoreDebounceDelay",
};
}
// Observes the state of the WebUI hosted in the glic window.
class WebUIStateListener : public Host::Observer {
public:
explicit WebUIStateListener(Host* host) : host_(host) {
host_->AddObserver(this);
states_.push_back(host_->GetPrimaryWebUiState());
}
~WebUIStateListener() override { host_->RemoveObserver(this); }
void WebUiStateChanged(mojom::WebUiState state) override {
states_.push_back(state);
}
// Returns if `state` has been seen. Consumes all observed states up to the
// point where this state is seen.
void WaitForWebUiState(mojom::WebUiState state) {
ASSERT_TRUE(base::test::RunUntil([&]() {
while (!states_.empty()) {
if (states_.front() != state) {
states_.pop_front();
continue;
}
return true;
}
return false;
})) << "Timed out waiting for WebUI state "
<< state << ". State =" << host_->GetPrimaryWebUiState();
}
private:
raw_ptr<Host> host_;
std::deque<mojom::WebUiState> states_;
};
struct ExecuteTestOptions {
// Test parameters passed to the JS test. See `ApiTestFixtureBase.testParams`.
base::Value params;
// Assert that the test function does not return, and instead destroys the
// test frame.
bool expect_guest_frame_destroyed = false;
// Whether to wait for the guest before starting the test.
// TODO(harringtond): This should always be true, but I'm seeing this error
// for tests where wait_for_guest needs to be overridden:
// DCHECK failed: false. Non-Profile BrowserContext passed to
// Profile::FromBrowserContext! If you have a test linked in chrome/ you
// need a chrome/ based test class such as TestingProfile in
// chrome/test/base/testing_profile.h or you need to subclass your test
// class from Profile, not from BrowserContext.
bool wait_for_guest = true;
};
class GlicApiTest : public NonInteractiveGlicTest {
public:
GlicApiTest() {
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&GlicApiTest::SorryPageRequestHandler, base::Unretained(this)));
embedded_test_server()->RegisterRequestMonitor(base::BindRepeating(
&GlicApiTest::OnEmbeddedTestServerHttpRequest, base::Unretained(this)));
add_mock_glic_query_param(
"test",
::testing::UnitTest::GetInstance()->current_test_info()->name());
features_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{
{features::kGlic,
{
{"glic-default-hotkey", "Ctrl+G"},
// Shorten load timeouts.
{features::kGlicPreLoadingTimeMs.name, "20"},
{features::kGlicMinLoadingTimeMs.name, "40"},
}},
{features::kGlicScrollTo, {}},
{features::kGlicClosedCaptioning, {}},
{features::kGlicApiActivationGating, {}},
{features::kGlicUserStatusCheck,
{{features::kGlicUserStatusRefreshApi.name, "true"},
{features::kGlicUserStatusThrottleInterval.name, "2s"}}},
},
/*disabled_features=*/
{
features::kGlicWarming,
});
base::CommandLine::ForCurrentProcess()->AppendSwitch(
::switches::kGlicHostLogging);
SetGlicPagePath("/glic/test.html");
}
~GlicApiTest() override = default;
void TearDownOnMainThread() override {
if (next_step_required_) {
FAIL() << "Test not finished: call ContinueJsTest()";
}
test::InteractiveGlicTest::TearDownOnMainThread();
}
GlicKeyedService* GetService() {
Profile* profile = browser()->profile();
return GlicKeyedServiceFactory::GetGlicKeyedService(profile);
}
Host* GetHost() {
Profile* profile = browser()->profile();
return &GlicKeyedServiceFactory::GetGlicKeyedService(profile)->host();
}
// Run the test typescript function. The typescript function must have the
// same name as the current test.
// If the test uses `advanceToNextStep()`, then ContinueJsTest() must be
// called later.
void ExecuteJsTest(ExecuteTestOptions options = {}) {
if (options.wait_for_guest) {
WaitForGuest();
}
content::RenderFrameHost* glic_guest_frame = FindGlicGuestMainFrame();
ASSERT_TRUE(glic_guest_frame);
std::string param_json;
base::JSONWriter::Write(options.params, &param_json);
ProcessTestResult(
options,
content::EvalJs(
glic_guest_frame,
base::StrCat(
{"runApiTest(",
base::NumberToString((TestTimeouts::action_max_timeout() * 0.9)
.InMilliseconds()),
",", param_json, ")"})));
}
// Continues test execution if `advanceToNextStep()` was used to return
// control to C++.
void ContinueJsTest(ExecuteTestOptions options = {}) {
ASSERT_TRUE(next_step_required_);
content::RenderFrameHost* glic_guest_frame = FindGlicGuestMainFrame();
next_step_required_ = false;
ASSERT_TRUE(glic_guest_frame);
std::string param_json;
base::JSONWriter::Write(options.params, &param_json);
ProcessTestResult(
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) {
// Note: Sometimes the previous guest frame is still around, but it won't
// have the runApiTest function. Loop until both conditions are met.
frame = FindGlicGuestMainFrame();
if (frame) {
auto result =
content::EvalJs(frame, {"typeof runApiTest !== 'undefined'"});
if (result.error.empty() && 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(&host());
listener.WaitForWebUiState(state);
}
const std::optional<base::Value>& step_data() const { return step_data_; }
protected:
// Just an error page at a specific /sorry/... URL.
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;
}
void ProcessTestResult(const ExecuteTestOptions& options,
const content::EvalJsResult& result) {
if (options.expect_guest_frame_destroyed) {
ASSERT_THAT(result.error, testing::HasSubstr("RenderFrame deleted."));
return;
}
ASSERT_THAT(result, content::EvalJsResult::IsOk());
if (result.value.is_dict()) {
auto* id = result.value.GetDict().Find("id");
if (id && id->is_string() && id->GetString() == "next-step") {
step_data_ = result.value.GetDict().Find("payload")->Clone();
}
next_step_required_ = true;
return;
}
ASSERT_THAT(result.ExtractString(), testing::Eq("pass"));
}
// Records all requests to the embedded test server.
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_;
bool next_step_required_ = false;
std::optional<base::Value> step_data_;
base::test::ScopedFeatureList features_;
};
class GlicApiTestWithOneTab : public GlicApiTest {
public:
GlicApiTestWithOneTab() {
scoped_feature_list_.InitWithFeatures({features::kGlicClosedCaptioning},
/*disabled_features=*/{});
}
void SetUpOnMainThread() override {
GlicApiTest::SetUpOnMainThread();
histogram_tester = std::make_unique<base::HistogramTester>();
// Load the test page in a tab, so that there is some page context.
RunTestSequence(InstrumentTab(kFirstTab),
NavigateWebContents(kFirstTab, page_url()),
OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
}
GURL page_url() {
return InProcessBrowserTest::embedded_test_server()->GetURL(
"/glic/test.html");
}
std::unique_ptr<base::HistogramTester> histogram_tester;
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
class GlicApiTestWithOneTabAndPreloading : public GlicApiTestWithOneTab {
public:
GlicApiTestWithOneTabAndPreloading() {
features_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{features::kGlic,
{
{"glic-default-hotkey", "Ctrl+G"},
// Shorten load timeouts.
{features::kGlicPreLoadingTimeMs.name, "20"},
{features::kGlicMinLoadingTimeMs.name, "40"},
}},
{features::kGlicApiActivationGating, {}},
{features::kGlicWarming,
{{features::kGlicWarmingDelayMs.name, "0"},
{features::kGlicWarmingJitterMs.name, "0"}}}},
/*disabled_features=*/
{});
// This will temporarily disable preloading to ensure that we don't load the
// web client before we've initialized the embedded test server and can set
// the correct URL.
GlicProfileManager::ForceMemoryPressureForTesting(
base::MemoryPressureMonitor::MemoryPressureLevel::
MEMORY_PRESSURE_LEVEL_CRITICAL);
GlicProfileManager::ForceConnectionTypeForTesting(
network::mojom::ConnectionType::CONNECTION_ETHERNET);
}
void SetUpOnMainThread() override {
GlicApiTest::SetUpOnMainThread();
RunTestSequence(InstrumentTab(kFirstTab),
NavigateWebContents(kFirstTab, page_url()));
}
void TearDown() override {
GlicApiTestWithOneTab::TearDown();
GlicProfileManager::ForceMemoryPressureForTesting(std::nullopt);
GlicProfileManager::ForceConnectionTypeForTesting(std::nullopt);
}
auto CreateAndWarmGlic() {
return Do([this] { GetService()->TryPreload(); });
}
auto ResetMemoryPressure() {
return Do([]() {
GlicProfileManager::ForceMemoryPressureForTesting(
base::MemoryPressureMonitor::MemoryPressureLevel::
MEMORY_PRESSURE_LEVEL_NONE);
});
}
private:
base::test::ScopedFeatureList features_;
};
class GlicApiTestWithOneTabAndContextualCueing : public GlicApiTestWithOneTab {
public:
GlicApiTestWithOneTabAndContextualCueing() {
contextual_cueing_features_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{features::kGlic,
{
{"glic-default-hotkey", "Ctrl+G"},
// Shorten load timeouts.
{features::kGlicPreLoadingTimeMs.name, "20"},
{features::kGlicMinLoadingTimeMs.name, "40"},
}},
{features::kGlicApiActivationGating, {}},
{contextual_cueing::kGlicZeroStateSuggestions, {}}},
/*disabled_features=*/
{
features::kGlicWarming,
});
}
// Create the mock service.
void SetUpBrowserContextKeyedServices(
content::BrowserContext* browser_context) override {
mock_cueing_service_ = static_cast<
testing::NiceMock<contextual_cueing::MockContextualCueingService>*>(
contextual_cueing::ContextualCueingServiceFactory::GetInstance()
->SetTestingFactoryAndUse(
browser_context,
base::BindRepeating([](content::BrowserContext* context)
-> std::unique_ptr<KeyedService> {
return std::make_unique<testing::NiceMock<
contextual_cueing::MockContextualCueingService>>();
})));
GlicApiTestWithOneTab::SetUpBrowserContextKeyedServices(browser_context);
}
void TearDownOnMainThread() override {
mock_cueing_service_ = nullptr;
GlicApiTestWithOneTab::TearDownOnMainThread();
}
contextual_cueing::MockContextualCueingService* mock_cueing_service() {
return mock_cueing_service_.get();
}
private:
raw_ptr<testing::NiceMock<contextual_cueing::MockContextualCueingService>>
mock_cueing_service_;
base::test::ScopedFeatureList contextual_cueing_features_;
};
class GlicApiTestWithFastTimeout : public GlicApiTest {
public:
GlicApiTestWithFastTimeout() {
features2_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{
features::kGlic,
{
// For slow binaries, use a longer timeout.
#if defined(SLOW_BINARY)
{features::kGlicMaxLoadingTimeMs.name, "6000"},
#else
{features::kGlicMaxLoadingTimeMs.name, "3000"},
#endif
},
}},
/*disabled_features=*/
{});
}
private:
base::test::ScopedFeatureList features2_;
};
// Note: Test names must match test function names in api_test.ts.
// TODO(harringtond): Many of these tests are minimal, and could be improved
// with additional cases and additional assertions.
// Just verify the test harness works.
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testDoNothing) {
ExecuteJsTest();
}
// Checks that all tests in api_test.ts have a corresponding test case in this
// file.
#if defined(SLOW_BINARY)
#define MAYBE_testAllTestsAreRegistered DISABLED_testAllTestsAreRegistered
#else
#define MAYBE_testAllTestsAreRegistered testAllTestsAreRegistered
#endif
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, MAYBE_testAllTestsAreRegistered) {
ExecuteJsTest();
ASSERT_TRUE(step_data()->is_list());
::testing::UnitTest* unit_test = ::testing::UnitTest::GetInstance();
std::set<std::string> test_suites;
const std::vector<std::string> suite_names = GetTestSuiteNames();
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(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();
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();
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testLoadWhileWindowClosed) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
window_controller().Close();
ExecuteJsTest();
// Make sure the WebUI transitions to kReady, otherwise the web client may be
// destroyed.
WaitForWebUiState(mojom::WebUiState::kReady);
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testInitializeFailsWindowClosed) {
base::HistogramTester histogram_tester;
// Immediately close the window to check behavior while window is closed.
// Fail client initialization, should see error page.
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
window_controller().Close();
ExecuteJsTest();
WaitForWebUiState(mojom::WebUiState::kError);
histogram_tester.ExpectUniqueSample("Glic.Host.WebClientState.OnDestroy",
/*WEB_CLIENT_INITIALIZE_FAILED=*/2, 1);
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testInitializeFailsWindowOpen) {
// Fail client initialization, should see error page.
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
ExecuteJsTest({
.params = base::Value(base::Value::Dict().Set("failWith", "error")),
});
WaitForWebUiState(mojom::WebUiState::kError);
// Closing and reopening the window should trigger a retry. This time the
// client initializes correctly.
window_controller().Close();
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
ExecuteJsTest({
.params = base::Value(base::Value::Dict().Set("failWith", "none")),
});
WaitForWebUiState(mojom::WebUiState::kReady);
}
// TODO(crbug.com/409042450): This is a flaky on MSAN.
#if defined(SLOW_BINARY)
#define MAYBE_testReload DISABLED_testReload
#else
#define MAYBE_testReload testReload
#endif
IN_PROC_BROWSER_TEST_F(GlicApiTest, MAYBE_testReload) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
WebUIStateListener listener(&host());
ExecuteJsTest({
.params = base::Value(
base::Value::Dict().Set("failWith", "reloadAfterInitialize")),
});
listener.WaitForWebUiState(mojom::WebUiState::kReady);
listener.WaitForWebUiState(mojom::WebUiState::kBeginLoad);
ExecuteJsTest({
.params = base::Value(base::Value::Dict().Set("failWith", "none")),
});
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testReloadWebUi) {
WebUIStateListener listener(&host());
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
ExecuteJsTest();
listener.WaitForWebUiState(mojom::WebUiState::kReady);
window_controller().Reload();
listener.WaitForWebUiState(mojom::WebUiState::kUninitialized);
ExecuteJsTest();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return host().GetPageHandlersForTesting().size() == 1; }));
// Reloading the WebUI should trigger loading a second page handler.
// That page handler should become the primary page handler.
// This assertion is a regression test for b/418258791.
ASSERT_TRUE(host().GetPrimaryPageHandlerForTesting());
}
// The client navigates to the 'sorry' page before it finishes initialize().
// Chrome should show this page.
IN_PROC_BROWSER_TEST_F(GlicApiTest, testSorryPageBeforeInitialize) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
WebUIStateListener listener(&host());
ExecuteJsTest({
.params = base::Value(base::Value::Dict().Set(
"failWith", "navigateToSorryPageBeforeInitialize")),
});
listener.WaitForWebUiState(mojom::WebUiState::kGuestError);
RunTestSequence(CheckControllerShowing(true));
// Simulate completing a captcha, navigating back.
ASSERT_EQ(true,
content::EvalJs(FindGlicGuestMainFrame(),
std::string("(()=>{window.location = '") +
GetGuestURL().spec() + "'; return true;})()"));
listener.WaitForWebUiState(mojom::WebUiState::kBeginLoad);
ExecuteJsTest({
.params = base::Value(base::Value::Dict().Set("failWith", "none")),
});
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testSorryPageAfterInitialize) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
WebUIStateListener listener(&host());
ExecuteJsTest({
.params = base::Value(base::Value::Dict().Set(
"failWith", "navigateToSorryPageAfterInitialize")),
});
listener.WaitForWebUiState(mojom::WebUiState::kGuestError);
RunTestSequence(CheckControllerShowing(true));
// Simulate completing a captcha, navigating back.
ASSERT_EQ(true,
content::EvalJs(FindGlicGuestMainFrame(),
std::string("(()=>{window.location = '") +
GetGuestURL().spec() + "'; return true;})()"));
listener.WaitForWebUiState(mojom::WebUiState::kBeginLoad);
ExecuteJsTest({
.params = base::Value(base::Value::Dict().Set("failWith", "none")),
});
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testInitializeFailsAfterReload) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
WebUIStateListener listener(&host());
ExecuteJsTest({
.params = base::Value(
base::Value::Dict().Set("failWith", "reloadAfterInitialize")),
});
listener.WaitForWebUiState(mojom::WebUiState::kReady);
listener.WaitForWebUiState(mojom::WebUiState::kBeginLoad);
ExecuteJsTest({
.params = base::Value(base::Value::Dict().Set("failWith", "error")),
});
listener.WaitForWebUiState(mojom::WebUiState::kError);
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithFastTimeout, testNoClientCreated) {
#if defined(SLOW_BINARY)
GTEST_SKIP() << "skip timeout test for slow binary";
#else
base::HistogramTester histogram_tester;
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
WebUIStateListener listener(&host());
ExecuteJsTest();
listener.WaitForWebUiState(mojom::WebUiState::kError);
// Note that the client does receive the bootstrap message, but never calls
// back, so from the host's perspective bootstrapping is still pending.
histogram_tester.ExpectUniqueSample("Glic.Host.WebClientState.OnDestroy",
0 /*BOOTSTRAP_PENDING*/, 1);
#endif
}
// In this test, the client page does not initiate the bootstrap process, so no
// client connects.
IN_PROC_BROWSER_TEST_F(GlicApiTestWithFastTimeout, testNoBootstrap) {
#if defined(SLOW_BINARY)
GTEST_SKIP() << "skip timeout test for slow binary";
#else
base::HistogramTester histogram_tester;
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
WebUIStateListener listener(&host());
ExecuteJsTest();
listener.WaitForWebUiState(mojom::WebUiState::kError);
histogram_tester.ExpectUniqueSample("Glic.Host.WebClientState.OnDestroy",
0 /*BOOTSTRAP_PENDING*/, 1);
#endif
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithFastTimeout, testInitializeTimesOut) {
#if defined(SLOW_BINARY)
GTEST_SKIP() << "skip timeout test for slow binary";
#else
base::HistogramTester histogram_tester;
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
WebUIStateListener listener(&host());
ExecuteJsTest({
.params = base::Value(base::Value::Dict().Set("failWith", "timeout")),
});
listener.WaitForWebUiState(mojom::WebUiState::kError);
histogram_tester.ExpectUniqueSample("Glic.Host.WebClientState.OnDestroy",
3 /*WEB_CLIENT_NOT_INITIALIZED*/, 1);
#endif
}
// Connect the client, and check that the special request header is sent.
IN_PROC_BROWSER_TEST_F(GlicApiTest, testRequestHeader) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
ExecuteJsTest();
auto main_request = std::ranges::find_if(
embedded_test_server_requests_, [&](const auto& request) {
return request.GetURL().path() == GetGuestURL().path();
});
ASSERT_NE(main_request, embedded_test_server_requests_.end());
ASSERT_THAT(
main_request->headers,
testing::AllOf(Contains(Pair("x-glic", "1")),
Contains(Pair("x-glic-chrome-channel",
testing::AnyOf("unknown", "canary", "dev",
"beta", "stable"))),
Contains(Pair("x-glic-chrome-version",
version_info::GetVersionNumber()))));
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testCreateTab) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents),
CheckTabCount(1));
ExecuteJsTest();
RunTestSequence(CheckTabCount(2));
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testCreateTabFailsWithUnsupportedScheme) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents),
CheckTabCount(1));
ExecuteJsTest();
RunTestSequence(CheckTabCount(1));
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testCreateTabInBackground) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents),
CheckTabCount(1));
// Creating a new tab via the glic API in the foreground should change the
// active tab.
ExecuteJsTest();
RunTestSequence(CheckTabCount(2));
tabs::TabInterface* active_tab =
InProcessBrowserTest::browser()->tab_strip_model()->GetActiveTab();
ASSERT_THAT(active_tab->GetContents()->GetURL().spec(),
testing::EndsWith("#foreground"));
// Creating a new tab via the glic API in the background should not change the
// active tab.
ContinueJsTest();
RunTestSequence(CheckTabCount(3));
active_tab =
InProcessBrowserTest::browser()->tab_strip_model()->GetActiveTab();
ASSERT_THAT(active_tab->GetContents()->GetURL().spec(),
testing::EndsWith("#foreground"));
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testCreateTabByClickingOnLink) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents),
CheckTabCount(1));
content::RenderFrameHost* guest_frame = FindGlicGuestMainFrame();
ExecuteJsTest();
ASSERT_TRUE(base::test::RunUntil([&]() {
return InProcessBrowserTest::browser()->tab_strip_model()->GetTabCount() ==
2;
})) << "Timed out waiting for tab count to increase. Tab count = "
<< InProcessBrowserTest::browser()->tab_strip_model()->GetTabCount();
// The guest frame shouldn't change.
ASSERT_EQ(guest_frame, FindGlicGuestMainFrame());
// This test is a regression test for b/416464184.
// Audio ducking should still work after clicking a link.
AudioDucker* audio_ducker =
AudioDucker::GetForPage(FindGlicGuestMainFrame()->GetPage());
ASSERT_TRUE(audio_ducker);
ASSERT_EQ(audio_ducker->GetAudioDuckingState(),
AudioDucker::AudioDuckingState::kDucking);
ContinueJsTest();
ASSERT_TRUE(base::test::RunUntil([&]() {
return audio_ducker->GetAudioDuckingState() ==
AudioDucker::AudioDuckingState::kNoDucking;
}));
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testCreateTabFailsIfNotActive) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testOpenGlicSettingsPage) {
ExecuteJsTest();
RunTestSequence(
InstrumentTab(kSettingsTab),
WaitForWebContentsReady(
kSettingsTab, chrome::GetSettingsUrl(chrome::kGlicSettingsSubpage)));
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testClosePanel) {
ExecuteJsTest();
RunTestSequence(WaitForHide(kGlicViewElementId));
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testShowProfilePicker) {
base::test::TestFuture<void> profile_picker_opened;
ProfilePicker::AddOnProfilePickerOpenedCallbackForTesting(
profile_picker_opened.GetCallback());
ExecuteJsTest();
ASSERT_TRUE(profile_picker_opened.Wait());
// TODO(harringtond): Try to test changing profiles.
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testPanelActive) {
browser_activator().SetMode(BrowserActivator::Mode::kFirst);
ExecuteJsTest();
// Opening a new browser window will deactivate the previous one, and make
// the panel not active.
NavigateParams params(browser()->profile(), GURL("about:blank"),
ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
params.disposition = WindowOpenDisposition::NEW_WINDOW;
base::WeakPtr<content::NavigationHandle> navigation_handle =
Navigate(&params);
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testIsBrowserOpen) {
browser_activator().SetMode(BrowserActivator::Mode::kFirst);
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
ExecuteJsTest();
// Open a new incognito tab so that Chrome doesn't exit, and close the first
// browser.
CreateIncognitoBrowser();
CloseBrowserAsynchronously(browser());
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testEnableDragResize) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
ExecuteJsTest();
RunTestSequence(WaitForCanResizeEnabled(/*enabled=*/true));
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testDisableDragResize) {
// Check the default resize setting here.
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents),
WaitForCanResizeEnabled(/*enabled=*/true));
ExecuteJsTest();
RunTestSequence(WaitForCanResizeEnabled(/*enabled=*/false));
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testInitiallyNotResizable) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
ExecuteJsTest();
RunTestSequence(WaitForCanResizeEnabled(/*enabled=*/false));
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTabAndContextualCueing,
testGetZeroStateSuggestions) {
EXPECT_CALL(*mock_cueing_service(),
GetContextualGlicZeroStateSuggestionsForFocusedTab(_, _, _, _))
.Times(1);
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTabAndContextualCueing,
testGetZeroStateSuggestionsFailsWhenHidden) {
EXPECT_CALL(*mock_cueing_service(),
GetContextualGlicZeroStateSuggestionsForFocusedTab(_, _, _, _))
.Times(0);
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTabAndPreloading,
testDeferredFocusedTabStateAtCreation) {
// Preload a web contents and then navigate.
RunTestSequence(
WaitForShow(kGlicButtonElementId), ResetMemoryPressure(),
ObserveState(glic::test::internal::kWebUiState, &host()),
CreateAndWarmGlic(),
WaitForState(glic::test::internal::kWebUiState,
mojom::WebUiState::kReady),
CheckControllerShowing(false),
NavigateWebContents(kFirstTab,
InProcessBrowserTest::embedded_test_server()->GetURL(
"/scrollable_page_with_content.html")));
ExecuteJsTest();
RunTestSequence(ToggleGlicWindow(GlicWindowMode::kDetached),
CheckControllerShowing(true));
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testGetFocusedTabStateV2) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testGetFocusedTabStateV2WithNavigation) {
// Confirm that the observer is notified through getFocusedTabState of the
// initial state, i.e. the first page navigation.
ExecuteJsTest();
// Navigate to another page in the existing tab.
RunTestSequence(NavigateWebContents(
kFirstTab, InProcessBrowserTest::embedded_test_server()->GetURL(
"/scrollable_page_with_content.html")));
// Confirm that the observer is notified through getFocusedTabState of the
// second page navigation.
ContinueJsTest();
// Open a new tab and navigate to a another page.
RunTestSequence(AddInstrumentedTab(
kSecondTab,
InProcessBrowserTest::embedded_test_server()->GetURL("/glic/test.html")));
// Confirm that the observer is notified through getFocusedTabState that due
// to a page navigation in a new tab, a new tab has gained focus.
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testGetFocusedTabStateV2WithNavigationWhenInactive) {
// Confirm that the observer is notified through getFocusedTabState of the
// initial state, i.e. the first page navigation. It should then hide.
ExecuteJsTest();
// Navigate to another page in the existing tab.
RunTestSequence(NavigateWebContents(
kFirstTab, InProcessBrowserTest::embedded_test_server()->GetURL(
"/scrollable_page_with_content.html")));
// Open a new tab, navigate to a another page, and open the glic window.
RunTestSequence(
AddInstrumentedTab(kSecondTab,
InProcessBrowserTest::embedded_test_server()->GetURL(
"/glic/test.html")),
OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
// Confirm that the observer only notified of this last state.
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testGetFocusedTabStateV2BrowserClosed) {
browser_activator().SetMode(BrowserActivator::Mode::kFirst);
// Note: ideally this test would only open Glic after the main browser is
// closed. This however crashes in `OpenGlicWindow()`.
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
// Open a new incognito window first so that Chrome doesn't exit, then close
// the first browser window.
CreateIncognitoBrowser();
CloseBrowserAsynchronously(browser());
ExecuteJsTest({.wait_for_guest = false});
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testGetContextFromFocusedTabWithoutPermission) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testGetContextFromFocusedTabWithNoRequestedData) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testGetContextFromFocusedTabWithAllRequestedData) {
ExecuteJsTest();
}
#if BUILDFLAG(ENABLE_PDF)
#define MAYBE_testGetContextFromFocusedTabWithPdfFile \
testGetContextFromFocusedTabWithPdfFile
#else
#define MAYBE_testGetContextFromFocusedTabWithPdfFile \
DISABLED_testGetContextFromFocusedTabWithPdfFile
#endif
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
MAYBE_testGetContextFromFocusedTabWithPdfFile) {
RunTestSequence(NavigateWebContents(
kFirstTab,
InProcessBrowserTest::embedded_test_server()->GetURL("/pdf/test.pdf")));
ExecuteJsTest();
}
// TODO(harringtond): Fix this, it hangs.
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, DISABLED_testCaptureScreenshot) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testPermissionAccess) {
ExecuteJsTest();
histogram_tester->ExpectUniqueSample(
"Glic.Sharing.ActiveTabSharingState.OnTabContextPermissionGranted",
ActiveTabSharingState::kActiveTabIsShared, 1);
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testClosedCaptioning) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testGetUserProfileInfo) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testGetUserProfileInfoDefersWhenInactive) {
ExecuteJsTest();
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testRefreshSignInCookies) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testSignInPauseState) {
// Check that Glic web client is open and can retrieve the user's info.
ExecuteJsTest({.expect_guest_frame_destroyed = false});
// Pause the sign-in.
auto* const identity_manager =
IdentityManagerFactory::GetForProfile(browser()->profile());
signin::SetInvalidRefreshTokenForPrimaryAccount(identity_manager);
// The guest frame should be destroyed, and the WebUI should show the sign-in
// panel.
ASSERT_TRUE(base::test::RunUntil(
[&]() { return FindGlicGuestMainFrame() == nullptr; }));
WaitForWebUiState(mojom::WebUiState::kSignIn);
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testSetContextAccessIndicator) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testSetAudioDucking) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testGetDisplayMedia) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testJournal) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testMetrics) {
browser()->profile()->GetPrefs()->SetBoolean(
prefs::kGlicClosedCaptioningEnabled, true);
ExecuteJsTest();
// Sleeping here is needed so that the calls made from the web client are
// handled by the browser before the check below.
sleepWithRunLoop(base::Milliseconds(100));
histogram_tester->ExpectUniqueSample(
"Glic.Sharing.ActiveTabSharingState.OnUserInputSubmitted",
ActiveTabSharingState::kTabContextPermissionNotGranted, 1);
histogram_tester->ExpectUniqueSample("Glic.Response.ClosedCaptionsShown",
true, 1);
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testScrollToFindsText) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testScrollToFindsTextNoTabContextPermission) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testScrollToFailsWhenInactive) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testScrollToNoMatchFound) {
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testSetSyntheticExperimentState) {
ExecuteJsTest();
ASSERT_TRUE(base::test::RunUntil([]() {
std::vector<variations::ActiveGroupId> trials =
g_browser_process->metrics_service()
->GetSyntheticTrialRegistry()
->GetCurrentSyntheticFieldTrialsForTest();
variations::ActiveGroupId expected =
variations::MakeActiveGroupId("TestTrial", "Enabled");
return std::ranges::any_of(trials, [&](const auto& trial) {
return trial.name == expected.name && trial.group == expected.group;
});
}));
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testSetSyntheticExperimentStateMultiProfile) {
ExecuteJsTest();
ASSERT_TRUE(base::test::RunUntil([]() {
std::vector<variations::ActiveGroupId> trials =
g_browser_process->metrics_service()
->GetSyntheticTrialRegistry()
->GetCurrentSyntheticFieldTrialsForTest();
variations::ActiveGroupId expected =
variations::MakeActiveGroupId("TestTrial", "MultiProfileDetected");
return std::ranges::any_of(trials, [&](const auto& trial) {
return trial.name == expected.name && trial.group == expected.group;
});
}));
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testCloseAndOpenWhileOpening) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
ExecuteJsTest();
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testNotifyPanelWillOpenIsCalledOnce) {
ExecuteJsTest();
histogram_tester->ExpectUniqueSample(
"Glic.Sharing.ActiveTabSharingState.OnPanelOpenAndReady",
ActiveTabSharingState::kTabContextPermissionNotGranted, 1);
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testGetOsHotkeyState) {
ExecuteJsTest();
g_browser_process->local_state()->SetString(prefs::kGlicLauncherHotkey,
"Ctrl+Shift+1");
ContinueJsTest();
g_browser_process->local_state()->SetString(prefs::kGlicLauncherHotkey, "");
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testSetWindowDraggableAreas) {
ExecuteJsTest();
const int x = 10;
const int y = 20;
const int width = 30;
const int height = 40;
RunTestSequence(
// Test points within the draggable area.
CheckPointIsWithinDraggableArea(gfx::Point(x, y), true),
CheckPointIsWithinDraggableArea(gfx::Point(x + width - 1, y), true),
CheckPointIsWithinDraggableArea(gfx::Point(x, y + height - 1), true),
CheckPointIsWithinDraggableArea(gfx::Point(x + width - 1, y + height - 1),
true),
// Test points at the edges of the draggable area.
CheckPointIsWithinDraggableArea(gfx::Point(x - 1, y), false),
CheckPointIsWithinDraggableArea(gfx::Point(x, y - 1), false),
CheckPointIsWithinDraggableArea(gfx::Point(x + width, y), false),
CheckPointIsWithinDraggableArea(gfx::Point(x, y + height), false));
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab,
testSetWindowDraggableAreasDefault) {
// TODO(crbug.com/404845792): Default draggable area is currently hardcoded in
// glic_page_handler.cc. This should be moved to a shared location and updated
// here.
const int x = 0;
const int y = 0;
const int width = 400;
const int height = 80;
ExecuteJsTest();
RunTestSequence(
// Test points within the draggable area.
CheckPointIsWithinDraggableArea(gfx::Point(x, y), true),
CheckPointIsWithinDraggableArea(gfx::Point(x + width - 1, y), true),
CheckPointIsWithinDraggableArea(gfx::Point(x, y + height - 1), true),
CheckPointIsWithinDraggableArea(gfx::Point(x + width - 1, y + height - 1),
true),
// Test points at the edges of the draggable area.
CheckPointIsWithinDraggableArea(gfx::Point(x - 1, y), false),
CheckPointIsWithinDraggableArea(gfx::Point(x, y - 1), false),
CheckPointIsWithinDraggableArea(gfx::Point(x + width, y), false),
CheckPointIsWithinDraggableArea(gfx::Point(x, y + height), false));
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testSetMinimumWidgetSize) {
ExecuteJsTest();
ASSERT_TRUE(step_data()->is_dict());
const auto& min_size = step_data()->GetDict();
const int width = min_size.FindInt("width").value();
const int height = min_size.FindInt("height").value();
RunTestSequence(CheckWidgetMinimumSize(gfx::Size(width, height)));
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testManualResizeChanged) {
window_controller().GetGlicWidget()->OnNativeWidgetUserResizeStarted();
// Check that the web client is notified of the beginning of the user
// initiated resizing event.
ExecuteJsTest();
window_controller().GetGlicWidget()->OnNativeWidgetUserResizeEnded();
// Check that the web client is notified of the ending of the user
// initiated resizing event.
ContinueJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testResizeWindowTooSmall) {
// Web client requests the window to be resized to 0x0, bellow the minimum
// dimensions (see GlicWindowController#GetLastRequestedSizeClamped), so it
// gets discarded in favor of the initial size.
gfx::Size expected_size = GlicWidget::GetInitialSize();
GlicWidget* glic_widget = window_controller().GetGlicWidget();
ASSERT_TRUE(glic_widget);
ExecuteJsTest();
gfx::Rect final_widget_bounds = glic_widget->GetWindowBoundsInScreen();
ASSERT_EQ(expected_size,
glic_widget->WidgetToVisibleBounds(final_widget_bounds).size());
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testResizeWindowTooLarge) {
// Web client requests the window to be resized to 20000x20000, above the
// maximum dimensions (see GlicWindowController#GetLastRequestedSizeClamped),
// so it gets discarded in favor of the max size. This max size is still
// larger than the display work area so we clamp the dimensions down to fit on
// screen.
ExecuteJsTest();
gfx::Rect display_bounds =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
GlicWidget* glic_widget = window_controller().GetGlicWidget();
ASSERT_TRUE(glic_widget);
gfx::Rect final_widget_bounds = glic_widget->GetWindowBoundsInScreen();
ASSERT_TRUE(display_bounds.Contains(final_widget_bounds));
}
IN_PROC_BROWSER_TEST_F(GlicApiTestWithOneTab, testResizeWindowWithinBounds) {
// Web client requests the window to be resized to 800x700, which are valid
// dimensions.
gfx::Size expected_size = gfx::Size(800, 700);
ExecuteJsTest(
{.params = base::Value(base::Value::Dict()
.Set("width", expected_size.width())
.Set("height", expected_size.height()))});
GlicWidget* glic_widget = window_controller().GetGlicWidget();
ASSERT_TRUE(glic_widget);
gfx::Rect final_widget_bounds = glic_widget->GetWindowBoundsInScreen();
ASSERT_EQ(expected_size,
glic_widget->WidgetToVisibleBounds(final_widget_bounds).size());
}
class GlicApiTestSystemSettingsTest : public GlicApiTestWithOneTab {
public:
GlicApiTestSystemSettingsTest() {
system_permission_settings::SetInstanceForTesting(&mock_platform_handle);
}
~GlicApiTestSystemSettingsTest() override {
system_permission_settings::SetInstanceForTesting(nullptr);
}
testing::NiceMock<system_permission_settings::MockPlatformHandle>
mock_platform_handle;
};
IN_PROC_BROWSER_TEST_F(GlicApiTestSystemSettingsTest,
testOpenOsMediaPermissionSettings) {
base::test::TestFuture<void> signal;
EXPECT_CALL(
mock_platform_handle,
OpenSystemSettings(testing::_, ContentSettingsType::MEDIASTREAM_MIC))
.WillOnce(base::test::InvokeFuture(signal));
// Trigger the openOsPermissionSettingsMenu API with 'media'.
ExecuteJsTest();
// Wait for OpenSystemSettings to be called.
EXPECT_TRUE(signal.Wait());
}
IN_PROC_BROWSER_TEST_F(GlicApiTestSystemSettingsTest,
testOpenOsGeoPermissionSettings) {
base::test::TestFuture<void> signal;
EXPECT_CALL(mock_platform_handle,
OpenSystemSettings(testing::_, ContentSettingsType::GEOLOCATION))
.WillOnce(base::test::InvokeFuture(signal));
// Trigger the openOsPermissionSettingsMenu API with 'geolocation'.
ExecuteJsTest();
// Wait for OpenSystemSettings to be called.
EXPECT_TRUE(signal.Wait());
}
IN_PROC_BROWSER_TEST_F(GlicApiTestSystemSettingsTest,
testGetOsMicrophonePermissionStatusAllowed) {
EXPECT_CALL(mock_platform_handle,
IsAllowed(ContentSettingsType::MEDIASTREAM_MIC))
.WillOnce(testing::Return(true));
// Trigger the GetOsMicrophonePermissionStatus API and check if it returns
// true as mocked by this test.
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTestSystemSettingsTest,
testGetOsMicrophonePermissionStatusNotAllowed) {
EXPECT_CALL(mock_platform_handle,
IsAllowed(ContentSettingsType::MEDIASTREAM_MIC))
.WillOnce(testing::Return(false));
// Trigger the GetOsMicrophonePermissionStatus API and check if it returns
// false as mocked by this test.
ExecuteJsTest();
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testNavigateToDifferentClientPage) {
base::HistogramTester histogram_tester;
WebUIStateListener listener(&host());
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
listener.WaitForWebUiState(mojom::WebUiState::kReady);
ExecuteJsTest({.params = base::Value(0)}); // test run count: 0.
listener.WaitForWebUiState(mojom::WebUiState::kBeginLoad);
listener.WaitForWebUiState(mojom::WebUiState::kReady);
ExecuteJsTest({.params = base::Value(1)}); // test run count: 1.
histogram_tester.ExpectUniqueSample("Glic.Host.WebClientState.OnCommit",
6 /*RESPONSIVE*/, 1);
histogram_tester.ExpectUniqueSample("Glic.Host.WebClientState.OnDestroy",
0 /*BOOTSTRAP_PENDING*/, 1);
}
// TODO(crbug.com/410881522): Re-enable this test
#if BUILDFLAG(IS_MAC)
#define MAYBE_testNavigateToBadPage DISABLED_testNavigateToBadPage
#else
#define MAYBE_testNavigateToBadPage testNavigateToBadPage
#endif
IN_PROC_BROWSER_TEST_F(GlicApiTestWithFastTimeout,
MAYBE_testNavigateToBadPage) {
#if defined(SLOW_BINARY)
GTEST_SKIP() << "skip timeout test for slow binary";
#else
// Client loads, and navigates to a new URL. We try to load the client again,
// but it fails.
WebUIStateListener listener(&host());
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
listener.WaitForWebUiState(mojom::WebUiState::kReady);
ExecuteJsTest({.params = base::Value(0)});
listener.WaitForWebUiState(mojom::WebUiState::kBeginLoad);
listener.WaitForWebUiState(mojom::WebUiState::kError);
// Open the glic window to trigger reloading the client.
// This time the client should load, falling back to the original URL.
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached,
GlicInstrumentMode::kHostAndContents));
ExecuteJsTest({.params = base::Value(1)});
#endif
}
IN_PROC_BROWSER_TEST_F(GlicApiTest, testCallingApiWhileHiddenRecordsMetrics) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached, GlicInstrumentMode::kNone));
ExecuteJsTest();
window_controller().Close();
base::HistogramTester histogram_tester;
ContinueJsTest();
histogram_tester.ExpectBucketCount("Glic.Api.RequestCounts.CreateTab",
GlicRequestEvent::kRequestReceived, 1);
histogram_tester.ExpectBucketCount(
"Glic.Api.RequestCounts.CreateTab",
GlicRequestEvent::kRequestReceivedWhileHidden, 1);
}
class GlicApiTestUserStatusCheckTest : public GlicApiTestWithOneTab {
protected:
void SetUpOnMainThread() override {
GlicApiTestWithOneTab::SetUpOnMainThread();
GetService()->enabling().SetUserStatusFetchOverrideForTest(
base::BindRepeating(&GlicApiTestUserStatusCheckTest::UserStatusFetch,
base::Unretained(this)));
}
void UserStatusFetch(
base::OnceCallback<void(const CachedUserStatus&)> callback) {
user_status_fetch_count_++;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), user_status_));
}
CachedUserStatus user_status_;
unsigned int user_status_fetch_count_ = 0;
};
void UpdatePrimaryAccountToBeManaged(Profile* profile) {
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
CoreAccountInfo core_account_info =
identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
AccountInfo account_info =
identity_manager->FindExtendedAccountInfo(core_account_info);
account_info.hosted_domain = gaia::ExtractDomainName(account_info.email);
signin::UpdateAccountInfoForAccount(identity_manager, account_info);
}
IN_PROC_BROWSER_TEST_F(GlicApiTestUserStatusCheckTest,
testMaybeRefreshUserStatus) {
Profile* profile = browser()->profile();
policy::ScopedManagementServiceOverrideForTesting platform_management(
policy::ManagementServiceFactory::GetForProfile(profile),
policy::EnterpriseManagementAuthority::CLOUD);
UpdatePrimaryAccountToBeManaged(profile);
ASSERT_FALSE(GlicEnabling::EnablementForProfile(profile).DisallowedByAdmin());
user_status_.user_status_code = UserStatusCode::DISABLED_BY_ADMIN;
ExecuteJsTest();
ASSERT_TRUE(base::test::RunUntil([&]() {
return GlicEnabling::EnablementForProfile(profile).DisallowedByAdmin();
}));
EXPECT_GE(user_status_fetch_count_, 1u);
}
IN_PROC_BROWSER_TEST_F(GlicApiTestUserStatusCheckTest,
testMaybeRefreshUserStatusThrottled) {
// As previous, but requests several updates (e.g., as though many errors
// were processed around the same time). An "enabled" status is assumed as
// otherwise the client will be unloaded.
//
// These expectations are a little loose, because we can't use mock time in
// browser tests yet, but they should be sufficient to catch a total lack of
// throttling, at least.
Profile* profile = browser()->profile();
policy::ScopedManagementServiceOverrideForTesting platform_management(
policy::ManagementServiceFactory::GetForProfile(profile),
policy::EnterpriseManagementAuthority::CLOUD);
UpdatePrimaryAccountToBeManaged(profile);
ASSERT_FALSE(GlicEnabling::EnablementForProfile(profile).DisallowedByAdmin());
user_status_.user_status_code = UserStatusCode::ENABLED;
ExecuteJsTest();
ASSERT_TRUE(base::test::RunUntil([&]() {
return user_status_fetch_count_ >= 2;
})) << "There should be at least two fetches (initial and delayed)";
{
base::RunLoop loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, loop.QuitClosure(), base::Seconds(5));
loop.Run();
}
EXPECT_LT(user_status_fetch_count_, 5u)
<< "We should not send most of the fetches";
}
// Given the time-based nature of debouncing, testing with non-mocked clocks can
// be flaky. This suite increases the applied delays to reduce the the chance of
// flakiness. This suite is disabled on all slow binaries.
#if defined(SLOW_BINARY)
#define MAYBE_GlicApiTestWithOneTabMoreDebounceDelay \
DISABLED_GlicApiTestWithOneTabMoreDebounceDelay
#else
#define MAYBE_GlicApiTestWithOneTabMoreDebounceDelay \
GlicApiTestWithOneTabMoreDebounceDelay
#endif
class MAYBE_GlicApiTestWithOneTabMoreDebounceDelay
: public GlicApiTestWithOneTab {
public:
MAYBE_GlicApiTestWithOneTabMoreDebounceDelay() {
features2_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{
features::kGlicTabFocusDataDedupDebounce,
{
// Set an arbitrarily high debounce delay to avoid flakiness.
{features::kGlicTabFocusDataDebounceDelayMs.name, "1000"},
},
}},
/*disabled_features=*/
{});
}
private:
base::test::ScopedFeatureList features2_;
};
// Confirm that the web client receives a minimal number of focused tab updates
// by triggering events that generate such updates.
// TODO(b/424242331): figure out why this is failing on linux-rel bot.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
#define MAYBE_testSingleFocusedTabUpdatesOnTabEvents \
testSingleFocusedTabUpdatesOnTabEvents
#else
#define MAYBE_testSingleFocusedTabUpdatesOnTabEvents \
DISABLED_testSingleFocusedTabUpdatesOnTabEvents
#endif
IN_PROC_BROWSER_TEST_F(MAYBE_GlicApiTestWithOneTabMoreDebounceDelay,
MAYBE_testSingleFocusedTabUpdatesOnTabEvents) {
// Initial state with first tab.
ExecuteJsTest();
// Navigate to another page in the first tab.
RunTestSequence(NavigateWebContents(
kFirstTab, InProcessBrowserTest::embedded_test_server()->GetURL(
"/scrollable_page_with_content.html")));
ContinueJsTest();
// Open a new tab that becomes active and navigate to a another page.
RunTestSequence(AddInstrumentedTab(
kSecondTab,
InProcessBrowserTest::embedded_test_server()->GetURL("/glic/test.html")));
ContinueJsTest();
}
} // namespace
} // namespace glic