summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/cookie-store
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/cookie-store
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/cookie-store')
-rw-r--r--testing/web-platform/tests/cookie-store/META.yml4
-rw-r--r--testing/web-platform/tests/cookie-store/README.md28
-rw-r--r--testing/web-platform/tests/cookie-store/change_eventhandler_for_document_cookie.https.window.js160
-rw-r--r--testing/web-platform/tests/cookie-store/change_eventhandler_for_http_cookie_and_set_cookie_headers.https.window.js206
-rw-r--r--testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_and_no_value.https.window.js58
-rw-r--r--testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_equals_in_value.https.window.js46
-rw-r--r--testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_multiple_values.https.window.js41
-rw-r--r--testing/web-platform/tests/cookie-store/cookieListItem_attributes.https.any.js197
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_empty.https.any.js28
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_multiple.https.any.js77
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_single.https.any.js50
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_delete_arguments.https.any.js171
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_delete_basic.https.any.js13
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_event_arguments.https.window.js65
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_event_basic.https.window.js24
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_event_delete.https.window.js22
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_event_overwrite.https.window.js22
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_getAll_arguments.https.any.js149
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_getAll_multiple.https.any.js29
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_getAll_set_basic.https.any.js16
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_get_arguments.https.any.js102
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_get_delete_basic.https.any.js14
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_get_set_across_frames.https.html46
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_get_set_across_origins.sub.https.html66
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_get_set_basic.https.any.js15
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_get_set_ordering.https.any.js42
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_in_detached_frame.https.html19
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_opaque_origin.https.html73
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_set_arguments.https.any.js311
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_special_names.https.any.js64
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_subscribe_arguments.https.any.js142
-rw-r--r--testing/web-platform/tests/cookie-store/cookieStore_subscriptions_empty.https.window.js13
-rw-r--r--testing/web-platform/tests/cookie-store/encoding.https.any.js19
-rw-r--r--testing/web-platform/tests/cookie-store/httponly_cookies.https.window.js69
-rw-r--r--testing/web-platform/tests/cookie-store/idlharness.tentative.https.any.js45
-rw-r--r--testing/web-platform/tests/cookie-store/resources/always_changing_sw.sub.js6
-rw-r--r--testing/web-platform/tests/cookie-store/resources/cookie-test-helpers.js226
-rw-r--r--testing/web-platform/tests/cookie-store/resources/cookie_helper.py84
-rw-r--r--testing/web-platform/tests/cookie-store/resources/empty_sw.js1
-rw-r--r--testing/web-platform/tests/cookie-store/resources/helper_iframe.sub.html31
-rw-r--r--testing/web-platform/tests/cookie-store/resources/helpers.js72
-rw-r--r--testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.https.sub.html41
-rw-r--r--testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.js14
-rw-r--r--testing/web-platform/tests/cookie-store/serviceworker_cookieStore_subscriptions_reset.https.html55
-rw-r--r--testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_mismatched_subscription.https.any.js44
-rw-r--r--testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_multiple_subscriptions.https.any.js68
-rw-r--r--testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_overlapping_subscriptions.https.any.js87
-rw-r--r--testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_single_subscription.https.any.js39
-rw-r--r--testing/web-platform/tests/cookie-store/serviceworker_oncookiechange_eventhandler_single_subscription.https.any.js39
49 files changed, 3253 insertions, 0 deletions
diff --git a/testing/web-platform/tests/cookie-store/META.yml b/testing/web-platform/tests/cookie-store/META.yml
new file mode 100644
index 0000000000..68c3002455
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/META.yml
@@ -0,0 +1,4 @@
+spec: https://wicg.github.io/cookie-store/
+suggested_reviewers:
+ - inexorabletash
+ - ayuishii
diff --git a/testing/web-platform/tests/cookie-store/README.md b/testing/web-platform/tests/cookie-store/README.md
new file mode 100644
index 0000000000..b8a1d0a609
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/README.md
@@ -0,0 +1,28 @@
+This directory contains tests for the
+[Cookie Store API](https://github.com/WICG/cookie-store).
+
+## Note on cookie naming conventions
+
+A simple origin cookie is a cookie named with the `__Host-` prefix
+which is always secure-flagged, always implicit-domain, always
+`/`-scoped, and hence always unambiguous in the cookie jar serialization
+and origin-scoped. It can be treated as a simple key/value pair.
+
+`"LEGACY"` in a cookie name here means it is an old-style unprefixed
+cookie name, so you can't tell e.g. whether it is Secure-flagged or
+`/`-pathed just by looking at it, and its flags, domain and path may
+vary even in a single cookie jar serialization leading to apparent
+duplicate entries, ambiguities, and complexity (i.e. it cannot be
+treated as a simple key/value pair.)
+
+Cookie names used in the tests are intended to be
+realistic. Traditional session cookie names are typically
+all-upper-case for broad framework compatibility. The more modern
+`"__Host-"` prefix has only one allowed casing. An expected upgrade
+path from traditional "legacy" cookie names to simple origin cookie
+names is simply to prefix the traditional name with the `"__Host-"`
+prefix.
+
+Many of the used cookie names are non-ASCII to ensure
+straightforward internationalization is possible at every API surface.
+These work in many modern browsers, though not yet all of them.
diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_document_cookie.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_document_cookie.https.window.js
new file mode 100644
index 0000000000..0a8b1bd21e
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_document_cookie.https.window.js
@@ -0,0 +1,160 @@
+// META: title=Cookie Store API: Observing 'change' events in document when cookies set via document.cookie
+// META: script=resources/cookie-test-helpers.js
+
+'use strict';
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringDocument('DOCUMENT-cookie=value; path=/');
+ assert_equals(
+ await getCookieString(),
+ 'DOCUMENT-cookie=value',
+ 'Cookie we wrote using document.cookie in cookie jar');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'DOCUMENT-cookie=value',
+ 'Cookie we wrote using document.cookie in HTTP cookie jar');
+ assert_equals(
+ await getCookieStringDocument(),
+ 'DOCUMENT-cookie=value',
+ 'Cookie we wrote using document.cookie in document.cookie');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'value'}]},
+ 'Cookie we wrote using document.cookie is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringDocument('DOCUMENT-cookie=new-value; path=/');
+ assert_equals(
+ await getCookieString(),
+ 'DOCUMENT-cookie=new-value',
+ 'Cookie we overwrote using document.cookie in cookie jar');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'DOCUMENT-cookie=new-value',
+ 'Cookie we overwrote using document.cookie in HTTP cookie jar');
+ assert_equals(
+ await getCookieStringDocument(),
+ 'DOCUMENT-cookie=new-value',
+ 'Cookie we overwrote using document.cookie in document.cookie');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'new-value'}]},
+ 'Cookie we overwrote using document.cookie is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringDocument('DOCUMENT-cookie=DELETED; path=/; max-age=0');
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Empty cookie jar after document.cookie' +
+ ' cookie-clearing using max-age=0');
+ assert_equals(
+ await getCookieStringHttp(),
+ undefined,
+ 'Empty HTTP cookie jar after document.cookie' +
+ ' cookie-clearing using max-age=0');
+ assert_equals(
+ await getCookieStringDocument(),
+ undefined,
+ 'Empty document.cookie cookie jar after document.cookie' +
+ ' cookie-clearing using max-age=0');
+ await verifyCookieChangeEvent(
+ eventPromise, {deleted: [{name: 'DOCUMENT-cookie'}]},
+ 'Deletion observed after document.cookie cookie-clearing' +
+ ' using max-age=0');
+}, 'document.cookie set/overwrite/delete observed by CookieStore');
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.set('DOCUMENT-cookie', 'value');
+ assert_equals(
+ await getCookieString(),
+ 'DOCUMENT-cookie=value',
+ 'Cookie we wrote using CookieStore in cookie jar');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'DOCUMENT-cookie=value',
+ 'Cookie we wrote using CookieStore in HTTP cookie jar');
+ assert_equals(
+ await getCookieStringDocument(),
+ 'DOCUMENT-cookie=value',
+ 'Cookie we wrote using CookieStore in document.cookie');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'value'}]},
+ 'Cookie we wrote using CookieStore is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.set('DOCUMENT-cookie', 'new-value');
+ assert_equals(
+ await getCookieString(),
+ 'DOCUMENT-cookie=new-value',
+ 'Cookie we overwrote using CookieStore in cookie jar');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'DOCUMENT-cookie=new-value',
+ 'Cookie we overwrote using CookieStore in HTTP cookie jar');
+ assert_equals(
+ await getCookieStringDocument(),
+ 'DOCUMENT-cookie=new-value',
+ 'Cookie we overwrote using CookieStore in document.cookie');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'DOCUMENT-cookie', value: 'new-value'}]},
+ 'Cookie we overwrote using CookieStore is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.delete('DOCUMENT-cookie');
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Empty cookie jar after CookieStore delete');
+ assert_equals(
+ await getCookieStringHttp(),
+ undefined,
+ 'Empty HTTP cookie jar after CookieStore delete');
+ assert_equals(
+ await getCookieStringDocument(),
+ undefined,
+ 'Empty document.cookie cookie jar after CookieStore delete');
+ await verifyCookieChangeEvent(
+ eventPromise, {deleted: [{name: 'DOCUMENT-cookie'}]},
+ 'Deletion observed after CookieStore delete');
+}, 'CookieStore set/overwrite/delete observed by document.cookie');
+
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringDocument('DOCUMENT-🍪=🔵; path=/');
+ assert_equals(
+ await getCookieString(),
+ 'DOCUMENT-🍪=🔵',
+ 'Cookie we wrote using document.cookie in cookie jar');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'DOCUMENT-🍪', value: '🔵'}]},
+ 'Cookie we wrote using document.cookie is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringDocument('DOCUMENT-🍪=DELETED; path=/; max-age=0');
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Empty cookie jar after document.cookie' +
+ ' cookie-clearing using max-age=0');
+ await verifyCookieChangeEvent(
+ eventPromise, {deleted: [{name: 'DOCUMENT-🍪'}]},
+ 'Deletion observed after document.cookie cookie-clearing' +
+ ' using max-age=0');
+}, 'CookieStore agrees with document.cookie on encoding non-ASCII cookies');
+
+
+cookie_test(async t => {
+ await cookieStore.set('DOCUMENT-🍪', '🔵');
+ assert_equals(
+ await getCookieStringDocument(),
+ 'DOCUMENT-🍪=🔵',
+ 'Cookie we wrote using CookieStore in document.cookie');
+
+ await cookieStore.delete('DOCUMENT-🍪');
+ assert_equals(
+ await getCookieStringDocument(),
+ undefined,
+ 'Empty cookie jar after CookieStore delete');
+}, 'document.cookie agrees with CookieStore on encoding non-ASCII cookies');
diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_http_cookie_and_set_cookie_headers.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_http_cookie_and_set_cookie_headers.https.window.js
new file mode 100644
index 0000000000..2028df5b4b
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_http_cookie_and_set_cookie_headers.https.window.js
@@ -0,0 +1,206 @@
+// META: title=Cookie Store API: Observing 'change' events in document when cookies set via Set-Cookie header
+// META: script=resources/cookie-test-helpers.js
+
+'use strict';
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringHttp('HTTP-cookie=value; path=/');
+ assert_equals(
+ await getCookieString(),
+ 'HTTP-cookie=value',
+ 'Cookie we wrote using HTTP in cookie jar');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'HTTP-cookie=value',
+ 'Cookie we wrote using HTTP in HTTP cookie jar');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'HTTP-cookie', value: 'value'}]},
+ 'Cookie we wrote using HTTP is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringHttp('HTTP-cookie=new-value; path=/');
+ assert_equals(
+ await getCookieString(),
+ 'HTTP-cookie=new-value',
+ 'Cookie we overwrote using HTTP in cookie jar');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'HTTP-cookie=new-value',
+ 'Cookie we overwrote using HTTP in HTTP cookie jar');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'HTTP-cookie', value: 'new-value'}]},
+ 'Cookie we overwrote using HTTP is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringHttp('HTTP-cookie=DELETED; path=/; max-age=0');
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Empty cookie jar after HTTP cookie-clearing using max-age=0');
+ assert_equals(
+ await getCookieStringHttp(),
+ undefined,
+ 'Empty HTTP cookie jar after HTTP cookie-clearing using max-age=0');
+ await verifyCookieChangeEvent(
+ eventPromise, {deleted: [{name: 'HTTP-cookie'}]},
+ 'Deletion observed after HTTP cookie-clearing using max-age=0');
+}, 'HTTP set/overwrite/delete observed in CookieStore');
+
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringHttp('HTTP-🍪=🔵; path=/');
+ assert_equals(
+ await getCookieString(),
+ 'HTTP-🍪=🔵',
+ 'Cookie we wrote using HTTP in cookie jar');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'HTTP-🍪', value: '🔵'}]},
+ 'Cookie we wrote using HTTP is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringHttp('HTTP-🍪=DELETED; path=/; max-age=0');
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Empty cookie jar after HTTP cookie-clearing using max-age=0');
+ await verifyCookieChangeEvent(
+ eventPromise, {deleted: [{name: 'HTTP-🍪'}]},
+ 'Deletion observed after HTTP cookie-clearing using max-age=0');
+
+}, 'CookieStore agreed with HTTP headers agree on encoding non-ASCII cookies');
+
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.set('TEST', 'value0');
+ assert_equals(
+ await getCookieString(),
+ 'TEST=value0',
+ 'Cookie jar contains only cookie we set');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'TEST=value0',
+ 'HTTP cookie jar contains only cookie we set');
+ await verifyCookieChangeEvent(
+ eventPromise,
+ {changed: [{name: 'TEST', value: 'value0'}]},
+ 'Observed value that was set');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.set('TEST', 'value');
+ assert_equals(
+ await getCookieString(),
+ 'TEST=value',
+ 'Cookie jar contains only cookie we set');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'TEST=value',
+ 'HTTP cookie jar contains only cookie we set');
+ await verifyCookieChangeEvent(
+ eventPromise,
+ {changed: [{name: 'TEST', value: 'value'}]},
+ 'Observed value that was overwritten');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.delete('TEST');
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Cookie jar does not contain cookie we deleted');
+ assert_equals(
+ await getCookieStringHttp(),
+ undefined,
+ 'HTTP cookie jar does not contain cookie we deleted');
+ await verifyCookieChangeEvent(
+ eventPromise,
+ {deleted: [{name: 'TEST'}]},
+ 'Observed cookie that was deleted');
+}, 'CookieStore set/overwrite/delete observed in HTTP headers');
+
+
+cookie_test(async t => {
+ await cookieStore.set('🍪', '🔵');
+ assert_equals(
+ await getCookieStringHttp(),
+ '🍪=🔵',
+ 'HTTP cookie jar contains only cookie we set');
+
+ await cookieStore.delete('🍪');
+ assert_equals(
+ await getCookieStringHttp(),
+ undefined,
+ 'HTTP cookie jar does not contain cookie we deleted');
+}, 'HTTP headers agreed with CookieStore on encoding non-ASCII cookies');
+
+
+cookie_test(async t => {
+ // Non-UTF-8 byte sequences cause the Set-Cookie to be dropped.
+ let eventPromise = observeNextCookieChangeEvent();
+ await setCookieBinaryHttp(
+ unescape(encodeURIComponent('HTTP-cookie=value')) + '\xef\xbf\xbd; path=/');
+ assert_equals(
+ await getCookieString(),
+ 'HTTP-cookie=value\ufffd',
+ 'Binary cookie we wrote using HTTP in cookie jar');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'HTTP-cookie=value\ufffd',
+ 'Binary cookie we wrote using HTTP in HTTP cookie jar');
+ assert_equals(
+ decodeURIComponent(escape(await getCookieBinaryHttp())),
+ 'HTTP-cookie=value\ufffd',
+ 'Binary cookie we wrote in binary HTTP cookie jar');
+ assert_equals(
+ await getCookieBinaryHttp(),
+ unescape(encodeURIComponent('HTTP-cookie=value')) + '\xef\xbf\xbd',
+ 'Binary cookie we wrote in binary HTTP cookie jar');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'HTTP-cookie', value: 'value\ufffd'}]},
+ 'Binary cookie we wrote using HTTP is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await setCookieBinaryHttp(
+ unescape(encodeURIComponent('HTTP-cookie=new-value')) + '\xef\xbf\xbd; path=/');
+ assert_equals(
+ await getCookieString(),
+ 'HTTP-cookie=new-value\ufffd',
+ 'Binary cookie we overwrote using HTTP in cookie jar');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'HTTP-cookie=new-value\ufffd',
+ 'Binary cookie we overwrote using HTTP in HTTP cookie jar');
+ assert_equals(
+ decodeURIComponent(escape(await getCookieBinaryHttp())),
+ 'HTTP-cookie=new-value\ufffd',
+ 'Binary cookie we overwrote in binary HTTP cookie jar');
+ assert_equals(
+ await getCookieBinaryHttp(),
+ unescape(encodeURIComponent('HTTP-cookie=new-value')) + '\xef\xbf\xbd',
+ 'Binary cookie we overwrote in binary HTTP cookie jar');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'HTTP-cookie', value: 'new-value\ufffd'}]},
+ 'Binary cookie we overwrote using HTTP is observed');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await setCookieBinaryHttp(
+ unescape(encodeURIComponent('HTTP-cookie=DELETED; path=/; max-age=0')));
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Empty cookie jar after binary HTTP cookie-clearing using max-age=0');
+ assert_equals(
+ await getCookieStringHttp(),
+ undefined,
+ 'Empty HTTP cookie jar after' +
+ ' binary HTTP cookie-clearing using max-age=0');
+ assert_equals(
+ await getCookieBinaryHttp(),
+ undefined,
+ 'Empty binary HTTP cookie jar after' +
+ ' binary HTTP cookie-clearing using max-age=0');
+ await verifyCookieChangeEvent(
+ eventPromise, {deleted: [{name: 'HTTP-cookie'}]},
+ 'Deletion observed after binary HTTP cookie-clearing using max-age=0');
+}, 'Binary HTTP set/overwrite/delete observed in CookieStore');
diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_and_no_value.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_and_no_value.https.window.js
new file mode 100644
index 0000000000..4498caf596
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_and_no_value.https.window.js
@@ -0,0 +1,58 @@
+// META: title=Cookie Store API: Observing 'change' events in document when modifications API is called with blank arguments
+// META: script=resources/cookie-test-helpers.js
+
+'use strict';
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.set('', 'first-value');
+ const actual1 =
+ (await cookieStore.getAll('')).map(({ value }) => value).join(';');
+ const expected1 = 'first-value';
+ assert_equals(actual1, expected1);
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: '', value: 'first-value'}]},
+ 'Observed no-name change');
+
+ await promise_rejects_js(
+ t,
+ TypeError,
+ cookieStore.set('', ''),
+ 'Expected promise rejection when setting a cookie with' +
+ ' no name and no value');
+
+ await promise_rejects_js(
+ t,
+ TypeError,
+ cookieStore.set({name: '', value: ''}),
+ 'Expected promise rejection when setting a cookie with' +
+ ' no name and no value');
+
+ const cookies = await cookieStore.getAll('');
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, '');
+ assert_equals(cookies[0].value, 'first-value',
+ 'Cookie with no name should still have previous value.');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.delete('');
+ await verifyCookieChangeEvent(
+ eventPromise, {deleted: [{name: ''}]},
+ 'Observed no-name deletion');
+
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Empty cookie jar');
+ assert_equals(
+ await getCookieStringHttp(),
+ undefined,
+ 'Empty HTTP cookie jar');
+ if (kHasDocument) {
+ assert_equals(
+ await getCookieStringDocument(),
+ undefined,
+ 'Empty document.cookie cookie jar');
+ }
+
+}, 'Verify behavior of no-name and no-value cookies.');
diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_equals_in_value.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_equals_in_value.https.window.js
new file mode 100644
index 0000000000..13d721786c
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_equals_in_value.https.window.js
@@ -0,0 +1,46 @@
+// META: title=Cookie Store API: Observing 'change' events in document when setting a cookie value containing "="
+// META: script=resources/cookie-test-helpers.js
+
+'use strict';
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.set('', 'first-value');
+ const initialCookies = await cookieStore.getAll('');
+ assert_equals(initialCookies.length, 1);
+ assert_equals(initialCookies[0].name, '');
+ assert_equals(initialCookies[0].value, 'first-value');
+
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: '', value: 'first-value'}]},
+ 'Observed no-name change');
+
+ await promise_rejects_js(
+ t,
+ TypeError,
+ cookieStore.set('', 'suspicious-value=resembles-name-and-value'),
+ 'Expected promise rejection when setting a cookie with' +
+ ' no name and "=" in value (via arguments)');
+
+ await promise_rejects_js(
+ t,
+ TypeError,
+ cookieStore.set(
+ {name: '', value: 'suspicious-value=resembles-name-and-value'}),
+ 'Expected promise rejection when setting a cookie with' +
+ ' no name and "=" in value (via options)');
+
+ const cookies = await cookieStore.getAll('');
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, '');
+ assert_equals(cookies[0].value, 'first-value',
+ 'Cookie with no name should still have previous value.');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.delete('');
+ await verifyCookieChangeEvent(
+ eventPromise, {deleted: [{name: ''}]},
+ 'Observed no-name deletion');
+
+}, "Verify that attempting to set a cookie with no name and with '=' in" +
+ " the value does not work.");
diff --git a/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_multiple_values.https.window.js b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_multiple_values.https.window.js
new file mode 100644
index 0000000000..60c6c16518
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/change_eventhandler_for_no_name_multiple_values.https.window.js
@@ -0,0 +1,41 @@
+// META: title=Cookie Store API: Observing 'change' events in document when modifications API is called multiple times with a blank name
+// META: script=resources/cookie-test-helpers.js
+
+'use strict';
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.set('', 'first-value');
+ let actual1 =
+ (await cookieStore.getAll('')).map(({ value }) => value).join(';');
+ let expected1 = 'first-value';
+ assert_equals(actual1, expected1);
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: '', value: 'first-value'}]},
+ 'Observed no-name change');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.set('', 'second-value');
+ let actual2 =
+ (await cookieStore.getAll('')).map(({ value }) => value).join(';');
+ let expected2 = 'second-value';
+ assert_equals(actual2, expected2);
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: '', value: 'second-value'}]},
+ 'Observed no-name change');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await cookieStore.delete('');
+ await verifyCookieChangeEvent(
+ eventPromise, {deleted: [{name: ''}]},
+ 'Observed no-name change');
+
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Empty cookie jar after testNoNameMultipleValues');
+ assert_equals(
+ await getCookieStringHttp(),
+ undefined,
+ 'Empty HTTP cookie jar after testNoNameMultipleValues');
+}, 'Verify behavior of multiple no-name cookies');
diff --git a/testing/web-platform/tests/cookie-store/cookieListItem_attributes.https.any.js b/testing/web-platform/tests/cookie-store/cookieListItem_attributes.https.any.js
new file mode 100644
index 0000000000..200cbd0692
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieListItem_attributes.https.any.js
@@ -0,0 +1,197 @@
+// META: title=Cookie Store API: cookieListItem attributes
+// META: global=window,serviceworker
+
+'use strict';
+
+const kCurrentHostname = (new URL(self.location.href)).hostname;
+
+const kOneDay = 24 * 60 * 60 * 1000;
+const kFourHundredDays = 400 * kOneDay;
+const kTenYears = 10 * 365 * kOneDay;
+const kFourHundredDaysFromNow = Date.now() + kFourHundredDays;
+const kTenYearsFromNow = Date.now() + kTenYears;
+
+const kCookieListItemKeys =
+ ['domain', 'expires', 'name', 'path', 'sameSite', 'secure', 'value'].sort();
+
+promise_test(async testCase => {
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+ assert_equals(cookie.domain, null);
+ assert_equals(cookie.path, '/');
+ assert_equals(cookie.expires, null);
+ assert_equals(cookie.secure, true);
+ assert_equals(cookie.sameSite, 'strict');
+ const itemKeys = Object.keys(cookie);
+ for (const key of kCookieListItemKeys) {
+ assert_in_array(key, itemKeys);
+ }
+}, 'CookieListItem - cookieStore.set defaults with positional name and value');
+
+promise_test(async testCase => {
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set({ name: 'cookie-name', value: 'cookie-value' });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+ assert_equals(cookie.domain, null);
+ assert_equals(cookie.path, '/');
+ assert_equals(cookie.expires, null);
+ assert_equals(cookie.secure, true);
+ assert_equals(cookie.sameSite, 'strict');
+ const itemKeys = Object.keys(cookie);
+ for (const key of kCookieListItemKeys) {
+ assert_in_array(key, itemKeys);
+ }
+}, 'CookieListItem - cookieStore.set defaults with name and value in options');
+
+promise_test(async testCase => {
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set({ name: 'cookie-name', value: 'cookie-value',
+ expires: kTenYearsFromNow });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+ assert_equals(cookie.domain, null);
+ assert_equals(cookie.path, '/');
+ assert_approx_equals(cookie.expires, kFourHundredDaysFromNow, kOneDay);
+ assert_equals(cookie.secure, true);
+ assert_equals(cookie.sameSite, 'strict');
+ const itemKeys = Object.keys(cookie);
+ for (const key of kCookieListItemKeys) {
+ assert_in_array(key, itemKeys);
+ }
+}, 'CookieListItem - cookieStore.set with expires set to a timestamp 10 ' +
+ 'years in the future');
+
+promise_test(async testCase => {
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set({ name: 'cookie-name', value: 'cookie-value',
+ expires: new Date(kTenYearsFromNow) });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+ assert_equals(cookie.domain, null);
+ assert_equals(cookie.path, '/');
+ assert_approx_equals(cookie.expires, kFourHundredDaysFromNow, kOneDay);
+ assert_equals(cookie.secure, true);
+}, 'CookieListItem - cookieStore.set with expires set to a Date 10 ' +
+ 'years in the future');
+
+promise_test(async testCase => {
+ await cookieStore.delete({ name: 'cookie-name', domain: kCurrentHostname });
+
+ await cookieStore.set({ name: 'cookie-name', value: 'cookie-value',
+ domain: kCurrentHostname });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', domain: kCurrentHostname });
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+ assert_equals(cookie.domain, kCurrentHostname);
+ assert_equals(cookie.path, '/');
+ assert_equals(cookie.expires, null);
+ assert_equals(cookie.secure, true);
+ assert_equals(cookie.sameSite, 'strict');
+ const itemKeys = Object.keys(cookie);
+ for (const key of kCookieListItemKeys) {
+ assert_in_array(key, itemKeys);
+ }
+}, 'CookieListItem - cookieStore.set with domain set to the current hostname');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory =
+ currentPath.substr(0, currentPath.lastIndexOf('/') + 1);
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+
+ await cookieStore.set({ name: 'cookie-name', value: 'cookie-value',
+ path: currentDirectory });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+ assert_equals(cookie.domain, null);
+ assert_equals(cookie.path, currentDirectory);
+ assert_equals(cookie.expires, null);
+ assert_equals(cookie.secure, true);
+ assert_equals(cookie.sameSite, 'strict');
+ const itemKeys = Object.keys(cookie);
+ for (const key of kCookieListItemKeys) {
+ assert_in_array(key, itemKeys);
+ }
+}, 'CookieListItem - cookieStore.set with path set to the current directory');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory = currentPath.substr(0, currentPath.lastIndexOf('/'));
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+
+ await cookieStore.set({ name: 'cookie-name', value: 'cookie-value',
+ path: currentDirectory });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+ assert_equals(cookie.domain, null);
+ assert_equals(cookie.path, currentDirectory + '/');
+ assert_equals(cookie.expires, null);
+ assert_equals(cookie.secure, true);
+ assert_equals(cookie.sameSite, 'strict');
+ const itemKeys = Object.keys(cookie);
+ for (const key of kCookieListItemKeys) {
+ assert_in_array(key, itemKeys);
+ }
+}, 'CookieListItem - cookieStore.set adds / to path if it does not end with /');
+
+['strict', 'lax', 'none'].forEach(sameSiteValue => {
+ promise_test(async testCase => {
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set({
+ name: 'cookie-name', value: 'cookie-value', sameSite: sameSiteValue });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+ assert_equals(cookie.domain, null);
+ assert_equals(cookie.path, '/');
+ assert_equals(cookie.expires, null);
+ assert_equals(cookie.secure, true);
+ assert_equals(cookie.sameSite, sameSiteValue);
+ const itemKeys = Object.keys(cookie);
+ for (const key of kCookieListItemKeys) {
+ assert_in_array(key, itemKeys);
+ }
+ }, `CookieListItem - cookieStore.set with sameSite set to ${sameSiteValue}`);
+
+});
diff --git a/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_empty.https.any.js b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_empty.https.any.js
new file mode 100644
index 0000000000..8cfd732f30
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_empty.https.any.js
@@ -0,0 +1,28 @@
+// META: title=Cookie Store API: ServiceWorker without cookie change subscriptions
+// META: global=window,serviceworker
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+
+'use strict';
+
+promise_test(async testCase => {
+ if (self.GLOBAL.isWindow()) {
+ const registration = await service_worker_unregister_and_register(
+ testCase, 'resources/empty_sw.js', 'resources/does/not/exist');
+ testCase.add_cleanup(() => registration.unregister());
+
+ // Wait for this service worker to become active before snapshotting the
+ // subscription state, for consistency with other tests.
+ await wait_for_state(testCase, registration.installing, 'activated');
+
+ self.registration = registration;
+ } else {
+ // Wait for this service worker to become active before snapshotting the
+ // subscription state, for consistency with other tests.
+ await new Promise(resolve => {
+ self.addEventListener('activate', event => { resolve(); });
+ });
+ }
+
+ const subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(subscriptions.length, 0);
+}, 'getSubscriptions returns an empty array when there are no subscriptions');
diff --git a/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_multiple.https.any.js b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_multiple.https.any.js
new file mode 100644
index 0000000000..9e153d03aa
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_multiple.https.any.js
@@ -0,0 +1,77 @@
+// META: title=Cookie Store API: ServiceWorker with multiple cookie change subscriptions
+// META: global=window,serviceworker
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+
+'use strict';
+
+// sort() comparator that uses the < operator.
+//
+// This is intended to be used for sorting strings. Using < is preferred to
+// localeCompare() because the latter has some implementation-dependent
+// behavior.
+function CompareStrings(a, b) {
+ return a < b ? -1 : (b < a ? 1 : 0);
+}
+
+promise_test(async testCase => {
+ let scope;
+
+ if (self.GLOBAL.isWindow()) {
+ scope = '/cookie-store/resources/does/not/exist';
+
+ const registration = await service_worker_unregister_and_register(
+ testCase, 'resources/empty_sw.js', scope);
+ testCase.add_cleanup(() => registration.unregister());
+
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await wait_for_state(testCase, registration.installing, 'activated');
+
+ self.registration = registration;
+ } else {
+ scope = '/cookie-store/does/not/exist';
+
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await new Promise(resolve => {
+ self.addEventListener('activate', event => { resolve(); });
+ });
+ }
+
+ {
+ const subscriptions = [{ name: 'cookie-name1', url: `${scope}/path1` }];
+ await registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => {
+ // For non-ServiceWorker environments, registration.unregister() cleans up
+ // cookie subscriptions.
+ if (self.GLOBAL.isWorker()) {
+ return registration.cookies.unsubscribe(subscriptions);
+ }
+ });
+ }
+ {
+ const subscriptions = [
+ { }, // Test the default values for subscription properties.
+ { name: 'cookie-prefix' },
+ ];
+ await registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => {
+ // For non-ServiceWorker environments, registration.unregister() cleans up
+ // cookie subscriptions.
+ if (self.GLOBAL.isWorker()) {
+ return registration.cookies.unsubscribe(subscriptions);
+ }
+ });
+ }
+
+ const subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(subscriptions.length, 3);
+
+ subscriptions.sort((a, b) => CompareStrings(`${a.name}`, `${b.name}`));
+
+ assert_equals(subscriptions[0].name, 'cookie-name1');
+
+ assert_equals(subscriptions[1].name, 'cookie-prefix');
+
+ assert_false('name' in subscriptions[2]);
+}, 'getSubscriptions returns a subscription passed to subscribe');
diff --git a/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_single.https.any.js b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_single.https.any.js
new file mode 100644
index 0000000000..98ec19df3f
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStoreManager_getSubscriptions_single.https.any.js
@@ -0,0 +1,50 @@
+// META: title=Cookie Store API: ServiceWorker with one cookie change subscription
+// META: global=window,serviceworker
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+
+'use strict';
+
+promise_test(async testCase => {
+ let scope;
+
+ if (self.GLOBAL.isWindow()) {
+ scope = '/cookie-store/resources/does/not/exist';
+
+ const registration = await service_worker_unregister_and_register(
+ testCase, 'resources/empty_sw.js', scope);
+ testCase.add_cleanup(() => registration.unregister());
+
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await wait_for_state(testCase, registration.installing, 'activated');
+
+ self.registration = registration;
+ } else {
+ scope = '/cookie-store/does/not/exist';
+
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await new Promise(resolve => {
+ self.addEventListener('activate', event => { resolve(); });
+ });
+ }
+
+ {
+ const subscriptions = [{ name: 'cookie-name', url: `${scope}/path` }];
+ await registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => {
+ // For non-ServiceWorker environments, registration.unregister() cleans up
+ // cookie subscriptions.
+ if (self.GLOBAL.isWorker()) {
+ return registration.cookies.unsubscribe(subscriptions);
+ }
+ });
+ }
+
+ const subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(subscriptions.length, 1);
+
+ assert_equals(subscriptions[0].name, 'cookie-name');
+ assert_equals(subscriptions[0].url,
+ (new URL(`${scope}/path`, self.location.href)).href);
+}, 'getSubscriptions returns a subscription passed to subscribe');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_delete_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_delete_arguments.https.any.js
new file mode 100644
index 0000000000..ddae23888f
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_delete_arguments.https.any.js
@@ -0,0 +1,171 @@
+// META: title=Cookie Store API: cookieStore.delete() arguments
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+
+ await cookieStore.delete('cookie-name');
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.delete with positional name');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ await cookieStore.delete({ name: 'cookie-name' });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.delete with name in options');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value',
+ domain: `.${currentDomain}` }));
+}, 'cookieStore.delete domain starts with "."');
+
+promise_test(async testCase => {
+ await promise_rejects_js(testCase, TypeError, cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', domain: 'example.com' }));
+}, 'cookieStore.delete with domain that is not equal current host');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', domain: currentDomain });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', domain: currentDomain });
+ });
+
+ await cookieStore.delete({ name: 'cookie-name', domain: currentDomain });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.delete with domain set to the current hostname');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+ const subDomain = `sub.${currentDomain}`;
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.delete(
+ { name: 'cookie-name', domain: subDomain }));
+}, 'cookieStore.delete with domain set to a subdomain of the current hostname');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+ assert_not_equals(currentDomain[0] === '.',
+ 'this test assumes that the current hostname does not start with .');
+ const domainSuffix = currentDomain.substr(1);
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.delete(
+ { name: 'cookie-name', domain: domainSuffix }));
+}, 'cookieStore.delete with domain set to a non-domain-matching suffix of ' +
+ 'the current hostname');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory =
+ currentPath.substr(0, currentPath.lastIndexOf('/') + 1);
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', path: currentDirectory });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ });
+
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.delete with path set to the current directory');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory =
+ currentPath.substr(0, currentPath.lastIndexOf('/') + 1);
+ const subDirectory = currentDirectory + "subdir/";
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', path: currentDirectory });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ });
+
+ await cookieStore.delete({ name: 'cookie-name', path: subDirectory });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.delete with path set to subdirectory of the current directory');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory = currentPath.substr(0, currentPath.lastIndexOf('/'));
+ await cookieStore.set(
+ { name: 'cookie-name',
+ value: 'cookie-value',
+ path: currentDirectory + '/' });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ });
+
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.delete with missing / at the end of path');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory =
+ currentPath.substr(0, currentPath.lastIndexOf('/') + 1);
+ const invalidPath = currentDirectory.substr(1);
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.delete(
+ { name: 'cookie-name', path: invalidPath }));
+}, 'cookieStore.delete with path that does not start with /');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const cookie_attributes = await cookieStore.get('cookie-name');
+ assert_equals(cookie_attributes.name, 'cookie-name');
+ assert_equals(cookie_attributes.value, 'cookie-value');
+
+ await cookieStore.delete(cookie_attributes);
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.delete with get result');
+
+promise_test(async testCase => {
+ await cookieStore.set('', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('');
+ });
+
+ await cookieStore.delete('');
+ const cookie = await cookieStore.get('');
+ assert_equals(cookie, null);
+}, 'cookieStore.delete with positional empty name');
+
+promise_test(async testCase => {
+ await cookieStore.set('', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('');
+ });
+
+ await cookieStore.delete({ name: '' });
+ const cookie = await cookieStore.get('');
+ assert_equals(cookie, null);
+}, 'cookieStore.delete with empty name in options');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_delete_basic.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_delete_basic.https.any.js
new file mode 100644
index 0000000000..08a1fac5af
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_delete_basic.https.any.js
@@ -0,0 +1,13 @@
+// META: title=Cookie Store API: cookieStore.delete() return type
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async testCase => {
+ const p = cookieStore.delete('cookie-name');
+ assert_true(p instanceof Promise,
+ 'cookieStore.delete() returns a promise');
+ const result = await p;
+ assert_equals(result, undefined,
+ 'cookieStore.delete() promise resolves to undefined');
+}, 'cookieStore.delete return type is Promise<void>');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_event_arguments.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_event_arguments.https.window.js
new file mode 100644
index 0000000000..bcb698eeb0
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_event_arguments.https.window.js
@@ -0,0 +1,65 @@
+'use strict';
+
+test(() => {
+ const event = new CookieChangeEvent('change');
+ assert_true(event instanceof CookieChangeEvent);
+ assert_equals(event.type, 'change');
+ assert_equals(event.changed.length, 0);
+ assert_equals(event.deleted.length, 0);
+}, 'CookieChangeEvent construction with default arguments');
+
+test(() => {
+ const event = new CookieChangeEvent('change', {
+ changed: [
+ { name: 'changed-name1', value: 'changed-value1' },
+ { name: 'changed-name2', value: 'changed-value2' },
+ ],
+ });
+ assert_true(event instanceof CookieChangeEvent);
+ assert_equals(event.type, 'change');
+ assert_equals(event.changed.length, 2);
+ assert_equals(event.changed[0].name, 'changed-name1');
+ assert_equals(event.changed[0].value, 'changed-value1');
+ assert_equals(event.changed[1].name, 'changed-name2');
+ assert_equals(event.changed[1].value, 'changed-value2');
+ assert_equals(event.deleted.length, 0);
+}, 'CookieChangeEvent construction with changed cookie list');
+
+test(() => {
+ const event = new CookieChangeEvent('change', {
+ deleted: [
+ { name: 'deleted-name1', value: 'deleted-value1' },
+ { name: 'deleted-name2', value: 'deleted-value2' },
+ ],
+ });
+ assert_true(event instanceof CookieChangeEvent);
+ assert_equals(event.type, 'change');
+ assert_equals(event.changed.length, 0);
+ assert_equals(event.deleted.length, 2);
+ assert_equals(event.deleted[0].name, 'deleted-name1');
+ assert_equals(event.deleted[0].value, 'deleted-value1');
+ assert_equals(event.deleted[1].name, 'deleted-name2');
+ assert_equals(event.deleted[1].value, 'deleted-value2');
+}, 'CookieChangeEvent construction with deleted cookie list');
+
+test(() => {
+ const event = new CookieChangeEvent('change', {
+ changed: [
+ { name: 'changed-name1', value: 'changed-value1' },
+ { name: 'changed-name2', value: 'changed-value2' },
+ ],
+ deleted: [
+ { name: 'deleted-name1', value: 'deleted-value1' },
+ ],
+ });
+ assert_true(event instanceof CookieChangeEvent);
+ assert_equals(event.type, 'change');
+ assert_equals(event.changed.length, 2);
+ assert_equals(event.changed[0].name, 'changed-name1');
+ assert_equals(event.changed[0].value, 'changed-value1');
+ assert_equals(event.changed[1].name, 'changed-name2');
+ assert_equals(event.changed[1].value, 'changed-value2');
+ assert_equals(event.deleted.length, 1);
+ assert_equals(event.deleted[0].name, 'deleted-name1');
+ assert_equals(event.deleted[0].value, 'deleted-value1');
+}, 'CookieChangeEvent construction with changed and deleted cookie lists'); \ No newline at end of file
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_event_basic.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_event_basic.https.window.js
new file mode 100644
index 0000000000..c0075d6adc
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_event_basic.https.window.js
@@ -0,0 +1,24 @@
+'use strict';
+
+promise_test(async testCase => {
+ const eventPromise = new Promise((resolve) => {
+ cookieStore.onchange = resolve;
+ });
+
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const event = await eventPromise;
+ assert_true(event instanceof CookieChangeEvent);
+
+ assert_equals(event.changed, event.changed);
+ assert_equals(event.deleted, event.deleted);
+
+ assert_equals(event.type, 'change');
+ assert_equals(event.changed.length, 1);
+ assert_equals(event.changed[0].name, 'cookie-name');
+ assert_equals(event.changed[0].value, 'cookie-value');
+ assert_equals(event.deleted.length, 0);
+}, 'cookieStore fires change event for cookie set by cookieStore.set()');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_event_delete.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_event_delete.https.window.js
new file mode 100644
index 0000000000..e8c6fc036a
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_event_delete.https.window.js
@@ -0,0 +1,22 @@
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const eventPromise = new Promise((resolve) => {
+ cookieStore.onchange = resolve;
+ });
+ await cookieStore.delete('cookie-name');
+ const event = await eventPromise;
+ assert_true(event instanceof CookieChangeEvent);
+ assert_equals(event.type, 'change');
+ assert_equals(event.deleted.length, 1);
+ assert_equals(event.deleted[0].name, 'cookie-name');
+ assert_equals(
+ event.deleted[0].value, undefined,
+ 'Cookie change events for deletions should not have cookie values');
+ assert_equals(event.changed.length, 0);
+}, 'cookieStore fires change event for cookie deleted by cookieStore.delete()'); \ No newline at end of file
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_event_overwrite.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_event_overwrite.https.window.js
new file mode 100644
index 0000000000..3acffea41c
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_event_overwrite.https.window.js
@@ -0,0 +1,22 @@
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const eventPromise = new Promise((resolve) => {
+ cookieStore.onchange = resolve;
+ });
+
+ await cookieStore.set('cookie-name', 'new-cookie-value');
+
+ const event = await eventPromise;
+ assert_true(event instanceof CookieChangeEvent);
+ assert_equals(event.type, 'change');
+ assert_equals(event.changed.length, 1);
+ assert_equals(event.changed[0].name, 'cookie-name');
+ assert_equals(event.changed[0].value, 'new-cookie-value');
+ assert_equals(event.deleted.length, 0);
+}, 'cookieStore fires change event for cookie overwritten by cookieStore.set()');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_getAll_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_getAll_arguments.https.any.js
new file mode 100644
index 0000000000..5055a42e5d
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_getAll_arguments.https.any.js
@@ -0,0 +1,149 @@
+// META: title=Cookie Store API: cookieStore.getAll() arguments
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ await cookieStore.set('cookie-name-2', 'cookie-value-2');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name-2');
+ });
+
+ const cookies = await cookieStore.getAll();
+ cookies.sort((a, b) => a.name.localeCompare(b.name));
+ assert_equals(cookies.length, 2);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+ assert_equals(cookies[1].name, 'cookie-name-2');
+ assert_equals(cookies[1].value, 'cookie-value-2');
+}, 'cookieStore.getAll with no arguments');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ await cookieStore.set('cookie-name-2', 'cookie-value-2');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name-2');
+ });
+
+ const cookies = await cookieStore.getAll({});
+ cookies.sort((a, b) => a.name.localeCompare(b.name));
+ assert_equals(cookies.length, 2);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+ assert_equals(cookies[1].name, 'cookie-name-2');
+ assert_equals(cookies[1].value, 'cookie-value-2');
+}, 'cookieStore.getAll with empty options');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ await cookieStore.set('cookie-name-2', 'cookie-value-2');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name-2');
+ });
+
+ const cookies = await cookieStore.getAll('cookie-name');
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+}, 'cookieStore.getAll with positional name');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ await cookieStore.set('cookie-name-2', 'cookie-value-2');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name-2');
+ });
+
+ const cookies = await cookieStore.getAll({ name: 'cookie-name' });
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+}, 'cookieStore.getAll with name in options');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ await cookieStore.set('cookie-name-2', 'cookie-value-2');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name-2');
+ });
+
+ const cookies = await cookieStore.getAll('cookie-name',
+ { name: 'wrong-cookie-name' });
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+}, 'cookieStore.getAll with name in both positional arguments and options');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ let target_url = self.location.href;
+ if (self.GLOBAL.isWorker()) {
+ target_url = target_url + '/path/within/scope';
+ }
+
+ const cookies = await cookieStore.getAll({ url: target_url });
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+}, 'cookieStore.getAll with absolute url in options');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ let target_path = self.location.pathname;
+ if (self.GLOBAL.isWorker()) {
+ target_path = target_path + '/path/within/scope';
+ }
+
+ const cookies = await cookieStore.getAll({ url: target_path });
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+}, 'cookieStore.getAll with relative url in options');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const invalid_url =
+ `${self.location.protocol}//${self.location.host}/different/path`;
+ await promise_rejects_js(testCase, TypeError, cookieStore.getAll(
+ { url: invalid_url }));
+}, 'cookieStore.getAll with invalid url path in options');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const invalid_url =
+ `${self.location.protocol}//www.example.com${self.location.pathname}`;
+ await promise_rejects_js(testCase, TypeError, cookieStore.getAll(
+ { url: invalid_url }));
+}, 'cookieStore.getAll with invalid url host in options');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_getAll_multiple.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_getAll_multiple.https.any.js
new file mode 100644
index 0000000000..10dcacbd68
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_getAll_multiple.https.any.js
@@ -0,0 +1,29 @@
+// META: title=Cookie Store API: cookieStore.getAll() with multiple cookies
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ await cookieStore.set('cookie-name-2', 'cookie-value-2');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name-2');
+ });
+ await cookieStore.set('cookie-name-3', 'cookie-value-3');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name-3');
+ });
+
+ const cookies = await cookieStore.getAll();
+ cookies.sort((a, b) => a.name.localeCompare(b.name));
+ assert_equals(cookies.length, 3);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+ assert_equals(cookies[1].name, 'cookie-name-2');
+ assert_equals(cookies[1].value, 'cookie-value-2');
+ assert_equals(cookies[2].name, 'cookie-name-3');
+ assert_equals(cookies[2].value, 'cookie-value-3');
+}, 'cookieStore.getAll returns multiple cookies written by cookieStore.set');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_getAll_set_basic.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_getAll_set_basic.https.any.js
new file mode 100644
index 0000000000..dee78e1867
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_getAll_set_basic.https.any.js
@@ -0,0 +1,16 @@
+// META: title=Cookie Store API: Interaction between cookieStore.set() and cookieStore.getAll()
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookies = await cookieStore.getAll('cookie-name');
+
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+}, 'cookieStore.getAll returns the cookie written by cookieStore.set');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_get_arguments.https.any.js
new file mode 100644
index 0000000000..a56032f03e
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_get_arguments.https.any.js
@@ -0,0 +1,102 @@
+// META: title=Cookie Store API: cookieStore.get() arguments
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.get());
+}, 'cookieStore.get with no arguments returns TypeError');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.get({}));
+},'cookieStore.get with empty options returns TypeError');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.get with positional name');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const cookie = await cookieStore.get({ name: 'cookie-name' });
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.get with name in options');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const cookie = await cookieStore.get('cookie-name',
+ { name: 'wrong-cookie-name' });
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.get with name in both positional arguments and options');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ let target_url = self.location.href;
+ if (self.GLOBAL.isWorker()) {
+ target_url = target_url + '/path/within/scope';
+ }
+
+ const cookie = await cookieStore.get({ url: target_url });
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.get with absolute url in options');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ let target_path = self.location.pathname;
+ if (self.GLOBAL.isWorker()) {
+ target_path = target_path + '/path/within/scope';
+ }
+
+ const cookie = await cookieStore.get({ url: target_path });
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.get with relative url in options');
+
+promise_test(async testCase => {
+ const invalid_url =
+ `${self.location.protocol}//${self.location.host}/different/path`;
+ await promise_rejects_js(testCase, TypeError, cookieStore.get(
+ { url: invalid_url }));
+}, 'cookieStore.get with invalid url path in options');
+
+promise_test(async testCase => {
+ const invalid_url =
+ `${self.location.protocol}//www.example.com${self.location.pathname}`;
+ await promise_rejects_js(testCase, TypeError, cookieStore.get(
+ { url: invalid_url }));
+}, 'cookieStore.get with invalid url host in options');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_delete_basic.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_get_delete_basic.https.any.js
new file mode 100644
index 0000000000..9337669afd
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_get_delete_basic.https.any.js
@@ -0,0 +1,14 @@
+// META: title=Cookie Store API: Interaction between cookieStore.set() and cookieStore.delete()
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ await cookieStore.delete('cookie-name');
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.get returns null for a cookie deleted by cookieStore.delete');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_frames.https.html b/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_frames.https.html
new file mode 100644
index 0000000000..f7c737b422
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_frames.https.html
@@ -0,0 +1,46 @@
+<!doctype html>
+<meta charset='utf-8'>
+<title>Async Cookies: cookieStore basic API across frames</title>
+<link rel='help' href='https://github.com/WICG/cookie-store'>
+<link rel='author' href='jarrydg@chromium.org' title='Jarryd Goodman'>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<style>iframe { display: none; }</style>
+<iframe id='iframe'></iframe>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const iframe = document.getElementById('iframe');
+ const frameCookieStore = iframe.contentWindow.cookieStore;
+
+ const oldCookie = await frameCookieStore.get('cookie-name');
+ assert_equals(oldCookie, null,
+ 'Precondition not met: cookie store should be empty');
+
+ await cookieStore.set('cookie-name', 'cookie-value');
+ t.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const frameCookie = await frameCookieStore.get('cookie-name');
+ assert_equals(frameCookie.value, 'cookie-value');
+}, 'cookieStore.get() sees cookieStore.set() in frame');
+
+promise_test(async t => {
+ const iframe = document.getElementById('iframe');
+ const frameCookieStore = iframe.contentWindow.cookieStore;
+
+ const oldCookie = await frameCookieStore.get('cookie-name');
+ assert_equals(oldCookie, null,
+ 'Precondition not met: cookie store should be empty');
+
+ await frameCookieStore.set('cookie-name', 'cookie-value');
+ t.add_cleanup(async () => {
+ await frameCookieStore.delete('cookie-name');
+ });
+
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.get() in frame sees cookieStore.set()')
+</script>
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_origins.sub.https.html b/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_origins.sub.https.html
new file mode 100644
index 0000000000..c67ef98bcc
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_get_set_across_origins.sub.https.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<meta charset='utf-8'>
+<title>Async Cookies: cookieStore basic API across origins</title>
+<link rel='help' href='https://github.com/WICG/cookie-store'>
+<link rel='author' href='jarrydg@chromium.org' title='Jarryd Goodman'>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='resources/helpers.js'></script>
+<style>iframe { display: none; }</style>
+
+<script>
+'use strict';
+
+const kPath = '/cookie-store/resources/helper_iframe.sub.html';
+const kCorsBase = `https://{{domains[www1]}}:{{ports[https][0]}}`;
+const kCorsUrl = `${kCorsBase}${kPath}`;
+
+promise_test(async t => {
+ const iframe = await createIframe(kCorsUrl, t);
+ assert_true(iframe != null);
+
+ iframe.contentWindow.postMessage({
+ opname: 'set-cookie',
+ name: 'cookie-name',
+ value: 'cookie-value',
+ }, kCorsBase);
+ t.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', domain: '{{host}}' });
+ });
+ await waitForMessage();
+
+ const cookies = await cookieStore.getAll();
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-value');
+}, 'cookieStore.get() sees cookieStore.set() in cross-origin frame');
+
+promise_test(async t => {
+ const iframe = await createIframe(kCorsUrl, t);
+ assert_true(iframe != null);
+
+ await cookieStore.set({
+ name: 'cookie-name',
+ value: 'cookie-value',
+ domain: '{{host}}',
+ });
+
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+
+ iframe.contentWindow.postMessage({
+ opname: 'get-cookie',
+ name: 'cookie-name',
+ }, kCorsBase);
+ t.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', domain: '{{host}}' });
+ });
+
+ const message = await waitForMessage();
+
+ const { frameCookie } = message;
+ assert_not_equals(frameCookie, null);
+ assert_equals(frameCookie.name, 'cookie-name');
+ assert_equals(frameCookie.value, 'cookie-value');
+}, 'cookieStore.get() in cross-origin frame sees cookieStore.set()');
+</script>
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_set_basic.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_get_set_basic.https.any.js
new file mode 100644
index 0000000000..127f758f5f
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_get_set_basic.https.any.js
@@ -0,0 +1,15 @@
+// META: title=Cookie Store API: Interaction between cookieStore.set() and cookieStore.get()
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.get returns the cookie written by cookieStore.set');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_get_set_ordering.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_get_set_ordering.https.any.js
new file mode 100644
index 0000000000..6b7e73950c
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_get_set_ordering.https.any.js
@@ -0,0 +1,42 @@
+// META: title=Cookie Store API: Cookie ordering
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async t => {
+ await cookieStore.set('ordered-1', 'cookie-value1');
+ await cookieStore.set('ordered-2', 'cookie-value2');
+ await cookieStore.set('ordered-3', 'cookie-value3');
+ // NOTE: this assumes no concurrent writes from elsewhere; it also
+ // uses three separate cookie jar read operations where a single getAll
+ // would be more efficient, but this way the CookieStore does the filtering
+ // for us.
+ const matchingValues = await Promise.all(['1', '2', '3'].map(
+ async suffix => (await cookieStore.get('ordered-' + suffix)).value));
+ const actual = matchingValues.join(';');
+ const expected = 'cookie-value1;cookie-value2;cookie-value3';
+ assert_equals(actual, expected);
+}, 'Set three simple origin session cookies sequentially and ensure ' +
+ 'they all end up in the cookie jar in order.');
+
+promise_test(async t => {
+ await Promise.all([
+ cookieStore.set('ordered-unordered1', 'unordered-cookie-value1'),
+ cookieStore.set('ordered-unordered2', 'unordered-cookie-value2'),
+ cookieStore.set('ordered-unordered3', 'unordered-cookie-value3')
+ ]);
+ // NOTE: this assumes no concurrent writes from elsewhere; it also
+ // uses three separate cookie jar read operations where a single getAll
+ // would be more efficient, but this way the CookieStore does the filtering
+ // for us and we do not need to sort.
+ const matchingCookies = await Promise.all(['1', '2', '3'].map(
+ suffix => cookieStore.get('ordered-unordered' + suffix)));
+ const actual = matchingCookies.map(({ value }) => value).join(';');
+ const expected =
+ 'unordered-cookie-value1;' +
+ 'unordered-cookie-value2;' +
+ 'unordered-cookie-value3';
+ assert_equals(actual, expected);
+}, 'Set three simple origin session cookies in undefined order using ' +
+ 'Promise.all and ensure they all end up in the cookie jar in any ' +
+ 'order. ');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_in_detached_frame.https.html b/testing/web-platform/tests/cookie-store/cookieStore_in_detached_frame.https.html
new file mode 100644
index 0000000000..08a7b5b8e4
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_in_detached_frame.https.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>cookieStore on DOMWindow of detached iframe (crbug.com/774626)</title>
+<link rel="help" href="https://github.com/WICG/cookie-store">
+<link rel="author" href="pwnall@chromium.org" title="Victor Costan">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe"></iframe>
+<script>
+'use strict';
+
+test(() => {
+ const iframe = document.getElementById('iframe');
+ const frameWindow = iframe.contentWindow;
+
+ iframe.parentNode.removeChild(iframe);
+ assert_equals(null, frameWindow.cookieStore);
+});
+</script>
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_opaque_origin.https.html b/testing/web-platform/tests/cookie-store/cookieStore_opaque_origin.https.html
new file mode 100644
index 0000000000..94a13fe63f
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_opaque_origin.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Cookie Store API: Opaque origins for cookieStore</title>
+<link rel=help href="https://wicg.github.io/cookie-store/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+const apiCalls = {
+ 'get': 'cookieStore.get("cookie-name")',
+ 'getAll': 'cookieStore.getAll()',
+ 'set': 'cookieStore.set("cookie-name", "cookie-value")',
+ 'delete': 'cookieStore.delete("cookie-name")'
+};
+
+const script = `
+<script>
+ "use strict";
+ window.onmessage = async () => {
+ try {
+ await %s;
+ window.parent.postMessage({result: "no exception"}, "*");
+ } catch (ex) {
+ window.parent.postMessage({result: ex.name}, "*");
+ };
+ };
+<\/script>
+`;
+
+function load_iframe(apiCall, sandbox) {
+ return new Promise(resolve => {
+ const iframe = document.createElement('iframe');
+ iframe.onload = () => { resolve(iframe); };
+ if (sandbox)
+ iframe.sandbox = sandbox;
+ iframe.srcdoc = script.replace("%s", apiCalls[apiCall]);
+ iframe.style.display = 'none';
+ document.documentElement.appendChild(iframe);
+ });
+}
+
+function wait_for_message(iframe) {
+ return new Promise(resolve => {
+ self.addEventListener('message', function listener(e) {
+ if (e.source === iframe.contentWindow) {
+ resolve(e.data);
+ self.removeEventListener('message', listener);
+ }
+ });
+ });
+}
+
+promise_test(async t => {
+ for (apiCall in apiCalls) {
+ const iframe = await load_iframe(apiCall);
+ iframe.contentWindow.postMessage({}, '*');
+ const message = await wait_for_message(iframe);
+ assert_equals(message.result, 'no exception',
+ 'cookieStore ${apiCall} should not throw');
+ }
+}, 'cookieStore in non-sandboxed iframe should not throw');
+
+promise_test(async t => {
+ for (apiCall in apiCalls) {
+ const iframe = await load_iframe(apiCall, 'allow-scripts');
+ iframe.contentWindow.postMessage({}, '*');
+ const message = await wait_for_message(iframe);
+ assert_equals(message.result, 'SecurityError',
+ 'cookieStore ${apiCall} should throw SecurityError');
+ }
+}, 'cookieStore in sandboxed iframe should throw SecurityError');
+
+</script>
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_set_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_set_arguments.https.any.js
new file mode 100644
index 0000000000..8ff66bf777
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_set_arguments.https.any.js
@@ -0,0 +1,311 @@
+// META: title=Cookie Store API: cookieStore.set() arguments
+// META: global=window,serviceworker
+
+'use strict';
+
+promise_test(async testCase => {
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.set with positional name and value');
+
+promise_test(async testCase => {
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set({ name: 'cookie-name', value: 'cookie-value' });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.set with name and value in options');
+
+promise_test(async testCase => {
+ await promise_rejects_js(testCase, TypeError,
+ cookieStore.set('', 'suspicious-value=resembles-name-and-value'));
+}, "cookieStore.set with empty name and an '=' in value");
+
+promise_test(async testCase => {
+ await cookieStore.delete('cookie-name');
+ cookieStore.set('cookie-name', 'suspicious-value=resembles-name-and-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'suspicious-value=resembles-name-and-value');
+}, "cookieStore.set with normal name and an '=' in value");
+
+promise_test(async testCase => {
+ const tenYears = 10 * 365 * 24 * 60 * 60 * 1000;
+ const tenYearsFromNow = Date.now() + tenYears;
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set(
+ { name: 'cookie-name',
+ value: 'cookie-value',
+ expires: new Date(tenYearsFromNow) });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.set with expires set to a future Date');
+
+promise_test(async testCase => {
+ const tenYears = 10 * 365 * 24 * 60 * 60 * 1000;
+ const tenYearsAgo = Date.now() - tenYears;
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set(
+ { name :'cookie-name',
+ value: 'cookie-value',
+ expires: new Date(tenYearsAgo) });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.set with expires set to a past Date');
+
+promise_test(async testCase => {
+ const tenYears = 10 * 365 * 24 * 60 * 60 * 1000;
+ const tenYearsFromNow = Date.now() + tenYears;
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', expires: tenYearsFromNow });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.set with expires set to a future timestamp');
+
+promise_test(async testCase => {
+ const tenYears = 10 * 365 * 24 * 60 * 60 * 1000;
+ const tenYearsAgo = Date.now() - tenYears;
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', expires: tenYearsAgo });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.set with expires set to a past timestamp');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.set(
+ { name: 'cookie-name',
+ value: 'cookie-value',
+ domain: `.${currentDomain}` }));
+}, 'cookieStore.set domain starts with "."');
+
+promise_test(async testCase => {
+ await promise_rejects_js(testCase, TypeError, cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', domain: 'example.com' }));
+}, 'cookieStore.set with domain that is not equal current host');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+ await cookieStore.delete({ name: 'cookie-name', domain: currentDomain });
+
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', domain: currentDomain });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', domain: currentDomain });
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.set with domain set to the current hostname');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+ const subDomain = `sub.${currentDomain}`;
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', domain: subDomain }));
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.set with domain set to a subdomain of the current hostname');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+ assert_not_equals(currentDomain[0] === '.',
+ 'this test assumes that the current hostname does not start with .');
+ const domainSuffix = currentDomain.substr(1);
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', domain: domainSuffix }));
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.set with domain set to a non-domain-matching suffix of the ' +
+ 'current hostname');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set('cookie-name', 'cookie-value1');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value2', domain: currentDomain });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', domain: currentDomain });
+ });
+
+ const cookies = await cookieStore.getAll('cookie-name');
+ assert_equals(cookies.length, 2);
+
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[1].name, 'cookie-name');
+
+ const values = cookies.map((cookie) => cookie.value);
+ values.sort();
+ assert_array_equals(values, ['cookie-value1', 'cookie-value2']);
+}, 'cookieStore.set default domain is null and differs from current hostname');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory =
+ currentPath.substr(0, currentPath.lastIndexOf('/') + 1);
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', path: currentDirectory });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+}, 'cookieStore.set with path set to the current directory');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory =
+ currentPath.substr(0, currentPath.lastIndexOf('/') + 1);
+ const subDirectory = currentDirectory + "subdir/";
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ await cookieStore.delete({ name: 'cookie-name', path: subDirectory });
+
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', path: subDirectory });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', path: subDirectory });
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.set with path set to a subdirectory of the current directory');
+
+promise_test(async testCase => {
+ await cookieStore.delete('cookie-name');
+
+ await cookieStore.set('cookie-name', 'cookie-old-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-new-value', path: '/' });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', path: '/' });
+ });
+
+ const cookies = await cookieStore.getAll('cookie-name');
+ assert_equals(cookies.length, 1);
+ assert_equals(cookies[0].name, 'cookie-name');
+ assert_equals(cookies[0].value, 'cookie-new-value');
+}, 'cookieStore.set default path is /');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory = currentPath.substr(0, currentPath.lastIndexOf('/'));
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+
+ await cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', path: currentDirectory });
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete({ name: 'cookie-name', path: currentDirectory });
+ });
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'cookie-value');
+ assert_equals(cookie.path, currentDirectory + '/');
+}, 'cookieStore.set adds / to path that does not end with /');
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentPath = currentUrl.pathname;
+ const currentDirectory =
+ currentPath.substr(0, currentPath.lastIndexOf('/') + 1);
+ const invalidPath = currentDirectory.substr(1);
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.set(
+ { name: 'cookie-name', value: 'cookie-value', path: invalidPath }));
+}, 'cookieStore.set with path that does not start with /');
+
+promise_test(async testCase => {
+ await cookieStore.set('cookie-name', 'old-cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const cookie_attributes = await cookieStore.get('cookie-name');
+ assert_equals(cookie_attributes.name, 'cookie-name');
+ assert_equals(cookie_attributes.value, 'old-cookie-value');
+
+ cookie_attributes.value = 'new-cookie-value';
+ await cookieStore.set(cookie_attributes);
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie.name, 'cookie-name');
+ assert_equals(cookie.value, 'new-cookie-value');
+}, 'cookieStore.set with get result');
+
+promise_test(async testCase => {
+ // The maximum attribute value size is specified as 1024 bytes at https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size.
+ await cookieStore.delete('cookie-name');
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.set(
+ { name: 'cookie-name',
+ value: 'cookie-value',
+ path: '/' + 'a'.repeat(1023) + '/' }));
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.set checks if the path is too long');
+
+promise_test(async testCase => {
+ // The maximum attribute value size is specified as 1024 bytes at https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size.
+ await cookieStore.delete('cookie-name');
+
+ await promise_rejects_js(testCase, TypeError, cookieStore.set(
+ { name: 'cookie-name',
+ value: 'cookie-value',
+ domain: 'a'.repeat(1025) }));
+ const cookie = await cookieStore.get('cookie-name');
+ assert_equals(cookie, null);
+}, 'cookieStore.set checks if the domain is too long');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_special_names.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_special_names.https.any.js
new file mode 100644
index 0000000000..6b18c4f066
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_special_names.https.any.js
@@ -0,0 +1,64 @@
+// META: title=Cookie Store API: cookieStore.set()/get()/delete() for cookies with special names
+// META: global=window,serviceworker
+
+'use strict';
+
+['__Secure-', '__Host-'].forEach(prefix => {
+ promise_test(async testCase => {
+ await cookieStore.set(`${prefix}cookie-name`, `secure-cookie-value`);
+ assert_equals(
+ (await cookieStore.get(`${prefix}cookie-name`)).value,
+ 'secure-cookie-value',
+ `Setting ${prefix} cookies should not fail in secure context`);
+
+ try { await cookieStore.delete(`${prefix}cookie-name`); } catch (e) {}
+ }, `cookieStore.set with ${prefix} name on secure origin`);
+
+ promise_test(async testCase => {
+ // This test is for symmetry with the non-secure case. In non-secure
+ // contexts, the set() should fail even if the expiration date makes
+ // the operation a no-op.
+ await cookieStore.set(
+ { name: `${prefix}cookie-name`, value: `secure-cookie-value`,
+ expires: Date.now() - (24 * 60 * 60 * 1000)});
+ assert_equals(await cookieStore.get(`${prefix}cookie-name`), null);
+ try { await cookieStore.delete(`${prefix}cookie-name`); } catch (e) {}
+ }, `cookieStore.set of expired ${prefix} cookie name on secure origin`);
+
+ promise_test(async testCase => {
+ assert_equals(
+ await cookieStore.delete(`${prefix}cookie-name`), undefined,
+ `Deleting ${prefix} cookies should not fail in secure context`);
+ }, `cookieStore.delete with ${prefix} name on secure origin`);
+});
+
+promise_test(async testCase => {
+ const currentUrl = new URL(self.location.href);
+ const currentDomain = currentUrl.hostname;
+ await promise_rejects_js(testCase, TypeError,
+ cookieStore.set({ name: '__Host-cookie-name', value: 'cookie-value',
+ domain: currentDomain }));
+}, 'cookieStore.set with __Host- prefix and a domain option');
+
+promise_test(async testCase => {
+ await cookieStore.set({ name: '__Host-cookie-name', value: 'cookie-value',
+ path: "/" });
+
+ assert_equals(
+ (await cookieStore.get(`__Host-cookie-name`)).value, "cookie-value");
+
+ await promise_rejects_js(testCase, TypeError,
+ cookieStore.set( { name: '__Host-cookie-name', value: 'cookie-value',
+ path: "/path" }));
+}, 'cookieStore.set with __Host- prefix a path option');
+
+promise_test(async testCase => {
+ let exceptionThrown = false;
+ try {
+ await cookieStore.set(unescape('cookie-name%0D1'), 'cookie-value');
+ } catch (e) {
+ assert_equals (e.name, "TypeError", "cookieStore thrown an incorrect exception -");
+ exceptionThrown = true;
+ }
+ assert_true(exceptionThrown, "No exception thrown.");
+}, 'cookieStore.set with malformed name.');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_subscribe_arguments.https.any.js b/testing/web-platform/tests/cookie-store/cookieStore_subscribe_arguments.https.any.js
new file mode 100644
index 0000000000..ca5f55d645
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_subscribe_arguments.https.any.js
@@ -0,0 +1,142 @@
+// META: title=Cookie Store API: cookieStore.subscribe() arguments
+// META: global=window,serviceworker
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+
+'use strict';
+
+promise_test(async testCase => {
+ if (self.GLOBAL.isWindow()) {
+ const registration = await service_worker_unregister_and_register(
+ testCase, 'resources/empty_sw.js',
+ '/cookie-store/resources/does/not/exist');
+ testCase.add_cleanup(() => registration.unregister());
+
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await wait_for_state(testCase, registration.installing, 'activated');
+
+ self.registration = registration;
+ } else {
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await new Promise(resolve => {
+ self.addEventListener('activate', event => { resolve(); });
+ });
+ }
+
+ {
+ const subscriptions = [{ name: 'cookie-name' }];
+ await self.registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions));
+ }
+
+ const subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(subscriptions.length, 1);
+
+ assert_equals(subscriptions[0].name, 'cookie-name');
+ assert_equals(subscriptions[0].url, registration.scope);
+}, 'cookieStore.subscribe without url in option');
+
+promise_test(async testCase => {
+ if (self.GLOBAL.isWindow()) {
+ const registration = await service_worker_unregister_and_register(
+ testCase, 'resources/empty_sw.js',
+ '/cookie-store/resources/does/not/exist');
+ testCase.add_cleanup(() => registration.unregister());
+
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await wait_for_state(testCase, registration.installing, 'activated');
+
+ self.registration = registration;
+ } else if (!self.registration.active) {
+ // If service worker is not active yet, it must wait for it to enter the
+ // 'activated' state before subscribing to cookiechange events.
+ await new Promise(resolve => {
+ self.addEventListener('activate', event => { resolve(); });
+ });
+ }
+
+ await promise_rejects_js(testCase, TypeError,
+ registration.cookies.subscribe(
+ { name: 'cookie-name', url: '/wrong/path' }));
+}, 'cookieStore.subscribe with invalid url path in option');
+
+promise_test(async testCase => {
+ if (self.GLOBAL.isWindow()) {
+ const registration = await service_worker_unregister_and_register(
+ testCase, 'resources/empty_sw.js',
+ '/cookie-store/resources/does/not/exist');
+ testCase.add_cleanup(() => registration.unregister());
+
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await wait_for_state(testCase, registration.installing, 'activated');
+
+ self.registration = registration;
+ } else if (!self.registration.active) {
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await new Promise(resolve => {
+ self.addEventListener('activate', event => { resolve(); });
+ });
+ }
+
+ {
+ const subscriptions = [{ name: 'cookie-name' }];
+ // Call subscribe for same subscription multiple times to verify that it is
+ // idempotent.
+ await self.registration.cookies.subscribe(subscriptions);
+ await self.registration.cookies.subscribe(subscriptions);
+ await self.registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions));
+ }
+
+ const subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(subscriptions.length, 1);
+
+ assert_equals(subscriptions[0].name, 'cookie-name');
+ assert_equals(subscriptions[0].url, registration.scope);
+}, 'cookieStore.subscribe is idempotent');
+
+promise_test(async testCase => {
+ if (self.GLOBAL.isWindow()) {
+ const registration = await service_worker_unregister_and_register(
+ testCase, 'resources/empty_sw.js',
+ '/cookie-store/resources/does/not/exist');
+ testCase.add_cleanup(() => registration.unregister());
+
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await wait_for_state(testCase, registration.installing, 'activated');
+
+ self.registration = registration;
+ } else if (!self.registration.active) {
+ // Must wait for the service worker to enter the 'activated' state before
+ // subscribing to cookiechange events.
+ await new Promise(resolve => {
+ self.addEventListener('activate', event => { resolve(); });
+ });
+ }
+
+ {
+ const subscriptions = [
+ { name: 'cookie-name1' },
+ { name: 'cookie-name2' },
+ ];
+ await self.registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions));
+
+ // Call unsubscribe for same subscription multiple times to verify that it
+ // is idempotent.
+ await registration.cookies.unsubscribe([subscriptions[0]]);
+ await registration.cookies.unsubscribe([subscriptions[0]]);
+ await registration.cookies.unsubscribe([subscriptions[0]]);
+ }
+
+ const subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(subscriptions.length, 1);
+
+ assert_equals(subscriptions[0].name, 'cookie-name2');
+ assert_equals(subscriptions[0].url, registration.scope);
+}, 'CookieStore.unsubscribe is idempotent');
diff --git a/testing/web-platform/tests/cookie-store/cookieStore_subscriptions_empty.https.window.js b/testing/web-platform/tests/cookie-store/cookieStore_subscriptions_empty.https.window.js
new file mode 100644
index 0000000000..907a34b4de
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/cookieStore_subscriptions_empty.https.window.js
@@ -0,0 +1,13 @@
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+
+'use strict';
+
+promise_test(async testCase => {
+ const registration = await service_worker_unregister_and_register(
+ testCase, 'resources/empty_sw.js', 'resources/does/not/exist');
+ testCase.add_cleanup(() => registration.unregister());
+ await wait_for_state(testCase, registration.installing, 'activated');
+
+ const subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(0, subscriptions.length);
+}, 'Newly registered and activated service worker has no subscriptions');
diff --git a/testing/web-platform/tests/cookie-store/encoding.https.any.js b/testing/web-platform/tests/cookie-store/encoding.https.any.js
new file mode 100644
index 0000000000..941639bdae
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/encoding.https.any.js
@@ -0,0 +1,19 @@
+// META: title=Cookie Store API: cookie encoding
+// META: global=window,serviceworker
+// META: script=resources/cookie-test-helpers.js
+
+'use strict';
+
+cookie_test(async t => {
+ await setCookieStringHttp('\uFEFFcookie=value; path=/');
+ const cookie = await cookieStore.get('\uFEFFcookie');
+ assert_equals(cookie.name, '\uFEFFcookie');
+ assert_equals(cookie.value, 'value');
+}, 'BOM not stripped from name');
+
+cookie_test(async t => {
+ await setCookieStringHttp('cookie=\uFEFFvalue; path=/');
+ const cookie = await cookieStore.get('cookie');
+ assert_equals(cookie.name, 'cookie');
+ assert_equals(cookie.value, '\uFEFFvalue');
+}, 'BOM not stripped from value');
diff --git a/testing/web-platform/tests/cookie-store/httponly_cookies.https.window.js b/testing/web-platform/tests/cookie-store/httponly_cookies.https.window.js
new file mode 100644
index 0000000000..8a10e358ef
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/httponly_cookies.https.window.js
@@ -0,0 +1,69 @@
+// META: script=resources/cookie-test-helpers.js
+
+'use strict';
+
+cookie_test(async t => {
+ let eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringHttp('HTTPONLY-cookie=value; path=/; httponly');
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'HttpOnly cookie we wrote using HTTP in cookie jar' +
+ ' is invisible to script');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'HTTPONLY-cookie=value',
+ 'HttpOnly cookie we wrote using HTTP in HTTP cookie jar');
+
+ await setCookieStringHttp('HTTPONLY-cookie=new-value; path=/; httponly');
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'HttpOnly cookie we overwrote using HTTP in cookie jar' +
+ ' is invisible to script');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'HTTPONLY-cookie=new-value',
+ 'HttpOnly cookie we overwrote using HTTP in HTTP cookie jar');
+
+ eventPromise = observeNextCookieChangeEvent();
+ await setCookieStringHttp(
+ 'HTTPONLY-cookie=DELETED; path=/; max-age=0; httponly');
+ assert_equals(
+ await getCookieString(),
+ undefined,
+ 'Empty cookie jar after HTTP cookie-clearing using max-age=0');
+ assert_equals(
+ await getCookieStringHttp(),
+ undefined,
+ 'Empty HTTP cookie jar after HTTP cookie-clearing using max-age=0');
+
+ // HTTPONLY cookie changes should not have been observed; perform
+ // a dummy change to verify that nothing else was queued up.
+ await cookieStore.set('TEST', 'dummy');
+ await verifyCookieChangeEvent(
+ eventPromise, {changed: [{name: 'TEST', value: 'dummy'}]},
+ 'HttpOnly cookie deletion was not observed');
+}, 'HttpOnly cookies are not observed');
+
+
+cookie_test(async t => {
+ document.cookie = 'cookie1=value1; path=/';
+ document.cookie = 'cookie2=value2; path=/; httponly';
+ document.cookie = 'cookie3=value3; path=/';
+ assert_equals(
+ await getCookieStringHttp(), 'cookie1=value1; cookie3=value3',
+ 'Trying to store an HttpOnly cookie with document.cookie fails');
+}, 'HttpOnly cookies can not be set by document.cookie');
+
+
+// Historical: Early iterations of the proposal included an httpOnly option.
+cookie_test(async t => {
+ await cookieStore.set('cookie1', 'value1');
+ await cookieStore.set('cookie2', 'value2', {httpOnly: true});
+ await cookieStore.set('cookie3', 'value3');
+ assert_equals(
+ await getCookieStringHttp(),
+ 'cookie1=value1; cookie2=value2; cookie3=value3',
+ 'httpOnly is not an option for CookieStore.set()');
+}, 'HttpOnly cookies can not be set by CookieStore');
diff --git a/testing/web-platform/tests/cookie-store/idlharness.tentative.https.any.js b/testing/web-platform/tests/cookie-store/idlharness.tentative.https.any.js
new file mode 100644
index 0000000000..6312f3c4ba
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/idlharness.tentative.https.any.js
@@ -0,0 +1,45 @@
+// META: global=window,worker
+// META: timeout=long
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+'use strict';
+
+// https://wicg.github.io/cookie-store/
+
+idl_test(
+ ['cookie-store'],
+ ['service-workers', 'html', 'dom'],
+ async (idl_array, t) => {
+ const isServiceWorker = 'ServiceWorkerGlobalScope' in self
+ && self instanceof ServiceWorkerGlobalScope;
+
+ if (isServiceWorker) {
+ idl_array.add_objects({
+ ExtendableCookieChangeEvent: [
+ 'new ExtendableCookieChangeEvent("cookiechange")'],
+ ServiceWorkerGlobalScope: ['self'],
+ });
+ } else if (self.GLOBAL.isWindow()) {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/empty_sw.js', 'resources/does/not/exist');
+ t.add_cleanup(() => registration.unregister());
+
+ // Global property referenced by idl_array.add_objects().
+ self.registration = registration;
+
+ idl_array.add_objects({
+ CookieChangeEvent: ['new CookieChangeEvent("change")'],
+ Window: ['self'],
+ });
+ }
+
+ if (isServiceWorker || self.GLOBAL.isWindow()) {
+ idl_array.add_objects({
+ CookieStore: ['self.cookieStore'],
+ CookieStoreManager: ['self.registration.cookies'],
+ ServiceWorkerRegistration: ['self.registration'],
+ });
+ }
+ }
+);
diff --git a/testing/web-platform/tests/cookie-store/resources/always_changing_sw.sub.js b/testing/web-platform/tests/cookie-store/resources/always_changing_sw.sub.js
new file mode 100644
index 0000000000..9fdf99848f
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/resources/always_changing_sw.sub.js
@@ -0,0 +1,6 @@
+// This script changes every time it is fetched.
+
+// When used as a service worker script, this causes the Service Worker to be
+// updated on every ServiceWorkerRegistration.update() call.
+
+// The following bytes change on every fetch: {{uuid()}}
diff --git a/testing/web-platform/tests/cookie-store/resources/cookie-test-helpers.js b/testing/web-platform/tests/cookie-store/resources/cookie-test-helpers.js
new file mode 100644
index 0000000000..178947ad6e
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/resources/cookie-test-helpers.js
@@ -0,0 +1,226 @@
+'use strict';
+
+// TODO(jsbell): Once ServiceWorker is supported, add arbitrary path coverage.
+const kPath = location.pathname.replace(/[^/]+$/, '');
+
+// True when running in a document context as opposed to a worker context
+const kHasDocument = typeof document !== 'undefined';
+
+// True when running on unsecured 'http:' rather than secured 'https:'.
+const kIsUnsecured = location.protocol !== 'https:';
+
+const kCookieHelperCgi = 'resources/cookie_helper.py';
+
+// Approximate async equivalent to the document.cookie getter but with
+// important differences: optional additional getAll arguments are
+// forwarded, and an empty cookie jar returns undefined.
+//
+// This is intended primarily for verification against expected cookie
+// jar contents. It should produce more readable messages using
+// assert_equals in failing cases than assert_object_equals would
+// using parsed cookie jar contents and also allows expectations to be
+// written more compactly.
+async function getCookieString(...args) {
+ const cookies = await cookieStore.getAll(...args);
+ return cookies.length
+ ? cookies.map(({name, value}) =>
+ (name ? (name + '=') : '') + value).join('; ')
+ : undefined;
+}
+
+// Approximate async equivalent to the document.cookie getter but from
+// the server's point of view. Returns UTF-8 interpretation. Allows
+// sub-path to be specified.
+//
+// Unlike document.cookie, this returns undefined when no cookies are
+// present.
+async function getCookieStringHttp(extraPath = null) {
+ const url =
+ kCookieHelperCgi + ((extraPath == null) ? '' : ('/' + extraPath));
+ const response = await fetch(url, { credentials: 'include' });
+ const text = await response.text();
+ assert_equals(
+ response.ok,
+ true,
+ 'CGI should have succeeded in getCookieStringHttp\n' + text);
+ assert_equals(
+ response.headers.get('content-type'),
+ 'text/plain; charset=utf-8',
+ 'CGI did not return UTF-8 text in getCookieStringHttp');
+ if (text === '')
+ return undefined;
+ assert_equals(
+ text.indexOf('cookie='),
+ 0,
+ 'CGI response did not begin with "cookie=" and was not empty: ' + text);
+ return decodeURIComponent(text.replace(/^cookie=/, ''));
+}
+
+// Approximate async equivalent to the document.cookie getter but from
+// the server's point of view. Returns binary string
+// interpretation. Allows sub-path to be specified.
+//
+// Unlike document.cookie, this returns undefined when no cookies are
+// present.
+async function getCookieBinaryHttp(extraPath = null) {
+ const url =
+ kCookieHelperCgi +
+ ((extraPath == null) ?
+ '' :
+ ('/' + extraPath)) + '?charset=iso-8859-1';
+ const response = await fetch(url, { credentials: 'include' });
+ const text = await response.text();
+ assert_equals(
+ response.ok,
+ true,
+ 'CGI should have succeeded in getCookieBinaryHttp\n' + text);
+ assert_equals(
+ response.headers.get('content-type'),
+ 'text/plain; charset=iso-8859-1',
+ 'CGI did not return ISO 8859-1 text in getCookieBinaryHttp');
+ if (text === '')
+ return undefined;
+ assert_equals(
+ text.indexOf('cookie='),
+ 0,
+ 'CGI response did not begin with "cookie=" and was not empty: ' + text);
+ return unescape(text.replace(/^cookie=/, ''));
+}
+
+// Approximate async equivalent to the document.cookie setter but from
+// the server's point of view.
+async function setCookieStringHttp(setCookie) {
+ const encodedSetCookie = encodeURIComponent(setCookie);
+ const url = kCookieHelperCgi;
+ const headers = new Headers();
+ headers.set(
+ 'content-type',
+ 'application/x-www-form-urlencoded; charset=utf-8');
+ const response = await fetch(
+ url,
+ {
+ credentials: 'include',
+ method: 'POST',
+ headers: headers,
+ body: 'set-cookie=' + encodedSetCookie,
+ });
+ const text = await response.text();
+ assert_equals(
+ response.ok,
+ true,
+ 'CGI should have succeeded in setCookieStringHttp set-cookie: ' +
+ setCookie + '\n' + text);
+ assert_equals(
+ response.headers.get('content-type'),
+ 'text/plain; charset=utf-8',
+ 'CGI did not return UTF-8 text in setCookieStringHttp');
+ assert_equals(
+ text,
+ 'set-cookie=' + encodedSetCookie,
+ 'CGI did not faithfully echo the set-cookie value');
+}
+
+// Approximate async equivalent to the document.cookie setter but from
+// the server's point of view. This version sets a binary cookie rather
+// than a UTF-8 one.
+async function setCookieBinaryHttp(setCookie) {
+ const encodedSetCookie = escape(setCookie).split('/').join('%2F');
+ const url = kCookieHelperCgi + '?charset=iso-8859-1';
+ const headers = new Headers();
+ headers.set(
+ 'content-type',
+ 'application/x-www-form-urlencoded; charset=iso-8859-1');
+ const response = await fetch(url, {
+ credentials: 'include',
+ method: 'POST',
+ headers: headers,
+ body: 'set-cookie=' + encodedSetCookie
+ });
+ const text = await response.text();
+ assert_equals(
+ response.ok,
+ true,
+ 'CGI should have succeeded in setCookieBinaryHttp set-cookie: ' +
+ setCookie + '\n' + text);
+ assert_equals(
+ response.headers.get('content-type'),
+ 'text/plain; charset=iso-8859-1',
+ 'CGI did not return Latin-1 text in setCookieBinaryHttp');
+ assert_equals(
+ text,
+ 'set-cookie=' + encodedSetCookie,
+ 'CGI did not faithfully echo the set-cookie value');
+}
+
+// Async document.cookie getter; converts '' to undefined which loses
+// information in the edge case where a single ''-valued anonymous
+// cookie is visible.
+async function getCookieStringDocument() {
+ if (!kHasDocument)
+ throw 'document.cookie not available in this context';
+ return String(document.cookie || '') || undefined;
+}
+
+// Async document.cookie setter
+async function setCookieStringDocument(setCookie) {
+ if (!kHasDocument)
+ throw 'document.cookie not available in this context';
+ document.cookie = setCookie;
+}
+
+// Observe the next 'change' event on the cookieStore. Typical usage:
+//
+// const eventPromise = observeNextCookieChangeEvent();
+// await /* something that modifies cookies */
+// await verifyCookieChangeEvent(
+// eventPromise, {changed: [{name: 'name', value: 'value'}]});
+//
+function observeNextCookieChangeEvent() {
+ return new Promise(resolve => {
+ cookieStore.addEventListener('change', e => resolve(e), {once: true});
+ });
+}
+
+async function verifyCookieChangeEvent(eventPromise, expected, description) {
+ description = description ? description + ': ' : '';
+ expected = Object.assign({changed:[], deleted:[]}, expected);
+ const event = await eventPromise;
+ assert_equals(event.changed.length, expected.changed.length,
+ description + 'number of changed cookies');
+ for (let i = 0; i < event.changed.length; ++i) {
+ assert_equals(event.changed[i].name, expected.changed[i].name,
+ description + 'changed cookie name');
+ assert_equals(event.changed[i].value, expected.changed[i].value,
+ description + 'changed cookie value');
+ }
+ assert_equals(event.deleted.length, expected.deleted.length,
+ description + 'number of deleted cookies');
+ for (let i = 0; i < event.deleted.length; ++i) {
+ assert_equals(event.deleted[i].name, expected.deleted[i].name,
+ description + 'deleted cookie name');
+ assert_equals(event.deleted[i].value, expected.deleted[i].value,
+ description + 'deleted cookie value');
+ }
+}
+
+// Helper function for promise_test with cookies; cookies
+// named in these tests are cleared before/after the test
+// body function is executed.
+async function cookie_test(func, description) {
+
+ // Wipe cookies used by tests before and after the test.
+ async function deleteAllCookies() {
+ (await cookieStore.getAll()).forEach(({name, value}) => {
+ cookieStore.delete(name);
+ });
+ }
+
+ return promise_test(async t => {
+ await deleteAllCookies();
+ try {
+ return await func(t);
+ } finally {
+ await deleteAllCookies();
+ }
+ }, description);
+}
diff --git a/testing/web-platform/tests/cookie-store/resources/cookie_helper.py b/testing/web-platform/tests/cookie-store/resources/cookie_helper.py
new file mode 100644
index 0000000000..71dd8b82ee
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/resources/cookie_helper.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+
+# Active wptserve handler for cookie operations.
+#
+# This must support the following requests:
+#
+# - GET with the following query parameters:
+# - charset: (optional) character set for response (default: utf-8)
+# A cookie: request header (if present) is echoed in the body with a
+# cookie= prefix followed by the urlencoded bytes from the header.
+# Used to inspect the cookie jar from an HTTP request header context.
+# - POST with form-data in the body and the following query-or-form parameters:
+# - set-cookie: (optional; repeated) echoed in the set-cookie: response
+# header and also echoed in the body with a set-cookie= prefix
+# followed by the urlencoded bytes from the parameter; multiple occurrences
+# are CRLF-delimited.
+# Used to set cookies from an HTTP response header context.
+#
+# The response has 200 status and content-type: text/plain; charset=<charset>
+import encodings, re
+
+from urllib.parse import parse_qs, quote
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+# NOTE: These are intentionally very lax to permit testing
+DISALLOWED_IN_COOKIE_NAME_RE = re.compile(br'[;\0-\x1f\x7f]')
+DISALLOWED_IN_HEADER_RE = re.compile(br'[\0-\x1f\x7f]')
+
+# Ensure common charset names do not end up with different
+# capitalization or punctuation
+CHARSET_OVERRIDES = {
+ encodings.codecs.lookup(charset).name: charset
+ for charset in (u'utf-8', u'iso-8859-1',)
+}
+
+def quote_str(cookie_str):
+ return isomorphic_encode(quote(isomorphic_decode(cookie_str), u'', encoding=u'iso-8859-1'))
+
+def parse_qs_str(query_str):
+ args = parse_qs(isomorphic_decode(query_str), keep_blank_values=True, encoding=u'iso-8859-1')
+ binary_args = {}
+ for key, val in args.items():
+ binary_args[isomorphic_encode(key)] = [isomorphic_encode(x) for x in val]
+ return binary_args
+
+def main(request, response):
+ assert request.method in (
+ u'GET',
+ u'POST',
+ ), u'request method was neither GET nor POST: %r' % request.method
+ qd = (isomorphic_encode(request.url).split(b'#')[0].split(b'?', 1) + [b''])[1]
+ if request.method == u'POST':
+ qd += b'&' + request.body
+ args = parse_qs_str(qd)
+
+ charset = encodings.codecs.lookup([isomorphic_decode(x) for x in args.get(b'charset', [u'utf-8'])][-1]).name
+ charset = CHARSET_OVERRIDES.get(charset, charset)
+ headers = [(b'content-type', b'text/plain; charset=' + isomorphic_encode(charset))]
+ body = []
+ if request.method == u'POST':
+ for set_cookie in args.get(b'set-cookie', []):
+ if b'=' in set_cookie.split(b';', 1)[0]:
+ name, rest = set_cookie.split(b'=', 1)
+ assert re.search(
+ DISALLOWED_IN_COOKIE_NAME_RE,
+ name
+ ) is None, b'name had disallowed characters: %r' % name
+ else:
+ rest = set_cookie
+ assert re.search(
+ DISALLOWED_IN_HEADER_RE,
+ rest
+ ) is None, b'rest had disallowed characters: %r' % rest
+ headers.append((b'set-cookie', set_cookie))
+ body.append(b'set-cookie=' + quote_str(set_cookie))
+
+ else:
+ cookie = request.headers.get(b'cookie')
+ if cookie is not None:
+ body.append(b'cookie=' + quote_str(cookie))
+ body = b'\r\n'.join(body)
+ headers.append((b'content-length', len(body)))
+ return 200, headers, body
diff --git a/testing/web-platform/tests/cookie-store/resources/empty_sw.js b/testing/web-platform/tests/cookie-store/resources/empty_sw.js
new file mode 100644
index 0000000000..2b0ae22612
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/resources/empty_sw.js
@@ -0,0 +1 @@
+// Empty service worker script.
diff --git a/testing/web-platform/tests/cookie-store/resources/helper_iframe.sub.html b/testing/web-platform/tests/cookie-store/resources/helper_iframe.sub.html
new file mode 100644
index 0000000000..9017eace44
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/resources/helper_iframe.sub.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset='utf-8'>
+<link rel='author' href='jarrydg@chromium.org' title='Jarryd Goodman'>
+<script>
+ 'use strict';
+
+ // Writing a cookie:
+ // Input: { cookieToSet: { name: 'cookie-name', value: 'cookie-value' } }
+ // Response: "Cookie has been set"
+ //
+ // Read a cookie.
+ // Command: { existingCookieName: 'cookie-name' }
+ // Response: Result of cookieStore.get('cookie-name'):
+ // { frameCookie: { name: 'cookie-name', value: 'cookie-value' } }
+ window.addEventListener('message', async function (event) {
+ const { opname } = event.data;
+ if (opname === 'set-cookie') {
+ const { name, value } = event.data
+ await cookieStore.set({
+ name,
+ value,
+ domain: '{{host}}',
+ });
+ event.source.postMessage('Cookie has been set', event.origin);
+ } else if (opname === 'get-cookie') {
+ const { name } = event.data
+ const frameCookie = await cookieStore.get(name);
+ event.source.postMessage({frameCookie}, event.origin);
+ }
+ });
+</script>
diff --git a/testing/web-platform/tests/cookie-store/resources/helpers.js b/testing/web-platform/tests/cookie-store/resources/helpers.js
new file mode 100644
index 0000000000..8d5dddef65
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/resources/helpers.js
@@ -0,0 +1,72 @@
+/**
+ * Promise based helper function who's return promise will resolve
+ * once the iframe src has been loaded
+ * @param {string} url the url to set the iframe src
+ * @param {test} t a test object to add a cleanup function to
+ * @return {Promise} when resolved, will return the iframe
+ */
+self.createIframe = (url, t) => new Promise(resolve => {
+ const iframe = document.createElement('iframe');
+ iframe.addEventListener('load', () => {resolve(iframe);}, {once: true});
+ iframe.src = url;
+ document.documentElement.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+});
+
+/**
+ * @description - Function unregisters any service workers in this scope
+ * and then creates a new registration. The function returns
+ * a promise that resolves when the registered service worker
+ * becomes activated. The resolved promise yields the
+ * service worker registration
+ * @param {testCase} t - test case to add cleanup functions to
+ */
+self.createServiceWorker = async (t, sw_registration_name, scope_url) => {
+ let registration = await navigator.serviceWorker.getRegistration(scope_url);
+ if (registration)
+ await registration.unregister();
+
+ registration = await navigator.serviceWorker.register(sw_registration_name,
+ {scope_url});
+ t.add_cleanup(() => registration.unregister());
+
+ return new Promise(resolve => {
+ const serviceWorker = registration.installing || registration.active ||
+ registration.waiting;
+ serviceWorker.addEventListener('statechange', event => {
+ if (event.target.state === 'activated') {
+ resolve(serviceWorker);
+ }
+ });
+ })
+}
+
+/**
+ * Function that will return a promise that resolves when a message event
+ * is fired. Returns a promise that resolves to the message that was received
+ */
+self.waitForMessage = () => new Promise(resolve => {
+ window.addEventListener('message', event => {
+ resolve(event.data);
+ }, {once: true});
+});
+
+/**
+ * Sends a message via MessageChannel and waits for the response
+ * @param {*} message
+ * @returns {Promise} resolves with the response payload
+ */
+self.sendMessageOverChannel = (message, target) => {
+ return new Promise(function(resolve, reject) {
+ const messageChannel = new MessageChannel();
+ messageChannel.port1.onmessage = event => {
+ if (event.data.error) {
+ reject(event.data.error);
+ } else {
+ resolve(event.data);
+ }
+ };
+
+ target.postMessage(message, [messageChannel.port2]);
+ })
+};
diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.https.sub.html b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.https.sub.html
new file mode 100644
index 0000000000..6879c5da92
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.https.sub.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<meta charset='utf-8'>
+<title>Async Cookies: cookieStore API in ServiceWorker across origins</title>
+<link rel='help' href='https://github.com/WICG/cookie-store'>
+<link rel='author' href='jarrydg@chromium.org' title='Jarryd Goodman'>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='resources/helpers.js'></script>
+<style>iframe {display: none}</style>
+<script>
+'use strict';
+
+const kPath = '/cookie-store/resources/helper_iframe.sub.html';
+const kCorsBase = `https://{{domains[www1]}}:{{ports[https][0]}}`;
+const kCorsUrl = `${kCorsBase}${kPath}`;
+
+promise_test(async t => {
+ const iframe = await createIframe(kCorsUrl, t);
+ assert_true(iframe != null);
+
+ const serviceWorker = await createServiceWorker(t,
+ 'serviceworker_cookieStore_cross_origin.js', '/does/not/exist');
+
+
+ iframe.contentWindow.postMessage({
+ opname: 'set-cookie',
+ name: 'cookie-name',
+ value: 'cookie-value',
+ }, kCorsBase);
+ t.add_cleanup(async () => { await cookieStore.delete('cookie-name'); });
+
+ await waitForMessage();
+
+ const { workerCookies } = await sendMessageOverChannel({ op: 'get-cookies' },
+ serviceWorker);
+
+ assert_equals(workerCookies.length, 1);
+ assert_equals(workerCookies[0].name, 'cookie-name');
+ assert_equals(workerCookies[0].value, 'cookie-value');
+}, 'cookieStore.get() in ServiceWorker reads cookie set in cross-origin frame');
+</script>
diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.js b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.js
new file mode 100644
index 0000000000..fa1c4084fd
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_cross_origin.js
@@ -0,0 +1,14 @@
+self.GLOBAL = {
+ isWindow: () => false,
+ isWorker: () => false,
+ isShadowRealm: () => false,
+};
+
+self.addEventListener('message', async event => {
+ if (event.data.op === 'get-cookies') {
+ const workerCookies = await cookieStore.getAll();
+ event.ports[0].postMessage({ workerCookies }, {
+ domain: event.origin,
+ });
+ }
+});
diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_subscriptions_reset.https.html b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_subscriptions_reset.https.html
new file mode 100644
index 0000000000..a1124e9220
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/serviceworker_cookieStore_subscriptions_reset.https.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Cookie Store API: reset cookie change subscription list</title>
+<link rel="help" href="https://github.com/WICG/cookie-store">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js">
+</script>
+<script src='resources/helpers.js'></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/empty_sw.js', 'resources/does/not/exist');
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.cookies.subscribe(
+ [{ name: 'cookie-name' }]);
+ const original_subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(original_subscriptions.length, 1,
+ 'subscription count before unregistration');
+
+ await registration.unregister();
+
+ const new_registration = await navigator.serviceWorker.register(
+ 'resources/empty_sw.js', { scope: 'resources/does/not/exist' });
+ t.add_cleanup(() => new_registration.unregister());
+ await wait_for_state(t, new_registration.installing, 'activated');
+
+ const new_subscriptions = await new_registration.cookies.getSubscriptions();
+ assert_equals(new_subscriptions.length, 0,
+ 'subscription count after unregistration');
+}, 'cookiechange subscriptions reset across service worker unregistrations');
+
+promise_test(async t => {
+ const registration = await service_worker_unregister_and_register(
+ t, 'resources/always_changing_sw.sub.js', 'resources/does/not/exist');
+ t.add_cleanup(() => registration.unregister());
+ await wait_for_state(t, registration.installing, 'activated');
+ await registration.cookies.subscribe(
+ [{ name: 'cookie-name' }]);
+ const original_subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(original_subscriptions.length, 1,
+ 'subscription count before update');
+
+ await registration.update();
+ const worker = await wait_for_update(t, registration);
+ await wait_for_state(t, worker, 'activated');
+
+ const update_subscriptions = await registration.cookies.getSubscriptions();
+ assert_equals(update_subscriptions.length, 1,
+ 'subscription count after update');
+}, 'cookiechange subscriptions persist across service worker updates');
+</script>
diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_mismatched_subscription.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_mismatched_subscription.https.any.js
new file mode 100644
index 0000000000..30d8d70940
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_mismatched_subscription.https.any.js
@@ -0,0 +1,44 @@
+// META: title=Cookie Store API: cookiechange event in ServiceWorker with mismatched subscription
+// META: global=serviceworker
+
+'use strict';
+
+const kScope = '/cookie-store/does/not/exist';
+
+// Resolves when the service worker receives the 'activate' event.
+const kServiceWorkerActivatedPromise = new Promise((resolve) => {
+ self.addEventListener('activate', event => { resolve(); });
+});
+
+// Resolves when a cookiechange event is received.
+const kCookieChangeReceivedPromise = new Promise((resolve) => {
+ self.addEventListener('cookiechange', (event) => {
+ resolve(event);
+ });
+});
+
+promise_test(async testCase => {
+ await kServiceWorkerActivatedPromise;
+
+ const subscriptions = [{ name: 'cookie-name', url: `${kScope}/path` }];
+ await registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions));
+
+ await cookieStore.set('another-cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('another-cookie-name');
+ });
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const event = await kCookieChangeReceivedPromise;
+ assert_equals(event.type, 'cookiechange');
+ assert_equals(event.changed.length, 1);
+ assert_equals(event.changed[0].name, 'cookie-name');
+ assert_equals(event.changed[0].value, 'cookie-value');
+ assert_equals(event.deleted.length, 0);
+ assert_true(event instanceof ExtendableCookieChangeEvent);
+ assert_true(event instanceof ExtendableEvent);
+}, 'cookiechange not dispatched for change that does not match subscription');
diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_multiple_subscriptions.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_multiple_subscriptions.https.any.js
new file mode 100644
index 0000000000..cd0657c0bc
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_multiple_subscriptions.https.any.js
@@ -0,0 +1,68 @@
+// META: title=Cookie Store API: cookiechange event in ServiceWorker with multiple subscriptions
+// META: global=serviceworker
+
+'use strict';
+
+const kScope = '/cookie-store/does/not/exist';
+
+// Resolves when the service worker receives the 'activate' event.
+const kServiceWorkerActivatedPromise = new Promise((resolve) => {
+ self.addEventListener('activate', event => { resolve(); });
+});
+
+// Accumulates cookiechange events dispatched to the service worker.
+let g_cookie_changes = [];
+
+// Resolved when a cookiechange event is received. Rearmed by
+// RearmCookieChangeReceivedPromise().
+let g_cookie_change_received_promise = null;
+let g_cookie_change_received_promise_resolver = null;
+self.addEventListener('cookiechange', (event) => {
+ g_cookie_changes.push(event);
+ if (g_cookie_change_received_promise_resolver) {
+ g_cookie_change_received_promise_resolver();
+ }
+});
+function RearmCookieChangeReceivedPromise() {
+ g_cookie_change_received_promise = new Promise((resolve) => {
+ g_cookie_change_received_promise_resolver = resolve;
+ });
+}
+RearmCookieChangeReceivedPromise();
+
+promise_test(async testCase => {
+ await kServiceWorkerActivatedPromise;
+
+ {
+ const subscriptions = [{ name: 'cookie-name1', url: `${kScope}/path1` }];
+ await registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions));
+ }
+ {
+ const subscriptions = [
+ { }, // Test the default values for subscription properties.
+ { name: 'cookie-name2' },
+ ];
+ await registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions));
+ }
+
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ testCase.add_cleanup(() => { g_cookie_changes = []; });
+
+ await g_cookie_change_received_promise;
+ testCase.add_cleanup(() => RearmCookieChangeReceivedPromise());
+
+ assert_equals(g_cookie_changes.length, 1);
+ const event = g_cookie_changes[0];
+ assert_equals(event.type, 'cookiechange');
+ assert_equals(event.changed.length, 1);
+ assert_equals(event.changed[0].name, 'cookie-name');
+ assert_equals(event.changed[0].value, 'cookie-value');
+ assert_equals(event.deleted.length, 0);
+ assert_true(event instanceof ExtendableCookieChangeEvent);
+ assert_true(event instanceof ExtendableEvent);
+}, 'cookiechange dispatched with cookie change that matches subscription');
diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_overlapping_subscriptions.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_overlapping_subscriptions.https.any.js
new file mode 100644
index 0000000000..1f433aeb94
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_overlapping_subscriptions.https.any.js
@@ -0,0 +1,87 @@
+// META: title=Cookie Store API: cookiechange event in ServiceWorker with overlapping subscriptions
+// META: global=serviceworker
+
+'use strict';
+
+const kScope = '/cookie-store/does/not/exist';
+
+// Resolves when the service worker receives the 'activate' event.
+const kServiceWorkerActivatedPromise = new Promise((resolve) => {
+ self.addEventListener('activate', event => { resolve(); });
+});
+
+// Accumulates cookiechange events dispatched to the service worker.
+let g_cookie_changes = [];
+
+// Resolved when a cookiechange event is received. Rearmed by
+// RearmCookieChangeReceivedPromise().
+let g_cookie_change_received_promise = null;
+let g_cookie_change_received_promise_resolver = null;
+self.addEventListener('cookiechange', (event) => {
+ g_cookie_changes.push(event);
+ if (g_cookie_change_received_promise_resolver) {
+ g_cookie_change_received_promise_resolver();
+ RearmCookieChangeReceivedPromise();
+ }
+});
+function RearmCookieChangeReceivedPromise() {
+ g_cookie_change_received_promise = new Promise((resolve) => {
+ g_cookie_change_received_promise_resolver = resolve;
+ });
+}
+RearmCookieChangeReceivedPromise();
+
+promise_test(async testCase => {
+ await kServiceWorkerActivatedPromise;
+
+ const subscriptions = [
+ { name: 'cookie-name' },
+ { url: `${kScope}/path` }
+ ];
+ await registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions));
+
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+ testCase.add_cleanup(() => { g_cookie_changes = []; });
+
+ await g_cookie_change_received_promise;
+ testCase.add_cleanup(() => RearmCookieChangeReceivedPromise());
+
+ // To ensure that we are accounting for all events dispatched by the first
+ // cookie change, we initiate and listen for a final cookie change that we
+ // know will dispatch a single event.
+ await cookieStore.set('coo', 'coo-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('coo');
+ });
+ testCase.add_cleanup(() => { g_cookie_changes = []; });
+
+ await g_cookie_change_received_promise;
+ testCase.add_cleanup(() => RearmCookieChangeReceivedPromise());
+
+ assert_equals(g_cookie_changes.length, 2);
+ {
+ const event = g_cookie_changes[0];
+ assert_equals(event.type, 'cookiechange');
+ assert_equals(event.changed.length, 1);
+ assert_equals(event.changed[0].name, 'cookie-name');
+ assert_equals(event.changed[0].value, 'cookie-value');
+ assert_equals(event.deleted.length, 0);
+ assert_true(event instanceof ExtendableCookieChangeEvent);
+ assert_true(event instanceof ExtendableEvent);
+ }
+ {
+ const event = g_cookie_changes[1];
+ assert_equals(event.type, 'cookiechange');
+ assert_equals(event.changed.length, 1);
+ assert_equals(event.changed[0].name, 'coo');
+ assert_equals(event.changed[0].value, 'coo-value');
+ assert_equals(event.deleted.length, 0);
+ assert_true(event instanceof ExtendableCookieChangeEvent);
+ assert_true(event instanceof ExtendableEvent);
+ }
+}, '1 cookiechange event dispatched with cookie change that matches multiple ' +
+ 'subscriptions');
diff --git a/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_single_subscription.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_single_subscription.https.any.js
new file mode 100644
index 0000000000..3ccb4b303d
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/serviceworker_cookiechange_eventhandler_single_subscription.https.any.js
@@ -0,0 +1,39 @@
+// META: title=Cookie Store API: cookiechange event in ServiceWorker with single subscription
+// META: global=serviceworker
+
+'use strict';
+
+const kScope = '/cookie-store/does/not/exist';
+
+// Resolves when the service worker receives the 'activate' event.
+const kServiceWorkerActivatedPromise = new Promise((resolve) => {
+ self.addEventListener('activate', event => { resolve(); });
+});
+
+// Resolves when a cookiechange event is received.
+const kCookieChangeReceivedPromise = new Promise(resolve => {
+ self.addEventListener('cookiechange', event => { resolve(event); });
+});
+
+promise_test(async testCase => {
+ await kServiceWorkerActivatedPromise;
+
+ const subscriptions = [{ name: 'cookie-name', url: `${kScope}/path` }];
+ await registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions));
+
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const event = await kCookieChangeReceivedPromise;
+ assert_equals(event.type, 'cookiechange');
+ assert_equals(event.changed.length, 1);
+ assert_equals(event.changed[0].name, 'cookie-name');
+ assert_equals(event.changed[0].value, 'cookie-value');
+ assert_equals(event.deleted.length, 0);
+ assert_true(event instanceof ExtendableCookieChangeEvent);
+ assert_true(event instanceof ExtendableEvent);
+}, 'cookiechange dispatched with cookie change that matches subscription ' +
+ 'to cookiechange event handler registered with addEventListener');
diff --git a/testing/web-platform/tests/cookie-store/serviceworker_oncookiechange_eventhandler_single_subscription.https.any.js b/testing/web-platform/tests/cookie-store/serviceworker_oncookiechange_eventhandler_single_subscription.https.any.js
new file mode 100644
index 0000000000..8def244089
--- /dev/null
+++ b/testing/web-platform/tests/cookie-store/serviceworker_oncookiechange_eventhandler_single_subscription.https.any.js
@@ -0,0 +1,39 @@
+// META: title=Cookie Store API: oncookiechange event in ServiceWorker with single subscription
+// META: global=serviceworker
+
+'use strict';
+
+const kScope = '/cookie-store/does/not/exist';
+
+// Resolves when the service worker receives the 'activate' event.
+const kServiceWorkerActivatedPromise = new Promise((resolve) => {
+ self.addEventListener('activate', event => { resolve(); });
+});
+
+// Resolves when a cookiechange event is received.
+const kCookieChangeReceivedPromise = new Promise(resolve => {
+ self.oncookiechange = event => { resolve(event); };
+});
+
+promise_test(async testCase => {
+ await kServiceWorkerActivatedPromise;
+
+ const subscriptions = [{ name: 'cookie-name', url: `${kScope}/path` }];
+ await registration.cookies.subscribe(subscriptions);
+ testCase.add_cleanup(() => registration.cookies.unsubscribe(subscriptions));
+
+ await cookieStore.set('cookie-name', 'cookie-value');
+ testCase.add_cleanup(async () => {
+ await cookieStore.delete('cookie-name');
+ });
+
+ const event = await kCookieChangeReceivedPromise;
+ assert_equals(event.type, 'cookiechange');
+ assert_equals(event.changed.length, 1);
+ assert_equals(event.changed[0].name, 'cookie-name');
+ assert_equals(event.changed[0].value, 'cookie-value');
+ assert_equals(event.deleted.length, 0);
+ assert_true(event instanceof ExtendableCookieChangeEvent);
+ assert_true(event instanceof ExtendableEvent);
+}, 'cookiechange dispatched with cookie change that matches subscription ' +
+ 'to cookiechange event handler registered with addEventListener');