Files
brave-core/browser/email_aliases/email_aliases_browsertest.cc
T
Netzenbot 51176a84e1 Fix flaky EmailAliasesBrowserTest.ContextMenuAuthorizedManage (#36306)
The test intermittently fails with "deepQuery is not defined" because
InjectHelpers injects the helper function into the bubble's WebContents
before the WebUI navigation has committed. WaitForLoadStop returns
immediately when no navigation is pending (e.g., the WebContents is
still at about:blank), so deepQuery is injected into the pre-navigation
context, then lost when chrome://email-aliases.panel/ loads.

Wait for the WebContents to commit a non-empty, non-about:blank URL
before calling WaitForLoadStop and injecting JavaScript.

Fix brave/brave-browser#55000
2026-05-12 14:06:07 -04:00

510 lines
18 KiB
C++

/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include <algorithm>
#include <memory>
#include <vector>
#include "base/check_deref.h"
#include "base/functional/callback_helpers.h"
#include "base/path_service.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/time/time.h"
#include "brave/browser/brave_account/brave_account_service_factory.h"
#include "brave/browser/email_aliases/email_aliases_service_factory.h"
#include "brave/browser/ui/email_aliases/email_aliases_controller.h"
#include "brave/browser/ui/webui/brave_settings_ui.h"
#include "brave/components/brave_account/features.h"
#include "brave/components/brave_account/mock_brave_account_authentication.h"
#include "brave/components/constants/brave_paths.h"
#include "brave/components/email_aliases/email_aliases_api.h"
#include "brave/components/email_aliases/email_aliases_service.h"
#include "brave/components/email_aliases/features.h"
#include "brave/components/email_aliases/test_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/views/bubble/webui_bubble_manager.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/grit/brave_components_strings.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/test_navigation_observer.h"
#include "services/network/public/cpp/network_switches.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
namespace email_aliases {
namespace {
constexpr char kSuccessEmail[] = "success@domain.com";
std::unique_ptr<net::test_server::HttpResponse> ManageHandler(
const net::test_server::HttpRequest& request) {
if (!request.GetURL().has_path() ||
!request.GetURL().path().starts_with("/manage")) {
return nullptr;
}
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
if (request.method == net::test_server::HttpMethod::METHOD_GET) {
auto make_entry = [](const std::string& alias) {
email_aliases::AliasListEntry le;
le.alias = alias;
le.email = kSuccessEmail;
le.status = "active";
return le;
};
email_aliases::AliasListResponse list;
list.result.push_back(make_entry("first@alias.com"));
list.result.push_back(make_entry("second@alias.com"));
list.result.push_back(make_entry("third@alias.com"));
response->set_code(net::HTTP_OK);
response->set_content_type("application/json");
response->set_content(*base::WriteJson(list.ToValue()));
} else if (request.method == net::test_server::HttpMethod::METHOD_POST) {
response->set_code(net::HTTP_OK);
response->set_content_type("application/json");
email_aliases::GenerateAliasResponse generate;
generate.alias = "new@alias.com";
generate.message = "created";
response->set_content(*base::WriteJson(generate.ToValue()));
} else if (request.method == net::test_server::HttpMethod::METHOD_PUT) {
response->set_code(net::HTTP_OK);
response->set_content_type("application/json");
email_aliases::AliasEditedResponse save;
save.message = "updated";
response->set_content(*base::WriteJson(save.ToValue()));
}
return response;
}
} // namespace
class EmailAliasesBrowserTestBase : public InProcessBrowserTest {
public:
EmailAliasesBrowserTestBase() {
BraveSettingsUI::ShouldExposeElementsForTesting() = true;
}
~EmailAliasesBrowserTestBase() override {
BraveSettingsUI::ShouldExposeElementsForTesting() = false;
}
void SetUpBrowserContextKeyedServices(
content::BrowserContext* context) override {
if (features::IsEmailAliasesEnabled()) {
EmailAliasesServiceFactory::GetInstance()->SetTestingFactory(
context,
base::BindLambdaForTesting([&](content::BrowserContext* context)
-> std::unique_ptr<KeyedService> {
return std::make_unique<EmailAliasesService>(
brave_account_auth_.BindAndGetRemote(),
context->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess(),
CHECK_DEREF(user_prefs::UserPrefs::Get(context)));
}));
}
}
void SetUpOnMainThread() override {
https_server_.RegisterRequestHandler(base::BindRepeating(&ManageHandler));
https_server_.ServeFilesFromDirectory(
base::PathService::CheckedGet(brave::DIR_TEST_DATA));
https_server_.StartAcceptingConnections();
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
ON_CALL(GetBraveAccountAuth(),
GetServiceToken(brave_account::mojom::Service::kEmailAliases,
testing::_))
.WillByDefault([](auto service, auto callback) {
auto token = brave_account::mojom::GetServiceTokenResult::New();
token->serviceToken = "email-aliases-token";
std::move(callback).Run(base::ok(std::move(token)));
});
}
void SetUp() override {
ASSERT_TRUE(https_server_.InitializeAndListen());
InProcessBrowserTest::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
InProcessBrowserTest::SetUpCommandLine(command_line);
mock_cert_verifier_.SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(
network::switches::kHostResolverRules,
"MAP * " + https_server_.host_port_pair().ToString());
}
void SetUpInProcessBrowserTestFixture() override {
InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
InProcessBrowserTest::TearDownInProcessBrowserTestFixture();
}
content::WebContents* ActiveWebContents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
content::WebContents* Navigate(const GURL& url) {
ui_test_utils::NavigateToURLWithDisposition(
browser(), url, WindowOpenDisposition::CURRENT_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
return ActiveWebContents();
}
void InjectHelpers(content::WebContents* contents) {
ASSERT_TRUE(base::test::RunUntil([&]() {
return !contents->GetLastCommittedURL().is_empty() &&
!contents->GetLastCommittedURL().IsAboutBlank();
}));
ASSERT_TRUE(content::WaitForLoadStop(contents));
constexpr char kDeepQuery[] = R"js(
function deepQuery(selector) {
const query = (root, selector) =>{
const e = root.querySelector(selector);
if (e) return e;
for (const el of root.querySelectorAll('*')) {
if (!el.shadowRoot) continue;
const found = query(el.shadowRoot, selector);
if (found) return found;
}
return null;
}
return query(document, selector);
};
)js";
ASSERT_TRUE(content::ExecJs(contents, kDeepQuery));
}
void Wait(const std::string& id, content::WebContents* contents = nullptr) {
constexpr const char kScript[] = R"js(
(async () => {
let waiter = () => {
return !deepQuery($1)
};
while (waiter()) {
await new Promise(r => setTimeout(r, 10));
}
return true;
})();
)js";
ASSERT_EQ(true, content::EvalJs((contents ? contents : ActiveWebContents()),
content::JsReplace(kScript, id)));
}
void WaitDisappear(const std::string& selector,
content::WebContents* contents = nullptr) {
constexpr const char kScript[] = R"js(
(async () => {
let waiter = () => {
return deepQuery($1)
};
while (waiter()) {
await new Promise(r => setTimeout(r, 10));
}
return true;
})();
)js";
ASSERT_EQ(true, content::EvalJs((contents ? contents : ActiveWebContents()),
content::JsReplace(kScript, selector)));
}
void SetText(const std::string& id,
const std::string& text,
content::WebContents* contents = nullptr) {
constexpr char kSetText[] = R"js(
(() => {
const element = deepQuery($1);
element.value = $2;
element.dispatchEvent(new Event('input', {bubbles: true}));
element.dispatchEvent(new Event('change', {bubbles: true}));
return true;
})();
)js";
ASSERT_EQ(true, content::EvalJs((contents ? contents : ActiveWebContents()),
content::JsReplace(kSetText, id, text)));
}
std::string GetText(const std::string& id) {
constexpr char kGetText[] = R"js( deepQuery($1).value )js";
return content::EvalJs(ActiveWebContents(),
content::JsReplace(kGetText, id))
.ExtractString();
}
bool AwaitText(const std::string& id, const std::string& expected_text) {
constexpr char kAwaitText[] = R"js(
(async () => {
let waiter = () => {
return deepQuery($1).value !== $2
};
while (waiter()) {
await new Promise(r => setTimeout(r, 10));
}
return true;
})()
)js";
return content::EvalJs(ActiveWebContents(),
content::JsReplace(kAwaitText, id, expected_text))
.ExtractBool();
}
void Click(const std::string& id, content::WebContents* contents = nullptr) {
constexpr const char kClick[] = R"js(
(async () => {
let waiter = () => {
return !deepQuery($1)
};
while (waiter()) {
await new Promise(r => setTimeout(r, 10));
}
return deepQuery($1).onClick();
})();
)js";
auto ignore = content::ExecJs((contents ? contents : ActiveWebContents()),
content::JsReplace(kClick, id));
}
EmailAliasesService* email_aliases_service() {
return EmailAliasesServiceFactory::GetServiceForProfile(
browser()->profile());
}
void RunContextMenuOn(const std::string& element_id) {
const int x =
content::EvalJs(ActiveWebContents(),
content::JsReplace("getElementX($1)", element_id))
.ExtractInt();
const int y =
content::EvalJs(ActiveWebContents(),
content::JsReplace("getElementY($1)", element_id))
.ExtractInt();
ASSERT_TRUE(content::ExecJs(
ActiveWebContents(),
content::JsReplace("document.getElementById($1).focus()", element_id)));
ActiveWebContents()
->GetPrimaryMainFrame()
->GetRenderViewHost()
->GetWidget()
->ShowContextMenuAtPoint(gfx::Point(x, y),
ui::mojom::MenuSourceType::kMouse);
}
testing::NiceMock<brave_account::MockBraveAccountAuthentication>&
GetBraveAccountAuth() {
return brave_account_auth_;
}
private:
content::ContentMockCertVerifier mock_cert_verifier_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
testing::NiceMock<brave_account::MockBraveAccountAuthentication>
brave_account_auth_;
};
class EmailAliasesBrowserTest : public EmailAliasesBrowserTestBase {
public:
EmailAliasesBrowserTest() {
feature_list_.InitWithFeatures(
{{email_aliases::features::kEmailAliases},
brave_account::features::BraveAccountFeatureForTesting()},
{});
}
private:
base::test::ScopedFeatureList feature_list_;
};
class EmailAliasesBrowserNoFeatureTest : public EmailAliasesBrowserTestBase {
public:
EmailAliasesBrowserNoFeatureTest() {
feature_list_.InitAndDisableFeature(email_aliases::features::kEmailAliases);
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserNoFeatureTest, NoContextMenuItem) {
Navigate(GURL("https://a.test/email_aliases/inputs.html"));
ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS);
RunContextMenuOn("type-email");
menu_waiter.WaitForMenuOpenAndClose();
EXPECT_FALSE(std::ranges::contains(menu_waiter.GetCapturedEnabledCommandIds(),
IDC_NEW_EMAIL_ALIAS));
EXPECT_FALSE(menu_waiter.IsCommandExecuted());
}
IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest,
NoContextMenuItemOnNonsuitableField) {
Navigate(GURL("https://a.test/email_aliases/inputs.html"));
ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS);
RunContextMenuOn("type-url");
menu_waiter.WaitForMenuOpenAndClose();
EXPECT_FALSE(std::ranges::contains(menu_waiter.GetCapturedEnabledCommandIds(),
IDC_NEW_EMAIL_ALIAS));
EXPECT_FALSE(menu_waiter.IsCommandExecuted());
}
IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuNotAuthorized) {
const GURL settings_page("chrome://settings/email-aliases");
Navigate(GURL("https://a.test/email_aliases/inputs.html"));
content::TestNavigationObserver waiter(settings_page);
waiter.StartWatchingNewWebContents();
ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS);
RunContextMenuOn("type-email");
menu_waiter.WaitForMenuOpenAndClose();
waiter.WaitForNavigationFinished();
EXPECT_EQ(ActiveWebContents()->GetLastCommittedURL(), settings_page);
}
IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuAuthorized) {
auto* service = email_aliases_service();
{
auto initilized = test::AuthStateObserver::Setup(service, true);
}
service->GetAuth()->SetAuthEmailForTesting(kSuccessEmail);
Navigate(GURL("https://a.test/email_aliases/inputs.html"));
InjectHelpers(ActiveWebContents());
EmailAliasesController::DisableAutoCloseBubbleForTesting(true);
auto* email_aliases_controller =
browser()->GetFeatures().email_aliases_controller();
EXPECT_EQ("", GetText("#type-email"));
ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS);
RunContextMenuOn("type-email");
menu_waiter.WaitForMenuOpenAndClose();
// Wait for bubble.
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !!email_aliases_controller->GetBubbleForTesting(); }));
auto* bubble = email_aliases_controller->GetBubbleForTesting();
InjectHelpers(bubble);
Wait("#create-alias-button:not([isDisabled=\"true\"])", bubble);
Click("#create-alias-button:not([isDisabled=\"true\"])", bubble);
// Wait for bubble close
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !email_aliases_controller->GetBubbleForTesting(); }));
EXPECT_TRUE(AwaitText("#type-email", "new@alias.com"));
}
IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuAuthorizedManage) {
email_aliases_service()->GetAuth()->SetAuthEmailForTesting(kSuccessEmail);
const GURL settings_page("chrome://settings/email-aliases");
Navigate(GURL("https://a.test/email_aliases/inputs.html"));
content::TestNavigationObserver waiter(settings_page);
waiter.StartWatchingNewWebContents();
EmailAliasesController::DisableAutoCloseBubbleForTesting(true);
auto* email_aliases_controller =
browser()->GetFeatures().email_aliases_controller();
ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS);
RunContextMenuOn("type-email");
menu_waiter.WaitForMenuOpenAndClose();
// Wait for bubble.
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !!email_aliases_controller->GetBubbleForTesting(); }));
auto* bubble = email_aliases_controller->GetBubbleForTesting();
InjectHelpers(bubble);
Wait("#manage-button", bubble);
Click("#manage-button", bubble);
waiter.WaitForNavigationFinished();
EXPECT_EQ(ActiveWebContents()->GetLastCommittedURL(), settings_page);
}
IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuAuthorizedCancel) {
email_aliases_service()->GetAuth()->SetAuthEmailForTesting(kSuccessEmail);
Navigate(GURL("https://a.test/email_aliases/inputs.html"));
InjectHelpers(ActiveWebContents());
EmailAliasesController::DisableAutoCloseBubbleForTesting(true);
auto* email_aliases_controller =
browser()->GetFeatures().email_aliases_controller();
ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS);
RunContextMenuOn("type-email");
menu_waiter.WaitForMenuOpenAndClose();
// Wait for bubble.
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !!email_aliases_controller->GetBubbleForTesting(); }));
auto* bubble = email_aliases_controller->GetBubbleForTesting();
InjectHelpers(bubble);
Wait("#cancel-button", bubble);
Click("#cancel-button", bubble);
// Wait for bubble close
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !email_aliases_controller->GetBubbleForTesting(); }));
EXPECT_TRUE(AwaitText("#type-email", "")); // text not changed
}
IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, LogInLogOut) {
// Prepare auth token
email_aliases_service()->GetAuth()->SetAuthEmailForTesting(kSuccessEmail);
// Settings in logged-in state
Navigate(GURL("chrome://settings/email-aliases"));
InjectHelpers(ActiveWebContents());
Wait("#create-new-item-button");
// Reset token
email_aliases_service()->GetAuth()->SetAuthEmailForTesting("");
WaitDisappear("#create-new-item-button"); // Settings in sing-in state.
// Logged-in again
email_aliases_service()->GetAuth()->SetAuthEmailForTesting(kSuccessEmail);
Wait("#create-new-item-button"); // Logged-in state.
}
} // namespace email_aliases