blob: 8af88283615a7993f841cd69d22b2e1e4e2bffb5 [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 "TestWKWebView.h"
#import "WebExtensionUtilities.h"
#import <JavaScriptCore/MathCommon.h>
#import <wtf/darwin/DispatchExtras.h>
namespace TestWebKitAPI {
static auto *tabsManifest = @{
@"manifest_version": @3,
@"name": @"Tabs Test",
@"description": @"Tabs Test",
@"version": @"1",
@"options_page": @"options.html",
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
@"permissions": @[ @"tabs" ],
};
static auto *tabsManifestV2 = @{
@"manifest_version": @2,
@"name": @"Tabs Test",
@"description": @"Tabs Test",
@"version": @"1",
@"permissions": @[ @"*://localhost/*" ],
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
};
static auto *tabsContentScriptManifest = @{
@"manifest_version": @3,
@"name": @"Tabs Test",
@"description": @"Tabs Test",
@"version": @"1",
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
@"content_scripts": @[ @{
@"js": @[ @"content.js" ],
@"matches": @[ @"*://localhost/*" ],
@"all_frames": @YES,
} ],
};
static auto *activeTabManifest = @{
@"manifest_version": @3,
@"name": @"Tabs Test",
@"description": @"Tabs Test",
@"version": @"1",
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
@"permissions": @[ @"activeTab" ],
};
TEST(WKWebExtensionAPITabs, Errors)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertThrows(() => browser.tabs.get('bad'), /'tabId' value is invalid, because a number is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.get(-3), /'tabId' value is invalid, because it is not a tab identifier/i)",
@"browser.test.assertThrows(() => browser.tabs.duplicate('bad'), /'tabId' value is invalid, because a number is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.remove('bad'), /'tabIDs' value is invalid, because a number or an array of numbers is expected, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.remove(['bad']), /'tabIDs' value is invalid, because a number or an array of numbers is expected, but an array of other values was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.reload('bad'), /an unknown argument was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.goBack('bad'), /'tabId' value is invalid, because a number is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.goForward('bad'), /'tabId' value is invalid, because a number is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.getZoom('bad'), /'tabId' value is invalid, because a number is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.detectLanguage('bad'), /'tabId' value is invalid, because a number is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.toggleReaderMode('bad'), /'tabId' value is invalid, because a number is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.setZoom('bad'), /'zoomFactor' value is invalid, because a number is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.setZoom(1, 'bad'), /'zoomFactor' value is invalid, because a number is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.update('bad'), /'properties' value is invalid, because an object is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.update(1, 'bad'), /'properties' value is invalid, because an object is expected/i)",
@"browser.test.assertThrows(() => browser.tabs.update(1, { 'openerTabId': true }), /'openerTabId' is expected to be a number, but a boolean was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.update(1, { 'openerTabId': 4.5 }), /'openerTabId' value is invalid, because '4.5' is not a tab identifier/i)",
@"browser.test.assertThrows(() => browser.tabs.create({ 'url': 1234 }), /'url' is expected to be a string/i)",
@"browser.test.assertThrows(() => browser.tabs.query({ status: 'bad' }), /'status' value is invalid, because it must specify either 'loading' or 'complete'/i)",
@"browser.test.assertThrows(() => browser.tabs.query({ url: 12345 }), /'info' value is invalid, because 'url' is expected to be a string or an array of strings, but a number was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.query({ url: ['bad', 12345] }), /'url' is expected to be a string or an array of strings, but an array of other values was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.query({ windowId: 'bad' }), /'windowId' is expected to be a number, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.query({ windowId: -5 }), /'windowId' value is invalid, because '-5' is not a window identifier/i)",
@"browser.test.assertThrows(() => browser.tabs.query({ active: 'true' }), /'active' is expected to be a boolean, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.query({ highlighted: 123 }), /'highlighted' is expected to be a boolean, but a number was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.query({ index: 'five' }), /'index' is expected to be a number, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.query({ audible: 'yes' }), /'audible' is expected to be a boolean, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.captureVisibleTab('bad', { format: 'png' }), /an unknown argument was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.captureVisibleTab(undefined, { format: 'bad' }), /'format' value is invalid, because it must specify either 'png' or 'jpeg'/i)",
@"browser.test.assertThrows(() => browser.tabs.captureVisibleTab(undefined, 'bad'), /an unknown argument was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.captureVisibleTab(undefined, { format: 'jpeg', quality: 'high' }), /'quality' is expected to be a number, but a string was provided/i)",
@"browser.test.assertThrows(() => browser.tabs.captureVisibleTab(undefined, { format: 'jpeg', quality: 200 }), /'quality' value is invalid, because it must specify a value between 0 and 100/i)",
@"browser.test.assertThrows(() => browser.tabs.sendMessage(1, 'message', { frameId: 0, documentId: 'some-document-id' }), /cannot specify both 'frameId' and 'documentId'/i);",
@"browser.test.assertThrows(() => browser.tabs.connect(1, { frameId: 0, documentId: 'some-document-id' }), /cannot specify both 'frameId' and 'documentId'/i);",
@"await browser.test.assertRejects(browser.tabs.captureVisibleTab(9999), /window not found/i)",
@"await browser.test.assertRejects(browser.tabs.captureVisibleTab(), /'activeTab' permission or granted host permissions for the current website are required/i)",
@"await browser.test.assertRejects(browser.tabs.get(9999), /tab not found/i)",
@"await browser.test.assertRejects(browser.tabs.duplicate(9999), /tab not found/i)",
@"await browser.test.assertRejects(browser.tabs.remove(9999), /tab '9999' was not found/i)",
@"await browser.test.assertRejects(browser.tabs.reload(9999), /tab not found/i)",
@"await browser.test.assertRejects(browser.tabs.goBack(9999), /tab not found/i)",
@"await browser.test.assertRejects(browser.tabs.goForward(9999), /tab not found/i)",
@"await browser.test.assertRejects(browser.tabs.setZoom(9999, 1.5), /tab not found/i)",
@"await browser.test.assertRejects(browser.tabs.detectLanguage(9999), /tab not found/i)",
@"await browser.test.assertRejects(browser.tabs.toggleReaderMode(9999), /tab not found/i)",
@"browser.test.notifyPass()"
]);
Util::loadAndRunExtension(tabsManifest, @{ @"background.js": backgroundScript });
backgroundScript = Util::constructScript(@[
@"browser.test.assertThrows(() => browser.tabs.executeScript(), /a required argument is missing/i)",
@"await browser.test.assertRejects(browser.tabs.executeScript(9999, { code: 'code to execute' }), /tab not found/i)",
@"browser.test.assertThrows(() => browser.tabs.executeScript({ code: 'code to execute', file: 'path/to/file.js' }), /it cannot specify both 'file' and 'code'./i)",
@"browser.test.assertThrows(() => browser.tabs.executeScript({ }), /it must specify either 'file' or 'code'./i)",
@"browser.test.assertThrows(() => browser.tabs.executeScript({ file: 0, frameId: 0 }), /'file' is expected to be a string, but a number was provided./i)",
@"browser.test.assertThrows(() => browser.tabs.executeScript({ code: 'code to execute', frameId: 0, allFrames: true }), /it cannot specify both 'allFrames' and 'frameId'./i)",
@"browser.test.assertThrows(() => browser.tabs.executeScript({ file: 'path/to/file.js', frameId: '0' }), /'frameId' is expected to be a number, but a string was provided./i)",
@"browser.test.assertThrows(() => browser.tabs.executeScript({ file: 'path/to/file.js', allFrames: 'true' }), /'allFrames' is expected to be a boolean, but a string was provided./i)",
@"browser.test.assertThrows(() => browser.tabs.executeScript({ file: 'path/to/file.js', frameId: -1 }), /'-1' is not a frame identifier./i)",
@"browser.test.assertThrows(() => browser.tabs.insertCSS(), /a required argument is missing./i)",
@"await browser.test.assertRejects(browser.tabs.insertCSS(9999, { code: 'code to execute' }), /tab not found/i)",
@"browser.test.assertThrows(() => browser.tabs.insertCSS({ code: 'code to execute', file: 'path/to/file.js' }), /it cannot specify both 'file' and 'code'./i)",
@"browser.test.assertThrows(() => browser.tabs.insertCSS({ }), /it must specify either 'file' or 'code'./i)",
@"browser.test.assertThrows(() => browser.tabs.insertCSS({ file: 'path/to/file.js', frameId: '0' }), /'frameId' is expected to be a number, but a string was provided./i)",
@"browser.test.assertThrows(() => browser.tabs.insertCSS({ code: 'code to execute', frameId: 0, allFrames: true }), /it cannot specify both 'allFrames' and 'frameId'./i)",
@"browser.test.assertThrows(() => browser.tabs.insertCSS({ file: 'path/to/file.js', frameId: '0' }), /'frameId' is expected to be a number, but a string was provided./i)",
@"browser.test.assertThrows(() => browser.tabs.insertCSS({ file: 'path/to/file.js', allFrames: 'true' }), /'allFrames' is expected to be a boolean, but a string was provided./i)",
@"browser.test.assertThrows(() => browser.tabs.insertCSS({ file: 'path/to/file.js', frameId: -1 }), /'-1' is not a frame identifier./i)",
@"browser.test.assertThrows(() => browser.tabs.insertCSS({ code: 'body { color: red }', cssOrigin: 'bad' }), /'cssOrigin' value is invalid, because it must specify either 'author' or 'user'/i);",
@"browser.test.assertThrows(() => browser.tabs.removeCSS(), /a required argument is missing./i)",
@"await browser.test.assertRejects(browser.tabs.removeCSS(9999, { code: 'code to execute' }), /tab not found/i)",
@"browser.test.assertThrows(() => browser.tabs.removeCSS({ code: 'code to execute', file: 'path/to/file.js' }), /it cannot specify both 'file' and 'code'./i)",
@"browser.test.assertThrows(() => browser.tabs.removeCSS({ }), /it must specify either 'file' or 'code'./i)",
@"browser.test.assertThrows(() => browser.tabs.removeCSS({ file: 0, frameId: 0 }), /'file' is expected to be a string, but a number was provided./i)",
@"browser.test.assertThrows(() => browser.tabs.removeCSS({ code: 'code to execute', frameId: 0, allFrames: true }), /it cannot specify both 'allFrames' and 'frameId'./i)",
@"browser.test.assertThrows(() => browser.tabs.removeCSS({ file: 'path/to/file.js', allFrames: 'true' }), /'allFrames' is expected to be a boolean, but a string was provided./i)",
@"browser.test.assertThrows(() => browser.tabs.removeCSS({ file: 'path/to/file.js', frameId: -1 }), /'-1' is not a frame identifier./i)",
@"browser.test.notifyPass()"
]);
Util::loadAndRunExtension(tabsManifestV2, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPITabs, Create)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const initialTabsCount = allWindows[0].tabs.length",
@"const newTab = await browser.tabs.create({})",
@"const updatedWindows = await browser.windows.getAll({ populate: true })",
@"const updatedTabs = updatedWindows[0].tabs",
@"browser.test.assertEq(updatedTabs.length, initialTabsCount + 1, 'A new tab should have been added')",
@"browser.test.assertEq(newTab.index, initialTabsCount, 'The new tab should be the last tab in the window')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *window = manager.get().defaultWindow;
auto originalOpenNewTab = manager.get().internalDelegate.openNewTab;
manager.get().internalDelegate.openNewTab = ^(WKWebExtensionTabConfiguration *configuration, WKWebExtensionContext *context, void (^completionHandler)(id<WKWebExtensionTab>, NSError *)) {
EXPECT_NS_EQUAL(configuration.window, window);
EXPECT_EQ(configuration.index, window.tabs.count);
EXPECT_NULL(configuration.parentTab);
EXPECT_NULL(configuration.url);
EXPECT_TRUE(configuration.shouldBeActive);
EXPECT_TRUE(configuration.shouldAddToSelection);
EXPECT_FALSE(configuration.shouldBePinned);
EXPECT_FALSE(configuration.shouldBeMuted);
EXPECT_FALSE(configuration.shouldReaderModeBeActive);
originalOpenNewTab(configuration, context, completionHandler);
};
[manager run];
}
TEST(WKWebExtensionAPITabs, CreateTabsOverflowIndex)
{
auto *backgroundScript = Util::constructScript(@[
@"var allWindows = await browser.windows.getAll({ populate: true })",
@"var initialTabsCount = allWindows[0].tabs.length",
@"var largeIndex = 13000000000000000000000000000000000",
@"var newTab = await browser.tabs.create({ index: largeIndex })",
@"var updatedWindows = await browser.windows.getAll({ populate: true })",
@"var updatedTabs = updatedWindows[0].tabs",
@"browser.test.assertEq(updatedTabs.length, initialTabsCount + 1, 'One new tab should have been added')",
@"browser.test.assertEq(newTab.index, initialTabsCount, 'The new tab should be posted at the end')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *window = manager.get().defaultWindow;
auto originalOpenNewTab = manager.get().internalDelegate.openNewTab;
manager.get().internalDelegate.openNewTab = ^(WKWebExtensionTabConfiguration *configuration, WKWebExtensionContext *context, void (^completionHandler)(id<WKWebExtensionTab>, NSError *)) {
EXPECT_NS_EQUAL(configuration.window, window);
EXPECT_EQ(configuration.index, JSC::maxSafeIntegerAsUInt64());
originalOpenNewTab(configuration, context, completionHandler);
};
[manager run];
}
TEST(WKWebExtensionAPITabs, CreateTabsZeroIndex)
{
auto *backgroundScript = Util::constructScript(@[
@"var allWindows = await browser.windows.getAll({ populate: true })",
@"var initialTabsCount = allWindows[0].tabs.length",
@"var newTab = await browser.tabs.create({ index: 0 })",
@"var updatedWindows = await browser.windows.getAll({ populate: true })",
@"var updatedTabs = updatedWindows[0].tabs",
@"browser.test.assertEq(updatedTabs.length, initialTabsCount + 1, 'One new tab should have been added')",
@"browser.test.assertEq(newTab.index, initialTabsCount - 1, 'The new tab should be posted at the start')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *window = manager.get().defaultWindow;
auto originalOpenNewTab = manager.get().internalDelegate.openNewTab;
manager.get().internalDelegate.openNewTab = ^(WKWebExtensionTabConfiguration *configuration, WKWebExtensionContext *context, void (^completionHandler)(id<WKWebExtensionTab>, NSError *)) {
EXPECT_NS_EQUAL(configuration.window, window);
EXPECT_EQ(configuration.index, (NSUInteger)0);
originalOpenNewTab(configuration, context, completionHandler);
};
[manager run];
}
TEST(WKWebExtensionAPITabs, CreateWithSpecifiedOptions)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const initialTabsCount = allWindows[0].tabs.length",
@"const newTab = await browser.tabs.create({",
@" url: 'https://example.com/',",
@" openerTabId: allWindows[0].tabs[0].id,",
@" active: false,",
@" pinned: true,",
@" muted: true,",
@" openInReaderMode: true,",
@" index: 1",
@"})",
@"const updatedWindows = await browser.windows.getAll({ populate: true })",
@"const updatedTabs = updatedWindows[0].tabs",
@"browser.test.assertEq(updatedTabs.length, initialTabsCount + 1, 'A new tab should have been added')",
@"browser.test.assertEq(newTab.index, 1, 'The new tab should be at index 1')",
@"browser.test.assertFalse(newTab.active, 'The new tab should not be active')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *window = manager.get().defaultWindow;
auto *tab = manager.get().defaultTab;
auto originalOpenNewTab = manager.get().internalDelegate.openNewTab;
manager.get().internalDelegate.openNewTab = ^(WKWebExtensionTabConfiguration *configuration, WKWebExtensionContext *context, void (^completionHandler)(id<WKWebExtensionTab>, NSError *)) {
EXPECT_NS_EQUAL(configuration.window, window);
EXPECT_EQ(configuration.index, 1lu);
EXPECT_NS_EQUAL(configuration.parentTab, tab);
EXPECT_NS_EQUAL(configuration.url, [NSURL URLWithString:@"https://example.com/"]);
EXPECT_FALSE(configuration.shouldBeActive);
EXPECT_FALSE(configuration.shouldAddToSelection);
EXPECT_TRUE(configuration.shouldBePinned);
EXPECT_TRUE(configuration.shouldBeMuted);
EXPECT_TRUE(configuration.shouldReaderModeBeActive);
originalOpenNewTab(configuration, context, completionHandler);
};
[manager run];
}
TEST(WKWebExtensionAPITabs, CreateWithRelativeURL)
{
auto *backgroundScript = Util::constructScript(@[
@"const newTab = await browser.tabs.create({",
@" url: 'test.html'",
@"})",
@"browser.test.assertEq(newTab.url, browser.runtime.getURL('test.html'), 'The new tab should have the correct URL')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript, @"test.html": @"Hello world!" });
auto originalOpenNewTab = manager.get().internalDelegate.openNewTab;
manager.get().internalDelegate.openNewTab = ^(WKWebExtensionTabConfiguration *configuration, WKWebExtensionContext *context, void (^completionHandler)(id<WKWebExtensionTab>, NSError *)) {
EXPECT_NS_EQUAL(configuration.url, [NSURL URLWithString:@"test.html" relativeToURL:manager.get().context.baseURL].absoluteURL);
originalOpenNewTab(configuration, context, completionHandler);
};
[manager run];
}
TEST(WKWebExtensionAPITabs, Duplicate)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const initialTabsCount = allWindows[0].tabs.length",
@"const duplicatedTab = await browser.tabs.duplicate(allWindows[0].tabs[0].id)",
@"const updatedWindows = await browser.windows.getAll({ populate: true })",
@"const updatedTabs = updatedWindows[0].tabs",
@"browser.test.assertEq(updatedTabs.length, initialTabsCount + 1, 'A tab should have been duplicated')",
@"browser.test.assertEq(duplicatedTab.index, initialTabsCount, 'The duplicated tab should be the last tab in the window')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *window = manager.get().defaultWindow;
auto *tab = manager.get().defaultTab;
auto originalDuplicate = tab.duplicate;
tab.duplicate = ^(WKWebExtensionTabConfiguration *configuration, void (^completionHandler)(TestWebExtensionTab *, NSError *)) {
EXPECT_NS_EQUAL(configuration.window, window);
EXPECT_EQ(configuration.index, window.tabs.count);
EXPECT_NULL(configuration.parentTab);
EXPECT_NULL(configuration.url);
EXPECT_TRUE(configuration.shouldBeActive);
EXPECT_TRUE(configuration.shouldAddToSelection);
EXPECT_FALSE(configuration.shouldBePinned);
EXPECT_FALSE(configuration.shouldBeMuted);
EXPECT_FALSE(configuration.shouldReaderModeBeActive);
originalDuplicate(configuration, completionHandler);
};
[manager run];
}
TEST(WKWebExtensionAPITabs, DuplicateWithOptions)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const initialTabsCount = allWindows[0].tabs.length",
@"const duplicatedTab = await browser.tabs.duplicate(allWindows[0].tabs[0].id, {",
@" active: false,",
@" index: 1",
@"})",
@"const updatedWindows = await browser.windows.getAll({ populate: true })",
@"const updatedTabs = updatedWindows[0].tabs",
@"browser.test.assertEq(updatedTabs.length, initialTabsCount + 1, 'A tab should have been duplicated')",
@"browser.test.assertEq(duplicatedTab.index, 1, 'The duplicated tab should be at index 1')",
@"browser.test.assertFalse(duplicatedTab.active, 'The duplicated tab should not be active')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *window = manager.get().defaultWindow;
auto *tab = manager.get().defaultTab;
auto originalDuplicate = tab.duplicate;
tab.duplicate = ^(WKWebExtensionTabConfiguration *configuration, void (^completionHandler)(TestWebExtensionTab *, NSError *)) {
EXPECT_NS_EQUAL(configuration.window, window);
EXPECT_EQ(configuration.index, 1lu);
EXPECT_NULL(configuration.parentTab);
EXPECT_NULL(configuration.url);
EXPECT_FALSE(configuration.shouldBeActive);
EXPECT_FALSE(configuration.shouldAddToSelection);
EXPECT_FALSE(configuration.shouldBePinned);
EXPECT_FALSE(configuration.shouldBeMuted);
EXPECT_FALSE(configuration.shouldReaderModeBeActive);
originalDuplicate(configuration, completionHandler);
};
[manager run];
}
TEST(WKWebExtensionAPITabs, Update)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const secondTab = allWindows[0].tabs[1]",
@"browser.test.assertFalse(secondTab.active, 'The tab should not be initially active')",
@"browser.test.assertFalse(secondTab.highlighted, 'The tab should not be initially highlighted')",
@"browser.test.assertFalse(secondTab.pinned, 'The tab should not be initially pinned')",
@"browser.test.assertFalse(secondTab.mutedInfo.muted, 'The tab should not be initially muted')",
@"browser.test.assertEq(secondTab.openerTabId, undefined, 'The initial tab should not have an openerTabId')",
@"const updatedTab = await browser.tabs.update(secondTab.id, {",
@" active: true,",
@" highlighted: true,",
@" pinned: true,",
@" muted: true,",
@" openerTabId: allWindows[0].tabs[0].id",
@"})",
@"browser.test.assertTrue(updatedTab.active, 'The tab should be active after update')",
@"browser.test.assertTrue(updatedTab.highlighted, 'The tab should be highlighted after update')",
@"browser.test.assertTrue(updatedTab.pinned, 'The tab should be pinned after update')",
@"browser.test.assertTrue(updatedTab.mutedInfo.muted, 'The tab should be muted after update')",
@"browser.test.assertEq(updatedTab.openerTabId, allWindows[0].tabs[0].id, 'The openerTabId should match the first tab after update')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, UpdateWithoutTabId)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const activeTab = allWindows[0].tabs[0]",
@"browser.test.assertFalse(activeTab.mutedInfo.muted, 'The tab should not be initially muted')",
@"const updatedTab = await browser.tabs.update({",
@" muted: true,",
@"})",
@"browser.test.assertTrue(updatedTab.mutedInfo.muted, 'The tab should be muted after update')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 1lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, Get)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const windowId = allWindows[0].id",
@"const tabId = allWindows[0].tabs[0].id",
@"const tab = await browser.tabs.get(tabId)",
@"browser.test.assertEq(typeof tab, 'object', 'The tab should be an object')",
@"browser.test.assertEq(tab.id, tabId, 'The tab id should match the one used with tabs.get()')",
@"browser.test.assertEq(tab.windowId, windowId, 'The tab windowId should match the window id from windows.getAll()')",
@"browser.test.assertEq(tab.index, 0, 'The tab index should be 0')",
@"browser.test.assertEq(tab.status, 'complete', 'The tab status should be complete')",
@"browser.test.assertTrue(tab.active, 'The tab active state should be true')",
@"browser.test.assertTrue(tab.selected, 'The tab selected state should be true')",
@"browser.test.assertTrue(tab.highlighted, 'The tab highlighted state should be true')",
@"browser.test.assertFalse(tab.incognito, 'The tab incognito state should be false')",
@"browser.test.assertEq(tab.url, '', 'The tab url should be an empty string')",
@"browser.test.assertEq(tab.title, '', 'The tab title should be an empty string')",
@"browser.test.assertEq(tab.width, 800, 'The tab width should be 800')",
@"browser.test.assertEq(tab.height, 600, 'The tab height should be 600')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, GetCurrentFromBackgroundPage)
{
auto *backgroundScript = Util::constructScript(@[
@"const [currentTab] = await browser.tabs.query({ active: true, currentWindow: true })",
@"const tab = await browser.tabs.getCurrent()",
@"browser.test.assertDeepEq(tab, currentTab, 'The current tab should be')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, GetCurrentFromOptionsPage)
{
auto *optionsScript = Util::constructScript(@[
@"const tab = await browser.tabs.getCurrent()",
@"browser.test.assertEq(typeof tab, 'object', 'The tab should be')",
@"browser.test.assertTrue(tab.active, 'The current tab should be active')",
@"browser.test.notifyPass()"
]);
auto *resources = @{
@"background.js": @"// Not Used",
@"options.html": @"<script type='module' src='options.js'></script>",
@"options.js": optionsScript
};
auto manager = Util::loadExtension(tabsManifest, resources);
[manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2lu);
auto *optionsPageURL = manager.get().context.optionsPageURL;
EXPECT_NOT_NULL(optionsPageURL);
auto *defaultTab = manager.get().defaultTab;
EXPECT_NOT_NULL(defaultTab);
[defaultTab changeWebViewIfNeededForURL:optionsPageURL forExtensionContext:manager.get().context];
[defaultTab.webView loadRequest:[NSURLRequest requestWithURL:optionsPageURL]];
[manager run];
}
TEST(WKWebExtensionAPITabs, GetSelected)
{
auto *backgroundScript = Util::constructScript(@[
@"const [selectedTab] = await browser.tabs.query({ active: true, currentWindow: true })",
@"const tab = await browser.tabs.getSelected()",
@"browser.test.assertDeepEq(tab, selectedTab, 'The selected tab should match the active tab in the current window')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifestV2, @{ @"background.js": backgroundScript });
[manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, Query)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"browser.test.assertEq(allWindows?.length, 2, 'There should be 2 windows')",
@"const windowIdOne = allWindows?.[0]?.id",
@"const windowIdTwo = allWindows?.[1]?.id",
@"const tabIdOne = allWindows?.[0]?.tabs?.[0]?.id",
@"const tabIdTwo = allWindows?.[0]?.tabs?.[1]?.id",
@"const tabIdThree = allWindows?.[1]?.tabs?.[0]?.id",
@"const tabIdFour = allWindows?.[1]?.tabs?.[1]?.id",
@"const tabIdFive = allWindows?.[1]?.tabs?.[2]?.id",
@"const tabsInWindowOne = await browser.tabs.query({ windowId: windowIdOne })",
@"const tabsInWindowTwo = await browser.tabs.query({ windowId: windowIdTwo })",
@"browser.test.assertEq(tabsInWindowOne?.length, 2, 'There should be 2 tabs in the first window')",
@"browser.test.assertEq(tabsInWindowTwo?.length, 3, 'There should be 3 tabs in the second window')",
@"const thirdTab = await browser.tabs.query({ index: 0, windowId: windowIdTwo })",
@"browser.test.assertEq(thirdTab?.[0]?.id, tabIdThree, 'Third tab ID should match the first tab of the second window')",
@"const activeTabs = await browser.tabs.query({ active: true })",
@"browser.test.assertEq(activeTabs?.length, 2, 'There should be 2 active tabs across all windows')",
@"const hiddenTabs = await browser.tabs.query({ hidden: true })",
@"browser.test.assertEq(hiddenTabs?.length, 3, 'There should be 3 hidden tabs across all windows')",
@"const lastFocusedTabs = await browser.tabs.query({ lastFocusedWindow: true })",
@"browser.test.assertEq(lastFocusedTabs?.length, 2, 'There should be 2 tabs in the last focused window')",
@"const pinnedTabs = await browser.tabs.query({ pinned: true })",
@"browser.test.assertEq(pinnedTabs?.length, 0, 'There should be no pinned tabs')",
@"const loadingTabs = await browser.tabs.query({ status: 'loading' })",
@"browser.test.assertEq(loadingTabs?.length, 0, 'There should be no tabs loading')",
@"const completeTabs = await browser.tabs.query({ status: 'complete' })",
@"browser.test.assertEq(completeTabs?.length, 5, 'There should be 5 tabs with loading complete')",
@"browser.test.notifyPass()"
]);
auto manager = Util::parseExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *windowOne = manager.get().defaultWindow;
[windowOne openNewTab];
auto *windowTwo = [manager openNewWindow];
[windowTwo openNewTab];
[windowTwo openNewTab];
auto *windowThree = [manager openNewWindowUsingPrivateBrowsing:YES];
[windowThree openNewTab];
EXPECT_EQ(manager.get().windows.count, 3lu);
EXPECT_EQ(windowOne.tabs.count, 2lu);
EXPECT_EQ(windowTwo.tabs.count, 3lu);
EXPECT_EQ(windowThree.tabs.count, 2lu);
[manager loadAndRun];
}
TEST(WKWebExtensionAPITabs, QueryWithPrivateAccess)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const normalWindows = allWindows.filter(window => !window.incognito)",
@"const incognitoWindows = allWindows.filter(window => window.incognito)",
@"const windowIdOne = normalWindows?.[0]?.id",
@"const windowIdTwo = normalWindows?.[1]?.id",
@"const incognitoWindowId = incognitoWindows?.[0]?.id",
@"const tabIdOne = normalWindows?.[0]?.tabs?.[0]?.id",
@"const tabIdTwo = normalWindows?.[0]?.tabs?.[1]?.id",
@"const tabIdThree = normalWindows?.[1]?.tabs?.[0]?.id",
@"const tabIdFour = normalWindows?.[1]?.tabs?.[1]?.id",
@"const tabIdFive = normalWindows?.[1]?.tabs?.[2]?.id",
@"const incognitoTabIdOne = incognitoWindows?.[0]?.tabs?.[0]?.id",
@"const incognitoTabIdTwo = incognitoWindows?.[0]?.tabs?.[1]?.id",
@"const tabsInWindowOne = await browser.tabs.query({ windowId: windowIdOne })",
@"const tabsInWindowTwo = await browser.tabs.query({ windowId: windowIdTwo })",
@"const tabsInIncognitoWindow = await browser.tabs.query({ windowId: incognitoWindowId })",
@"browser.test.assertEq(tabsInWindowOne?.length, 2, 'There should be 2 tabs in the first normal window')",
@"browser.test.assertEq(tabsInWindowTwo?.length, 3, 'There should be 3 tabs in the second normal window')",
@"browser.test.assertEq(tabsInIncognitoWindow?.length, 2, 'There should be 2 tabs in the incognito window')",
@"const thirdTab = await browser.tabs.query({ index: 0, windowId: windowIdTwo })",
@"browser.test.assertEq(thirdTab?.[0]?.id, tabIdThree, 'Third tab ID should match the first tab of the second window')",
@"const activeTabs = await browser.tabs.query({ active: true })",
@"browser.test.assertEq(activeTabs?.length, 3, 'There should be 3 active tabs across all windows')",
@"const hiddenTabs = await browser.tabs.query({ hidden: true })",
@"browser.test.assertEq(hiddenTabs?.length, 4, 'There should be 4 hidden tabs across all windows')",
@"const lastFocusedTabs = await browser.tabs.query({ lastFocusedWindow: true })",
@"browser.test.assertEq(lastFocusedTabs?.length, 2, 'There should be 2 tabs in the last focused window')",
@"const pinnedTabs = await browser.tabs.query({ pinned: true })",
@"browser.test.assertEq(pinnedTabs?.length, 0, 'There should be no pinned tabs')",
@"const loadingTabs = await browser.tabs.query({ status: 'loading' })",
@"browser.test.assertEq(loadingTabs?.length, 0, 'There should be no tabs loading')",
@"const completeTabs = await browser.tabs.query({ status: 'complete' })",
@"browser.test.assertEq(completeTabs?.length, 7, 'There should be 7 tabs with loading complete')",
@"browser.test.notifyPass()"
]);
auto manager = Util::parseExtension(tabsManifest, @{ @"background.js": backgroundScript });
manager.get().context.hasAccessToPrivateData = YES;
auto *windowOne = manager.get().defaultWindow;
[windowOne openNewTab];
auto *windowTwo = [manager openNewWindow];
[windowTwo openNewTab];
[windowTwo openNewTab];
auto *windowThree = [manager openNewWindowUsingPrivateBrowsing:YES];
[windowThree openNewTab];
EXPECT_EQ(manager.get().windows.count, 3lu);
EXPECT_EQ(windowOne.tabs.count, 2lu);
EXPECT_EQ(windowTwo.tabs.count, 3lu);
EXPECT_EQ(windowThree.tabs.count, 2lu);
[manager loadAndRun];
}
TEST(WKWebExtensionAPITabs, QueryWithAccessPrompt)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const windowIdOne = allWindows?.[0]?.id",
@"const tabIdOne = allWindows?.[0]?.tabs?.[0]?.id",
@"const tabIdTwo = allWindows?.[0]?.tabs?.[1]?.id",
@"browser.test.assertTrue(allWindows?.[0]?.tabs?.[0]?.url.startsWith('http://localhost'), 'First tab URL should be localhost')",
@"browser.test.assertEq(allWindows?.[0]?.tabs?.[1]?.url, '', 'Second tab URL should be empty')",
@"const tabsInWindowOne = await browser.tabs.query({ windowId: windowIdOne })",
@"browser.test.assertTrue(tabsInWindowOne.length >= 2, 'There should be at least 2 tabs in window one')",
@"browser.test.assertTrue(tabsInWindowOne[0].url.startsWith('http://localhost'), 'First tab URL should be localhost')",
@"browser.test.assertEq(tabsInWindowOne[1].url, '', 'Second tab URL should be empty')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forPermission:WKWebExtensionPermissionTabs];
__block bool firstPermissionRequest = true;
auto *localhostURL = server.requestWithLocalhost().URL;
auto *loopbackURL = server.request().URL;
manager.get().internalDelegate.promptForPermissionToAccessURLs = ^(id<WKWebExtensionTab>, NSSet<NSURL *> *requestedURLs, void (^completionHandler)(NSSet<NSURL *> *allowedURLs, NSDate *)) {
if (firstPermissionRequest) {
EXPECT_TRUE([requestedURLs containsObject:localhostURL]);
EXPECT_TRUE([requestedURLs containsObject:loopbackURL]);
// Only approve localhost for the first request.
dispatch_async(mainDispatchQueueSingleton(), ^{
completionHandler([NSSet setWithObject:localhostURL], nil);
});
firstPermissionRequest = false;
} else {
// The second request should only have loopbackURL, as localhostURL was granted previously.
EXPECT_FALSE([requestedURLs containsObject:localhostURL]);
EXPECT_TRUE([requestedURLs containsObject:loopbackURL]);
// Approve nothing for the second request.
dispatch_async(mainDispatchQueueSingleton(), ^{
completionHandler(NSSet.set, nil);
});
}
};
auto *windowOne = manager.get().defaultWindow;
[windowOne openNewTab];
[windowOne.tabs.firstObject.webView loadRequest:server.requestWithLocalhost()];
[windowOne.tabs.lastObject.webView loadRequest:server.request()];
EXPECT_EQ(manager.get().windows.count, 1lu);
EXPECT_EQ(windowOne.tabs.count, 2lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, QueryWithCurrentWindow)
{
auto *backgroundScript = Util::constructScript(@[
@"const tabs = await browser.tabs.query({ windowId: browser.windows.WINDOW_ID_CURRENT })",
@"browser.test.assertEq(tabs.length, 2, 'Should return exactly two tabs for the current window')",
@"browser.test.assertEq(typeof tabs[0].id, 'number', 'The first tab should have a valid id')",
@"browser.test.assertEq(typeof tabs[0].windowId, 'number', 'The first tab should have a valid windowId')",
@"browser.test.assertEq(typeof tabs[1].id, 'number', 'The second tab should have a valid id')",
@"browser.test.assertEq(typeof tabs[1].windowId, 'number', 'The second tab should have a valid windowId')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, QueryWithPermissionBypass)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const windowIdOne = allWindows?.[0]?.id",
@"const tabIdOne = allWindows?.[0]?.tabs?.[0]?.id",
@"const tabIdTwo = allWindows?.[0]?.tabs?.[1]?.id",
@"browser.test.assertTrue(allWindows?.[0]?.tabs?.[0]?.url.startsWith('http://localhost'), 'First tab URL should be localhost')",
@"browser.test.assertEq(allWindows?.[0]?.tabs?.[1]?.url, '', 'Second tab URL should be empty')",
@"const tabsInWindowOne = await browser.tabs.query({ windowId: windowIdOne })",
@"browser.test.assertTrue(tabsInWindowOne.length >= 2, 'There should be at least 2 tabs in window one')",
@"browser.test.assertTrue(tabsInWindowOne[0].url.startsWith('http://localhost'), 'First tab URL should be localhost')",
@"browser.test.assertEq(tabsInWindowOne[1].url, '', 'Second tab URL should be empty')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forPermission:WKWebExtensionPermissionTabs];
manager.get().internalDelegate.promptForPermissionToAccessURLs = ^(id<WKWebExtensionTab>, NSSet<NSURL *> *requestedURLs, void (^completionHandler)(NSSet<NSURL *> *allowedURLs, NSDate *)) {
dispatch_async(mainDispatchQueueSingleton(), ^{
completionHandler(NSSet.set, nil);
});
};
auto *windowOne = manager.get().defaultWindow;
[windowOne openNewTab];
windowOne.tabs.firstObject.shouldBypassPermissions = YES;
[windowOne.tabs.firstObject.webView loadRequest:server.requestWithLocalhost()];
[windowOne.tabs.lastObject.webView loadRequest:server.request()];
EXPECT_EQ(manager.get().windows.count, 1lu);
EXPECT_EQ(windowOne.tabs.count, 2lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, Zoom)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const windowId = allWindows[0].id",
@"const tabId = allWindows[0].tabs[0].id",
@"await browser.tabs.setZoom(tabId, 1.5)",
@"const zoomLevel = await browser.tabs.getZoom(tabId)",
@"browser.test.assertEq(typeof zoomLevel, 'number', 'The zoomLevel should be a number')",
@"browser.test.assertEq(zoomLevel, 1.5, 'The tab zoom level should be 1.5')",
@"browser.test.notifyPass()"
]);
Util::loadAndRunExtension(tabsManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPITabs, ToggleReaderMode)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const tabId = allWindows[0].tabs[0].id",
@"await browser.tabs.toggleReaderMode(tabId)",
@"let tab = await browser.tabs.get(tabId)",
@"browser.test.assertTrue(tab.isInReaderMode, 'The tab should be in reader mode after toggling')",
@"await browser.tabs.toggleReaderMode(tabId)",
@"tab = await browser.tabs.get(tabId)",
@"browser.test.assertFalse(tab.isInReaderMode, 'The tab should not be in reader mode after toggling again')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
__block size_t toggleReaderModeCounter = 0;
manager.get().defaultTab.setReaderModeShowing = ^(BOOL showing) {
++toggleReaderModeCounter;
};
[manager run];
ASSERT_EQ(toggleReaderModeCounter, 2lu);
}
TEST(WKWebExtensionAPITabs, DetectLanguage)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const tabId = allWindows[0].tabs[0].id",
@"const detectedLanguage = await browser.tabs.detectLanguage(tabId)",
@"browser.test.assertEq(detectedLanguage, 'en-US', 'The detected language should be English')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
__block bool detectWebpageLocaleCalled = false;
manager.get().defaultTab.webpageLocale = ^{
detectWebpageLocaleCalled = true;
return [NSLocale localeWithLocaleIdentifier:@"en-US"];
};
[manager run];
ASSERT_TRUE(detectWebpageLocaleCalled);
}
TEST(WKWebExtensionAPITabs, CaptureVisibleTab)
{
auto *backgroundScript = Util::constructScript(@[
@"const newTab = await browser.tabs.create({ url: 'http://example.com' })",
@"let dataURLPNG = await browser.tabs.captureVisibleTab(newTab.windowId, { format: 'png' })",
@"browser.test.assertTrue(dataURLPNG.startsWith('data:image/png;'), 'The data URL should start with the PNG mime type for PNG capture')",
@"let dataURLJPEG = await browser.tabs.captureVisibleTab(newTab.windowId, { format: 'jpeg', quality: 90 })",
@"browser.test.assertTrue(dataURLJPEG.startsWith('data:image/jpeg;'), 'The data URL should start with the JPEG mime type for JPEG capture')",
@"browser.test.notifyPass()",
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:[NSURL URLWithString:@"http://example.com/"]];
[manager run];
}
TEST(WKWebExtensionAPITabs, Reload)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const tabId = allWindows[0].tabs[0].id",
@"await browser.tabs.reload(tabId)",
@"await browser.tabs.reload(tabId, { bypassCache: true })",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
__block bool reloadCalled = false;
__block bool reloadFromOriginCalled = false;
manager.get().defaultTab.reload = ^(BOOL fromOrigin) {
if (fromOrigin)
reloadFromOriginCalled = true;
else
reloadCalled = true;
};
[manager run];
ASSERT_TRUE(reloadCalled);
ASSERT_TRUE(reloadFromOriginCalled);
}
TEST(WKWebExtensionAPITabs, GoBack)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const tabId = allWindows[0].tabs[0].id",
@"await browser.tabs.goBack(tabId)",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
__block bool goBackCalled = false;
manager.get().defaultTab.goBack = ^{
goBackCalled = true;
};
[manager run];
ASSERT_TRUE(goBackCalled);
}
TEST(WKWebExtensionAPITabs, GoForward)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const tabId = allWindows[0].tabs[0].id",
@"await browser.tabs.goForward(tabId)",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
__block bool goForwardCalled = false;
manager.get().defaultTab.goForward = ^{
goForwardCalled = true;
};
[manager run];
ASSERT_TRUE(goForwardCalled);
}
TEST(WKWebExtensionAPITabs, Remove)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const tabIdToRemove = allWindows[0].tabs[1].id",
@"await browser.tabs.remove(tabIdToRemove)",
@"const updatedAllWindows = await browser.windows.getAll({ populate: true })",
@"const remainingTabs = updatedAllWindows[0].tabs.map(tab => tab.id)",
@"browser.test.assertFalse(remainingTabs.includes(tabIdToRemove), 'The tab should be removed')",
@"await browser.test.assertRejects(browser.tabs.get(tabIdToRemove), /tab not found/i, 'The removed tab should not be retrievable with tabs.get()')",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, RemoveMultipleTabs)
{
auto *backgroundScript = Util::constructScript(@[
@"const allWindows = await browser.windows.getAll({ populate: true })",
@"const tabIdsToRemove = [allWindows[0].tabs[1].id, allWindows[0].tabs[2].id]",
@"await browser.tabs.remove(tabIdsToRemove)",
@"const updatedAllWindows = await browser.windows.getAll({ populate: true })",
@"const remainingTabs = updatedAllWindows[0].tabs.map(tab => tab.id)",
@"for (let id of tabIdsToRemove) {",
@" browser.test.assertFalse(remainingTabs.includes(id), 'Removed tabs should not be retrievable')",
@"}",
@"browser.test.notifyPass()"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager.get().defaultWindow openNewTab];
[manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 3lu);
[manager run];
}
TEST(WKWebExtensionAPITabs, CreatedEvent)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.tabs.onCreated.addListener((newTab) => {",
@" browser.test.assertFalse(newTab.active, 'The new tab should not be active')",
@" browser.test.assertFalse(newTab.pinned, 'The new tab should not be pinned')",
@" browser.test.assertEq(newTab.index, 1, 'The new tab index should be 1')",
@" browser.test.assertEq(newTab.openerTabId, undefined, 'The new tab should not have an openerTabId by default')",
@" browser.test.notifyPass()",
@"})",
@"browser.tabs.create({})",
]);
Util::loadAndRunExtension(tabsManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPITabs, UpdatedEvent)
{
auto *backgroundScript = Util::constructScript(@[
@"const newTab = await browser.tabs.create({ active: false, muted: false, pinned: false })",
@"let mutedUpdated = false",
@"let pinnedUpdated = false",
@"const verifyUpdates = () => {",
@" if (mutedUpdated && pinnedUpdated)",
@" browser.test.notifyPass()",
@"}",
@"browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {",
@" if ('mutedInfo' in changeInfo) {",
@" browser.test.assertEq(tabId, newTab.id, 'The updated tab should have the correct id for muted change')",
@" browser.test.assertTrue(changeInfo.mutedInfo.muted, 'The tab should be muted')",
@" browser.test.assertDeepEq(changeInfo.mutedInfo, tab.mutedInfo, 'The mutedInfo in changeInfo should match the mutedInfo of the tab')",
@" mutedUpdated = true",
@" }",
@" if ('pinned' in changeInfo) {",
@" browser.test.assertEq(tabId, newTab.id, 'The updated tab should have the correct id for pinned change')",
@" browser.test.assertTrue(changeInfo.pinned, 'The tab should be pinned')",
@" browser.test.assertEq(changeInfo.pinned, tab.pinned, 'The pinned status in changeInfo should match the pinned status of the tab')",
@" pinnedUpdated = true",
@" }",
@" verifyUpdates()",
@"})",
@"browser.tabs.update(newTab.id, { muted: true, pinned: true })"
]);
Util::loadAndRunExtension(tabsManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPITabs, UpdatedEventWithoutPrivateAccess)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {",
@" browser.test.notifyFail('tabs.onUpdated should not fire for private tabs when no permission is granted.')",
@"})",
@"setTimeout(() => browser.test.notifyPass(), 2000)",
@"browser.test.sendMessage('Load Tab')"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Load Tab"];
auto *privateWindow = [manager openNewWindowUsingPrivateBrowsing:YES];
[privateWindow.activeTab.webView loadRequest:server.requestWithLocalhost()];
[manager run];
}
TEST(WKWebExtensionAPITabs, UpdatedEventWithPrivateAccess)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {",
@" browser.test.notifyPass()",
@"})",
@"setTimeout(() => browser.test.notifyFail('tabs.onUpdated did not fire for private tab.'), 2000)",
@"browser.test.sendMessage('Load Tab')"
]);
auto manager = Util::parseExtension(tabsManifest, @{ @"background.js": backgroundScript });
manager.get().context.hasAccessToPrivateData = YES;
[manager load];
[manager runUntilTestMessage:@"Load Tab"];
auto *privateWindow = [manager openNewWindowUsingPrivateBrowsing:YES];
[privateWindow.activeTab.webView loadRequest:server.requestWithLocalhost()];
[manager run];
}
TEST(WKWebExtensionAPITabs, RemovedEvent)
{
auto *backgroundScript = Util::constructScript(@[
@"const newTab = await browser.tabs.create({})",
@"browser.tabs.onRemoved.addListener((tabId, removeInfo) => {",
@" browser.test.assertEq(tabId, newTab.id, 'The removed tab should have the correct id')",
@" browser.test.assertFalse(removeInfo.isWindowClosing, 'The window should not be closing')",
@" browser.test.notifyPass()",
@"})",
@"browser.tabs.remove(newTab.id)"
]);
Util::loadAndRunExtension(tabsManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPITabs, ReplacedEvent)
{
auto backgroundScript = Util::constructScript(@[
@"let createdTabId = null",
@"let removedTabId = null",
@"browser.tabs.onCreated.addListener((tab) => {",
@" browser.test.assertEq(createdTabId, null, 'No tab should be created yet')",
@" createdTabId = tab.id",
@"})",
@"browser.tabs.onRemoved.addListener((tabId) => {",
@" browser.test.assertEq(removedTabId, null, 'No tab should be removed yet')",
@" removedTabId = tabId",
@"})",
@"browser.tabs.onReplaced.addListener((addedTabId, removedTabId) => {",
@" browser.test.assertTrue(addedTabId !== removedTabId, 'The added tab should not match the removed tab')",
@" browser.test.assertEq(addedTabId, createdTabId, 'The added tab should match the created tab')",
@" browser.test.assertEq(removedTabId, initialTabId, 'The removed tab should match the initial tab')",
@" browser.test.notifyPass()",
@"})",
@"const allTabs = await browser.tabs.query({windowId: browser.windows.WINDOW_ID_CURRENT})",
@"const initialTabId = allTabs[0].id",
@"browser.test.sendMessage('Replace Tab')"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Replace Tab"];
auto initialTab = manager.get().defaultWindow.tabs.firstObject;
auto newTab = adoptNS([[TestWebExtensionTab alloc] initWithWindow:manager.get().defaultWindow extensionController:manager.get().controller]);
[manager.get().defaultWindow replaceTab:initialTab withTab:newTab.get()];
[manager run];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 1lu);
}
TEST(WKWebExtensionAPITabs, MovedEvent)
{
auto backgroundScript = Util::constructScript(@[
@"browser.tabs.onMoved.addListener((tabId, moveInfo) => {",
@" browser.test.assertEq(tabId, movedTabId, 'Moved tab id should match the provided tab id')",
@" browser.test.assertEq(moveInfo.fromIndex, 1, 'Tab should have been moved from index 1')",
@" browser.test.assertEq(moveInfo.toIndex, 2, 'Tab should have been moved to index 2')",
@" browser.test.notifyPass()",
@"})",
@"const allTabs = await browser.tabs.query({ currentWindow: true })",
@"const movedTabId = allTabs[1].id",
@"browser.test.sendMessage('Move Tab')"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *tabToMove = [manager.get().defaultWindow openNewTab];
[manager.get().defaultWindow openNewTab];
[manager runUntilTestMessage:@"Move Tab"];
[manager.get().defaultWindow moveTab:tabToMove toIndex:2];
[manager run];
}
TEST(WKWebExtensionAPITabs, DetachedAndAttachedEvent)
{
auto backgroundScript = Util::constructScript(@[
@"let detachedTabId",
@"const currentWindow = await browser.windows.getCurrent()",
@"const currentWindowId = currentWindow.id",
@"const allWindows = await browser.windows.getAll()",
@"const newWindow = allWindows.find(window => window.id !== currentWindowId)",
@"const newWindowId = newWindow.id",
@"browser.tabs.onDetached.addListener((tabId, detachInfo) => {",
@" detachedTabId = tabId",
@" browser.test.assertEq(detachInfo.oldWindowId, currentWindowId, 'Tab should have been detached from the current window')",
@" browser.test.assertEq(detachInfo.oldPosition, 1, 'Tab should have been detached from index 1')",
@"})",
@"browser.tabs.onAttached.addListener((tabId, attachInfo) => {",
@" browser.test.assertEq(detachedTabId, tabId, 'The detached and attached tab ids should match')",
@" browser.test.assertEq(attachInfo.newWindowId, newWindowId, 'Tab should have been attached to the new window')",
@" browser.test.assertEq(attachInfo.newPosition, 0, 'Tab should have been attached at index 0')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Move Tab')"
]);
auto manager = Util::parseExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *tabToMove = [manager.get().defaultWindow openNewTab];
auto *secondWindow = [manager openNewWindow];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2ul);
EXPECT_EQ(secondWindow.tabs.count, 1ul);
[manager load];
[manager runUntilTestMessage:@"Move Tab"];
[secondWindow moveTab:tabToMove toIndex:0];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 1ul);
EXPECT_EQ(secondWindow.tabs.count, 2ul);
[manager run];
}
TEST(WKWebExtensionAPITabs, DetachAndAttachToWindowIDNone)
{
auto backgroundScript = Util::constructScript(@[
@"let detachedTabId",
@"const currentWindow = await browser.windows.getCurrent()",
@"const currentWindowId = currentWindow.id",
@"browser.tabs.onDetached.addListener((tabId, detachInfo) => {",
@" detachedTabId = tabId",
@" browser.test.assertEq(detachInfo.oldWindowId, currentWindowId, 'Tab should have been detached from the current window')",
@" browser.test.assertEq(detachInfo.oldPosition, 1, 'Tab should have been detached from index 1')",
@"})",
@"browser.tabs.onAttached.addListener((tabId, attachInfo) => {",
@" browser.test.assertEq(detachedTabId, tabId, 'The detached and attached tab ids should match')",
@" browser.test.assertEq(attachInfo.newWindowId, browser.windows.WINDOW_ID_NONE, 'Tab should have been attached to WINDOW_ID_NONE')",
@" browser.test.assertTrue(isNaN(attachInfo.newPosition), 'Tab should have been attached at index NaN')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Detach Tab')"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *tabToMove = [manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2ul);
[manager runUntilTestMessage:@"Detach Tab"];
tabToMove.window = nil;
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 1ul);
[manager run];
}
TEST(WKWebExtensionAPITabs, DetachAndAttachFromWindowIDNone)
{
auto backgroundScript = Util::constructScript(@[
@"let detachedTabId",
@"const currentWindow = await browser.windows.getCurrent()",
@"const currentWindowId = currentWindow.id",
@"browser.tabs.onDetached.addListener((tabId, detachInfo) => {",
@" detachedTabId = tabId",
@" browser.test.assertEq(detachInfo.oldWindowId, browser.windows.WINDOW_ID_NONE, 'Tab should have been detached from WINDOW_ID_NONE')",
@" browser.test.assertTrue(isNaN(detachInfo.oldPosition), 'Tab should have been detached from index NaN')",
@"})",
@"browser.tabs.onAttached.addListener((tabId, attachInfo) => {",
@" browser.test.assertEq(detachedTabId, tabId, 'The detached and attached tab ids should match')",
@" browser.test.assertEq(attachInfo.newWindowId, currentWindowId, 'Tab should have been attached to the current window')",
@" browser.test.assertEq(attachInfo.newPosition, 1, 'Tab should have been attached to index 1')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Attach Tab')"
]);
auto manager = Util::loadExtension(tabsManifest, @{ @"background.js": backgroundScript });
auto *tabToMove = [manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2ul);
// Detach before loading the extension.
tabToMove.window = nil;
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 1ul);
[manager runUntilTestMessage:@"Attach Tab"];
tabToMove.window = manager.get().defaultWindow;
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2ul);
[manager run];
}
TEST(WKWebExtensionAPITabs, ActivatedEvent)
{
auto *backgroundScript = Util::constructScript(@[
@"const newTab = await browser.tabs.create({ active: false })",
@"browser.tabs.onActivated.addListener((activeInfo) => {",
@" browser.test.assertEq(activeInfo.tabId, newTab.id, 'The activated tab should have the correct id')",
@" browser.test.notifyPass()",
@"})",
@"browser.tabs.update(newTab.id, { active: true })"
]);
Util::loadAndRunExtension(tabsManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPITabs, HighlightedEvent)
{
auto *backgroundScript = Util::constructScript(@[
@"const initialTabs = await browser.tabs.query({ active: true, currentWindow: true })",
@"const newTab = await browser.tabs.create({ active: false })",
@"browser.tabs.onActivated.addListener((activeInfo) => {",
@" browser.test.notifyFail('The tab should be highlighted but not activated')",
@"})",
@"browser.tabs.onHighlighted.addListener((highlightInfo) => {",
@" browser.test.assertTrue(highlightInfo.tabIds.includes(newTab.id), 'The highlighted tabs should contain the new tab id')",
@" browser.test.assertTrue(highlightInfo.tabIds.includes(initialTabs[0].id), 'The initial tab should still be highlighted')",
@" browser.test.notifyPass()",
@"})",
@"browser.tabs.update(newTab.id, { active: false, highlighted: true })"
]);
Util::loadAndRunExtension(tabsManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPITabs, HighlightedAlsoActivatesTab)
{
auto *backgroundScript = Util::constructScript(@[
@"const newTab = await browser.tabs.create({ active: false })",
@"let tabActivated = false",
@"let tabHighlighted = false",
@"let checkCompletion = () => {",
@" if (tabActivated && tabHighlighted)",
@" browser.test.notifyPass()",
@"}",
@"browser.tabs.onActivated.addListener((activeInfo) => {",
@" tabActivated = true",
@" browser.test.assertEq(activeInfo.tabId, newTab.id, 'The activated tab should have the correct id')",
@" checkCompletion()",
@"})",
@"browser.tabs.onHighlighted.addListener((highlightInfo) => {",
@" tabHighlighted = true",
@" browser.test.assertTrue(highlightInfo.tabIds.includes(newTab.id), 'The highlighted tabs should contain the new tab id')",
@" checkCompletion()",
@"})",
@"browser.tabs.update(newTab.id, { highlighted: true })"
]);
Util::loadAndRunExtension(tabsManifest, @{ @"background.js": backgroundScript });
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPITabs, DISABLED_SendMessage)
#else
TEST(WKWebExtensionAPITabs, SendMessage)
#endif
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => {",
@" browser.test.assertEq(message, 'Ready')",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" const response = await browser.test.assertSafeResolve(() => browser.tabs.sendMessage(tabId, { content: 'Hello' }))",
@" browser.test.assertEq(response?.content, 'Received', 'Should get the response from the content script')",
@" browser.test.notifyPass()",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message, sender, sendResponse) => {",
@" browser.test.assertEq(message.content, 'Hello', 'Should receive the correct message content')",
@" browser.test.assertEq(typeof sender, 'object', 'sender should be an object')",
@" browser.test.assertEq(typeof sender.url, 'string', 'sender.url should be a string')",
@" browser.test.assertEq(typeof sender.origin, 'string', 'sender.origin should be a string')",
@" browser.test.assertTrue(sender.url.startsWith('webkit-extension://'), 'sender.url should start with webkit-extension://')",
@" browser.test.assertTrue(sender.origin.startsWith('webkit-extension://'), 'sender.origin should start with webkit-extension://')",
@" browser.test.assertEq(sender.tab, undefined, 'sender.tab should be undefined')",
@" browser.test.assertEq(sender.frameId, undefined, 'sender.frameId should be undefined')",
@" sendResponse({ content: 'Received' })",
@"})",
@"setTimeout(() => browser.runtime.sendMessage('Ready'), 1000)"
]);
auto manager = Util::loadExtension(tabsContentScriptManifest, @{ @"background.js": backgroundScript, @"content.js": contentScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPITabs, DISABLED_SendMessageWithAsyncReply)
#else
TEST(WKWebExtensionAPITabs, SendMessageWithAsyncReply)
#endif
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => {",
@" browser.test.assertEq(message, 'Ready')",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" const response = await browser.test.assertSafeResolve(() => browser.tabs.sendMessage(tabId, { content: 'Hello' }))",
@" browser.test.assertEq(response?.content, 'Received', 'Should get the response from the content script')",
@" browser.test.notifyPass()",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message, sender, sendResponse) => {",
@" browser.test.assertEq(message.content, 'Hello', 'Should receive the correct message content')",
@" browser.test.assertEq(typeof sender, 'object', 'sender should be an object')",
@" browser.test.assertEq(typeof sender.url, 'string', 'sender.url should be a string')",
@" browser.test.assertEq(typeof sender.origin, 'string', 'sender.origin should be a string')",
@" browser.test.assertTrue(sender.url.startsWith('webkit-extension://'), 'sender.url should start with webkit-extension://')",
@" browser.test.assertTrue(sender.origin.startsWith('webkit-extension://'), 'sender.origin should start with webkit-extension://')",
@" browser.test.assertEq(sender.tab, undefined, 'sender.tab should be undefined')",
@" browser.test.assertEq(sender.frameId, undefined, 'sender.frameId should be undefined')",
@" setTimeout(() => sendResponse({ content: 'Received' }), 1000)",
@" return true",
@"})",
@"setTimeout(() => browser.runtime.sendMessage('Ready'), 1000)"
]);
auto manager = Util::loadExtension(tabsContentScriptManifest, @{ @"background.js": backgroundScript, @"content.js": contentScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPITabs, DISABLED_SendMessageWithPromiseReply)
#else
TEST(WKWebExtensionAPITabs, SendMessageWithPromiseReply)
#endif
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message, 'Ready')",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" const response = await browser.test.assertSafeResolve(() => browser.tabs.sendMessage(tabId, { content: 'Hello' }))",
@" browser.test.assertEq(response?.content, 'Received', 'Should get the response from the content script')",
@" browser.test.notifyPass()",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message, sender) => {",
@" browser.test.assertEq(message.content, 'Hello', 'Should receive the correct message content')",
@" browser.test.assertEq(typeof sender, 'object', 'sender should be an object')",
@" browser.test.assertEq(typeof sender.url, 'string', 'sender.url should be a string')",
@" browser.test.assertEq(typeof sender.origin, 'string', 'sender.origin should be a string')",
@" browser.test.assertTrue(sender.url.startsWith('webkit-extension://'), 'sender.url should start with webkit-extension://')",
@" browser.test.assertTrue(sender.origin.startsWith('webkit-extension://'), 'sender.origin should start with webkit-extension://')",
@" browser.test.assertEq(sender.tab, undefined, 'sender.tab should be undefined')",
@" browser.test.assertEq(sender.frameId, undefined, 'sender.frameId should be undefined')",
@" return Promise.resolve({ content: 'Received' })",
@"})",
@"setTimeout(() => browser.runtime.sendMessage('Ready'), 1000)"
]);
auto manager = Util::loadExtension(tabsContentScriptManifest, @{ @"background.js": backgroundScript, @"content.js": contentScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPITabs, DISABLED_SendMessageWithAsyncPromiseReply)
#else
TEST(WKWebExtensionAPITabs, SendMessageWithAsyncPromiseReply)
#endif
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message, 'Ready')",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" const response = await browser.test.assertSafeResolve(() => browser.tabs.sendMessage(tabId, { content: 'Hello' }))",
@" browser.test.assertEq(response?.content, 'Received', 'Should get the response from the content script')",
@" browser.test.notifyPass()",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message, sender) => {",
@" browser.test.assertEq(message.content, 'Hello', 'Should receive the correct message content')",
@" browser.test.assertEq(typeof sender, 'object', 'sender should be an object')",
@" browser.test.assertEq(typeof sender.url, 'string', 'sender.url should be a string')",
@" browser.test.assertEq(typeof sender.origin, 'string', 'sender.origin should be a string')",
@" browser.test.assertTrue(sender.url.startsWith('webkit-extension://'), 'sender.url should start with webkit-extension://')",
@" browser.test.assertTrue(sender.origin.startsWith('webkit-extension://'), 'sender.origin should start with webkit-extension://')",
@" browser.test.assertEq(sender.tab, undefined, 'sender.tab should be undefined')",
@" browser.test.assertEq(sender.frameId, undefined, 'sender.frameId should be undefined')",
@" return new Promise((resolve) => {",
@" setTimeout(() => resolve({ content: 'Received' }), 1000)",
@" })",
@"})",
@"setTimeout(() => browser.runtime.sendMessage('Ready'), 1000)"
]);
auto manager = Util::loadExtension(tabsContentScriptManifest, @{ @"background.js": backgroundScript, @"content.js": contentScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPITabs, DISABLED_SendMessageWithoutReply)
#else
TEST(WKWebExtensionAPITabs, SendMessageWithoutReply)
#endif
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" const response = await browser.tabs.sendMessage(tabId, { content: 'Hello' })",
@" browser.test.assertEq(response, undefined, 'Should resolve with undefined as there was no reply')",
@" browser.test.notifyPass()",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message, sender) => {",
@" browser.test.assertEq(message.content, 'Hello', 'Should receive the correct message content')",
@" browser.test.assertEq(typeof sender, 'object', 'sender should be an object')",
@" browser.test.assertEq(typeof sender.url, 'string', 'sender.url should be a string')",
@" browser.test.assertEq(typeof sender.origin, 'string', 'sender.origin should be a string')",
@" browser.test.assertTrue(sender.url.startsWith('webkit-extension://'), 'sender.url should start with webkit-extension://')",
@" browser.test.assertTrue(sender.origin.startsWith('webkit-extension://'), 'sender.origin should start with webkit-extension://')",
@" browser.test.assertEq(sender.tab, undefined, 'sender.tab should be undefined')",
@" browser.test.assertEq(sender.frameId, undefined, 'sender.frameId should be undefined')",
@" return false",
@"})",
@"setTimeout(() => browser.runtime.sendMessage('Ready'), 1000)"
]);
auto manager = Util::loadExtension(tabsContentScriptManifest, @{ @"background.js": backgroundScript, @"content.js": contentScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
TEST(WKWebExtensionAPITabs, SendMessageFromBackgroundPageToFullPageExtensionContent)
{
auto *optionsScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message, sender, sendResponse) => {",
@" browser.test.assertEq(message?.content, 'Hello', 'Should receive the correct message from the background page')",
@" browser.test.assertEq(typeof sender, 'object', 'sender should be an object')",
@" browser.test.notifyPass()",
@"})",
@"browser.runtime.sendMessage({ content: 'Ready' })",
]);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => {",
@" browser.test.assertEq(message?.content, 'Ready', 'Should receive the correct message from the options page')",
@" browser.test.assertEq(typeof sender, 'object', 'sender should be an object')",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" browser.tabs.sendMessage(tabId, { content: 'Hello' })",
@"})",
@"browser.test.sendMessage('Load Tab')",
]);
auto *resources = @{
@"background.js": backgroundScript,
@"options.html": @"<script type='module' src='options.js'></script>",
@"options.js": optionsScript
};
auto manager = Util::loadExtension(tabsManifest, resources);
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultWindow openNewTab];
EXPECT_EQ(manager.get().defaultWindow.tabs.count, 2lu);
auto *optionsPageURL = manager.get().context.optionsPageURL;
EXPECT_NOT_NULL(optionsPageURL);
auto *defaultTab = manager.get().defaultTab;
EXPECT_NOT_NULL(defaultTab);
[defaultTab changeWebViewIfNeededForURL:optionsPageURL forExtensionContext:manager.get().context];
[defaultTab.webView loadRequest:[NSURLRequest requestWithURL:optionsPageURL]];
[manager run];
}
TEST(WKWebExtensionAPITabs, SendMessageFromBackgroundToSubframe)
{
TestWebKitAPI::HTTPServer server({
{ "/main"_s, { { { "Content-Type"_s, "text/html"_s } },
"<body><script>"
" document.write('<iframe src=\"http://127.0.0.1:' + location.port + '/subframe\"></iframe>')"
"</script></body>"_s
} },
{ "/subframe"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequestMain = server.requestWithLocalhost("/main"_s);
auto *urlRequestSubframe = server.request("/subframe"_s);
auto *backgroundScript = Util::constructScript(@[
@"browser.test.sendMessage('Load Tab')",
@"await new Promise(resolve => setTimeout(resolve, 1500))",
@"const tabs = await browser.tabs.query({ })",
@"browser.test.assertEq(tabs.length, 1)",
@"const response = await browser.tabs.sendMessage(tabs[0].id, { content: 'Hello Subframe' })",
@"browser.test.assertEq(response?.content, 'Received from subframe', 'Response should be received from subframe')",
@"browser.test.notifyPass()",
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message, sender) => {",
@" browser.test.assertEq(message?.content, 'Hello Subframe', 'Should receive the correct message from the background page')",
@" return Promise.resolve({ content: 'Received from subframe' })",
@"})"
]);
auto *manifest = @{
@"manifest_version": @3,
@"name": @"Test",
@"description": @"Test",
@"version": @"1",
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
@"content_scripts": @[@{
@"matches": @[ @"*://127.0.0.1/*" ],
@"js": @[ @"content.js" ],
@"all_frames": @YES
}]
};
auto *resources = @{
@"background.js": backgroundScript,
@"content.js": contentScript
};
auto manager = Util::loadExtension(manifest, resources);
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequestSubframe.URL];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:urlRequestMain];
[manager run];
}
TEST(WKWebExtensionAPITabs, SendMessageFromBackgroundToSpecificFrame)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<iframe src='/frame.html'></iframe>"_s } },
{ "/frame.html"_s, { { { "Content-Type"_s, "text/html"_s } }, " "_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message, 'Begin Test', 'Message content should match')",
@" const frameId = sender?.frameId",
@" browser.test.assertTrue(frameId !== 0, 'frameId should not be 0 for an iframe')",
@" const response = await browser.tabs.sendMessage(sender.tab.id, 'Hello, iframe!', { frameId })",
@" browser.test.assertEq(response, 'Message received', 'Should receive response from content script')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Load Tab')"
]);
auto *contentScript = Util::constructScript(@[
@"if (window.top === window) {",
@" browser.runtime.onMessage.addListener(() => {",
@" browser.test.notifyFail('Main frame should not receive message intended for iframe')",
@" })",
@"} else {",
@" browser.runtime.onMessage.addListener((message, sender, sendResponse) => {",
@" browser.test.assertEq(message, 'Hello, iframe!', 'Message should be received in the iframe')",
@" sendResponse('Message received')",
@" })",
@" browser.runtime.sendMessage('Begin Test')",
@"}"
]);
auto *resources = @{
@"background.js": backgroundScript,
@"content.js": contentScript,
};
auto manager = Util::loadExtension(tabsContentScriptManifest, resources);
auto *matchPattern = [WKWebExtensionMatchPattern matchPatternWithScheme:@"*" host:@"localhost" path:@"/*"];
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forMatchPattern:matchPattern];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()];
[manager run];
}
TEST(WKWebExtensionAPITabs, SendMessageFromBackgroundToSpecificDocument)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<iframe src='/frame.html'></iframe>"_s } },
{ "/frame.html"_s, { { { "Content-Type"_s, "text/html"_s } }, " "_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message, 'Begin Test', 'Message content should match')",
@" const documentId = sender?.documentId",
@" browser.test.assertEq(typeof documentId, 'string', 'sender.documentId should be a string')",
@" browser.test.assertEq(documentId.length, 36, 'sender.documentId.length should be 36')",
@" const response = await browser.tabs.sendMessage(sender.tab.id, 'Hello, iframe!', { documentId })",
@" browser.test.assertEq(response, 'Message received', 'Should receive response from content script')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Load Tab')"
]);
auto *contentScript = Util::constructScript(@[
@"if (window.top === window) {",
@" browser.runtime.onMessage.addListener(() => {",
@" browser.test.notifyFail('Main frame should not receive message intended for iframe')",
@" })",
@"} else {",
@" browser.runtime.onMessage.addListener((message, sender, sendResponse) => {",
@" browser.test.assertEq(message, 'Hello, iframe!', 'Background script message should be received')",
@" sendResponse('Message received')",
@" })",
@" browser.runtime.sendMessage('Begin Test')",
@"}"
]);
auto *resources = @{
@"background.js": backgroundScript,
@"content.js": contentScript,
};
auto manager = Util::loadExtension(tabsContentScriptManifest, resources);
auto *matchPattern = [WKWebExtensionMatchPattern matchPatternWithScheme:@"*" host:@"localhost" path:@"/*"];
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forMatchPattern:matchPattern];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()];
[manager run];
}
TEST(WKWebExtensionAPITabs, SendMessageBackAndForwardNavigation)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<iframe src='/frame'></iframe>"_s } },
{ "/frame"_s, { { { "Content-Type"_s, "text/html"_s } }, "<p>Frame Content</p>"_s } }
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"const expectedHostnames = ['localhost', '127.0.0.1', 'localhost', '127.0.0.1']",
@"let step = 0",
@"browser.test.onMessage.addListener(async (message) => {",
@" browser.test.assertEq(message, 'Go', 'Message content should match')",
@" const [tab] = await browser.tabs.query({ active: true, currentWindow: true })",
@" const frames = await browser.webNavigation.getAllFrames({ tabId: tab.id })",
@" const responses = await Promise.all(frames.map(async (frame) => {",
@" return await browser.tabs.sendMessage(tab.id, 'Ping', { frameId: frame.frameId })",
@" }))",
@" browser.test.assertEq(responses.length, 2, 'Should receive 2 responses')",
@" const mainFrameHost = new URL(responses[0]).hostname",
@" const subframeHost = new URL(responses[1]).hostname",
@" browser.test.assertEq(mainFrameHost, expectedHostnames[step], 'Main frame host should be')",
@" browser.test.assertEq(subframeHost, expectedHostnames[step], 'Subframe host should be')",
@" ++step",
@" browser.test.sendMessage('Messages Sent')",
@"})",
@"browser.test.sendMessage('Ready')"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message) => {",
@" browser.test.assertEq(message, 'Ping', 'Message content should match')",
@" return Promise.resolve(window?.location?.href)",
@"})"
]);
auto *resources = @{
@"background.js": backgroundScript,
@"content.js": contentScript,
};
auto *manifest = @{
@"manifest_version": @3,
@"name": @"Tabs Test",
@"description": @"Tabs Test",
@"version": @"1",
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
@"content_scripts": @[ @{
@"js": @[ @"content.js" ],
@"matches": @[ @"*://*/*" ],
@"all_frames": @YES,
} ],
@"permissions": @[ @"webNavigation", @"tabs" ]
};
auto manager = Util::loadExtension(manifest, resources);
auto *localhostRequest = server.requestWithLocalhost();
auto *altHostRequest = server.request();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:localhostRequest.URL];
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:altHostRequest.URL];
[manager runUntilTestMessage:@"Ready"];
auto *webView = manager.get().defaultTab.webView;
// Load localhost page.
[webView synchronouslyLoadRequest:localhostRequest];
[manager sendTestMessage:@"Go"];
[manager runUntilTestMessage:@"Messages Sent"];
// Load 127.0.0.1 page.
[webView synchronouslyLoadRequest:altHostRequest];
[manager sendTestMessage:@"Go"];
[manager runUntilTestMessage:@"Messages Sent"];
// Go back to localhost.
[webView synchronouslyGoBack];
[manager sendTestMessage:@"Go"];
[manager runUntilTestMessage:@"Messages Sent"];
// Go forward to 127.0.0.1.
[webView synchronouslyGoForward];
[manager sendTestMessage:@"Go"];
[manager runUntilTestMessage:@"Messages Sent"];
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPITabs, DISABLED_Connect)
#else
TEST(WKWebExtensionAPITabs, Connect)
#endif
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message?.readyToConnect, true, 'Should be ready to connect')",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" const port = browser.tabs.connect(tabId, { name: 'testPort' })",
@" browser.test.assertEq(typeof port, 'object', 'Port should be an object')",
@" browser.test.assertEq(port?.name, 'testPort', 'Port name should be testPort')",
@" browser.test.assertEq(port?.sender, null, 'Port sender should be null')",
@" port.postMessage('Hello')",
@" port.onMessage.addListener((response) => {",
@" browser.test.assertEq(response, 'Received', 'Should get the response from the content script')",
@" browser.test.notifyPass()",
@" })",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onConnect.addListener((port) => {",
@" browser.test.assertEq(port.name, 'testPort', 'Port name should be testPort')",
@" browser.test.assertEq(port.error, null, 'Port error should be null')",
@" browser.test.assertEq(typeof port.sender, 'object', 'sender should be an object')",
@" browser.test.assertEq(typeof port.sender.url, 'string', 'sender.url should be a string')",
@" browser.test.assertEq(typeof port.sender.origin, 'string', 'sender.origin should be a string')",
@" browser.test.assertTrue(port.sender.url.startsWith('webkit-extension://'), 'sender.url should start with webkit-extension://')",
@" browser.test.assertTrue(port.sender.origin.startsWith('webkit-extension://'), 'sender.origin should start with webkit-extension://')",
@" browser.test.assertEq(port.sender.tab, undefined, 'sender.tab should be undefined')",
@" browser.test.assertEq(port.sender.frameId, undefined, 'sender.frameId should be undefined')",
@" port.onMessage.addListener((message) => {",
@" browser.test.assertEq(message, 'Hello', 'Should receive the correct message content')",
@" port.postMessage('Received')",
@" })",
@"})",
@"setTimeout(() => {",
@" browser.runtime.sendMessage({ readyToConnect: true })",
@"}, 1000)"
]);
auto manager = Util::loadExtension(tabsContentScriptManifest, @{ @"background.js": backgroundScript, @"content.js": contentScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
TEST(WKWebExtensionAPITabs, ConnectToSpecificFrame)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<iframe src='/frame.html'></iframe>"_s } },
{ "/frame.html"_s, { { { "Content-Type"_s, "text/html"_s } }, " "_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message, 'Begin Test', 'Message content should match')",
@" const frameId = sender?.frameId",
@" browser.test.assertTrue(frameId !== 0, 'frameId should not be 0 for an iframe')",
@" const port = browser.tabs.connect(sender.tab.id, { frameId })",
@" browser.test.assertEq(typeof port, 'object', 'Port should be an object')",
@" port.onMessage.addListener((response) => {",
@" browser.test.assertEq(response, 'Message received', 'Should receive response from content script')",
@" browser.test.notifyPass()",
@" })",
@" port.postMessage('Hello, iframe!')",
@"})",
@"browser.test.sendMessage('Load Tab')"
]);
auto *contentScript = Util::constructScript(@[
@"if (window.top === window) {",
@" browser.runtime.onConnect.addListener(() => {",
@" browser.test.notifyFail('Main frame should not receive connection intended for iframe')",
@" })",
@"} else {",
@" browser.runtime.onConnect.addListener((port) => {",
@" port.onMessage.addListener((message) => {",
@" browser.test.assertEq(message, 'Hello, iframe!', 'Background script message should be received')",
@" port.postMessage('Message received')",
@" })",
@" })",
@" browser.runtime.sendMessage('Begin Test')",
@"}"
]);
auto *resources = @{
@"background.js": backgroundScript,
@"content.js": contentScript,
};
auto manager = Util::loadExtension(tabsContentScriptManifest, resources);
auto *matchPattern = [WKWebExtensionMatchPattern matchPatternWithScheme:@"*" host:@"localhost" path:@"/*"];
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forMatchPattern:matchPattern];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()];
[manager run];
}
TEST(WKWebExtensionAPITabs, ConnectToSpecificDocument)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<iframe src='/frame.html'></iframe>"_s } },
{ "/frame.html"_s, { { { "Content-Type"_s, "text/html"_s } }, " "_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message, 'Begin Test', 'Message content should match')",
@" const documentId = sender?.documentId",
@" browser.test.assertEq(typeof documentId, 'string', 'sender.documentId should be a string')",
@" browser.test.assertEq(documentId.length, 36, 'sender.documentId.length should be 36')",
@" const port = browser.tabs.connect(sender.tab.id, { documentId })",
@" browser.test.assertEq(typeof port, 'object', 'Port should be an object')",
@" port.onMessage.addListener((response) => {",
@" browser.test.assertEq(response, 'Message received', 'Should receive response from content script')",
@" browser.test.notifyPass()",
@" })",
@" port.postMessage('Hello, iframe!')",
@"})",
@"browser.test.sendMessage('Load Tab')"
]);
auto *contentScript = Util::constructScript(@[
@"if (window.top === window) {",
@" browser.runtime.onConnect.addListener(() => {",
@" browser.test.notifyFail('Main frame should not receive connection intended for iframe')",
@" })",
@"} else {",
@" browser.runtime.onConnect.addListener((port) => {",
@" port.onMessage.addListener((message) => {",
@" browser.test.assertEq(message, 'Hello, iframe!', 'Background script message should be received')",
@" port.postMessage('Message received')",
@" })",
@" })",
@" browser.runtime.sendMessage('Begin Test')",
@"}"
]);
auto *resources = @{
@"background.js": backgroundScript,
@"content.js": contentScript,
};
auto manager = Util::loadExtension(tabsContentScriptManifest, resources);
auto *matchPattern = [WKWebExtensionMatchPattern matchPatternWithScheme:@"*" host:@"localhost" path:@"/*"];
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forMatchPattern:matchPattern];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()];
[manager run];
}
TEST(WKWebExtensionAPITabs, ConnectToSubframe)
{
TestWebKitAPI::HTTPServer server({
{ "/main"_s, { { { "Content-Type"_s, "text/html"_s } },
"<body><script>"
" document.write('<iframe src=\"http://127.0.0.1:' + location.port + '/subframe\"></iframe>')"
"</script></body>"_s
} },
{ "/subframe"_s, { { { "Content-Type"_s, "text/html"_s } }, "<p>Subframe content</p>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *urlRequestMain = server.requestWithLocalhost("/main"_s);
auto *urlRequestSubframe = server.request("/subframe"_s);
auto *backgroundScript = Util::constructScript(@[
@"browser.test.sendMessage('Load Tab')",
@"await new Promise(resolve => setTimeout(resolve, 1500))",
@"const tabs = await browser.tabs.query({ })",
@"browser.test.assertEq(tabs.length, 1)",
@"const port = browser.tabs.connect(tabs[0].id, { name: 'testPort' })",
@"browser.test.assertEq(typeof port, 'object', 'Port should be an object')",
@"browser.test.assertEq(port.name, 'testPort', 'Port name should be testPort')",
@"port.onMessage.addListener((response) => {",
@" browser.test.assertEq(response, 'Received from subframe', 'Should get the response from the subframe')",
@" browser.test.notifyPass()",
@"})",
@"port.postMessage('Hello Subframe')"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onConnect.addListener((port) => {",
@" browser.test.assertEq(port.name, 'testPort', 'Port name should be testPort')",
@" port.onMessage.addListener((message) => {",
@" browser.test.assertEq(message, 'Hello Subframe', 'Should receive the correct message content')",
@" port.postMessage('Received from subframe')",
@" })",
@"})"
]);
auto *manifest = @{
@"manifest_version": @3,
@"name": @"Test",
@"description": @"Test",
@"version": @"1",
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
@"content_scripts": @[@{
@"matches": @[ @"*://127.0.0.1/*" ],
@"js": @[ @"content.js" ],
@"all_frames": @YES
}]
};
auto *resources = @{
@"background.js": backgroundScript,
@"content.js": contentScript
};
auto manager = Util::loadExtension(manifest, resources);
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequestSubframe.URL];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:urlRequestMain];
[manager run];
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPITabs, DISABLED_PortDisconnect)
#else
TEST(WKWebExtensionAPITabs, PortDisconnect)
#endif
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message?.readyToConnect, true, 'Should be ready to connect')",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" const port = browser.tabs.connect(tabId)",
@" browser.test.assertEq(typeof port, 'object', 'Port should be an object')",
@" browser.test.assertEq(port?.name, '', 'Port name should be empty')",
@" browser.test.assertEq(port?.sender, null, 'Port sender should be null')",
@" port.postMessage('Hello')",
@" port.onDisconnect.addListener(() => {",
@" browser.test.assertTrue(true, 'Should trigger the onDisconnect event in the background script')",
@" })",
@" port.disconnect()",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onConnect.addListener((port) => {",
@" port.onMessage.addListener((message) => {",
@" browser.test.assertEq(message, 'Hello', 'Should receive the correct message content')",
@" })",
@" port.onDisconnect.addListener(() => {",
@" browser.test.assertTrue(true, 'Should trigger the onDisconnect event in the content script')",
@" browser.test.notifyPass()",
@" })",
@"})",
@"setTimeout(() => {",
@" browser.runtime.sendMessage({ readyToConnect: true })",
@"}, 1000)"
]);
auto manager = Util::loadExtension(tabsContentScriptManifest, @{ @"background.js": backgroundScript, @"content.js": contentScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPITabs, DISABLED_ConnectWithMultipleListeners)
#else
TEST(WKWebExtensionAPITabs, ConnectWithMultipleListeners)
#endif
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message?.readyToConnect, true, 'Should be ready to connect')",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" const port = browser.tabs.connect(tabId)",
@" browser.test.assertEq(typeof port, 'object', 'Port should be an object')",
@" browser.test.assertEq(port?.name, '', 'Port name should be empty')",
@" browser.test.assertEq(port?.sender, null, 'Port sender should be null')",
@" port.postMessage('Hello')",
@" let receivedMessages = 0",
@" port.onMessage.addListener((response) => {",
@" browser.test.assertEq(response, 'Received', 'Should get the response from the content script')",
@" if (++receivedMessages === 2)",
@" browser.test.notifyPass()",
@" })",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"let firstListenerHandled = false",
@"let secondListenerHandled = false",
@"browser.runtime.onConnect.addListener((port) => {",
@" port.onMessage.addListener((message) => {",
@" if (!firstListenerHandled) {",
@" browser.test.assertEq(message, 'Hello', 'Should receive the correct message content')",
@" port.postMessage('Received')",
@" firstListenerHandled = true",
@" }",
@" })",
@"})",
@"browser.runtime.onConnect.addListener((port) => {",
@" port.onMessage.addListener((message) => {",
@" if (!secondListenerHandled) {",
@" browser.test.assertEq(message, 'Hello', 'Should receive the correct message content')",
@" port.postMessage('Received')",
@" secondListenerHandled = true",
@" }",
@" })",
@"})",
@"setTimeout(() => {",
@" browser.runtime.sendMessage({ readyToConnect: true })",
@"}, 1000)"
]);
auto manager = Util::loadExtension(tabsContentScriptManifest, @{ @"background.js": backgroundScript, @"content.js": contentScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPITabs, DISABLED_PortDisconnectWithMultipleListeners)
#else
TEST(WKWebExtensionAPITabs, PortDisconnectWithMultipleListeners)
#endif
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message?.readyToConnect, true, 'Should be ready to connect')",
@" const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@" const tabId = tabs[0].id",
@" const port = browser.tabs.connect(tabId)",
@" browser.test.assertEq(typeof port, 'object', 'Port should be an object')",
@" browser.test.assertEq(port?.name, '', 'Port name should be empty')",
@" browser.test.assertEq(port?.sender, null, 'Port sender should be null')",
@" port.postMessage('Hello')",
@" let receivedMessages = 0",
@" port.onMessage.addListener((response) => {",
@" browser.test.assertEq(response, 'Received', 'Should get the response from the content script')",
@" if (++receivedMessages === 2)",
@" browser.test.notifyPass()",
@" })",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onConnect.addListener((port) => {",
@" port.onMessage.addListener((message) => {",
@" browser.test.assertEq(message, 'Hello', 'Should receive the correct message content')",
@" port.postMessage('Received')",
@" port.disconnect()",
@" })",
@"})",
@"browser.runtime.onConnect.addListener((port) => {",
@" port.onMessage.addListener((message) => {",
@" browser.test.assertEq(message, 'Hello', 'Should receive the correct message content')",
@" port.postMessage('Received')",
@" })",
@"})",
@"setTimeout(() => {",
@" browser.runtime.sendMessage({ readyToConnect: true })",
@"}, 1000)"
]);
auto manager = Util::loadExtension(tabsContentScriptManifest, @{ @"background.js": backgroundScript, @"content.js": contentScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
TEST(WKWebExtensionAPITabs, ExecuteScript)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<iframe src='/frame.html'></iframe>"_s } },
{ "/frame.html"_s, { { { "Content-Type"_s, "text/html"_s } }, "<body style='background-color: blue'></body>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
static auto *javaScript = @"document.body.style.background = 'pink'";
auto *backgroundScript = Util::constructScript(@[
@"const tabs = await browser.tabs.query({ active: true, currentWindow: true })",
@"const tabId = tabs[0].id",
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" if (tab.status === 'complete') {",
@" let results = await browser.tabs.executeScript(tabId, { allFrames: false, file: 'executeScript.js' })",
@" browser.test.assertEq(results[0], 'pink')",
@" results = await browser.tabs.executeScript(tabId, { frameId: 0, file: 'executeScript.js' })",
@" browser.test.assertEq(results[0], 'pink')",
@" results = await browser.tabs.executeScript(tabId, { allFrames: true, file: 'executeScript.js' })",
@" browser.test.assertEq(results[0], 'pink')",
@" browser.test.assertEq(results[1], 'pink')",
@" results = await browser.tabs.executeScript(tabId, { allFrames: false, code: \"document.body.style.background = 'pink'\" })",
@" browser.test.assertEq(results[0], 'pink')",
@" browser.test.assertSafeResolve(() => browser.tabs.executeScript(tabId, { allFrames: false, frameId: 0, file: 'executeScript.js' }))",
@" browser.test.notifyPass()",
@" }",
@"})",
@"browser.test.sendMessage('Load Tab')",
]);
auto manager = Util::loadExtension(tabsManifestV2, @{ @"background.js": backgroundScript, @"executeScript.js": javaScript });
auto *urlRequest = server.requestWithLocalhost();
auto *url = urlRequest.URL;
auto *matchPattern = [WKWebExtensionMatchPattern matchPatternWithScheme:url.scheme host:url.host path:@"/*"];
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forMatchPattern:matchPattern];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
TEST(WKWebExtensionAPITabs, ExecuteScriptWithFrameId)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<iframe src='/frame.html'></iframe>"_s } },
{ "/frame.html"_s, { { { "Content-Type"_s, "text/html"_s } }, " "_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message, 'Hello from frame')",
@" const frameId = sender.frameId",
@" browser.test.assertTrue(frameId !== 0, 'frameId should not be 0 for an iframe')",
@" const results = await browser.tabs.executeScript(sender.tab.id, { frameId, code: 'location.pathname' })",
@" browser.test.assertEq(results[0], '/frame.html', 'Should execute in the iframe and return the correct path')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Load Tab')"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.sendMessage('Hello from frame')",
]);
static auto *manifest = @{
@"manifest_version": @2,
@"name": @"Tabs Test",
@"description": @"Tabs Test",
@"version": @"1",
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
@"content_scripts": @[ @{
@"js": @[ @"content.js" ],
@"matches": @[ @"*://localhost/frame.html" ],
@"all_frames": @YES
} ],
};
auto *resources = @{
@"background.js": backgroundScript,
@"content.js": contentScript,
};
auto manager = Util::loadExtension(manifest, resources);
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:server.requestWithLocalhost("/frame.html"_s).URL];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()];
[manager run];
}
TEST(WKWebExtensionAPITabs, ExecuteScriptWithDocumentId)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<span>Main Document</span><iframe src='/frame.html'></iframe>"_s } },
{ "/frame.html"_s, { { { "Content-Type"_s, "text/html"_s } }, "<span>Frame Document</span>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener(async (message, sender) => {",
@" browser.test.assertEq(message, 'Hello from frame')",
@" const documentId = sender?.documentId",
@" browser.test.assertEq(typeof documentId, 'string', 'sender.documentId should be')",
@" const result = await browser.tabs.executeScript(sender?.tab?.id, { documentId, code: 'document.body.innerText.trim()' })",
@" browser.test.assertEq(result?.[0], 'Frame Document', 'Result should be')",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Load Tab')"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.sendMessage('Hello from frame')",
]);
static auto *manifest = @{
@"manifest_version": @2,
@"name": @"Tabs Test",
@"description": @"Tabs Test",
@"version": @"1",
@"background": @{
@"scripts": @[ @"background.js" ],
@"persistent": @NO
},
@"content_scripts": @[ @{
@"js": @[ @"content.js" ],
@"matches": @[ @"*://localhost/frame.html" ],
@"all_frames": @YES
} ],
};
auto *resources = @{
@"background.js": backgroundScript,
@"content.js": contentScript,
};
auto manager = Util::loadExtension(manifest, resources);
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:server.requestWithLocalhost("/frame.html"_s).URL];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost()];
[manager run];
}
TEST(WKWebExtensionAPITabs, ExecuteScriptJSONTypes)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } }
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" if (tab.status === 'complete') {",
@" const expectedResult = { 'boolean': true, 'number': 42, 'string': 'Test String', 'object': { 'key': 'value' }, 'array': [1, 2, 3] };",
@" function returnJSONTypes() {",
@" return { 'boolean': true, 'number': 42, 'string': 'Test String', 'object': { 'key': 'value' }, 'array': [1, 2, 3] };",
@" }",
@" const results = await browser.tabs.executeScript(tabId, { code: '(' + returnJSONTypes + ')()' });",
@" browser.test.assertDeepEq(results?.[0], expectedResult);",
@" browser.test.notifyPass()",
@" }",
@"})",
@"browser.test.sendMessage('Load Tab')",
]);
auto manager = Util::loadExtension(tabsManifestV2, @{ @"background.js": backgroundScript });
auto *urlRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequest.URL];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
TEST(WKWebExtensionAPITabs, InsertAndRemoveCSSInMainFrame)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, ""_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
static auto *css = @"body { background-color: pink !important }";
auto *backgroundScript = Util::constructScript(@[
@"const pinkValue = 'rgb(255, 192, 203)'",
@"const blueValue = 'rgb(0, 0, 255)'",
@"const transparentValue = 'rgba(0, 0, 0, 0)'",
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" if (tab.status !== 'complete')",
@" return",
@" await browser.tabs.insertCSS(tabId, { allFrames: false, file: 'styles.css' })",
@" let result = await browser.tabs.executeScript(tabId, { code: \"window.getComputedStyle(document.body).getPropertyValue('background-color')\", allFrames: false })",
@" browser.test.assertEq(result[0], pinkValue)",
@" await browser.tabs.removeCSS(tabId, { allFrames: false, file: 'styles.css' })",
@" result = await browser.tabs.executeScript(tabId, { code: \"window.getComputedStyle(document.body).getPropertyValue('background-color')\", allFrames: false })",
@" browser.test.assertEq(result[0], transparentValue)",
@" await browser.tabs.insertCSS(tabId, { frameId: 0, file: 'styles.css' })",
@" result = await browser.tabs.executeScript(tabId, { code: \"window.getComputedStyle(document.body).getPropertyValue('background-color')\", allFrames: false })",
@" browser.test.assertEq(result[0], pinkValue)",
@" await browser.tabs.removeCSS(tabId, { frameId: 0, file: 'styles.css' })",
@" result = await browser.tabs.executeScript(tabId, { code: \"window.getComputedStyle(document.body).getPropertyValue('background-color')\", allFrames: false })",
@" browser.test.assertEq(result[0], transparentValue)",
@" await browser.tabs.insertCSS(tabId, { frameId: 0, code: 'body { background-color: blue !important }' })",
@" result = await browser.tabs.executeScript(tabId, { code: \"window.getComputedStyle(document.body).getPropertyValue('background-color')\", allFrames: false })",
@" browser.test.assertEq(result[0], blueValue)",
@" await browser.tabs.removeCSS(tabId, { frameId: 0, code: 'body { background-color: blue !important }' })",
@" result = await browser.tabs.executeScript(tabId, { code: \"window.getComputedStyle(document.body).getPropertyValue('background-color')\", allFrames: false })",
@" browser.test.assertEq(result[0], transparentValue)",
// Stylesheet being removed should match the one inserted.
@" await browser.tabs.insertCSS(tabId, { frameId: 0, code: 'body { background-color: blue !important }' })",
@" await browser.tabs.removeCSS(tabId, { allFrames: true, code: 'body { background-color: blue !important }' })",
@" result = await browser.tabs.executeScript(tabId, { code: \"window.getComputedStyle(document.body).getPropertyValue('background-color')\", allFrames: false })",
@" browser.test.assertEq(result[0], blueValue)",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Load Tab')",
]);
auto manager = Util::loadExtension(tabsManifestV2, @{ @"background.js": backgroundScript, @"styles.css": css });
auto *urlRequest = server.requestWithLocalhost();
auto *url = urlRequest.URL;
auto *matchPattern = [WKWebExtensionMatchPattern matchPatternWithScheme:url.scheme host:url.host path:@"/*"];
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forMatchPattern:matchPattern];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
TEST(WKWebExtensionAPITabs, InsertAndRemoveCSSInAllFrames)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<body><iframe src='/frame.html'/></body>"_s } },
{ "/frame.html"_s, { { { "Content-Type"_s, "text/html"_s } }, "<body style='background-color: blue'></body>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
static auto *css = @"body { background-color: pink !important }";
auto *backgroundScript = Util::constructScript(@[
@"const pinkValue = 'rgb(255, 192, 203)'",
@"const blueValue = 'rgb(0, 0, 255)'",
@"const transparentValue = 'rgba(0, 0, 0, 0)'",
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" if (tab.status !== 'complete')",
@" return",
@" await browser.tabs.insertCSS(tabId, { allFrames: true, file: 'styles.css' })",
@" let result = await browser.tabs.executeScript(tabId, { code: \"window.getComputedStyle(document.body).getPropertyValue('background-color')\", allFrames: true })",
@" browser.test.assertEq(result[0], pinkValue)",
@" browser.test.assertEq(result[1], pinkValue)",
@" await browser.tabs.removeCSS(tabId, { allFrames: true, file: 'styles.css' })",
@" result = await browser.tabs.executeScript(tabId, { code: \"window.getComputedStyle(document.body).getPropertyValue('background-color')\", allFrames: true })",
@" browser.test.assertEq(result[0], transparentValue)",
@" browser.test.assertEq(result[1], blueValue)",
@" browser.test.notifyPass()",
@"})",
@"browser.test.sendMessage('Load Tab')",
]);
auto manager = Util::loadExtension(tabsManifestV2, @{ @"background.js": backgroundScript, @"styles.css": css });
auto *urlRequest = server.requestWithLocalhost();
auto *url = urlRequest.URL;
auto *matchPattern = [WKWebExtensionMatchPattern matchPatternWithScheme:url.scheme host:url.host path:@"/*"];
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forMatchPattern:matchPattern];
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
TEST(WKWebExtensionAPITabs, CSSUserOrigin)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<body style='color: red'></body>"_s } }
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" if (changeInfo.status === 'complete') {",
@" await browser.test.assertSafeResolve(() => browser.tabs.insertCSS(tabId, { code: 'body { color: green !important }', cssOrigin: 'user' }))",
@" let result = await browser.test.assertSafeResolve(() => browser.tabs.executeScript(tabId, { code: 'getComputedStyle(document.body).color' }))",
@" browser.test.assertEq(result[0], 'rgb(0, 128, 0)', 'Color should be green')",
@" browser.test.notifyPass()",
@" }",
@"})",
@"browser.test.sendMessage('Load Tab')"
]);
auto *urlRequest = server.requestWithLocalhost();
auto manager = Util::loadExtension(tabsManifestV2, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
TEST(WKWebExtensionAPITabs, CSSAuthorOrigin)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<style> body { color: red; } </style>"_s } }
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" if (changeInfo.status === 'complete') {",
@" await browser.test.assertSafeResolve(() => browser.tabs.insertCSS(tabId, { code: 'body { color: green !important; }', cssOrigin: 'author' }))",
@" let result = await browser.test.assertSafeResolve(() => browser.tabs.executeScript(tabId, { code: 'getComputedStyle(document.body).color' }))",
@" browser.test.assertEq(result[0], 'rgb(0, 128, 0)', 'Color should be green')",
@" browser.test.notifyPass()",
@" }",
@"})",
@"browser.test.sendMessage('Load Tab')"
]);
auto *urlRequest = server.requestWithLocalhost();
auto manager = Util::loadExtension(tabsManifestV2, @{ @"background.js": backgroundScript });
[manager runUntilTestMessage:@"Load Tab"];
[manager.get().defaultTab.webView loadRequest:urlRequest];
[manager run];
}
TEST(WKWebExtensionAPITabs, UnsupportedMV3APIs)
{
// Manifest v3 deprecates these APIs, so they should be an undefined property.
static auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertEq(browser.tabs.insertCSS, undefined)",
@"browser.test.assertEq(browser.tabs.removeCSS, undefined)",
@"browser.test.assertEq(browser.tabs.executeScript, undefined)",
@"browser.test.assertEq(browser.tabs.getSelected, undefined)",
@"browser.test.notifyPass()"
]);
Util::loadAndRunExtension(tabsManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPITabs, ActiveTab)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<head><title>Test Title</title></head>"_s } },
{ "/next.html"_s, { { { "Content-Type"_s, "text/html"_s } }, "<head><title>Next Title</title></head>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"const [currentTab] = await browser.tabs.query({ active: true, currentWindow: true })",
@"let updateCount = 0",
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" browser.test.assertEq(tabId, currentTab.id, 'Only the tab we expect should be changing')",
@" if ('url' in changeInfo && 'title' in changeInfo) {",
@" ++updateCount",
@" if (updateCount === 1) {",
@" browser.test.assertEq(changeInfo.url, '', 'URL should be empty before user gesture')",
@" browser.test.assertEq(changeInfo.title, '', 'Title should be empty before user gesture')",
@" browser.test.sendMessage('Perform User Gesture')",
@" } else if (updateCount === 2) {",
@" browser.test.assertTrue(changeInfo.url.includes('localhost'), 'URL should be localhost after user gesture')",
@" browser.test.assertEq(changeInfo.title, 'Test Title', 'Title should be provided after user gesture')",
@" browser.test.sendMessage('Load Next Page')",
@" } else if (updateCount === 3) {",
@" browser.test.assertTrue(changeInfo.url.includes('localhost'), 'URL should be localhost after same site navigation')",
@" browser.test.assertEq(changeInfo.title, 'Next Title', 'Title should be provided after same site navigation')",
@" browser.test.sendMessage('Load IP Address')",
@" } else {",
@" browser.test.assertEq(updateCount, 4, 'Update count should be')",
@" browser.test.assertEq(changeInfo.url, '', 'URL should be empty after navigation without permission')",
@" browser.test.assertEq(changeInfo.title, '', 'Title should be empty after navigation without permission')",
@" browser.test.notifyPass()",
@" }",
@" }",
@"})",
@"browser.test.sendMessage('Load Localhost')",
]);
auto manager = Util::loadExtension(activeTabManifest, @{ @"background.js": backgroundScript });
auto *localhostRequest = server.requestWithLocalhost();
auto *addressRequest = server.request();
[manager runUntilTestMessage:@"Load Localhost"];
[manager.get().defaultTab.webView loadRequest:localhostRequest];
[manager runUntilTestMessage:@"Perform User Gesture"];
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL]);
[manager.get().context userGesturePerformedInTab:manager.get().defaultTab];
EXPECT_TRUE([manager.get().context hasPermission:WKWebExtensionPermissionTabs inTab:manager.get().defaultTab]);
EXPECT_TRUE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL]);
[manager runUntilTestMessage:@"Load Next Page"];
EXPECT_TRUE([manager.get().context hasPermission:WKWebExtensionPermissionTabs inTab:manager.get().defaultTab]);
EXPECT_TRUE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL]);
[manager.get().defaultTab.webView loadRequest:server.requestWithLocalhost("/next.html"_s)];
[manager runUntilTestMessage:@"Load IP Address"];
[manager.get().defaultTab.webView loadRequest:addressRequest];
[manager run];
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL inTab:manager.get().defaultTab]);
}
TEST(WKWebExtensionAPITabs, UserGestureWithoutActiveTab)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<head><title>Test Title</title></head>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"const [currentTab] = await browser.tabs.query({ active: true, currentWindow: true })",
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" browser.test.assertEq(tabId, currentTab.id, 'Only the tab we expect should be changing')",
@" if ('url' in changeInfo && 'title' in changeInfo) {",
@" browser.test.assertEq(changeInfo.url, '', 'URL should be empty before user gesture')",
@" browser.test.assertEq(changeInfo.title, '', 'Title should be empty before user gesture')",
@" browser.test.sendMessage('Perform User Gesture')",
@" }",
@"})",
@"browser.test.sendMessage('Load Localhost')"
]);
auto manager = Util::loadExtension(activeTabManifest, @{ @"background.js": backgroundScript });
// Reset activeTab, WKWebExtensionAPITabs.ActiveTab tests that.
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusUnknown forPermission:WKWebExtensionPermissionActiveTab];
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionActiveTab]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs]);
auto *localhostRequest = server.requestWithLocalhost();
auto *addressRequest = server.request();
[manager runUntilTestMessage:@"Load Localhost"];
[manager.get().defaultTab.webView loadRequest:localhostRequest];
[manager runUntilTestMessage:@"Perform User Gesture"];
[manager.get().context userGesturePerformedInTab:manager.get().defaultTab];
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionActiveTab]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:addressRequest.URL inTab:manager.get().defaultTab]);
}
TEST(WKWebExtensionAPITabs, ActiveTabWithDeniedPermissions)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<head><title>Test Title</title></head>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"const [currentTab] = await browser.tabs.query({ active: true, currentWindow: true })",
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" browser.test.assertEq(tabId, currentTab.id, 'Only the tab we expect should be changing')",
@" if ('url' in changeInfo && 'title' in changeInfo) {",
@" browser.test.assertEq(changeInfo.url, '', 'URL should be empty before user gesture')",
@" browser.test.assertEq(changeInfo.title, '', 'Title should be empty before user gesture')",
@" browser.test.sendMessage('Perform User Gesture')",
@" }",
@"})",
@"browser.test.sendMessage('Load Localhost')"
]);
auto manager = Util::loadExtension(activeTabManifest, @{ @"background.js": backgroundScript });
auto *localhostRequest = server.requestWithLocalhost();
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusDeniedExplicitly forURL:localhostRequest.URL];
EXPECT_TRUE([manager.get().context hasPermission:WKWebExtensionPermissionActiveTab]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
[manager runUntilTestMessage:@"Load Localhost"];
[manager.get().defaultTab.webView loadRequest:localhostRequest];
[manager runUntilTestMessage:@"Perform User Gesture"];
[manager.get().context userGesturePerformedInTab:manager.get().defaultTab];
EXPECT_TRUE([manager.get().context hasPermission:WKWebExtensionPermissionActiveTab]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
}
TEST(WKWebExtensionAPITabs, ActiveTabRemovedWithDeniedPermissions)
{
TestWebKitAPI::HTTPServer server({
{ "/"_s, { { { "Content-Type"_s, "text/html"_s } }, "<head><title>Test Title</title></head>"_s } },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto *backgroundScript = Util::constructScript(@[
@"const [currentTab] = await browser.tabs.query({ active: true, currentWindow: true })",
@"browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {",
@" browser.test.assertEq(tabId, currentTab.id, 'Only the tab we expect should be changing')",
@" if ('url' in changeInfo && 'title' in changeInfo) {",
@" browser.test.assertEq(changeInfo.url, '', 'URL should be empty before user gesture')",
@" browser.test.assertEq(changeInfo.title, '', 'Title should be empty before user gesture')",
@" browser.test.sendMessage('Perform User Gesture')",
@" }",
@"})",
@"browser.test.sendMessage('Load Localhost')"
]);
auto manager = Util::loadExtension(activeTabManifest, @{ @"background.js": backgroundScript });
auto *localhostRequest = server.requestWithLocalhost();
EXPECT_TRUE([manager.get().context hasPermission:WKWebExtensionPermissionActiveTab]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
[manager runUntilTestMessage:@"Load Localhost"];
[manager.get().defaultTab.webView loadRequest:localhostRequest];
[manager runUntilTestMessage:@"Perform User Gesture"];
EXPECT_TRUE([manager.get().context hasPermission:WKWebExtensionPermissionActiveTab]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
[manager.get().context userGesturePerformedInTab:manager.get().defaultTab];
EXPECT_TRUE([manager.get().context hasPermission:WKWebExtensionPermissionActiveTab]);
EXPECT_TRUE([manager.get().context hasPermission:WKWebExtensionPermissionTabs inTab:manager.get().defaultTab]);
EXPECT_TRUE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusDeniedExplicitly forURL:localhostRequest.URL];
EXPECT_TRUE([manager.get().context hasPermission:WKWebExtensionPermissionActiveTab]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs]);
EXPECT_FALSE([manager.get().context hasPermission:WKWebExtensionPermissionTabs inTab:manager.get().defaultTab]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL]);
EXPECT_FALSE([manager.get().context hasAccessToURL:localhostRequest.URL inTab:manager.get().defaultTab]);
}
} // namespace TestWebKitAPI
#endif // ENABLE(WK_WEB_EXTENSIONS)