blob: f292d77ee298ee2675431c25beff9cd11cae85b9 [file] [log] [blame] [edit]
/*
* Copyright (C) 2023-2024 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#if ENABLE(WK_WEB_EXTENSIONS)
#import "HTTPServer.h"
#import "TestCocoaImageAndCocoaColor.h"
#import "TestUIDelegate.h"
#import "TestWKWebView.h"
#import "TestWebExtensionsDelegate.h"
#import "WebExtensionUtilities.h"
#import <WebKit/WKWebViewConfigurationPrivate.h>
#import <wtf/cocoa/TypeCastsCocoa.h>
#if USE(APPKIT)
#import "AppKitSPI.h"
#endif
namespace TestWebKitAPI {
static auto *menusManifest = @{
@"manifest_version": @3,
@"name": @"Menus Test",
@"description": @"Menus Test",
@"version": @"1",
@"permissions": @[ @"menus", @"contextMenus", @"activeTab" ],
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
@"commands": @{ },
@"action": @{
@"default_title": @"Test Action",
@"default_icon": @{
@"16": @"toolbar-16.png",
@"32": @"toolbar-32.png",
},
},
};
TEST(WKWebExtensionAPIMenus, Errors)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertThrows(() => browser.menus.create({ id: { }, title: 'Test' }), /'id' is expected to be a string or a number, but an object was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ id: '', title: 'Test' }), /'id' value is invalid, because it must not be empty/i)",
@"browser.test.assertThrows(() => browser.menus.create({ id: NaN, title: 'Test' }), /'id' is expected to be a string or a number, but NaN was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ id: Infinity, title: 'Test' }), /'id' is expected to be a string or a number, but Infinity was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ title: 123 }), /'title' is expected to be a string, but a number was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ title: '' }), /'title' value is invalid, because it must not be empty/i)",
@"browser.test.assertThrows(() => browser.menus.create({ contexts: 'all', title: 'Test' }), /'contexts' is expected to be an array of strings, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ contexts: [ 42 ], title: 'Test' }), /'contexts' is expected to be an array of strings, but a number was provided in the array/i)",
@"browser.test.assertThrows(() => browser.menus.create({ contexts: [ NaN ], title: 'Test' }), /'contexts' is expected to be an array of strings, but NaN was provided in the array/i)",
@"browser.test.assertThrows(() => browser.menus.create({ contexts: [ Infinity ], title: 'Test' }), /'contexts' is expected to be an array of strings, but Infinity was provided in the array/i)",
@"browser.test.assertThrows(() => browser.menus.create({ parentId: { }, title: 'Test' }), /'parentId' is expected to be a string or a number, but an object was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ parentId: '', title: 'Test' }), /'parentId' value is invalid, because it must not be empty/i)",
@"browser.test.assertThrows(() => browser.menus.create({ parentId: NaN, title: 'Test' }), /'parentId' is expected to be a string or a number, but NaN was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ parentId: Infinity, title: 'Test' }), /'parentId' is expected to be a string or a number, but Infinity was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ documentUrlPatterns: 'https://*.example.com', title: 'Test' }), /'documentUrlPatterns' is expected to be an array of strings, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ documentUrlPatterns: [ 'bad' ], title: 'Test' }), /'documentUrlPatterns' value is invalid, because 'bad' is not a valid pattern/i)",
@"browser.test.assertThrows(() => browser.menus.create({ documentUrlPatterns: [ 'foo://*/*' ], title: 'Test' }), /'documentUrlPatterns' value is invalid, because '.*' is not a valid pattern/i)",
@"browser.test.assertThrows(() => browser.menus.create({ targetUrlPatterns: 'https://*.example.com', title: 'Test' }), /'targetUrlPatterns' is expected to be an array of strings, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ targetUrlPatterns: [ 'bad' ], title: 'Test' }), /'targetUrlPatterns' value is invalid, because 'bad' is not a valid pattern/i)",
@"browser.test.assertThrows(() => browser.menus.create({ type: 123, title: 'Test' }), /'type' is expected to be a string, but a number was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ type: 'bad', title: 'Test' }), /'type' value is invalid, because it must specify either 'normal', 'checkbox', 'radio', or 'separator'/i)",
@"browser.test.assertThrows(() => browser.menus.create({ command: 123, title: 'Test' }), /'command' is expected to be a string, but a number was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ command: '', title: 'Test' }), /'command' value is invalid, because it must not be empty/i)",
@"browser.test.assertThrows(() => browser.menus.create({ checked: 'bad', title: 'Test' }), /'checked' is expected to be a boolean, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ visible: 'bad', title: 'Test' }), /'visible' is expected to be a boolean, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ enabled: 'bad', title: 'Test' }), /'enabled' is expected to be a boolean, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ icons: 123, title: 'Test' }), /'icons' is expected to be a string or an object or null, but a number was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ icons: { 16: 123 }, title: 'Test' }), /'icons\\[16]' value is invalid, because a string is expected, but a number was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ icons: { '16': 123 }, title: 'Test' }), /'icons\\[16]' value is invalid, because a string is expected, but a number was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ icons: { '1.2': 'test.png' }, title: 'Test' }), /'icons' value is invalid, because '1.2' is not a valid dimension/i)",
@"browser.test.assertThrows(() => browser.menus.create({ onclick: 'bad', title: 'Test' }), /'onclick' is expected to be a value, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ onclick: { }, title: 'Test' }), /'onclick' is expected to be a value, but an object was provided/i)",
@"browser.test.assertThrows(() => browser.menus.create({ onclick: new RegExp(), title: 'Test' }), /'onclick' value is invalid, because it must be a function/i)",
@"browser.test.notifyPass()"
]);
Util::loadAndRunExtension(menusManifest, @{ @"background.js": backgroundScript });
}
#if USE(APPKIT)
using CocoaMenuItem = NSMenuItem;
using CocoaMenuAction = NSMenuItem;
constexpr auto CocoaMenuItemStateOn = NSControlStateValueOn;
constexpr auto CocoaMenuItemStateOff = NSControlStateValueOff;
#else
using CocoaMenuItem = UIMenuElement;
using CocoaMenuAction = UIAction;
constexpr auto CocoaMenuItemStateOn = UIMenuElementStateOn;
constexpr auto CocoaMenuItemStateOff = UIMenuElementStateOff;
#endif
static inline void performMenuItemAction(auto *menuItem)
{
#if USE(APPKIT)
[menuItem.target performSelector:menuItem.action withObject:nil];
#else
[dynamic_objc_cast<CocoaMenuAction>(menuItem) performWithSender:nil target:nil];
#endif
}
TEST(WKWebExtensionAPIMenus, MenuCreateWithVariousIds)
{
auto *backgroundScript = Util::constructScript(@[
@"let menuItemWithStringId = browser.menus.create({",
@" id: 'string-id',",
@" title: 'String ID',",
@" contexts: [ 'all' ]",
@"})",
@"browser.test.assertEq(typeof menuItemWithStringId, 'string')",
@"browser.test.assertEq(menuItemWithStringId, 'string-id')",
@"let menuItemWithNumericId = browser.menus.create({",
@" id: 12345,",
@" title: 'Numeric ID',",
@" contexts: [ 'all' ]",
@"})",
@"browser.test.assertEq(typeof menuItemWithNumericId, 'number')",
@"browser.test.assertEq(menuItemWithNumericId, 12345)",
@"let menuItemWithOmittedId = browser.menus.create({",
@" title: 'Omitted ID',",
@" contexts: [ 'all' ]",
@"})",
@"function validateUUIDv4(uuid) {",
@" let regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[8|9|a|b][0-9a-f]{3}-[0-9a-f]{12}$/i",
@" return regex.test(uuid)",
@"}",
@"browser.test.assertEq(typeof menuItemWithOmittedId, 'string')",
@"browser.test.assertTrue(validateUUIDv4(menuItemWithOmittedId), 'ID should be a UUID v4 string')",
@"browser.test.notifyPass()"
]);
Util::loadAndRunExtension(menusManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIMenus, CreateMenuWithDeprecatedKeys)
{
auto *backgroundScript = Util::constructScript(@[
@"const createImageData = (size, color) => {",
@" const context = new OffscreenCanvas(size, size).getContext('2d')",
@" context.fillStyle = color",
@" context.fillRect(0, 0, size, size)",
@" return context.getImageData(0, 0, size, size)",
@"}",
@"const lightImageData = createImageData(16, 'black')",
@"const darkImageData = createImageData(16, 'white')",
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-with-deprecated-color-schemes-key',",
@" title: 'Menu Item Duplicate Color Schemes',",
@" iconVariants: [",
@" { 16: darkImageData, 'colorSchemes': [ 'dark' ], 'color_schemes': [ 'light' ] },",
@" { 16: lightImageData, 'colorSchemes': [ 'light' ], 'color_schemes': [ 'dark' ] }",
@" ],",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-with-deprecated-icon-variants-key',",
@" title: 'Menu Item Duplicate Icon Variants',",
@" iconVariants: [",
@" { 16: darkImageData, 'colorSchemes': [ 'dark' ] },",
@" { 16: lightImageData, 'colorSchemes': [ 'light' ] }",
@" ],",
@" icon_variants: [",
@" { 16: darkImageData, 'colorSchemes': [ 'light' ] },",
@" { 16: lightImageData, 'colorSchemes': [ 'dark' ] }",
@" ],",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.sendMessage('Menus Created')",
]);
auto *resources = @{
@"background.js": backgroundScript,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Created"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 2lu);
auto *firstMenuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([firstMenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(firstMenuItem.title, @"Menu Item Duplicate Color Schemes");
auto *secondMenuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.lastObject);
EXPECT_TRUE([secondMenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(secondMenuItem.title, @"Menu Item Duplicate Icon Variants");
Util::performWithAppearance(Util::Appearance::Dark, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(firstMenuItem.image), [CocoaColor whiteColor]));
EXPECT_TRUE(Util::compareColors(Util::pixelColor(secondMenuItem.image), [CocoaColor whiteColor]));
});
Util::performWithAppearance(Util::Appearance::Light, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(firstMenuItem.image), [CocoaColor blackColor]));
EXPECT_TRUE(Util::compareColors(Util::pixelColor(secondMenuItem.image), [CocoaColor blackColor]));
});
}
TEST(WKWebExtensionAPIMenus, ActionMenus)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'top-level-1',",
@" title: 'Top Level 1',",
@" contexts: [ 'action' ]",
@"})",
@"browser.menus.create({",
@" id: 'top-level-2',",
@" title: 'Top Level 2',",
@" contexts: [ 'page' ]",
@"})",
@"browser.menus.create({",
@" id: 'top-level-3',",
@" title: 'Top Level 3',",
@" contexts: [ 'action', 'page' ],",
@" documentUrlPatterns: ['*://localhost/*']",
@"})",
@"browser.menus.create({",
@" id: 'top-level-4',",
@" title: 'Top Level 4'",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'top-level-1')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.assertEq(tab.url, '')",
@" browser.test.assertEq(tab.title, '')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')"
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
// Reset activeTab, WKWebExtensionAPIMenus.ActionMenusWithActiveTab tests that.
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusUnknown forPermission:WKWebExtensionPermissionActiveTab];
[manager runUntilTestMessage:@"Menus Created"];
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()];
[manager runForTimeInterval:1];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
auto *expectedTitles = @[ @"Top Level 1", @"Top Level 3" ];
EXPECT_EQ(menuItems.count, expectedTitles.count);
[menuItems enumerateObjectsUsingBlock:^(CocoaMenuItem *menuItem, NSUInteger index, BOOL *stop) {
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, expectedTitles[index]);
}];
performMenuItemAction(menuItems.firstObject);
[manager run];
}
TEST(WKWebExtensionAPIMenus, ActionMenusWithActiveTab)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'top-level-1',",
@" title: 'Top Level 1',",
@" contexts: [ 'action' ]",
@"})",
@"browser.menus.create({",
@" id: 'top-level-2',",
@" title: 'Top Level 2',",
@" contexts: [ 'page' ]",
@"})",
@"browser.menus.create({",
@" id: 'top-level-3',",
@" title: 'Top Level 3',",
@" contexts: [ 'action', 'page' ]",
@"})",
@"browser.menus.create({",
@" id: 'top-level-4',",
@" title: 'Top Level 4'",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'top-level-1')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.assertEq(typeof tab.url, 'string')",
@" browser.test.assertTrue(tab.url.startsWith('http://localhost:'))",
@" browser.test.assertEq(typeof tab.title, 'string')",
@" browser.test.assertTrue(tab.title.length > 0)",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')"
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
EXPECT_FALSE([manager.get().context hasActiveUserGestureInTab:manager.get().defaultTab]);
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<title>Test</title>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()];
[manager runForTimeInterval:1];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
auto *expectedTitles = @[ @"Top Level 1", @"Top Level 3" ];
EXPECT_EQ(menuItems.count, expectedTitles.count);
[menuItems enumerateObjectsUsingBlock:^(CocoaMenuItem *menuItem, NSUInteger index, BOOL *stop) {
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, expectedTitles[index]);
}];
performMenuItemAction(menuItems.firstObject);
[manager run];
EXPECT_TRUE([manager.get().context hasActiveUserGestureInTab:manager.get().defaultTab]);
}
TEST(WKWebExtensionAPIMenus, ActionSubmenus)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'top-level-1',",
@" title: 'Top Level 1',",
@" contexts: [ 'action' ]",
@"})",
@"browser.menus.create({",
@" id: 'submenu-1',",
@" parentId: 'top-level-1',",
@" title: 'Submenu 1'",
@"})",
@"browser.menus.create({",
@" id: 'submenu-2',",
@" parentId: 'top-level-1',",
@" title: 'Submenu 2',",
@" contexts: [ 'action' ]",
@"})",
@"browser.menus.create({",
@" id: 'submenu-excluded',",
@" parentId: 'top-level-1',",
@" title: 'Excluded Submenu',",
@" contexts: [ 'page' ]",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'submenu-1')",
@" browser.test.assertEq(info.parentMenuItemId, 'top-level-1')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *topLevelMenuItem = menuItems.firstObject;
EXPECT_TRUE([topLevelMenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(topLevelMenuItem.title, @"Top Level 1");
auto *expectedSubmenuTitles = @[ @"Submenu 1", @"Submenu 2" ];
#if USE(APPKIT)
EXPECT_EQ(topLevelMenuItem.submenu.itemArray.count, expectedSubmenuTitles.count);
[topLevelMenuItem.submenu.itemArray enumerateObjectsUsingBlock:^(CocoaMenuItem *submenuItem, NSUInteger index, BOOL *stop) {
EXPECT_TRUE([submenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(submenuItem.title, expectedSubmenuTitles[index]);
}];
auto *submenuItem = topLevelMenuItem.submenu.itemArray.firstObject;
#else
auto *parentMenu = dynamic_objc_cast<UIMenu>(topLevelMenuItem);
EXPECT_EQ(parentMenu.children.count, expectedSubmenuTitles.count);
[parentMenu.children enumerateObjectsUsingBlock:^(CocoaMenuItem *action, NSUInteger index, BOOL *stop) {
EXPECT_TRUE([action isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(action.title, expectedSubmenuTitles[index]);
}];
auto *submenuItem = parentMenu.children.firstObject;
#endif
performMenuItemAction(submenuItem);
[manager run];
}
TEST(WKWebExtensionAPIMenus, ActionSubmenusUpdate)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'top-level-1',",
@" title: 'Top Level 1',",
@" contexts: [ 'action' ]",
@"})",
@"browser.menus.create({",
@" id: 'top-level-2',",
@" title: 'Top Level 2',",
@" contexts: [ 'tab' ]",
@"})",
@"browser.menus.create({",
@" id: 'submenu-1',",
@" parentId: 'top-level-2',",
@" title: 'Submenu 1'",
@"})",
@"browser.menus.create({",
@" id: 'submenu-2',",
@" parentId: 'top-level-2',",
@" title: 'Submenu 2',",
@" contexts: [ 'action' ]",
@"})",
@"browser.menus.create({",
@" id: 'submenu-excluded',",
@" parentId: 'top-level-2',",
@" title: 'Excluded Submenu'",
@"})",
@"browser.menus.update('submenu-1', {",
@" parentId: 'top-level-1',",
@" contexts: [ ]",
@"})",
@"browser.menus.update('submenu-2', {",
@" parentId: 'top-level-1',",
@" contexts: [ 'action' ]",
@"})",
@"browser.menus.update('submenu-excluded', {",
@" parentId: 'top-level-1',",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'submenu-1')",
@" browser.test.assertEq(info.parentMenuItemId, 'top-level-1')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *topLevelMenuItem = menuItems.firstObject;
EXPECT_TRUE([topLevelMenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(topLevelMenuItem.title, @"Top Level 1");
auto *expectedSubmenuTitles = @[ @"Submenu 1", @"Submenu 2" ];
#if USE(APPKIT)
EXPECT_EQ(topLevelMenuItem.submenu.itemArray.count, expectedSubmenuTitles.count);
[topLevelMenuItem.submenu.itemArray enumerateObjectsUsingBlock:^(CocoaMenuItem *submenuItem, NSUInteger index, BOOL *stop) {
EXPECT_TRUE([submenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(submenuItem.title, expectedSubmenuTitles[index]);
}];
auto *submenuItem = topLevelMenuItem.submenu.itemArray.firstObject;
#else
auto *parentMenu = dynamic_objc_cast<UIMenu>(topLevelMenuItem);
EXPECT_EQ(parentMenu.children.count, expectedSubmenuTitles.count);
[parentMenu.children enumerateObjectsUsingBlock:^(CocoaMenuItem *action, NSUInteger index, BOOL *stop) {
EXPECT_TRUE([action isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(action.title, expectedSubmenuTitles[index]);
}];
auto *submenuItem = parentMenu.children.firstObject;
#endif
performMenuItemAction(submenuItem);
[manager run];
}
TEST(WKWebExtensionAPIMenus, TabMenus)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'tab-item-1',",
@" title: 'Tab Item 1',",
@" contexts: [ 'tab' ]",
@"})",
@"browser.menus.create({",
@" id: 'tab-item-2',",
@" title: 'Tab Item 2',",
@" contexts: [ 'tab' ],",
@" documentUrlPatterns: ['*://localhost/*']",
@"})",
@"browser.menus.create({",
@" id: 'excluded-item',",
@" title: 'Excluded Item',",
@" contexts: [ 'image' ]",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'tab-item-1')",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()];
[manager runForTimeInterval:1];
auto *menuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(menuItems.count, 1lu);
#if USE(APPKIT)
menuItems = menuItems.firstObject.submenu.itemArray;
#else
auto *parentMenu = dynamic_objc_cast<UIMenu>(menuItems.firstObject);
menuItems = parentMenu.children;
#endif
EXPECT_EQ(menuItems.count, 2lu);
auto *expectedTitles = @[ @"Tab Item 1", @"Tab Item 2" ];
EXPECT_EQ(menuItems.count, expectedTitles.count);
[menuItems enumerateObjectsUsingBlock:^(CocoaMenuItem *menuItem, NSUInteger index, BOOL *stop) {
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, expectedTitles[index]);
}];
performMenuItemAction(menuItems.firstObject);
[manager run];
}
TEST(WKWebExtensionAPIMenus, MenuItemProperties)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'menu-item-properties',",
@" title: 'Menu &Item with Ampersand && More',",
@" type: 'checkbox',",
@" checked: true,",
@" contexts: [ 'all' ],",
@" enabled: false,",
@" visible: false,",
@" icons: { 16: 'icon-16.png', 20: 'icon-20.png' }",
@"})",
@"browser.menus.create({",
@" id: 'menu-item-command',",
@" title: 'Menu Item with Command',",
@" command: '_execute_action',",
@" contexts: [ 'all' ]",
@"})",
@"browser.action.onClicked.addListener((tab) => {",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto *smallIcon = Util::makePNGData(CGSizeMake(16, 16), @selector(greenColor));
auto *largerIcon = Util::makePNGData(CGSizeMake(20, 20), @selector(blueColor));
auto *resources = @{
@"background.js": backgroundScript,
@"icon-16.png": smallIcon,
@"icon-20.png": largerIcon,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Created"];
auto *menuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(menuItems.count, 1lu);
#if USE(APPKIT)
menuItems = menuItems.firstObject.submenu.itemArray;
#else
auto *parentMenu = dynamic_objc_cast<UIMenu>(menuItems.firstObject);
menuItems = parentMenu.children;
#endif
EXPECT_EQ(menuItems.count, 2lu);
auto *firstMenuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([firstMenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_EQ(firstMenuItem.state, CocoaMenuItemStateOn);
EXPECT_NS_EQUAL(firstMenuItem.title, @"Menu Item with Ampersand & More");
EXPECT_NOT_NULL(firstMenuItem.image);
#if USE(APPKIT)
EXPECT_FALSE(firstMenuItem.enabled);
EXPECT_TRUE(firstMenuItem.hidden);
EXPECT_TRUE(CGSizeEqualToSize(firstMenuItem.image.size, CGSizeMake(16, 16)));
#else
EXPECT_EQ(firstMenuItem.attributes, (UIMenuElementAttributesDisabled | UIMenuElementAttributesHidden));
EXPECT_TRUE(CGSizeEqualToSize(firstMenuItem.image.size, CGSizeMake(20, 20)));
#endif
auto *secondMenuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.lastObject);
EXPECT_TRUE([secondMenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(secondMenuItem.title, @"Menu Item with Command");
performMenuItemAction(secondMenuItem);
[manager run];
}
TEST(WKWebExtensionAPIMenus, MenuItemPropertiesUpdate)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'menu-item-properties',",
@" title: 'Old Title',",
@"})",
@"browser.menus.create({",
@" id: 'menu-item-command',",
@" title: 'Old Command Title',",
@"})",
@"browser.menus.update('menu-item-properties', {",
@" id: 'menu-item-properties-new',",
@" title: 'Menu &Item with Ampersand && More',",
@" type: 'checkbox',",
@" checked: true,",
@" contexts: [ 'all' ],",
@" enabled: false,",
@" visible: false,",
@" icons: { 16: 'icon-16.png', 20: 'icon-20.png' }",
@"})",
@"browser.menus.update('menu-item-command', {",
@" id: 'menu-item-command-new',",
@" title: 'Menu Item with Command',",
@" command: '_execute_action',",
@" contexts: [ 'all' ]",
@"})",
@"browser.action.onClicked.addListener((tab) => {",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto *smallIcon = Util::makePNGData(CGSizeMake(16, 16), @selector(greenColor));
auto *largerIcon = Util::makePNGData(CGSizeMake(20, 20), @selector(blueColor));
auto *resources = @{
@"background.js": backgroundScript,
@"icon-16.png": smallIcon,
@"icon-20.png": largerIcon,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Created"];
auto *menuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(menuItems.count, 1lu);
#if USE(APPKIT)
menuItems = menuItems.firstObject.submenu.itemArray;
#else
auto *parentMenu = dynamic_objc_cast<UIMenu>(menuItems.firstObject);
menuItems = parentMenu.children;
#endif
EXPECT_EQ(menuItems.count, 2lu);
auto *firstMenuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([firstMenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_EQ(firstMenuItem.state, CocoaMenuItemStateOn);
EXPECT_NS_EQUAL(firstMenuItem.title, @"Menu Item with Ampersand & More");
EXPECT_NOT_NULL(firstMenuItem.image);
#if USE(APPKIT)
EXPECT_FALSE(firstMenuItem.enabled);
EXPECT_TRUE(firstMenuItem.hidden);
EXPECT_TRUE(CGSizeEqualToSize(firstMenuItem.image.size, CGSizeMake(16, 16)));
#else
EXPECT_EQ(firstMenuItem.attributes, (UIMenuElementAttributesDisabled | UIMenuElementAttributesHidden));
EXPECT_TRUE(CGSizeEqualToSize(firstMenuItem.image.size, CGSizeMake(20, 20)));
#endif
auto *secondMenuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.lastObject);
EXPECT_TRUE([secondMenuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(secondMenuItem.title, @"Menu Item with Command");
performMenuItemAction(secondMenuItem);
[manager run];
}
#if ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS)
TEST(WKWebExtensionAPIMenus, MenuItemWithIconVariants)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-with-icon-variants',",
@" title: 'Menu Item with Icon Variants',",
@" iconVariants: [",
@" { 16: 'icon-dark-16.png', 'colorSchemes': [ 'dark' ] },",
@" { 16: 'icon-light-16.png', 'colorSchemes': [ 'light' ] }",
@" ],",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.sendMessage('Menus Created')",
]);
auto *darkIcon16 = Util::makePNGData(CGSizeMake(16, 16), @selector(whiteColor));
auto *lightIcon16 = Util::makePNGData(CGSizeMake(16, 16), @selector(blackColor));
auto *resources = @{
@"background.js": backgroundScript,
@"icon-dark-16.png": darkIcon16,
@"icon-light-16.png": lightIcon16,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Created"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *menuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, @"Menu Item with Icon Variants");
#if USE(APPKIT)
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(16, 16)));
#else
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(20, 20)));
#endif
Util::performWithAppearance(Util::Appearance::Dark, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor whiteColor]));
});
Util::performWithAppearance(Util::Appearance::Light, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor blackColor]));
});
}
TEST(WKWebExtensionAPIMenus, MenuItemWithImageDataVariants)
{
auto *backgroundScript = Util::constructScript(@[
@"const createImageData = (size, color) => {",
@" const context = new OffscreenCanvas(size, size).getContext('2d')",
@" context.fillStyle = color",
@" context.fillRect(0, 0, size, size)",
@" return context.getImageData(0, 0, size, size)",
@"}",
@"const darkImageData = createImageData(16, 'white')",
@"const lightImageData = createImageData(16, 'black')",
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-with-image-data-variants',",
@" title: 'Menu Item with ImageData Variants',",
@" iconVariants: [",
@" { 16: darkImageData, 'colorSchemes': [ 'dark' ] },",
@" { 16: lightImageData, 'colorSchemes': [ 'light' ] }",
@" ],",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.sendMessage('Menus Created')",
]);
auto *resources = @{
@"background.js": backgroundScript,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Created"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *menuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, @"Menu Item with ImageData Variants");
#if USE(APPKIT)
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(16, 16)));
#else
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(20, 20)));
#endif
Util::performWithAppearance(Util::Appearance::Dark, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor whiteColor]));
});
Util::performWithAppearance(Util::Appearance::Light, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor blackColor]));
});
}
TEST(WKWebExtensionAPIMenus, MenuItemWithWithNoValidVariants)
{
auto *backgroundScript = Util::constructScript(@[
@"const createImageData = (size, color) => {",
@" const context = new OffscreenCanvas(size, size).getContext('2d')",
@" context.fillStyle = color",
@" context.fillRect(0, 0, size, size)",
@" return context.getImageData(0, 0, size, size)",
@"}",
@"const validImageData = createImageData(16, 'white')",
@"await browser.test.assertThrows(() => browser.menus.create({",
@" id: 'submenu-item-invalid-dimension',",
@" parentId: 'top-level-item',",
@" title: 'Submenu with Invalid Dimension Key',",
@" iconVariants: [",
@" { 'sixteen': validImageData, 'colorSchemes': [ 'light' ] }",
@" ],",
@" contexts: [ 'action' ]",
@"}), /'iconVariants\\[0\\]' value is invalid, because 'sixteen' is not a valid dimension/)",
@"await browser.test.assertThrows(() => browser.menus.create({",
@" id: 'submenu-item-invalid-color-scheme',",
@" parentId: 'top-level-item',",
@" title: 'Submenu with Invalid Color Scheme',",
@" iconVariants: [",
@" { '16': validImageData, 'colorSchemes': [ 'bad' ] }",
@" ],",
@" contexts: [ 'action' ]",
@"}), /'iconVariants\\[0\\]\\['colorSchemes'\\]' value is invalid, because it must specify either 'light' or 'dark'/)",
@"browser.test.notifyPass()"
]);
auto *resources = @{
@"background.js": backgroundScript,
};
Util::loadAndRunExtension(menusManifest, resources);
}
TEST(WKWebExtensionAPIMenus, MenuItemWithMixedValidAndInvalidIconVariants)
{
auto *backgroundScript = Util::constructScript(@[
@"const createImageData = (size, color) => {",
@" const context = new OffscreenCanvas(size, size).getContext('2d')",
@" context.fillStyle = color",
@" context.fillRect(0, 0, size, size)",
@" return context.getImageData(0, 0, size, size)",
@"}",
@"const validImageData = createImageData(16, 'black')",
@"const invalidImageData = createImageData(16, 'white')",
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-mixed',",
@" title: 'Menu Item with Mixed Variants',",
@" iconVariants: [",
@" { 'sixteen': invalidImageData, 'colorSchemes': [ 'dark' ] },",
@" { '16': validImageData, 'colorSchemes': [ 'light' ] }",
@" ],",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.sendMessage('Menus Created')",
]);
auto *resources = @{
@"background.js": backgroundScript,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Created"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *menuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, @"Menu Item with Mixed Variants");
#if USE(APPKIT)
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(16, 16)));
#else
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(20, 20)));
#endif
Util::performWithAppearance(Util::Appearance::Light, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor blackColor]));
});
Util::performWithAppearance(Util::Appearance::Dark, ^{
// Should still be black, as light variant is used.
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor blackColor]));
});
}
TEST(WKWebExtensionAPIMenus, MenuItemWithAnySizeVariantAndSVGDataURL)
{
auto *backgroundScript = Util::constructScript(@[
@"const whiteSVGData = 'data:image/svg+xml;base64,' + btoa(`",
@" <svg width=\"100\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\">",
@" <rect width=\"100\" height=\"100\" fill=\"white\" />",
@" </svg>`)",
@"const blackSVGData = 'data:image/svg+xml;base64,' + btoa(`",
@" <svg width=\"100\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\">",
@" <rect width=\"100\" height=\"100\" fill=\"black\" />",
@" </svg>`)",
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-with-svg-variants',",
@" title: 'Menu Item with SVG Icon Variants',",
@" iconVariants: [",
@" { any: whiteSVGData, 'colorSchemes': [ 'dark' ] },",
@" { any: blackSVGData, 'colorSchemes': [ 'light' ] }",
@" ],",
@" contexts: [ 'all' ]",
@"}))",
@"browser.test.sendMessage('Menus Created')"
]);
auto *resources = @{
@"background.js": backgroundScript,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Created"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *menuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, @"Menu Item with SVG Icon Variants");
#if USE(APPKIT)
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(16, 16)));
#else
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(20, 20)));
#endif
Util::performWithAppearance(Util::Appearance::Dark, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor whiteColor]));
});
Util::performWithAppearance(Util::Appearance::Light, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor blackColor]));
});
}
TEST(WKWebExtensionAPIMenus, UpdateMenuItemWithIconVariants)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-without-icon-variants',",
@" title: 'Menu Item without Icon Variants',",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.assertSafe(() => browser.menus.update('menu-item-without-icon-variants', {",
@" title: 'Menu Item with Icon Variants',",
@" iconVariants: [",
@" { 16: 'icon-dark-16.png', 'colorSchemes': [ 'dark' ] },",
@" { 16: 'icon-light-16.png', 'colorSchemes': [ 'light' ] }",
@" ]",
@"}))",
@"browser.test.sendMessage('Menus Updated')",
]);
auto *darkIcon16 = Util::makePNGData(CGSizeMake(16, 16), @selector(whiteColor));
auto *lightIcon16 = Util::makePNGData(CGSizeMake(16, 16), @selector(blackColor));
auto *resources = @{
@"background.js": backgroundScript,
@"icon-dark-16.png": darkIcon16,
@"icon-light-16.png": lightIcon16,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Updated"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *menuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, @"Menu Item with Icon Variants");
#if USE(APPKIT)
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(16, 16)));
#else
EXPECT_TRUE(CGSizeEqualToSize(menuItem.image.size, CGSizeMake(20, 20)));
#endif
Util::performWithAppearance(Util::Appearance::Dark, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor whiteColor]));
});
Util::performWithAppearance(Util::Appearance::Light, ^{
EXPECT_TRUE(Util::compareColors(Util::pixelColor(menuItem.image), [CocoaColor blackColor]));
});
}
TEST(WKWebExtensionAPIMenus, ClearMenuItemIconVariantsWithNull)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-with-icon-variants',",
@" title: 'Menu Item with Icon Variants',",
@" iconVariants: [",
@" { 16: 'icon-dark-16.png', 'colorSchemes': [ 'dark' ] },",
@" { 16: 'icon-light-16.png', 'colorSchemes': [ 'light' ] }",
@" ],",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.assertSafe(() => browser.menus.update('menu-item-with-icon-variants', {",
@" iconVariants: null,",
@" title: 'Menu Item without Icon Variants'",
@"}))",
@"browser.test.sendMessage('Menus Updated')",
]);
auto *darkIcon16 = Util::makePNGData(CGSizeMake(16, 16), @selector(whiteColor));
auto *lightIcon16 = Util::makePNGData(CGSizeMake(16, 16), @selector(blackColor));
auto *resources = @{
@"background.js": backgroundScript,
@"icon-dark-16.png": darkIcon16,
@"icon-light-16.png": lightIcon16,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Updated"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *menuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, @"Menu Item without Icon Variants");
// Icon should be null after clearing.
EXPECT_NULL(menuItem.image);
}
TEST(WKWebExtensionAPIMenus, ClearMenuItemIconVariantsWithEmpty)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-with-icon-variants',",
@" title: 'Menu Item with Icon Variants',",
@" iconVariants: [",
@" { 16: 'icon-dark-16.png', 'colorSchemes': [ 'dark' ] },",
@" { 16: 'icon-light-16.png', 'colorSchemes': [ 'light' ] }",
@" ],",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.assertSafe(() => browser.menus.update('menu-item-with-icon-variants', {",
@" iconVariants: [ ],",
@" title: 'Menu Item without Icon Variants'",
@"}))",
@"browser.test.sendMessage('Menus Updated')",
]);
auto *darkIcon16 = Util::makePNGData(CGSizeMake(16, 16), @selector(whiteColor));
auto *lightIcon16 = Util::makePNGData(CGSizeMake(16, 16), @selector(blackColor));
auto *resources = @{
@"background.js": backgroundScript,
@"icon-dark-16.png": darkIcon16,
@"icon-light-16.png": lightIcon16,
};
auto manager = Util::loadExtension(menusManifest, resources);
[manager runUntilTestMessage:@"Menus Updated"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *menuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, @"Menu Item without Icon Variants");
// Icon should be null after clearing.
EXPECT_NULL(menuItem.image);
}
TEST(WKWebExtensionAPIMenus, MenuItemWithSymbolImageIconVariants)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-with-symbol-variants',",
@" title: 'Menu Item with Symbol Variants',",
@" iconVariants: [",
@" { any: 'symbol:star' }",
@" ],",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.sendMessage('Menus Created')",
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *menuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, @"Menu Item with Symbol Variants");
#if PLATFORM(MAC)
EXPECT_TRUE([menuItem.image isKindOfClass:NSImage.class]);
EXPECT_TRUE(menuItem.image._isSymbolImage);
#else
EXPECT_TRUE([menuItem.image isKindOfClass:UIImage.class]);
EXPECT_TRUE(menuItem.image.isSymbolImage);
#endif
}
#endif // ENABLE(WK_WEB_EXTENSIONS_ICON_VARIANTS)
TEST(WKWebExtensionAPIMenus, MenuItemWithSymbolImageIcon)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertSafe(() => browser.menus.create({",
@" id: 'menu-item-with-symbol-icon',",
@" title: 'Menu Item with Symbol Icon',",
@" icons: {",
@" 16: 'symbol:star'",
@" },",
@" contexts: [ 'action' ]",
@"}))",
@"browser.test.sendMessage('Menus Created')",
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
auto *action = [manager.get().context actionForTab:manager.get().defaultTab];
auto *menuItems = action.menuItems;
EXPECT_EQ(menuItems.count, 1lu);
auto *menuItem = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
EXPECT_TRUE([menuItem isKindOfClass:[CocoaMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, @"Menu Item with Symbol Icon");
#if PLATFORM(MAC)
EXPECT_TRUE([menuItem.image isKindOfClass:NSImage.class]);
EXPECT_TRUE(menuItem.image._isSymbolImage);
#else
EXPECT_TRUE([menuItem.image isKindOfClass:UIImage.class]);
EXPECT_TRUE(menuItem.image.isSymbolImage);
#endif
}
TEST(WKWebExtensionAPIMenus, ToggleCheckboxMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'checkbox-1',",
@" title: 'Checkbox 1',",
@" type: 'checkbox',",
@" checked: true,",
@" contexts: [ 'all' ]",
@"})",
@"browser.menus.create({",
@" id: 'checkbox-2',",
@" title: 'Checkbox 2',",
@" type: 'checkbox',",
@" contexts: [ 'all' ]",
@"})",
@"let clickCount = 0",
@"browser.menus.onClicked.addListener((info) => {",
@" if (info.menuItemId === 'checkbox-1') {",
@" browser.test.assertTrue(info.wasChecked)",
@" browser.test.assertFalse(info.checked)",
@" } else if (info.menuItemId === 'checkbox-2') {",
@" browser.test.assertFalse(info.wasChecked)",
@" browser.test.assertTrue(info.checked)",
@" }",
@" if (++clickCount === 2)",
@" browser.test.sendMessage('Menus Clicked')",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
auto *menuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(menuItems.count, 1lu);
#if USE(APPKIT)
menuItems = menuItems.firstObject.submenu.itemArray;
#else
auto *parentMenu = dynamic_objc_cast<UIMenu>(menuItems.firstObject);
menuItems = parentMenu.children;
#endif
EXPECT_EQ(menuItems.count, 2lu);
auto *checkbox1 = dynamic_objc_cast<CocoaMenuAction>(menuItems.firstObject);
auto *checkbox2 = dynamic_objc_cast<CocoaMenuAction>(menuItems.lastObject);
EXPECT_TRUE([checkbox1 isKindOfClass:[CocoaMenuItem class]]);
EXPECT_TRUE([checkbox2 isKindOfClass:[CocoaMenuItem class]]);
EXPECT_EQ(checkbox1.state, CocoaMenuItemStateOn);
EXPECT_EQ(checkbox2.state, CocoaMenuItemStateOff);
performMenuItemAction(checkbox1);
performMenuItemAction(checkbox2);
[manager runUntilTestMessage:@"Menus Clicked"];
auto *updatedMenuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(updatedMenuItems.count, 1lu);
#if USE(APPKIT)
updatedMenuItems = updatedMenuItems.firstObject.submenu.itemArray;
#else
parentMenu = dynamic_objc_cast<UIMenu>(updatedMenuItems.firstObject);
updatedMenuItems = parentMenu.children;
#endif
EXPECT_EQ(updatedMenuItems.count, 2lu);
checkbox1 = dynamic_objc_cast<CocoaMenuAction>(updatedMenuItems.firstObject);
checkbox2 = dynamic_objc_cast<CocoaMenuAction>(updatedMenuItems.lastObject);
EXPECT_EQ(checkbox1.state, CocoaMenuItemStateOff);
EXPECT_EQ(checkbox2.state, CocoaMenuItemStateOn);
}
TEST(WKWebExtensionAPIMenus, RadioItemGrouping)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'radio-1-group-1',",
@" title: 'Radio 1 Group 1',",
@" type: 'radio',",
@" checked: true,",
@" contexts: [ 'all' ]",
@"})",
@"browser.menus.create({",
@" id: 'radio-2-group-1',",
@" title: 'Radio 2 Group 1',",
@" type: 'radio',",
@" contexts: [ 'all' ]",
@"})",
@"browser.menus.create({",
@" title: 'Normal Item',",
@" visible: false,",
@" contexts: [ 'all' ]",
@"})",
@"browser.menus.create({",
@" id: 'radio-1-group-2',",
@" title: 'Radio 1 Group 2',",
@" type: 'radio',",
@" contexts: [ 'all' ]",
@"})",
@"browser.menus.create({",
@" id: 'radio-2-group-2',",
@" title: 'Radio 2 Group 2',",
@" type: 'radio',",
@" contexts: [ 'all' ]",
@"})",
@"browser.menus.create({",
@" id: 'radio-3-group-3',",
@" title: 'Radio 3 Group 2',",
@" type: 'radio',",
@" checked: true,",
@" contexts: [ 'all' ]",
@"})",
@"let clickCount = 0",
@"browser.menus.onClicked.addListener((info) => {",
@" if (info.menuItemId === 'radio-2-group-1') {",
@" browser.test.assertTrue(info.wasChecked)",
@" browser.test.assertTrue(info.checked)",
@" } else if (info.menuItemId === 'radio-2-group-2') {",
@" browser.test.assertFalse(info.wasChecked)",
@" browser.test.assertTrue(info.checked)",
@" }",
@" if (++clickCount === 2)",
@" browser.test.sendMessage('Menus Clicked')",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
auto *menuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(menuItems.count, 1lu);
#if USE(APPKIT)
menuItems = menuItems.firstObject.submenu.itemArray;
#else
auto *parentMenu = dynamic_objc_cast<UIMenu>(menuItems.firstObject);
menuItems = parentMenu.children;
#endif
EXPECT_EQ(menuItems.count, 6lu);
auto *radio1Group1 = dynamic_objc_cast<CocoaMenuAction>(menuItems[0]);
auto *radio2Group1 = dynamic_objc_cast<CocoaMenuAction>(menuItems[1]);
auto *radio1Group2 = dynamic_objc_cast<CocoaMenuAction>(menuItems[3]);
auto *radio2Group2 = dynamic_objc_cast<CocoaMenuAction>(menuItems[4]);
auto *radio3Group2 = dynamic_objc_cast<CocoaMenuAction>(menuItems[5]);
EXPECT_EQ(radio1Group1.state, CocoaMenuItemStateOn);
EXPECT_EQ(radio2Group1.state, CocoaMenuItemStateOff);
EXPECT_EQ(radio1Group2.state, CocoaMenuItemStateOff);
EXPECT_EQ(radio2Group2.state, CocoaMenuItemStateOff);
EXPECT_EQ(radio3Group2.state, CocoaMenuItemStateOn);
performMenuItemAction(radio1Group1);
performMenuItemAction(radio2Group2);
[manager runUntilTestMessage:@"Menus Clicked"];
auto *updatedMenuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(updatedMenuItems.count, 1lu);
#if USE(APPKIT)
updatedMenuItems = updatedMenuItems.firstObject.submenu.itemArray;
#else
parentMenu = dynamic_objc_cast<UIMenu>(updatedMenuItems.firstObject);
updatedMenuItems = parentMenu.children;
#endif
EXPECT_EQ(updatedMenuItems.count, 6lu);
radio1Group1 = dynamic_objc_cast<CocoaMenuAction>(updatedMenuItems[0]);
radio2Group1 = dynamic_objc_cast<CocoaMenuAction>(updatedMenuItems[1]);
radio1Group2 = dynamic_objc_cast<CocoaMenuAction>(updatedMenuItems[3]);
radio2Group2 = dynamic_objc_cast<CocoaMenuAction>(updatedMenuItems[4]);
radio3Group2 = dynamic_objc_cast<CocoaMenuAction>(updatedMenuItems[5]);
// The checked item as clicked again, so it does not change.
EXPECT_EQ(radio1Group1.state, CocoaMenuItemStateOn);
EXPECT_EQ(radio2Group1.state, CocoaMenuItemStateOff);
// A new item was clicked, so it does change.
EXPECT_EQ(radio1Group2.state, CocoaMenuItemStateOff);
EXPECT_EQ(radio2Group2.state, CocoaMenuItemStateOn);
EXPECT_EQ(radio3Group2.state, CocoaMenuItemStateOff);
}
TEST(WKWebExtensionAPIMenus, OnClick)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'click-item',",
@" title: 'Click Item',",
@" contexts: ['all'],",
@" onclick: (info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'click-item')",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.notifyPass()",
@" }",
@"}, () => browser.test.sendMessage('Menu Item Created'))"
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menu Item Created"];
auto *menuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(menuItems.count, 1lu);
performMenuItemAction(menuItems.firstObject);
[manager run];
}
TEST(WKWebExtensionAPIMenus, OnClickAfterUpdate)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'click-item',",
@" title: 'Click Item',",
@" contexts: ['all']",
@"})",
@"await browser.menus.update('click-item', {",
@" onclick: (info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'click-item')",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.notifyPass()",
@" }",
@"})",
@"browser.test.sendMessage('Menu Item Created')"
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menu Item Created"];
auto *menuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(menuItems.count, 1lu);
performMenuItemAction(menuItems.firstObject);
[manager run];
}
TEST(WKWebExtensionAPIMenus, ContextMenusNamespace)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertEq(typeof browser.contextMenus, 'object')",
@"browser.test.notifyPass()"
]);
Util::loadAndRunExtension(menusManifest, @{ @"background.js": backgroundScript });
}
#if PLATFORM(MAC)
TEST(WKWebExtensionAPIMenus, MacContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'context-menu-1',",
@" title: 'Context Menu 1'",
@"})",
@"browser.menus.create({",
@" id: 'context-menu-2',",
@" title: 'Context Menu 2'",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'context-menu-1')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.selectionText, undefined)",
@" browser.test.assertEq(typeof info.pageUrl, 'string')",
@" browser.test.assertTrue(info.pageUrl.startsWith('http://localhost:'))",
@" browser.test.assertEq(info.frameId, 0)",
@" browser.test.assertEq(info.editable, false)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *elementInfo, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *extensionSubmenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title isEqualToString:@"Menus Test"]) {
extensionSubmenuItem = menuItem;
break;
}
}
EXPECT_NOT_NULL(extensionSubmenuItem);
NSArray *expectedTitles = @[ @"Context Menu 1", @"Context Menu 2" ];
NSMenu *extensionMenu = extensionSubmenuItem.submenu;
EXPECT_EQ(extensionMenu.itemArray.count, expectedTitles.count);
[extensionMenu.itemArray enumerateObjectsUsingBlock:^(NSMenuItem *menuItem, NSUInteger index, BOOL *stop) {
EXPECT_TRUE([menuItem isKindOfClass:[NSMenuItem class]]);
EXPECT_NS_EQUAL(menuItem.title, expectedTitles[index]);
}];
performMenuItemAction(extensionMenu.itemArray.firstObject);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
EXPECT_FALSE([manager.get().context hasActiveUserGestureInTab:manager.get().defaultTab]);
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:urlRequest];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(200, 200) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(200, 200) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
EXPECT_TRUE([manager.get().context hasActiveUserGestureInTab:manager.get().defaultTab]);
}
TEST(WKWebExtensionAPIMenus, MacActiveTabContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'context-menu',",
@" title: 'Context Menu Item'",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'context-menu')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.selectionText, undefined)",
@" browser.test.assertEq(typeof info.pageUrl, 'string')",
@" browser.test.assertTrue(info.pageUrl.startsWith('http://localhost:'))",
@" browser.test.assertEq(info.frameId, 0)",
@" browser.test.assertEq(info.editable, false)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *selectionMenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title isEqualToString:@"Context Menu Item"]) {
selectionMenuItem = menuItem;
break;
}
}
EXPECT_NOT_NULL(selectionMenuItem);
performMenuItemAction(selectionMenuItem);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
EXPECT_FALSE([manager.get().context hasActiveUserGestureInTab:manager.get().defaultTab]);
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:server.requestWithLocalhost()];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(200, 200) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(200, 200) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
EXPECT_TRUE([manager.get().context hasActiveUserGestureInTab:manager.get().defaultTab]);
}
TEST(WKWebExtensionAPIMenus, MacURLPatternContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'url-pattern-menu-item',",
@" title: 'URL Pattern Item',",
@" contexts: ['link'],",
@" targetUrlPatterns: ['*://*.example.com/*'],",
@" documentUrlPatterns: ['*://localhost/*']",
@"})",
@"browser.menus.create({",
@" id: 'non-matching-menu-item',",
@" title: 'Non-Matching Item',",
@" contexts: ['link'],",
@" targetUrlPatterns: ['*://*.other.com/*'],",
@" documentUrlPatterns: ['*://*.apple.com/*']",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'url-pattern-menu-item')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.selectionText, 'Large Example Link')",
@" browser.test.assertEq(info.linkText, 'Large Example Link')",
@" browser.test.assertEq(info.linkUrl, 'http://example.com/test')",
@" browser.test.assertEq(typeof info.pageUrl, 'string')",
@" browser.test.assertTrue(info.pageUrl.startsWith('http://localhost:'))",
@" browser.test.assertEq(info.frameId, 0)",
@" browser.test.assertEq(info.editable, false)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *elementInfo, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *urlPatternMenuItem = nil;
NSMenuItem *nonMatchingMenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title isEqualToString:@"URL Pattern Item"])
urlPatternMenuItem = menuItem;
else if ([menuItem.title isEqualToString:@"Non-Matching Item"])
nonMatchingMenuItem = menuItem;
}
EXPECT_NOT_NULL(urlPatternMenuItem);
EXPECT_NULL(nonMatchingMenuItem);
performMenuItemAction(urlPatternMenuItem);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<a href='http://example.com/test' style='font-size: 100px'>Large Example Link</p>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:urlRequest];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(50, 50) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(50, 50) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
}
TEST(WKWebExtensionAPIMenus, MacSelectionContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'selection-menu-item',",
@" title: 'Selected: %s',",
@" contexts: [ 'selection' ]",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'selection-menu-item')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.selectionText, 'Example')",
@" browser.test.assertEq(typeof info.pageUrl, 'string')",
@" browser.test.assertTrue(info.pageUrl.startsWith('http://localhost:'))",
@" browser.test.assertEq(info.frameId, 0)",
@" browser.test.assertEq(info.editable, false)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *selectionMenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title hasPrefix:@"Selected: "]) {
selectionMenuItem = menuItem;
break;
}
}
EXPECT_NOT_NULL(selectionMenuItem);
EXPECT_NS_EQUAL(selectionMenuItem.title, @"Selected: Example");
performMenuItemAction(selectionMenuItem);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<p style='font-size: 100px'>Selection Example Text</p>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:urlRequest];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(200, 200) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(200, 200) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
}
TEST(WKWebExtensionAPIMenus, MacLinkContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'link-menu-item',",
@" title: 'Link Item',",
@" contexts: [ 'link' ]",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'link-menu-item')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.selectionText, 'Large Example Link')",
@" browser.test.assertEq(info.linkText, 'Large Example Link')",
@" browser.test.assertEq(info.linkUrl, 'http://example.com/test')",
@" browser.test.assertEq(typeof info.pageUrl, 'string')",
@" browser.test.assertTrue(info.pageUrl.startsWith('http://localhost:'))",
@" browser.test.assertEq(info.frameId, 0)",
@" browser.test.assertEq(info.editable, false)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *linkMenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title isEqualToString:@"Link Item"]) {
linkMenuItem = menuItem;
break;
}
}
EXPECT_NOT_NULL(linkMenuItem);
performMenuItemAction(linkMenuItem);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<a href='http://example.com/test' style='font-size: 100px'>Large Example Link</p>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:urlRequest];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(200, 200) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(200, 200) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
}
TEST(WKWebExtensionAPIMenus, MacImageContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'image-menu-item',",
@" title: 'Image Item',",
@" contexts: [ 'image' ]",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'image-menu-item')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.mediaType, 'image')",
@" browser.test.assertTrue(info.srcUrl.endsWith('/test.png'))",
@" browser.test.assertEq(typeof info.pageUrl, 'string')",
@" browser.test.assertTrue(info.pageUrl.startsWith('http://localhost:'))",
@" browser.test.assertEq(info.frameId, 0)",
@" browser.test.assertEq(info.editable, false)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *imageMenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title isEqualToString:@"Image Item"]) {
imageMenuItem = menuItem;
break;
}
}
EXPECT_NOT_NULL(imageMenuItem);
performMenuItemAction(imageMenuItem);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<img src='test.png' style='width: 400px; height: 400px'>"_s } },
{ "/test.png"_s, [NSData dataWithContentsOfURL:[NSBundle.test_resourcesBundle URLForResource:@"400x400-green" withExtension:@"png"]] },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:urlRequest];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(200, 200) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(200, 200) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
}
TEST(WKWebExtensionAPIMenus, MacVideoContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'video-menu-item',",
@" title: 'Video Item',",
@" contexts: [ 'video' ]",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'video-menu-item')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.mediaType, 'video')",
@" browser.test.assertTrue(info.srcUrl.endsWith('/test.mp4'))",
@" browser.test.assertEq(typeof info.pageUrl, 'string')",
@" browser.test.assertTrue(info.pageUrl.startsWith('http://localhost:'))",
@" browser.test.assertEq(info.frameId, 0)",
@" browser.test.assertEq(info.editable, false)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *videoMenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title isEqualToString:@"Video Item"]) {
videoMenuItem = menuItem;
break;
}
}
EXPECT_NOT_NULL(videoMenuItem);
performMenuItemAction(videoMenuItem);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<video src='test.mp4' style='width: 400px; height: 400px' controls></video>"_s } },
{ "/test.mp4"_s, [NSData dataWithContentsOfURL:[NSBundle.test_resourcesBundle URLForResource:@"test" withExtension:@"mp4"]] },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:urlRequest];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(200, 200) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(200, 200) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
}
TEST(WKWebExtensionAPIMenus, MacAudioContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'audio-menu-item',",
@" title: 'Audio Item',",
@" contexts: [ 'audio' ]",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'audio-menu-item')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.mediaType, 'audio')",
@" browser.test.assertTrue(info.srcUrl.endsWith('/test.m4a'))",
@" browser.test.assertEq(typeof info.pageUrl, 'string')",
@" browser.test.assertTrue(info.pageUrl.startsWith('http://localhost:'))",
@" browser.test.assertEq(info.frameId, 0)",
@" browser.test.assertEq(info.editable, false)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *audioMenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title isEqualToString:@"Audio Item"]) {
audioMenuItem = menuItem;
break;
}
}
EXPECT_NOT_NULL(audioMenuItem);
performMenuItemAction(audioMenuItem);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<audio src='test.m4a' style='width: 400px; height: 400px' controls></audio>"_s } },
{ "/test.m4a"_s, [NSData dataWithContentsOfURL:[NSBundle.test_resourcesBundle URLForResource:@"silence-long" withExtension:@"m4a"]] },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:urlRequest];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(200, 200) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(200, 200) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
}
TEST(WKWebExtensionAPIMenus, MacEditableContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'editable-menu-item',",
@" title: 'Editable Item',",
@" contexts: [ 'editable' ]",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'editable-menu-item')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.selectionText, 'Area')",
@" browser.test.assertEq(info.editable, true)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *editableMenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title isEqualToString:@"Editable Item"]) {
editableMenuItem = menuItem;
break;
}
}
EXPECT_NOT_NULL(editableMenuItem);
performMenuItemAction(editableMenuItem);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<textarea style='font-size: 50px; width: 400px; height: 400px; white-space: nowrap;'>Editable Text Area</textarea>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:urlRequest];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(200, 200) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(200, 200) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
}
TEST(WKWebExtensionAPIMenus, MacFrameContextMenuItems)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'frame-menu-item',",
@" title: 'Frame Item',",
@" contexts: [ 'page', 'frame' ]",
@"})",
@"browser.menus.onClicked.addListener((info, tab) => {",
@" browser.test.assertEq(typeof info, 'object')",
@" browser.test.assertEq(typeof tab, 'object')",
@" browser.test.assertEq(info.menuItemId, 'frame-menu-item')",
@" browser.test.assertEq(info.parentMenuItemId, undefined)",
@" browser.test.assertEq(info.selectionText, undefined)",
@" browser.test.assertEq(typeof info.frameUrl, 'string')",
@" browser.test.assertTrue(info.frameUrl.endsWith('frame.html'))",
@" browser.test.assertTrue(info.frameId !== 0)",
@" browser.test.assertEq(typeof tab.id, 'number')",
@" browser.test.assertEq(typeof tab.windowId, 'number')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Menus Created')",
]);
auto delegate = adoptNS([[TestUIDelegate alloc] init]);
__block bool gotContextMenu = false;
delegate.get().getContextMenuFromProposedMenu = ^(NSMenu *menu, _WKContextMenuElementInfo *, id<NSSecureCoding>, void (^completionHandler)(NSMenu *)) {
gotContextMenu = true;
NSMenuItem *frameMenuItem = nil;
for (NSMenuItem *menuItem in menu.itemArray) {
if ([menuItem.title isEqualToString:@"Frame Item"]) {
frameMenuItem = menuItem;
break;
}
}
EXPECT_NOT_NULL(frameMenuItem);
performMenuItemAction(frameMenuItem);
completionHandler(nil);
};
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Menus Created"];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<iframe src='frame.html' style='width: 400px; height: 400px'>"_s } },
{ "/frame.html"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:server.requestWithLocalhost("/frame.html"_s).URL];
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
configuration.get().webExtensionController = manager.get().controller;
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400) configuration:configuration.get()]);
webView.get().UIDelegate = delegate.get();
manager.get().defaultTab.webView = webView.get();
[webView synchronouslyLoadRequest:urlRequest];
[webView waitForNextPresentationUpdate];
[webView.get().window makeFirstResponder:webView.get()];
[webView mouseDownAtPoint:NSMakePoint(200, 200) simulatePressure:NO withFlags:0 eventType:NSEventTypeRightMouseDown];
[webView mouseUpAtPoint:NSMakePoint(200, 200) withFlags:0 eventType:NSEventTypeRightMouseUp];
Util::run(&gotContextMenu);
[manager run];
}
TEST(WKWebExtensionAPIMenus, ClickedMenuItemAndPermissionsRequest)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.menus.create({",
@" id: 'click-item',",
@" title: 'Click Item',",
@" contexts: ['all'],",
@" onclick: async (info, tab) => {",
@" try {",
@" const result = await browser.permissions.request({ 'permissions': [ 'menus' ] })",
@" if (result)",
@" browser.test.notifyPass()",
@" else",
@" browser.test.notifyFail('Permissions request was rejected')",
@" } catch (error) {",
@" browser.test.notifyFail('Permissions request failed')",
@" }",
@" }",
@"}, () => browser.test.sendMessage('Menu Item Created'))"
]);
auto manager = Util::loadExtension(menusManifest, @{ @"background.js": backgroundScript });
manager.get().internalDelegate.promptForPermissions = ^(id<WKWebExtensionTab> tab, NSSet<NSString *> *requestedPermissions, void (^callback)(NSSet<NSString *> *, NSDate *)) {
EXPECT_EQ(requestedPermissions.count, 1lu);
EXPECT_TRUE([requestedPermissions isEqualToSet:[NSSet setWithObject:@"menus"]]);
callback(requestedPermissions, nil);
};
[manager runUntilTestMessage:@"Menu Item Created"];
auto *menuItems = [manager.get().context menuItemsForTab:manager.get().defaultTab];
EXPECT_EQ(menuItems.count, 1lu);
performMenuItemAction(menuItems.firstObject);
[manager run];
}
#endif // PLATFORM(MAC)
} // namespace TestWebKitAPI
#endif // ENABLE(WK_WEB_EXTENSIONS)