diff --git a/testing/web-platform/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js b/testing/web-platform/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js
index e03aaa802d08..ee9202777e7e 100644
--- a/testing/web-platform/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js
+++ b/testing/web-platform/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js
@@ -2,6 +2,7 @@
// META: script=/storage/buckets/resources/util.js
promise_test(async t => {
+ prepareForBucketTest(t);
const arraySize = 1e6;
const objectStoreName = "storageManager";
const dbname =
diff --git a/testing/web-platform/tests/storage/buckets/bucket_names.tentative.https.any.js b/testing/web-platform/tests/storage/buckets/bucket_names.tentative.https.any.js
new file mode 100644
index 000000000000..e0f122dc47d8
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/bucket_names.tentative.https.any.js
@@ -0,0 +1,93 @@
+// META: title=Buckets API: Basic tests for bucket names.
+// META: script=resources/util.js
+// META: global=window,worker
+
+const kGoodBucketNameTests = [
+ ['abcdefghijklmnopqrstuvwxyz0123456789-_', 'with allowed characters'],
+ ['2021-01-01', 'with `-` in the middle'],
+ ['2021_01_01', 'with `_` in the middle'],
+ ['2021_01_01_', 'ending with `_`'],
+ ['2021-01-01-', 'ending with `-`'],
+];
+
+const kBadBucketNameTests = [
+ ['_bucket', 'start with `_`'],
+ ['-bucket', 'start with `-`'],
+ ['bucket name', 'have a space'],
+ ['bUcKet123', 'are not all lower case'],
+ ['bucket♦♥♠♣', 'are not in ASCII'],
+ ['2021/01/01', 'include an invalid special character'],
+ [' ', 'have no characters'],
+ ['', 'are an empty string'],
+ ['mjnkhtwsiyjsrxvrzzqafldfvomqopdjfiuxqelfkllcugrhvvblkvmiqlguhhqepoggyu',
+ 'exceed 64 chars']
+];
+
+// Test valid bucket names on open().
+kGoodBucketNameTests.forEach(test_data => {
+ const bucket_name = test_data[0];
+ const test_description = test_data[1];
+
+ promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+ const bucket = await navigator.storageBuckets.open(bucket_name);
+ assert_equals(bucket.name, bucket_name);
+
+ const buckets = await navigator.storageBuckets.keys();
+ assert_array_equals(buckets, [bucket_name]);
+ }, `open() allows bucket names ${test_description}`);
+});
+
+// Test invalid bucket names on open().
+kBadBucketNameTests.forEach(test_data => {
+ const bucket_name = test_data[0];
+ const test_description = test_data[1];
+
+ promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+ return promise_rejects_js(
+ testCase, TypeError,
+ navigator.storageBuckets.open(bucket_name));
+ }, `open() throws an error if bucket names ${test_description}`);
+});
+
+// Test valid bucket names on delete().
+kGoodBucketNameTests.forEach(test_data => {
+ const bucket_name = test_data[0];
+ const test_description = test_data[1];
+
+ promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+ await navigator.storageBuckets.open(bucket_name);
+ let buckets = await navigator.storageBuckets.keys();
+ assert_equals(buckets.length, 1);
+
+ await navigator.storageBuckets.delete(bucket_name);
+
+ buckets = await navigator.storageBuckets.keys();
+ assert_equals(buckets.length, 0);
+ }, `delete() allows bucket names ${test_description}`);
+});
+
+// Test invalid bucket names on delete().
+kBadBucketNameTests.forEach(test_data => {
+ const bucket_name = test_data[0];
+ const test_description = test_data[1];
+
+ promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+ return promise_rejects_js(
+ testCase, TypeError,
+ navigator.storageBuckets.delete(bucket_name));
+ }, `delete() throws an error if bucket names ${test_description}`);
+});
+
+promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+
+ await navigator.storageBuckets.open('bucket_name');
+ await navigator.storageBuckets.open('bucket_name');
+
+ const buckets = await navigator.storageBuckets.keys();
+ assert_array_equals(buckets, ['bucket_name']);
+}, 'open() does not store duplicate bucket names');
diff --git a/testing/web-platform/tests/storage/buckets/buckets_basic.tentative.https.any.js b/testing/web-platform/tests/storage/buckets/buckets_basic.tentative.https.any.js
new file mode 100644
index 000000000000..20ff227bac1e
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/buckets_basic.tentative.https.any.js
@@ -0,0 +1,50 @@
+// META: title=Buckets API: Basic tests for open(), keys(), delete().
+// META: script=resources/util.js
+// META: global=window,worker
+
+'use strict';
+
+// This test is for initial IDL version optimized for debugging.
+// Split and add extensive testing once implementation for the endpoints are
+// added and method definitions are more defined.
+promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+
+ await navigator.storageBuckets.open('bucket_name3');
+ await navigator.storageBuckets.open('bucket_name1');
+ await navigator.storageBuckets.open('bucket_name2');
+
+ const buckets = await navigator.storageBuckets.keys();
+ assert_array_equals(
+ buckets, ['bucket_name1', 'bucket_name2', 'bucket_name3']);
+}, 'keys() lists all stored bucket names alphabetically');
+
+promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+
+ await navigator.storageBuckets.open('bucket_name1');
+ await navigator.storageBuckets.open('bucket_name2');
+
+ let buckets = await navigator.storageBuckets.keys();
+ assert_array_equals(buckets, ['bucket_name1', 'bucket_name2']);
+
+ await navigator.storageBuckets.delete('bucket_name1');
+
+ buckets = await navigator.storageBuckets.keys();
+ assert_array_equals(buckets, ['bucket_name2']);
+}, 'delete() removes stored bucket name');
+
+promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+
+ await navigator.storageBuckets.open('bucket_name');
+
+ let buckets = await navigator.storageBuckets.keys();
+ assert_array_equals(buckets, ['bucket_name']);
+
+ await navigator.storageBuckets.delete('does-not-exist');
+
+ buckets = await navigator.storageBuckets.keys();
+ assert_equals(buckets.length, 1);
+ assert_equals(buckets[0], 'bucket_name');
+}, 'delete() does nothing if bucket name does not exist');
diff --git a/testing/web-platform/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js b/testing/web-platform/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js
new file mode 100644
index 000000000000..918364d73152
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js
@@ -0,0 +1,32 @@
+// META: title=Buckets API: Tests for bucket storage policies.
+// META: script=resources/util.js
+// META: global=window,worker
+
+'use strict';
+
+function sanitizeQuota(quota) {
+ return Math.max(1, Math.min(Number.MAX_SAFE_INTEGER, Math.floor(quota)));
+}
+
+async function testQuota(storageKeyQuota, quota, name) {
+ const safeQuota = sanitizeQuota(quota);
+ const bucket = await navigator.storageBuckets.open(name, { quota: safeQuota });
+ const estimateQuota = (await bucket.estimate()).quota;
+ assert_equals(estimateQuota, Math.min(safeQuota, storageKeyQuota));
+}
+
+promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+
+ const storageKeyQuota = (await navigator.storage.estimate()).quota;
+
+ testQuota(storageKeyQuota, 1, 'one');
+ testQuota(storageKeyQuota, storageKeyQuota / 4, 'quarter');
+ testQuota(storageKeyQuota, storageKeyQuota / 2, 'half');
+ testQuota(storageKeyQuota, storageKeyQuota - 1, 'one_less');
+ testQuota(storageKeyQuota, storageKeyQuota, 'origin_quota');
+ testQuota(storageKeyQuota, storageKeyQuota + 1, 'one_more');
+ testQuota(storageKeyQuota, storageKeyQuota * 2, 'twice');
+ testQuota(storageKeyQuota, storageKeyQuota * 4, 'four_times');
+ testQuota(storageKeyQuota, Number.MAX_SAFE_INTEGER, 'max_safe_int');
+}, 'For an individual bucket, the quota is the minimum of the requested quota and the StorageKey quota.');
diff --git a/testing/web-platform/tests/storage/buckets/detached-iframe.https.html b/testing/web-platform/tests/storage/buckets/detached-iframe.https.html
new file mode 100644
index 000000000000..a67c89efa3e8
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/detached-iframe.https.html
@@ -0,0 +1,62 @@
+
+
+
Storage Buckets API on detached iframe
+
+
+
+
+
+
+
diff --git a/testing/web-platform/tests/storage/buckets/idlharness-worker.https.any.js b/testing/web-platform/tests/storage/buckets/idlharness-worker.https.any.js
new file mode 100644
index 000000000000..9a1a3f80b9bc
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/idlharness-worker.https.any.js
@@ -0,0 +1,22 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+idl_test(
+ ['storage-buckets'],
+ ['html'],
+ async (idl_array, t) => {
+ idl_array.add_objects({
+ StorageBucketManager: ['navigator.storageBuckets'],
+ StorageBucket: []
+ });
+
+ if (self.Window) {
+ idl_array.add_objects({ Navigator: ['navigator'] });
+ } else {
+ idl_array.add_objects({ WorkerNavigator: ['navigator'] });
+ }
+ }
+);
diff --git a/testing/web-platform/tests/storage/buckets/opaque-origin.https.window.js b/testing/web-platform/tests/storage/buckets/opaque-origin.https.window.js
new file mode 100644
index 000000000000..c91d3faa67a3
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/opaque-origin.https.window.js
@@ -0,0 +1,58 @@
+// META: title=Storage Buckets API: Interface is not exposed in opaque origins.
+// META: script=resources/util.js
+// META: global=window
+
+const kSandboxWindowUrl = 'resources/opaque-origin-sandbox.html';
+
+function add_iframe(test, src, sandbox) {
+ const iframe = document.createElement('iframe');
+ iframe.src = src;
+ if (sandbox !== undefined) {
+ iframe.sandbox = sandbox;
+ }
+ document.body.appendChild(iframe);
+ test.add_cleanup(() => {
+ iframe.remove();
+ });
+}
+
+// |kSandboxWindowUrl| sends the result of methods on StorageBucketManager.
+// For windows using sandbox="allow-scripts", it must produce a rejected
+// promise.
+async function verify_results_from_sandboxed_child_window(test) {
+ const event_watcher = new EventWatcher(test, self, 'message');
+
+ const first_message_event = await event_watcher.wait_for('message');
+ assert_equals(
+ first_message_event.data,
+ 'navigator.storageBuckets.open(): REJECTED: SecurityError');
+
+ const second_message_event = await event_watcher.wait_for('message');
+ assert_equals(
+ second_message_event.data,
+ 'navigator.storageBuckets.keys(): REJECTED: SecurityError');
+
+ const third_message_event = await event_watcher.wait_for('message');
+ assert_equals(
+ third_message_event.data,
+ 'navigator.storageBuckets.delete(): REJECTED: SecurityError');
+}
+
+promise_test(async testCase => {
+ prepareForBucketTest(testCase);
+ add_iframe(testCase, kSandboxWindowUrl, /*sandbox=*/ 'allow-scripts');
+ await verify_results_from_sandboxed_child_window(testCase);
+}, 'StorageBucketManager methods must reject in a sandboxed iframe.');
+
+promise_test(async testCase => {
+ prepareForBucketTest(testCase);
+ const child_window_url = kSandboxWindowUrl +
+ '?pipe=header(Content-Security-Policy, sandbox allow-scripts)';
+
+ const child_window = window.open(child_window_url);
+ testCase.add_cleanup(() => {
+ child_window.close();
+ });
+
+ await verify_results_from_sandboxed_child_window(testCase);
+}, 'StorageBucketManager methods must reject in a sandboxed opened window.');
diff --git a/testing/web-platform/tests/storage/buckets/resources/opaque-origin-sandbox.html b/testing/web-platform/tests/storage/buckets/resources/opaque-origin-sandbox.html
new file mode 100644
index 000000000000..4a1ac39fdf6f
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/resources/opaque-origin-sandbox.html
@@ -0,0 +1,52 @@
+
+
+
diff --git a/testing/web-platform/tests/storage/buckets/resources/util.js b/testing/web-platform/tests/storage/buckets/resources/util.js
index 425303ce2c9c..5fff4894442a 100644
--- a/testing/web-platform/tests/storage/buckets/resources/util.js
+++ b/testing/web-platform/tests/storage/buckets/resources/util.js
@@ -4,7 +4,7 @@
// is over (whether it passes or fails).
async function prepareForBucketTest(test) {
// Verify initial state.
- assert_equals('', (await navigator.storageBuckets.keys()).join());
+ assert_equals((await navigator.storageBuckets.keys()).join(), '');
// Clean up after test.
test.add_cleanup(async function() {
const keys = await navigator.storageBuckets.keys();
diff --git a/testing/web-platform/tests/storage/buckets/storage_bucket_object.tentative.https.any.js b/testing/web-platform/tests/storage/buckets/storage_bucket_object.tentative.https.any.js
new file mode 100644
index 000000000000..52f1693ccb22
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/storage_bucket_object.tentative.https.any.js
@@ -0,0 +1,143 @@
+// META: title=Buckets API: Tests for the StorageBucket object.
+// META: global=window,worker
+
+'use strict';
+
+promise_test(async testCase => {
+ const bucket = await navigator.storageBuckets.open('bucket_name');
+ testCase.add_cleanup(async () => {
+ await navigator.storageBuckets.delete('bucket_name');
+ });
+ const persisted = await bucket.persisted();
+ assert_false(persisted);
+
+ // Also verify that the promise is rejected after the bucket is deleted.
+ await navigator.storageBuckets.delete('bucket_name');
+ await promise_rejects_dom(testCase, 'UnknownError', bucket.persisted());
+}, 'persisted() should default to false');
+
+promise_test(async testCase => {
+ const bucket = await navigator.storageBuckets.open('bucket_name');
+ testCase.add_cleanup(async () => {
+ await navigator.storageBuckets.delete('bucket_name');
+ });
+ const estimate = await bucket.estimate();
+ assert_greater_than(estimate.quota, 0);
+ assert_equals(estimate.usage, 0);
+
+ const cacheName = 'attachments';
+ const cacheKey = 'receipt1.txt';
+ var inboxCache = await bucket.caches.open(cacheName);
+ await inboxCache.put(cacheKey, new Response('bread x 2'))
+
+ const estimate2 = await bucket.estimate();
+ assert_equals(estimate.quota, estimate2.quota);
+ assert_less_than(estimate.usage, estimate2.usage);
+}, 'estimate() should retrieve quota usage');
+
+promise_test(async testCase => {
+ const bucket = await navigator.storageBuckets.open(
+ 'bucket_name', { durability: 'strict' });
+ testCase.add_cleanup(async () => {
+ await navigator.storageBuckets.delete('bucket_name');
+ });
+
+ const durability = await bucket.durability();
+ assert_equals('strict', durability);
+
+ await navigator.storageBuckets.delete('bucket_name');
+ await promise_rejects_dom(testCase, 'UnknownError', bucket.durability());
+}, 'durability() should retrieve bucket durability specified during creation');
+
+promise_test(async testCase => {
+ const bucket = await navigator.storageBuckets.open('bucket_name');
+ testCase.add_cleanup(async () => {
+ await navigator.storageBuckets.delete('bucket_name');
+ });
+
+ const durability = await bucket.durability();
+ assert_equals('relaxed', durability);
+}, 'Bucket durability defaults to relaxed');
+
+promise_test(async testCase => {
+ const oneYear = 365 * 24 * 60 * 60 * 1000;
+ const expiresDate = Date.now() + oneYear;
+ const bucket = await navigator.storageBuckets.open(
+ 'bucket_name', { expires: expiresDate });
+ testCase.add_cleanup(async () => {
+ await navigator.storageBuckets.delete('bucket_name');
+ });
+
+ const expires = await bucket.expires();
+ assert_equals(expires, expiresDate);
+}, 'expires() should retrieve expires date');
+
+promise_test(async testCase => {
+ const bucket = await navigator.storageBuckets.open('bucket_name');
+ testCase.add_cleanup(async () => {
+ await navigator.storageBuckets.delete('bucket_name');
+ });
+
+ const expires = await bucket.expires();
+ assert_equals(expires, null);
+
+ await navigator.storageBuckets.delete('bucket_name');
+ await promise_rejects_dom(testCase, 'UnknownError', bucket.expires());
+}, 'expires() should be defaulted to null');
+
+promise_test(async testCase => {
+ const bucket = await navigator.storageBuckets.open('bucket_name');
+ testCase.add_cleanup(async () => {
+ await navigator.storageBuckets.delete('bucket_name');
+ });
+
+ const oneYear = 365 * 24 * 60 * 60 * 1000;
+ const expiresDate = Date.now() + oneYear;
+ await bucket.setExpires(expiresDate);
+
+ const expires = await bucket.expires();
+ assert_equals(expires, expiresDate);
+
+ await navigator.storageBuckets.delete('bucket_name');
+ await promise_rejects_dom(testCase, 'UnknownError', bucket.setExpires(expiresDate));
+}, 'setExpires() should set bucket expires date');
+
+promise_test(async testCase => {
+ const oneDay = 24 * 60 * 60 * 1000;
+ const expiresDate = Date.now() + oneDay;
+ const bucket = await navigator.storageBuckets.open('bucket_name', {
+ expires: expiresDate
+ });
+ testCase.add_cleanup(async () => {
+ await navigator.storageBuckets.delete('bucket_name');
+ });
+ let expires = await bucket.expires();
+ assert_equals(expires, expiresDate);
+
+ const oneYear = 365 * oneDay;
+ const newExpiresDate = Date.now() + oneYear;
+ await bucket.setExpires(newExpiresDate);
+
+ expires = await bucket.expires();
+ assert_equals(expires, newExpiresDate);
+}, 'setExpires() should update expires date');
+
+promise_test(async testCase => {
+ const bucket = await navigator.storageBuckets.open(
+ 'bucket_name', { durability: 'strict' });
+ testCase.add_cleanup(async () => {
+ await navigator.storageBuckets.delete('bucket_name');
+ });
+
+ const same_bucket = await navigator.storageBuckets.open('bucket_name');
+ const durability = await bucket.durability();
+ const other_durability = await same_bucket.durability();
+ assert_equals(durability, other_durability);
+
+ // Delete the bucket and remake it.
+ await navigator.storageBuckets.delete('bucket_name');
+ const remade_bucket = await navigator.storageBuckets.open('bucket_name');
+ await promise_rejects_dom(testCase, 'UnknownError', bucket.durability());
+ const remade_durability = await remade_bucket.durability();
+ assert_not_equals(remade_durability, durability);
+}, 'two handles can refer to the same bucket, and a bucket name can be reused after deletion');