blob: ad0a5958974210d45f61fe3889ab4655a69e4c3a [file]
/*
* Copyright (C) 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 "WebExtensionUtilities.h"
namespace TestWebKitAPI {
static auto *storageManifest = @{
@"manifest_version": @3,
@"name": @"Storage Test",
@"description": @"Storage Test",
@"version": @"1",
@"permissions": @[ @"storage" ],
@"host_permissions": @[ @"*://localhost/*" ],
@"background": @{
@"scripts": @[ @"background.js" ],
@"type": @"module",
@"persistent": @NO,
},
@"content_scripts": @[ @{
@"js": @[ @"content.js" ],
@"matches": @[ @"*://localhost/*" ],
} ],
};
TEST(WKWebExtensionAPIStorage, Errors)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertThrows(() => browser?.storage?.local?.get(Date.now()), /'items' value is invalid, because an object or a string or an array of strings or null is expected, but a number was provided/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.getKeys('invalid'), /'callback' value is invalid, because a function is expected/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.getBytesInUse({}), /'keys' value is invalid, because a string or an array of strings or null is expected, but an object was provided/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.getBytesInUse([1]), /'keys' value is invalid, because a string or an array of strings or null is expected, but an array of other values was provided/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.set(), /A required argument is missing/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.set([]), /'items' value is invalid, because an object is expected/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.set({ 'key': () => { 'function' } }), /it is not JSON-serializable/i)",
@"browser.test.assertThrows(() => browser?.storage?.sync?.set({ 'key': 'a'.repeat(8193) }), /exceeded maximum size for a single item/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.remove(), /A required argument is missing/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.remove({}), /'keys' value is invalid, because a string or an array of strings is expected, but an object was provided/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.remove([1]), /'keys' value is invalid, because a string or an array of strings is expected, but an array of other values was provided/i)",
@"browser.test.assertThrows(() => browser?.storage?.local?.clear('key'), /'callback' value is invalid, because a function is expected/i)",
@"browser.test.assertThrows(() => browser?.storage?.session?.setAccessLevel('INVALID_ACCESS_LEVEL'), /'accessOptions' value is invalid, because an object is expected/i)",
@"browser.test.assertThrows(() => browser?.storage?.session?.setAccessLevel({ 'accessLevel': 'INVALID_ACCESS_LEVEL' }), /'accessLevel' value is invalid, because it must specify either 'TRUSTED_CONTEXTS' or 'TRUSTED_AND_UNTRUSTED_CONTEXTS'/i)",
@"browser.test.notifyPass()"
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, UndefinedProperties)
{
auto *backgroundScript = Util::constructScript(@[
@"browser.test.assertEq(browser?.storage?.local?.setAccessLevel, undefined)",
@"browser.test.assertEq(browser?.storage?.sync?.setAccessLevel, undefined)",
@"browser.test.assertEq(browser?.storage?.local?.QUOTA_BYTES_PER_ITEM, undefined)",
@"browser.test.assertEq(browser?.storage?.session?.QUOTA_BYTES_PER_ITEM, undefined)",
@"browser.test.assertEq(browser?.storage?.local?.MAX_ITEMS, undefined)",
@"browser.test.assertEq(browser?.storage?.session?.MAX_ITEMS, undefined)",
@"browser.test.assertEq(browser?.storage?.local?.MAX_WRITE_OPERATIONS_PER_HOUR, undefined)",
@"browser.test.assertEq(browser?.storage?.session?.MAX_WRITE_OPERATIONS_PER_HOUR, undefined)",
@"browser.test.assertEq(browser?.storage?.local?.MAX_WRITE_OPERATIONS_PER_MINUTE, undefined)",
@"browser.test.assertEq(browser?.storage?.session?.MAX_WRITE_OPERATIONS_PER_MINUTE, undefined)",
@"browser.test.notifyPass()"
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
// FIXME rdar://147858640
#if PLATFORM(IOS) && !defined(NDEBUG)
TEST(WKWebExtensionAPIStorage, DISABLED_SetAccessLevelTrustedContexts)
#else
TEST(WKWebExtensionAPIStorage, SetAccessLevelTrustedContexts)
#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",
@" await browser?.storage?.session?.setAccessLevel({ accessLevel: 'TRUSTED_CONTEXTS' })",
@" const response = await browser.test.assertSafeResolve(() => browser?.tabs?.sendMessage(tabId, { content: 'Access level set' }))",
@" browser.test.assertEq(response?.content, undefined)",
@" browser.test.notifyPass()",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message, sender, sendResponse) => {",
@" browser.test.assertEq(message?.content, 'Access level set', 'Should receive the correct message content')",
@" sendResponse({ content: browser?.storage?.session })",
@"})",
@"setTimeout(() => browser.runtime.sendMessage('Ready'), 1000)"
]);
auto manager = Util::loadExtension(storageManifest, @{ @"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(WKWebExtensionAPIStorage, DISABLED_SetAccessLevelTrustedAndUntrustedContexts)
#else
TEST(WKWebExtensionAPIStorage, SetAccessLevelTrustedAndUntrustedContexts)
#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",
@" await browser?.storage?.session?.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' })",
@" const response = await browser.test.assertSafeResolve(() => browser?.tabs?.sendMessage(tabId, { content: 'Access level set' }))",
@" browser.test.assertEq(response?.content, 'object')",
@" browser.test.notifyPass()",
@"})"
]);
auto *contentScript = Util::constructScript(@[
@"browser.runtime.onMessage.addListener((message, sender, sendResponse) => {",
@" browser.test.assertEq(message?.content, 'Access level set', 'Should receive the correct message content')",
@" sendResponse({ content: typeof browser?.storage?.session })",
@"})",
@"setTimeout(() => browser.runtime.sendMessage('Ready'), 1000)"
]);
auto manager = Util::loadExtension(storageManifest, @{ @"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(WKWebExtensionAPIStorage, Set)
{
auto *backgroundScript = Util::constructScript(@[
@"const data = { 'string': 'string', 'number': 1, 'boolean': true, 'dictionary': {'key': 'value'}, 'array': [1, true, 'string'], 'null': null, 'undefined': undefined }",
@"await browser?.storage?.local?.set(data)",
@"var result = await browser?.storage?.local?.get()",
@"browser.test.assertDeepEq(data, result)",
@"const additionalData = { 'newItem' : 'item' }",
@"await browser?.storage?.local?.set(additionalData)",
@"result = await browser?.storage?.local?.get()",
@"browser.test.assertDeepEq({ ...data, ...additionalData }, result)",
@"await browser?.storage?.local?.set({ 'boolean': false })",
@"result = await browser?.storage?.local?.get('boolean')",
@"browser.test.assertFalse(result?.boolean)",
@"const updatedArray = [ 'new', 'values' ]",
@"await browser?.storage?.local?.set({ 'array': updatedArray })",
@"result = await browser?.storage?.local?.get('array')",
@"browser.test.assertDeepEq(updatedArray, result?.array)",
// Ensure there isn't a maximum item quota if the storageArea isn't sync.
@"await browser.storage?.local?.clear()",
@"browser.test.assertSafeResolve(() => browser.storage?.local?.set({ 'key': 'a'.repeat(8193) }))",
@"browser.test.notifyPass()",
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, SetCustomObject)
{
auto *backgroundScript = Util::constructScript(@[
@"class BaseObject {",
@" constructor(name) {",
@" this.name = name",
@" }",
@" toJSON() {",
@" return { type: 'BaseObject', name: this.name }",
@" }",
@"}",
@"class DerivedObject extends BaseObject {",
@" constructor(name, value) {",
@" super(name)",
@" this.value = value",
@" }",
@" toJSON() {",
@" let baseSerialization = super.toJSON()",
@" return { ...baseSerialization, type: 'DerivedObject', value: this.value }",
@" }",
@"}",
@"const customObject = new DerivedObject('TestObject', 42)",
@"await browser.test.assertSafeResolve(() => browser?.storage?.local?.set({ customObject }))",
@"var result = await browser.test.assertSafeResolve(() => browser?.storage?.local?.get('customObject'))",
@"browser.test.assertDeepEq(customObject?.toJSON(), result?.customObject, 'Custom object should be correctly stored and retrieved')",
@"browser.test.notifyPass()",
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, Get)
{
auto *backgroundScript = Util::constructScript(@[
@"const data = { 'string': 'string', 'number': 1, 'boolean': true, 'dictionary': {'key': 'value'}, 'array': [1, true, 'string'], 'null': null, 'nan': NaN, '': 'empty' }",
@"await browser?.storage?.local?.set(data)",
@"var result = await browser?.storage?.local?.get()",
@"browser.test.assertDeepEq(data, result)",
@"result = await browser?.storage?.local?.get('boolean')",
@"browser.test.assertTrue(result?.boolean)",
@"result = await browser?.storage?.local?.get('nan')",
@"browser.test.assertEq(result?.nan, null)",
@"result = await browser?.storage?.local?.get('')",
@"browser.test.assertEq(result?.[''], 'empty')",
@"result = await browser?.storage?.local?.get([ 'string', 'number', 'boolean', 'dictionary', 'array', 'null', 'nan', '' ])",
@"browser.test.assertDeepEq(data, result)",
@"result = await browser?.storage?.local?.get({ 'boolean': false, 'unrecognized_key': 'default_value', 'array': [1, true, 'string'], 'null': 42 })",
@"browser.test.assertDeepEq({ 'boolean': true, 'unrecognized_key': 'default_value', 'array': [1, true, 'string'], 'null': null }, result)",
@"browser.test.notifyPass()",
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, GetWithDefaultValue)
{
auto *backgroundScript = Util::constructScript(@[
@"const data = { 'abc': 123 }",
@"await browser.storage.local.set(data)",
@"var result = await browser.storage.local.get()",
@"browser.test.assertDeepEq(data, result, 'Should retrieve the data that was set')",
@"result = await browser.storage.local.get({ 'abc': null, 'unrecognized_key': 'default_value' })",
@"browser.test.assertEq(result?.abc, 123, 'Should return the stored value when the key exists, even if default value is null')",
@"browser.test.assertEq(result?.unrecognized_key, 'default_value', 'Should return the default value for unrecognized keys')",
@"browser.test.notifyPass()",
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, GetKeys)
{
auto *backgroundScript = Util::constructScript(@[
@"const data = { 'string': 'string', 'number': 1, 'boolean': true, 'dictionary': {'key': 'value'}, 'array': [1, true, 'string'], 'null': null }",
@"await browser?.storage?.local?.set(data)",
@"var keys = await browser?.storage?.local?.getKeys()",
@"browser.test.assertEq(keys.length, 6, 'Should have 6 keys')",
@"browser.test.assertTrue(keys.includes('string'), 'Should include string key')",
@"browser.test.assertTrue(keys.includes('number'), 'Should include number key')",
@"browser.test.assertTrue(keys.includes('boolean'), 'Should include boolean key')",
@"browser.test.assertTrue(keys.includes('dictionary'), 'Should include dictionary key')",
@"browser.test.assertTrue(keys.includes('array'), 'Should include array key')",
@"browser.test.assertTrue(keys.includes('null'), 'Should include null key')",
@"await browser?.storage?.local?.remove('number')",
@"keys = await browser?.storage?.local?.getKeys()",
@"browser.test.assertEq(keys.length, 5, 'Should have 5 keys after removal')",
@"browser.test.assertFalse(keys.includes('number'), 'Should not include removed number key')",
@"await browser?.storage?.local?.clear()",
@"keys = await browser?.storage?.local?.getKeys()",
@"browser.test.assertEq(keys.length, 0, 'Should have no keys after clear')",
@"browser.test.notifyPass()",
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, GetBytesInUse)
{
auto *backgroundScript = Util::constructScript(@[
@"const data = { 'string': 'string', 'number': 1, 'boolean': true, 'dictionary': {'key': 'value'}, 'array': [1, true, 'string'] }",
@"await browser?.storage?.local?.set(data)",
@"var result = await browser?.storage?.local?.getBytesInUse()",
@"browser.test.assertEq(result, 79)",
@"result = await browser?.storage?.local?.getBytesInUse('array')",
@"browser.test.assertEq(result, 22)",
@"result = await browser?.storage?.local?.getBytesInUse([ 'boolean', 'dictionary' ])",
@"browser.test.assertEq(result, 36)",
@"await browser?.storage?.local?.remove('array')",
@"result = await browser?.storage?.local?.getBytesInUse()",
@"browser.test.assertEq(result, 57)",
@"await browser?.storage?.local?.clear()",
@"result = await browser?.storage?.local?.getBytesInUse()",
@"browser.test.assertEq(result, 0)",
@"browser.test.notifyPass()",
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, GetBytesInUseWhenEmpty)
{
auto *backgroundScript = Util::constructScript(@[
@"var result = await browser?.storage?.local?.getBytesInUse()",
@"browser.test.assertEq(result, 0)",
@"result = await browser?.storage?.local?.getBytesInUse('nonexistent')",
@"browser.test.assertEq(result, 0)",
@"result = await browser?.storage?.local?.getBytesInUse([ 'a', 'b', 'c' ])",
@"browser.test.assertEq(result, 0)",
@"browser.test.notifyPass()",
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, Remove)
{
auto *backgroundScript = Util::constructScript(@[
@"const data = { 'string': 'string', 'number': 1, 'boolean': true, 'dictionary': {'key': 'value'}, 'array': [1, true, 'string'] }",
@"await browser?.storage?.local?.set(data)",
@"await browser?.storage?.local?.remove('string')",
@"var result = await browser?.storage?.local?.get('string')",
@"browser.test.assertDeepEq(result, {})",
@"await browser?.storage?.local?.remove([ 'boolean', 'dictionary' ])",
@"result = await browser?.storage?.local?.get([ 'boolean', 'dictionary' ])",
@"browser.test.assertDeepEq(result, {})",
@"result = await browser?.storage?.local?.get()",
@"browser.test.assertDeepEq(result, { 'number': 1, 'array': [1, true, 'string'] })",
@"browser.test.notifyPass()",
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, Clear)
{
auto *backgroundScript = Util::constructScript(@[
@"const data = { 'string': 'string', 'number': 1, 'boolean': true, 'dictionary': {'key': 'value'}, 'array': [1, true, 'string'] }",
@"await browser?.storage?.local?.set(data)",
@"var result = await browser?.storage?.local?.getBytesInUse()",
@"browser.test.assertEq(result, 79)",
@"await browser?.storage?.local?.clear()",
@"result = await browser?.storage?.local?.getBytesInUse()",
@"browser.test.assertEq(result, 0)",
@"browser.test.notifyPass()",
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, StorageOnChanged)
{
auto *backgroundScript = Util::constructScript(@[
@"let changeCount = 0",
@"browser.storage.onChanged.addListener((changes, areaName) => {",
@" browser.test.assertEq(areaName, 'local', 'The storage area should be local')",
@" if (changeCount === 0) {",
@" browser.test.assertEq(changes?.string?.newValue, 'newString', 'The new value of string should be correct')",
@" browser.test.assertEq(changes?.string?.oldValue, undefined, 'The old value of string should be null')",
@" browser.test.assertEq(changes?.number?.newValue, 2, 'The new value of number should be correct')",
@" browser.test.assertEq(changes?.number?.oldValue, undefined, 'The old value of number should be null')",
@" browser.test.assertEq(changes?.boolean?.newValue, false, 'The new value of boolean should be correct')",
@" browser.test.assertEq(changes?.boolean?.oldValue, undefined, 'The old value of boolean should be null')",
@" browser.test.assertEq(changes?.dictionary?.newValue?.key, 'newValue', 'The new value of dictionary should be correct')",
@" browser.test.assertEq(changes?.dictionary?.oldValue, undefined, 'The old value of dictionary should be null')",
@" browser.test.assertEq(changes?.array?.newValue[0], 2, 'The new value of array[0] should be correct')",
@" browser.test.assertEq(changes?.array?.oldValue, undefined, 'The old value of array should be null')",
@" } else if (changeCount === 1) {",
@" browser.test.assertEq(changes?.string?.newValue, 'finalString', 'The new value of string should be correct')",
@" browser.test.assertEq(changes?.string?.oldValue, 'newString', 'The old value of string should be correct')",
@" browser.test.assertEq(changes?.number?.newValue, 3, 'The new value of number should be correct')",
@" browser.test.assertEq(changes?.number?.oldValue, 2, 'The old value of number should be correct')",
@" browser.test.assertEq(changes?.boolean?.newValue, true, 'The new value of boolean should be correct')",
@" browser.test.assertEq(changes?.boolean?.oldValue, false, 'The old value of boolean should be correct')",
@" browser.test.assertEq(changes?.dictionary?.newValue?.key, 'finalValue', 'The new value of dictionary should be correct')",
@" browser.test.assertEq(changes?.dictionary?.oldValue?.key, 'newValue', 'The old value of dictionary should be correct')",
@" browser.test.assertEq(changes?.array?.newValue[0], 3, 'The new value of array[0] should be correct')",
@" browser.test.assertEq(changes?.array?.oldValue[0], 2, 'The old value of array[0] should be correct')",
@" } else if (changeCount === 2) {",
@" browser.test.assertEq(changes?.string?.newValue, undefined, 'The string should be removed')",
@" browser.test.assertEq(changes?.string?.oldValue, 'finalString', 'The old value of string should be correct')",
@" browser.test.assertEq(changes?.number?.newValue, undefined, 'The number should be removed')",
@" browser.test.assertEq(changes?.number?.oldValue, 3, 'The old value of number should be correct')",
@" browser.test.assertEq(changes?.boolean?.newValue, undefined, 'The boolean should be removed')",
@" browser.test.assertEq(changes?.boolean?.oldValue, true, 'The old value of boolean should be correct')",
@" browser.test.assertEq(changes?.dictionary?.newValue, undefined, 'The dictionary should be removed')",
@" browser.test.assertEq(changes?.dictionary?.oldValue?.key, 'finalValue', 'The old value of dictionary should be correct')",
@" browser.test.assertEq(changes?.array?.newValue, undefined, 'The array should be removed')",
@" browser.test.assertEq(changes?.array?.oldValue[0], 3, 'The old value of array[0] should be correct')",
@" browser.test.notifyPass()",
@" }",
@" changeCount++",
@"})",
@"const initialData = { 'string': 'newString', 'number': 2, 'boolean': false, 'dictionary': { 'key': 'newValue' }, 'array': [ 2, false, 'newString' ] }",
@"await browser.storage.local.set(initialData)",
@"const updatedData = { 'string': 'finalString', 'number': 3, 'boolean': true, 'dictionary': { 'key': 'finalValue' }, 'array': [ 3, true, 'finalString' ] }",
@"await browser.storage.local.set(updatedData)",
@"await browser.storage.local.remove([ 'string', 'number', 'boolean', 'dictionary', 'array' ])"
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, StorageAreaOnChanged)
{
auto *backgroundScript = Util::constructScript(@[
@"let changeCount = 0",
@"browser.storage.local.onChanged.addListener((changes, areaName) => {",
@" browser.test.assertEq(areaName, 'local', 'The storage area should be local')",
@" if (changeCount === 0) {",
@" browser.test.assertEq(changes?.string?.newValue, 'newString', 'The new value of string should be correct')",
@" browser.test.assertEq(changes?.string?.oldValue, undefined, 'The old value of string should be null')",
@" browser.test.assertEq(changes?.number?.newValue, 2, 'The new value of number should be correct')",
@" browser.test.assertEq(changes?.number?.oldValue, undefined, 'The old value of number should be null')",
@" browser.test.assertEq(changes?.boolean?.newValue, false, 'The new value of boolean should be correct')",
@" browser.test.assertEq(changes?.boolean?.oldValue, undefined, 'The old value of boolean should be null')",
@" browser.test.assertEq(changes?.dictionary?.newValue?.key, 'newValue', 'The new value of dictionary should be correct')",
@" browser.test.assertEq(changes?.dictionary?.oldValue, undefined, 'The old value of dictionary should be null')",
@" browser.test.assertEq(changes?.array?.newValue[0], 2, 'The new value of array[0] should be correct')",
@" browser.test.assertEq(changes?.array?.oldValue, undefined, 'The old value of array should be null')",
@" } else if (changeCount === 1) {",
@" browser.test.assertEq(changes?.string?.newValue, 'finalString', 'The new value of string should be correct')",
@" browser.test.assertEq(changes?.string?.oldValue, 'newString', 'The old value of string should be correct')",
@" browser.test.assertEq(changes?.number?.newValue, 3, 'The new value of number should be correct')",
@" browser.test.assertEq(changes?.number?.oldValue, 2, 'The old value of number should be correct')",
@" browser.test.assertEq(changes?.boolean?.newValue, true, 'The new value of boolean should be correct')",
@" browser.test.assertEq(changes?.boolean?.oldValue, false, 'The old value of boolean should be correct')",
@" browser.test.assertEq(changes?.dictionary?.newValue?.key, 'finalValue', 'The new value of dictionary should be correct')",
@" browser.test.assertEq(changes?.dictionary?.oldValue?.key, 'newValue', 'The old value of dictionary should be correct')",
@" browser.test.assertEq(changes?.array?.newValue[0], 3, 'The new value of array[0] should be correct')",
@" browser.test.assertEq(changes?.array?.oldValue[0], 2, 'The old value of array[0] should be correct')",
@" } else if (changeCount === 2) {",
@" browser.test.assertEq(changes?.string?.newValue, undefined, 'The string should be removed')",
@" browser.test.assertEq(changes?.string?.oldValue, 'finalString', 'The old value of string should be correct')",
@" browser.test.assertEq(changes?.number?.newValue, undefined, 'The number should be removed')",
@" browser.test.assertEq(changes?.number?.oldValue, 3, 'The old value of number should be correct')",
@" browser.test.assertEq(changes?.boolean?.newValue, undefined, 'The boolean should be removed')",
@" browser.test.assertEq(changes?.boolean?.oldValue, true, 'The old value of boolean should be correct')",
@" browser.test.assertEq(changes?.dictionary?.newValue, undefined, 'The dictionary should be removed')",
@" browser.test.assertEq(changes?.dictionary?.oldValue?.key, 'finalValue', 'The old value of dictionary should be correct')",
@" browser.test.assertEq(changes?.array?.newValue, undefined, 'The array should be removed')",
@" browser.test.assertEq(changes?.array?.oldValue[0], 3, 'The old value of array[0] should be correct')",
@" browser.test.notifyPass()",
@" }",
@" changeCount++",
@"})",
@"const initialData = { 'string': 'newString', 'number': 2, 'boolean': false, 'dictionary': { 'key': 'newValue' }, 'array': [ 2, false, 'newString' ] }",
@"await browser.storage.local.set(initialData)",
@"const updatedData = { 'string': 'finalString', 'number': 3, 'boolean': true, 'dictionary': { 'key': 'finalValue' }, 'array': [ 3, true, 'finalString' ] }",
@"await browser.storage.local.set(updatedData)",
@"await browser.storage.local.remove([ 'string', 'number', 'boolean', 'dictionary', 'array' ])"
]);
Util::loadAndRunExtension(storageManifest, @{ @"background.js": backgroundScript });
}
TEST(WKWebExtensionAPIStorage, StorageFromSubframe)
{
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 *contentScript = Util::constructScript(@[
@"(async () => {",
@" await browser.storage.local.set({ key: 'value' })",
@" const result = await browser.storage.local.get('key')",
@" browser.test.assertEq(result?.key, 'value', 'Stored value should be retrievable')",
@" browser.test.notifyPass()",
@"})()"
]);
auto *manifest = @{
@"manifest_version": @3,
@"name": @"Test",
@"description": @"Test",
@"version": @"1",
@"permissions": @[ @"storage" ],
@"content_scripts": @[@{
@"matches": @[ @"*://127.0.0.1/*" ],
@"js": @[ @"content.js" ],
@"all_frames": @YES
}]
};
auto *resources = @{
@"content.js": contentScript
};
auto manager = Util::loadExtension(manifest, resources);
[manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forURL:urlRequestSubframe.URL];
[manager.get().defaultTab.webView loadRequest:urlRequestMain];
[manager run];
}
} // namespace TestWebKitAPI
#endif // ENABLE(WK_WEB_EXTENSIONS)