blob: 894d85c579f3c203c81a9a2f86c963a6d37ca96c [file] [log] [blame]
// 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 <memory>
#include <string>
#include "chrome/browser/controlled_frame/controlled_frame_test_base.h"
#include "chrome/browser/extensions/context_menu_matcher.h"
#include "chrome/browser/extensions/menu_manager.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "content/public/browser/render_frame_host.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/context_menu_interceptor.h"
#include "content/public/test/hit_test_region_observer.h"
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
namespace controlled_frame {
using testing::Each;
using testing::Eq;
constexpr char kEvalSuccessStr[] = "SUCCESS";
class ControlledFrameContextMenusTest : public ControlledFrameTestBase {
public:
ControlledFrameContextMenusTest()
: ControlledFrameTestBase(
/*channel=*/version_info::Channel::STABLE,
/*feature_setting=*/FeatureSetting::ENABLED,
/*flag_setting=*/FlagSetting::CONTROLLED_FRAME) {}
void SetUpOnMainThread() override {
ControlledFrameTestBase::SetUpOnMainThread();
StartContentServer("web_apps/simple_isolated_app");
}
const extensions::MenuItem::Id CreateMenuItemId(
const extensions::MenuItem::ExtensionKey& extension_key,
const std::string& string_uid) {
extensions::MenuItem::Id id;
id.extension_key = extension_key;
id.string_uid = string_uid;
return id;
}
void ExpectMenuItemWithIdAndTitle(
const extensions::MenuItem::ExtensionKey& extension_key,
const std::string& expected_id,
const std::string& expected_title) {
auto* menu_manager = extensions::MenuManager::Get(profile());
extensions::MenuItem* menu_item =
menu_manager->GetItemById(CreateMenuItemId(extension_key, expected_id));
ASSERT_TRUE(menu_item);
EXPECT_EQ(expected_title, menu_item->title());
}
const content::EvalJsResult CreateContextMenuItem(
content::RenderFrameHost* app_frame,
const std::string& id,
const std::string& title) {
return content::EvalJs(app_frame, content::JsReplace(R"(
new Promise(async (resolve, reject) => {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus || !frame.contextMenus.create) {
reject('FAIL: frame, frame.contextMenus, or ' +
'frame.contextMenus.create is undefined');
return;
}
await frame.contextMenus.create({ title: $2, id: $1 });
resolve('SUCCESS');
});
)",
id, title));
}
const content::EvalJsResult UpdateContextMenuItemTitle(
content::RenderFrameHost* app_frame,
const std::string& id,
const std::string& new_title) {
return content::EvalJs(app_frame, content::JsReplace(R"(
new Promise(async (resolve, reject) => {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus || !frame.contextMenus.update) {
reject('FAIL: frame, frame.contextMenus, or ' +
'frame.contextMenus.update is undefined');
return;
}
await frame.contextMenus.update(/*id=*/$1, { title: $2 });
resolve('SUCCESS');
});
)",
id, new_title));
}
const content::EvalJsResult RemoveContextMenuItem(
content::RenderFrameHost* app_frame,
const std::string& id) {
return content::EvalJs(app_frame, content::JsReplace(R"(
new Promise(async (resolve, reject) => {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus || !frame.contextMenus.remove) {
reject('FAIL: frame, frame.contextMenus, or ' +
'frame.contextMenus.remove is undefined');
return;
}
await frame.contextMenus.remove(/*id=*/$1);
resolve('SUCCESS');
});
)",
id));
}
const content::EvalJsResult RemoveAllContextMenuItems(
content::RenderFrameHost* app_frame) {
return content::EvalJs(app_frame, R"(
new Promise(async (resolve, reject) => {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus || !frame.contextMenus.removeAll) {
reject('FAIL: frame, frame.contextMenus, or ' +
'frame.contextMenus.removeAll is undefined');
return;
}
await frame.contextMenus.removeAll();
resolve('SUCCESS');
});
)");
}
void SimulateOpenContextMenu(content::RenderFrameHost* controlled_frame) {
CHECK(controlled_frame);
content::WaitForHitTestData(
content::WebContents::FromRenderFrameHost(controlled_frame));
auto context_menu_interceptor =
std::make_unique<content::ContextMenuInterceptor>(controlled_frame);
gfx::Point click_pos(1, 1);
content::SimulateMouseClickAt(
content::WebContents::FromRenderFrameHost(controlled_frame),
blink::WebInputEvent::kNoModifiers,
blink::WebMouseEvent::Button::kRight, click_pos);
context_menu_interceptor->Wait();
}
void SimulateClickContextMenuItem(
content::RenderFrameHost* controlled_frame) {
CHECK(controlled_frame);
// Create and build our test context menu.
std::unique_ptr<TestRenderViewContextMenu> menu(
TestRenderViewContextMenu::Create(
controlled_frame, controlled_frame->GetLastCommittedURL()));
// Look for the extension item in the menu, and execute it.
int command_id =
extensions::ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0);
CHECK(menu->IsCommandIdEnabled(command_id));
menu->ExecuteCommand(command_id, /*event_flags=*/0);
}
};
IN_PROC_BROWSER_TEST_F(ControlledFrameContextMenusTest, CreateShowContextClick) {
constexpr std::string kItemID = "107";
// Create IWA with ControlledFrame
const web_app::IsolatedWebAppUrlInfo url_info =
CreateAndInstallEmptyApp(web_app::ManifestBuilder());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(CreateControlledFrame(
app_frame, embedded_https_test_server().GetURL("/index.html")));
// Add JS with test open and click handlers
auto add_handler_script = content::JsReplace(
R"(
document.onShowHandler = function() {
document.onShowCount = (document.onShowCount ?? 0) + 1;
};
document.onClickedHandler = function(info) {
document.clickedMenuItemId =
[...(document.clickedMenuItemId ?? []), info.menuItem.id];
document.globalOnClickedCount = (document.globalOnClickedCount ?? 0) + 1;
};
new Promise(async (resolve, reject) => {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus || !frame.contextMenus.create) {
reject('FAIL: frame, frame.contextMenus, or ' +
'frame.contextMenus.create is undefined');
return;
}
frame.contextMenus.addEventListener('show', document.onShowHandler);
frame.contextMenus.addEventListener('click', document.onClickedHandler);
await frame.contextMenus.create(
{
title: 'test_title',
id: $2,
});
resolve('SUCCESS');
});
)", kEvalSuccessStr, kItemID);
ASSERT_EQ(content::EvalJs(app_frame, add_handler_script), kEvalSuccessStr);
extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame);
ASSERT_TRUE(web_view_guest);
content::RenderFrameHost* controlled_frame =
web_view_guest->GetGuestMainFrame();
ASSERT_TRUE(controlled_frame);
// Simulate right click and expect the listener to be triggered.
SimulateOpenContextMenu(controlled_frame);
ASSERT_EQ(content::EvalJs(app_frame, "document.onShowCount"), 1);
// Simulate the click on an item expect click and item id be registered
SimulateClickContextMenuItem(controlled_frame);
EXPECT_EQ(content::EvalJs(app_frame, "document.globalOnClickedCount"), 1);
EXPECT_THAT(content::EvalJs(app_frame, "document.clickedMenuItemId")
.TakeValue()
.TakeList(),
Each(Eq(kItemID)));
// We don't need any clean-up after
}
IN_PROC_BROWSER_TEST_F(ControlledFrameContextMenusTest, Create) {
const web_app::IsolatedWebAppUrlInfo url_info =
CreateAndInstallEmptyApp(web_app::ManifestBuilder());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(CreateControlledFrame(
app_frame, embedded_https_test_server().GetURL("/index.html")));
extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame);
auto* menu_manager = extensions::MenuManager::Get(profile());
const extensions::MenuItem::ExtensionKey extension_key(
/*extension_id=*/"",
web_view_guest->owner_rfh()->GetProcess()->GetDeprecatedID(),
web_view_guest->owner_rfh()->GetRoutingID(),
web_view_guest->view_instance_id());
EXPECT_EQ(0u, menu_manager->MenuItemsSize(extension_key));
static constexpr std::string kItem1ID = "1";
static constexpr std::string kItem1Title = "Test";
EXPECT_EQ(kEvalSuccessStr,
CreateContextMenuItem(app_frame, kItem1ID, kItem1Title));
ASSERT_EQ(1u, menu_manager->MenuItemsSize(extension_key));
ExpectMenuItemWithIdAndTitle(extension_key, kItem1ID, kItem1Title);
static constexpr std::string kItem2ID = "2";
static constexpr std::string kItem2Title = "Test2";
EXPECT_EQ(kEvalSuccessStr,
CreateContextMenuItem(app_frame, kItem2ID, kItem2Title));
ASSERT_EQ(2u, menu_manager->MenuItemsSize(extension_key));
ExpectMenuItemWithIdAndTitle(extension_key, kItem2ID, kItem2Title);
static constexpr std::string kItem3ID = "3";
static constexpr std::string kItem3Title = "Test3";
EXPECT_EQ(kEvalSuccessStr,
CreateContextMenuItem(app_frame, kItem3ID, kItem3Title));
ASSERT_EQ(3u, menu_manager->MenuItemsSize(extension_key));
ExpectMenuItemWithIdAndTitle(extension_key, kItem3ID, kItem3Title);
}
IN_PROC_BROWSER_TEST_F(ControlledFrameContextMenusTest, Update) {
const web_app::IsolatedWebAppUrlInfo url_info =
CreateAndInstallEmptyApp(web_app::ManifestBuilder());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(CreateControlledFrame(
app_frame, embedded_https_test_server().GetURL("/index.html")));
extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame);
auto* menu_manager = extensions::MenuManager::Get(profile());
static constexpr std::string kItem1ID = "1";
static constexpr std::string kItem1Title = "Test";
EXPECT_EQ(kEvalSuccessStr,
CreateContextMenuItem(app_frame, kItem1ID, kItem1Title));
const extensions::MenuItem::ExtensionKey extension_key(
/*extension_id=*/"",
web_view_guest->owner_rfh()->GetProcess()->GetDeprecatedID(),
web_view_guest->owner_rfh()->GetRoutingID(),
web_view_guest->view_instance_id());
ASSERT_EQ(1u, menu_manager->MenuItemsSize(extension_key));
ExpectMenuItemWithIdAndTitle(extension_key, kItem1ID, kItem1Title);
static constexpr std::string kItem1NewTitle = "Test1";
EXPECT_EQ(kEvalSuccessStr,
UpdateContextMenuItemTitle(app_frame, kItem1ID, kItem1NewTitle));
ASSERT_EQ(1u, menu_manager->MenuItemsSize(extension_key));
ExpectMenuItemWithIdAndTitle(extension_key, kItem1ID, kItem1NewTitle);
}
IN_PROC_BROWSER_TEST_F(ControlledFrameContextMenusTest, Remove) {
const web_app::IsolatedWebAppUrlInfo url_info =
CreateAndInstallEmptyApp(web_app::ManifestBuilder());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(CreateControlledFrame(
app_frame, embedded_https_test_server().GetURL("/index.html")));
extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame);
auto* menu_manager = extensions::MenuManager::Get(profile());
static constexpr std::string kItem1ID = "1";
static constexpr std::string kItem1Title = "Test1";
EXPECT_EQ(kEvalSuccessStr,
CreateContextMenuItem(app_frame, kItem1ID, kItem1Title));
EXPECT_EQ(kEvalSuccessStr, CreateContextMenuItem(app_frame, /*id=*/"2",
/*title=*/"Test2"));
EXPECT_EQ(kEvalSuccessStr, RemoveContextMenuItem(app_frame, kItem1ID));
const extensions::MenuItem::ExtensionKey extension_key(
/*extension_id=*/"",
web_view_guest->owner_rfh()->GetProcess()->GetDeprecatedID(),
web_view_guest->owner_rfh()->GetRoutingID(),
web_view_guest->view_instance_id());
ASSERT_EQ(1u, menu_manager->MenuItemsSize(extension_key));
extensions::MenuItem* deleted_item =
menu_manager->GetItemById(CreateMenuItemId(extension_key, kItem1ID));
EXPECT_FALSE(deleted_item);
}
IN_PROC_BROWSER_TEST_F(ControlledFrameContextMenusTest, RemoveAll) {
const web_app::IsolatedWebAppUrlInfo url_info =
CreateAndInstallEmptyApp(web_app::ManifestBuilder());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(CreateControlledFrame(
app_frame, embedded_https_test_server().GetURL("/index.html")));
extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame);
auto* menu_manager = extensions::MenuManager::Get(profile());
EXPECT_EQ(kEvalSuccessStr, CreateContextMenuItem(app_frame, /*id=*/"1",
/*title=*/"Test1"));
EXPECT_EQ(kEvalSuccessStr, CreateContextMenuItem(app_frame, /*id=*/"2",
/*title=*/"Test2"));
EXPECT_EQ(kEvalSuccessStr, RemoveAllContextMenuItems(app_frame));
const extensions::MenuItem::ExtensionKey extension_key(
/*extension_id=*/"",
web_view_guest->owner_rfh()->GetProcess()->GetDeprecatedID(),
web_view_guest->owner_rfh()->GetRoutingID(),
web_view_guest->view_instance_id());
ASSERT_EQ(0u, menu_manager->MenuItemsSize(extension_key));
}
// TODO(crbug.com/392208013): Fix and enable on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_ShowEvent DISABLED_ShowEvent
#else
#define MAYBE_ShowEvent ShowEvent
#endif // BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(ControlledFrameContextMenusTest, MAYBE_ShowEvent) {
const web_app::IsolatedWebAppUrlInfo url_info =
CreateAndInstallEmptyApp(web_app::ManifestBuilder());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(CreateControlledFrame(
app_frame, embedded_https_test_server().GetURL("/index.html")));
auto add_handler_script = content::JsReplace(
R"(
document.onShowHandler = function() {
document.onShowCount = (document.onShowCount ?? 0) + 1;
};
(function() {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus) {
return ('FAIL: frame or frame.contextMenus is undefined');
}
frame.contextMenus.addEventListener('show', document.onShowHandler);
return $1;
})();
)",
kEvalSuccessStr);
// Add a listener for 'show' then simulate right click and expect the
// listener to be triggered.
ASSERT_EQ(content::EvalJs(app_frame, add_handler_script), kEvalSuccessStr);
extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame);
ASSERT_TRUE(web_view_guest);
content::RenderFrameHost* controlled_frame =
web_view_guest->GetGuestMainFrame();
ASSERT_TRUE(controlled_frame);
SimulateOpenContextMenu(controlled_frame);
ASSERT_EQ(content::EvalJs(app_frame, "document.onShowCount"), 1);
auto remove_handler_script = content::JsReplace(
R"(
(function() {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus) {
return ('FAIL: frame or frame.contextMenus is undefined');
}
frame.contextMenus.removeEventListener('show', document.onShowHandler);
return $1;
})();
)",
kEvalSuccessStr);
// Remove the listener for 'onShow' then simulate right click and expect the
// listener to not be triggered.
ASSERT_EQ(content::EvalJs(app_frame, remove_handler_script), kEvalSuccessStr);
SimulateOpenContextMenu(controlled_frame);
ASSERT_EQ(content::EvalJs(app_frame, "document.onShowCount"), 1);
}
IN_PROC_BROWSER_TEST_F(ControlledFrameContextMenusTest, NoLegacyOnShowEvent) {
const web_app::IsolatedWebAppUrlInfo url_info =
CreateAndInstallEmptyApp(web_app::ManifestBuilder());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(CreateControlledFrame(
app_frame, embedded_https_test_server().GetURL("/index.html")));
const auto* check_legacy_event_script = R"(
new Promise(async (resolve, reject) => {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus) {
reject('FAIL: frame or frame.contextMenus is undefined');
return;
}
if (frame.contextMenus.onShow) {
reject('FAIL: contextMenus object contains an onShow attribute');
return;
}
resolve('SUCCESS');
});
)";
ASSERT_EQ(content::EvalJs(app_frame, check_legacy_event_script),
kEvalSuccessStr);
}
IN_PROC_BROWSER_TEST_F(ControlledFrameContextMenusTest, ClickEvent) {
constexpr std::string kItemID = "107";
const web_app::IsolatedWebAppUrlInfo url_info =
CreateAndInstallEmptyApp(web_app::ManifestBuilder());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(CreateControlledFrame(
app_frame, embedded_https_test_server().GetURL("/index.html")));
auto create_context_menu_script = content::JsReplace(R"(
new Promise(async (resolve, reject) => {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus || !frame.contextMenus.create) {
reject('FAIL: frame, frame.contextMenus, or ' +
'frame.contextMenus.create is undefined');
return;
}
await frame.contextMenus.create(
{
title: 'test_title',
id: $1,
});
resolve('SUCCESS');
});
)",
kItemID);
// Create a ContextMenu item with a inline listener.
ASSERT_EQ(content::EvalJs(app_frame, create_context_menu_script),
kEvalSuccessStr);
auto add_handler_script = content::JsReplace(
R"(
document.onClickedHandler = function(info) {
document.clickedMenuItemId =
[...(document.clickedMenuItemId ?? []), info.menuItem.id];
document.globalOnClickedCount = (document.globalOnClickedCount ?? 0) + 1;
};
(function() {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus) {
return ('FAIL: frame or frame.contextMenus is undefined');
}
frame.contextMenus.addEventListener('click', document.onClickedHandler);
return $1;
})();
)",
kEvalSuccessStr);
// Add a global listener for the 'clicked' event, then simulate clicking on
// menu item.
ASSERT_EQ(content::EvalJs(app_frame, add_handler_script), kEvalSuccessStr);
extensions::WebViewGuest* web_view_guest = GetWebViewGuest(app_frame);
ASSERT_TRUE(web_view_guest);
content::RenderFrameHost* controlled_frame =
web_view_guest->GetGuestMainFrame();
ASSERT_TRUE(controlled_frame);
SimulateClickContextMenuItem(controlled_frame);
EXPECT_EQ(content::EvalJs(app_frame, "document.globalOnClickedCount"), 1);
EXPECT_THAT(content::EvalJs(app_frame, "document.clickedMenuItemId")
.TakeValue()
.TakeList(),
Each(Eq(kItemID)));
auto remove_handler_script = content::JsReplace(
R"(
(function() {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus) {
return ('FAIL: frame or frame.contextMenus is undefined');
}
frame.contextMenus.removeEventListener('click', document.onClickedHandler);
return $1;
})();
)",
kEvalSuccessStr);
// Remove the global listener for 'click' then simulate clicking on menu
// item.
ASSERT_EQ(content::EvalJs(app_frame, remove_handler_script), kEvalSuccessStr);
SimulateClickContextMenuItem(controlled_frame);
EXPECT_EQ(content::EvalJs(app_frame, "document.globalOnClickedCount"), 1);
EXPECT_THAT(
content::EvalJs(app_frame, "document.clickedMenuItemId").ExtractList(),
Each(Eq(kItemID)));
}
IN_PROC_BROWSER_TEST_F(ControlledFrameContextMenusTest, NoLegacyOnClickEvent) {
const web_app::IsolatedWebAppUrlInfo url_info =
CreateAndInstallEmptyApp(web_app::ManifestBuilder());
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(CreateControlledFrame(
app_frame, embedded_https_test_server().GetURL("/index.html")));
const auto* check_legacy_event_script = R"(
new Promise(async (resolve, reject) => {
const frame = document.getElementsByTagName('controlledframe')[0];
if (!frame || !frame.contextMenus) {
reject('FAIL: frame or frame.contextMenus is undefined');
return;
}
if (frame.contextMenus.onClick) {
reject('FAIL: contextMenus object contains an onClick attribute');
return;
}
resolve('SUCCESS');
});
)";
ASSERT_EQ(content::EvalJs(app_frame, check_legacy_event_script),
kEvalSuccessStr);
}
} // namespace controlled_frame