summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/storage
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/storage')
-rw-r--r--testing/web-platform/tests/storage/META.yml4
-rw-r--r--testing/web-platform/tests/storage/README.md7
-rw-r--r--testing/web-platform/tests/storage/buckets/META.yml5
-rw-r--r--testing/web-platform/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js35
-rw-r--r--testing/web-platform/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js21
-rw-r--r--testing/web-platform/tests/storage/buckets/resources/cached-resource.txt1
-rw-r--r--testing/web-platform/tests/storage/buckets/resources/util.js57
-rw-r--r--testing/web-platform/tests/storage/estimate-indexeddb.https.any.js61
-rw-r--r--testing/web-platform/tests/storage/estimate-parallel.https.any.js13
-rw-r--r--testing/web-platform/tests/storage/estimate-usage-details-caches.https.tentative.any.js20
-rw-r--r--testing/web-platform/tests/storage/estimate-usage-details-indexeddb.https.tentative.any.js59
-rw-r--r--testing/web-platform/tests/storage/estimate-usage-details-service-workers.https.tentative.window.js38
-rw-r--r--testing/web-platform/tests/storage/estimate-usage-details.https.tentative.any.js12
-rw-r--r--testing/web-platform/tests/storage/helpers.js46
-rw-r--r--testing/web-platform/tests/storage/idlharness.https.any.js18
-rw-r--r--testing/web-platform/tests/storage/opaque-origin.https.window.js80
-rw-r--r--testing/web-platform/tests/storage/partitioned-estimate-usage-details-caches.tentative.https.sub.html74
-rw-r--r--testing/web-platform/tests/storage/partitioned-estimate-usage-details-indexeddb.tentative.https.sub.html84
-rw-r--r--testing/web-platform/tests/storage/partitioned-estimate-usage-details-service-workers.tentative.https.sub.html88
-rw-r--r--testing/web-platform/tests/storage/permission-query.https.any.js10
-rw-r--r--testing/web-platform/tests/storage/persist-permission-manual.https.html27
-rw-r--r--testing/web-platform/tests/storage/persisted.https.any.js14
-rw-r--r--testing/web-platform/tests/storage/quotachange-in-detached-iframe.tentative.https.html21
-rw-r--r--testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html30
-rw-r--r--testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html28
-rw-r--r--testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html30
-rw-r--r--testing/web-platform/tests/storage/resources/worker.js3
-rw-r--r--testing/web-platform/tests/storage/storagemanager-estimate.https.any.js60
-rw-r--r--testing/web-platform/tests/storage/storagemanager-persist-persisted-match.https.any.js9
-rw-r--r--testing/web-platform/tests/storage/storagemanager-persist.https.window.js10
-rw-r--r--testing/web-platform/tests/storage/storagemanager-persist.https.worker.js8
-rw-r--r--testing/web-platform/tests/storage/storagemanager-persisted.https.any.js10
32 files changed, 983 insertions, 0 deletions
diff --git a/testing/web-platform/tests/storage/META.yml b/testing/web-platform/tests/storage/META.yml
new file mode 100644
index 0000000000..2aad1fb513
--- /dev/null
+++ b/testing/web-platform/tests/storage/META.yml
@@ -0,0 +1,4 @@
+spec: https://storage.spec.whatwg.org/
+suggested_reviewers:
+ - annevk
+ - inexorabletash
diff --git a/testing/web-platform/tests/storage/README.md b/testing/web-platform/tests/storage/README.md
new file mode 100644
index 0000000000..8d0dca31b4
--- /dev/null
+++ b/testing/web-platform/tests/storage/README.md
@@ -0,0 +1,7 @@
+This directory contains the Storage test suite.
+
+To run the tests in this test suite within a browser, go to: <https://wpt.live/storage/>.
+
+The living standard is: <https://storage.spec.whatwg.org/>
+
+The API is at: <https://storage.spec.whatwg.org/#api>
diff --git a/testing/web-platform/tests/storage/buckets/META.yml b/testing/web-platform/tests/storage/buckets/META.yml
new file mode 100644
index 0000000000..4f215060f5
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/META.yml
@@ -0,0 +1,5 @@
+spec: https://github.com/WICG/storage-buckets
+suggested_reviewers:
+ - ayui
+ - jsbell
+ - pwnall
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
new file mode 100644
index 0000000000..ba82edb72e
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js
@@ -0,0 +1,35 @@
+// META: title=Bucket quota enforcement for indexeddb
+// META: script=/storage/buckets/resources/util.js
+
+promise_test(async t => {
+ const arraySize = 1e6;
+ const objectStoreName = "storageManager";
+ const dbname =
+ this.window ? window.location.pathname : 'estimate-worker.https.html';
+
+ let quota = arraySize / 2;
+ const bucket = await navigator.storageBuckets.open('idb', {quota});
+
+ await indexedDbDeleteRequest(bucket.indexedDB, dbname);
+
+ const db =
+ await indexedDbOpenRequest(t, bucket.indexedDB, dbname, (db_to_upgrade) => {
+ db_to_upgrade.createObjectStore(objectStoreName);
+ });
+
+ const txn = db.transaction(objectStoreName, 'readwrite');
+ const buffer = new ArrayBuffer(arraySize);
+ const view = new Uint8Array(buffer);
+
+ for (let i = 0; i < arraySize; i++) {
+ view[i] = Math.floor(Math.random() * 255);
+ }
+
+ const testBlob = new Blob([buffer], {type: 'binary/random'});
+ txn.objectStore(objectStoreName).add(testBlob, 1);
+
+ await promise_rejects_dom(
+ t, 'QuotaExceededError', transactionPromise(txn));
+
+ db.close();
+}, 'IDB respects bucket quota');
diff --git a/testing/web-platform/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js b/testing/web-platform/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js
new file mode 100644
index 0000000000..d6dce3675d
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js
@@ -0,0 +1,21 @@
+// META: title=Buckets API: Tests for bucket storage policies.
+// META: script=/storage/buckets/resources/util.js
+// META: global=window,worker
+
+'use strict';
+
+promise_test(async testCase => {
+ await prepareForBucketTest(testCase);
+
+ await promise_rejects_js(
+ testCase, TypeError,
+ navigator.storageBuckets.open('negative', {quota: -1}));
+
+ await promise_rejects_js(
+ testCase, TypeError, navigator.storageBuckets.open('zero', {quota: 0}));
+
+ await promise_rejects_js(
+ testCase, TypeError,
+ navigator.storageBuckets.open(
+ 'above_max', {quota: Number.MAX_SAFE_INTEGER + 1}));
+}, 'The open promise should reject with a TypeError when quota is requested outside the range of 1 to Number.MAX_SAFE_INTEGER.');
diff --git a/testing/web-platform/tests/storage/buckets/resources/cached-resource.txt b/testing/web-platform/tests/storage/buckets/resources/cached-resource.txt
new file mode 100644
index 0000000000..c57eff55eb
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/resources/cached-resource.txt
@@ -0,0 +1 @@
+Hello World! \ No newline at end of file
diff --git a/testing/web-platform/tests/storage/buckets/resources/util.js b/testing/web-platform/tests/storage/buckets/resources/util.js
new file mode 100644
index 0000000000..425303ce2c
--- /dev/null
+++ b/testing/web-platform/tests/storage/buckets/resources/util.js
@@ -0,0 +1,57 @@
+'use strict';
+
+// Makes sure initial bucket state is as expected and to clean up after the test
+// is over (whether it passes or fails).
+async function prepareForBucketTest(test) {
+ // Verify initial state.
+ assert_equals('', (await navigator.storageBuckets.keys()).join());
+ // Clean up after test.
+ test.add_cleanup(async function() {
+ const keys = await navigator.storageBuckets.keys();
+ for (const key of keys) {
+ await navigator.storageBuckets.delete(key);
+ }
+ });
+}
+
+function indexedDbOpenRequest(t, idb, dbname, upgrade_func) {
+ return new Promise((resolve, reject) => {
+ const openRequest = idb.open(dbname);
+ t.add_cleanup(() => {
+ indexedDbDeleteRequest(idb, dbname);
+ });
+
+ openRequest.onerror = () => {
+ reject(openRequest.error);
+ };
+ openRequest.onsuccess = () => {
+ resolve(openRequest.result);
+ };
+ openRequest.onupgradeneeded = event => {
+ upgrade_func(openRequest.result);
+ };
+ });
+}
+
+function indexedDbDeleteRequest(idb, name) {
+ return new Promise((resolve, reject) => {
+ const deleteRequest = idb.deleteDatabase(name);
+ deleteRequest.onerror = () => {
+ reject(deleteRequest.error);
+ };
+ deleteRequest.onsuccess = () => {
+ resolve();
+ };
+ });
+}
+
+function transactionPromise(txn) {
+ return new Promise((resolve, reject) => {
+ txn.onabort = () => {
+ reject(txn.error);
+ };
+ txn.oncomplete = () => {
+ resolve();
+ };
+ });
+}
diff --git a/testing/web-platform/tests/storage/estimate-indexeddb.https.any.js b/testing/web-platform/tests/storage/estimate-indexeddb.https.any.js
new file mode 100644
index 0000000000..f0b82b9fa0
--- /dev/null
+++ b/testing/web-platform/tests/storage/estimate-indexeddb.https.any.js
@@ -0,0 +1,61 @@
+// META: title=StorageManager: estimate() for indexeddb
+// META: script=/storage/buckets/resources/util.js
+
+test(t => {
+ assert_true('estimate' in navigator.storage);
+ assert_equals(typeof navigator.storage.estimate, 'function');
+ assert_true(navigator.storage.estimate() instanceof Promise);
+}, 'estimate() method exists and returns a Promise');
+
+promise_test(async t => {
+ const estimate = await navigator.storage.estimate();
+ assert_equals(typeof estimate, 'object');
+ assert_true('usage' in estimate);
+ assert_equals(typeof estimate.usage, 'number');
+ assert_true('quota' in estimate);
+ assert_equals(typeof estimate.quota, 'number');
+}, 'estimate() resolves to dictionary with members');
+
+promise_test(async t => {
+ const arraySize = 1e6;
+ const objectStoreName = "storageManager";
+ const dbname =
+ this.window ? window.location.pathname : 'estimate-worker.https.html';
+
+ await indexedDbDeleteRequest(indexedDB, dbname);
+ let estimate = await navigator.storage.estimate();
+
+ const usageBeforeCreate = estimate.usage;
+ const db =
+ await indexedDbOpenRequest(t, indexedDB, dbname, (db_to_upgrade) => {
+ db_to_upgrade.createObjectStore(objectStoreName);
+ });
+
+ estimate = await navigator.storage.estimate();
+ const usageAfterCreate = estimate.usage;
+
+ assert_greater_than(
+ usageAfterCreate, usageBeforeCreate,
+ 'estimated usage should increase after object store is created');
+
+ const txn = db.transaction(objectStoreName, 'readwrite');
+ const buffer = new ArrayBuffer(arraySize);
+ const view = new Uint8Array(buffer);
+
+ for (let i = 0; i < arraySize; i++) {
+ view[i] = Math.floor(Math.random() * 255);
+ }
+
+ const testBlob = new Blob([buffer], {type: 'binary/random'});
+ txn.objectStore(objectStoreName).add(testBlob, 1);
+
+ await transactionPromise(txn);
+
+ estimate = await navigator.storage.estimate();
+ const usageAfterPut = estimate.usage;
+ assert_greater_than(
+ usageAfterPut, usageAfterCreate,
+ 'estimated usage should increase after large value is stored');
+
+ db.close();
+}, 'estimate() shows usage increase after large value is stored');
diff --git a/testing/web-platform/tests/storage/estimate-parallel.https.any.js b/testing/web-platform/tests/storage/estimate-parallel.https.any.js
new file mode 100644
index 0000000000..090f004b85
--- /dev/null
+++ b/testing/web-platform/tests/storage/estimate-parallel.https.any.js
@@ -0,0 +1,13 @@
+// META: title=StorageManager: multiple estimate() calls in parallel
+
+promise_test(async t => {
+ let r1, r2;
+ await Promise.all([
+ navigator.storage.estimate().then(r => { r1 = r; }),
+ navigator.storage.estimate().then(r => { r2 = r; })
+ ]);
+ assert_true(('usage' in r1) && ('quota' in r1),
+ 'first response should have expected fields');
+ assert_true(('usage' in r2) && ('quota' in r2),
+ 'second response should have expected fields');
+}, 'Multiple estimate() calls in parallel should complete');
diff --git a/testing/web-platform/tests/storage/estimate-usage-details-caches.https.tentative.any.js b/testing/web-platform/tests/storage/estimate-usage-details-caches.https.tentative.any.js
new file mode 100644
index 0000000000..bf889f8418
--- /dev/null
+++ b/testing/web-platform/tests/storage/estimate-usage-details-caches.https.tentative.any.js
@@ -0,0 +1,20 @@
+// META: title=StorageManager: estimate() for caches
+
+promise_test(async t => {
+ let estimate = await navigator.storage.estimate();
+
+ const cachesUsageBeforeCreate = estimate.usageDetails.caches || 0;
+
+ const cacheName = 'testCache';
+ const cache = await caches.open(cacheName);
+ t.add_cleanup(() => caches.delete(cacheName));
+
+ await cache.put('/test.json', new Response('x'.repeat(1024*1024)));
+
+ estimate = await navigator.storage.estimate();
+ assert_true('caches' in estimate.usageDetails);
+ const cachesUsageAfterPut = estimate.usageDetails.caches;
+ assert_greater_than(
+ cachesUsageAfterPut, cachesUsageBeforeCreate,
+ 'estimated usage should increase after value is stored');
+}, 'estimate() shows usage increase after large value is stored');
diff --git a/testing/web-platform/tests/storage/estimate-usage-details-indexeddb.https.tentative.any.js b/testing/web-platform/tests/storage/estimate-usage-details-indexeddb.https.tentative.any.js
new file mode 100644
index 0000000000..551cede9c6
--- /dev/null
+++ b/testing/web-platform/tests/storage/estimate-usage-details-indexeddb.https.tentative.any.js
@@ -0,0 +1,59 @@
+// META: title=StorageManager: estimate() usage details for indexeddb
+// META: script=helpers.js
+// META: script=../IndexedDB/resources/support-promises.js
+
+promise_test(async t => {
+ const estimate = await navigator.storage.estimate()
+ assert_equals(typeof estimate.usageDetails, 'object');
+}, 'estimate() resolves to dictionary with usageDetails member');
+
+promise_test(async t => {
+ // We use 100KB here because db compaction usually happens every few MB
+ // 100KB is large enough to avoid a false positive (small amounts of metadata
+ // getting written for some random reason), and small enough to avoid
+ // compaction with a reasonably high probability.
+ const writeSize = 1024 * 100;
+ const objectStoreName = 'store';
+ const dbname = self.location.pathname;
+
+ await indexedDB.deleteDatabase(dbname);
+ let usageAfterWrite, usageBeforeWrite;
+ // TODO(crbug.com/906867): Refactor this test to better deal with db/log
+ // compaction flakiness
+ // The for loop here is to help make this test less flaky. The reason it is
+ // flaky is that database and/or log compaction could happen in the middle of
+ // this loop. The problem is that this test runs in a large batch of tests,
+ // and previous tests might have created a lot of garbage which could trigger
+ // compaction. Suppose the initial estimate shows 1MB usage before creating
+ // the db. Compaction could happen after this step and before we measure
+ // usage at the end, meaning the 1MB could be wiped to 0, an extra 1024 * 100
+ // is put in, and the actual increase in usage does not reach our expected
+ // increase. Loop 10 times here to be safe (and reduce the number of bot
+ // builds that fail); all it takes is one iteration without compaction for
+ // this to pass.
+ for (let i = 0; i < 10; i++) {
+ const db = await createDB(dbname, objectStoreName, t);
+ let estimate = await navigator.storage.estimate();
+
+ // If usage is 0, usageDetails does not include the usage (undefined)
+ usageBeforeWrite = estimate.usageDetails.indexedDB || 0;
+
+ const txn = db.transaction(objectStoreName, 'readwrite');
+ const valueToStore = largeValue(writeSize, Math.random() * 255);
+ txn.objectStore(objectStoreName).add(valueToStore, 1);
+
+ await transactionPromise(txn);
+
+ estimate = await navigator.storage.estimate();
+ usageAfterWrite = estimate.usageDetails.indexedDB;
+ db.close();
+
+ if (usageAfterWrite - usageBeforeWrite >= writeSize) {
+ break;
+ }
+ }
+
+ assert_greater_than_equal(usageAfterWrite - usageBeforeWrite,
+ writeSize);
+}, 'estimate() usage details reflects increase in indexedDB after large ' +
+ 'value is stored');
diff --git a/testing/web-platform/tests/storage/estimate-usage-details-service-workers.https.tentative.window.js b/testing/web-platform/tests/storage/estimate-usage-details-service-workers.https.tentative.window.js
new file mode 100644
index 0000000000..cf3a2aa943
--- /dev/null
+++ b/testing/web-platform/tests/storage/estimate-usage-details-service-workers.https.tentative.window.js
@@ -0,0 +1,38 @@
+// META: title=StorageManager: estimate() for service worker registrations
+const wait_for_active = worker => new Promise(resolve =>{
+ if (worker.active) { resolve(worker.active); }
+
+ const listen_for_active = worker => e => {
+ if (e.target.state === 'activated') { resolve(worker.active); }
+ }
+
+ if (worker.waiting) {
+ worker.waiting
+ .addEventListener('statechange', listen_for_active(worker.waiting));
+ }
+ if (worker.installing) {
+ worker.installing
+ .addEventListener('statechange', listen_for_active(worker.installing));
+ }
+});
+
+promise_test(async t => {
+ let estimate = await navigator.storage.estimate();
+ const usageBeforeCreate = estimate.usageDetails.serviceWorkerRegistrations ||
+ 0;
+ // Note: helpers.js is an arbitrary file; it could be any file that
+ // exists, but this test does not depend on the contents of said file.
+ const serviceWorkerRegistration = await
+ navigator.serviceWorker.register('./helpers.js');
+
+ t.add_cleanup(() => serviceWorkerRegistration.unregister());
+ await wait_for_active(serviceWorkerRegistration);
+
+ estimate = await navigator.storage.estimate();
+ assert_true('serviceWorkerRegistrations' in estimate.usageDetails);
+
+ const usageAfterCreate = estimate.usageDetails.serviceWorkerRegistrations;
+ assert_greater_than(
+ usageAfterCreate, usageBeforeCreate,
+ 'estimated usage should increase after service worker is registered');
+}, 'estimate() shows usage increase after large value is stored');
diff --git a/testing/web-platform/tests/storage/estimate-usage-details.https.tentative.any.js b/testing/web-platform/tests/storage/estimate-usage-details.https.tentative.any.js
new file mode 100644
index 0000000000..2a1cea5fb8
--- /dev/null
+++ b/testing/web-platform/tests/storage/estimate-usage-details.https.tentative.any.js
@@ -0,0 +1,12 @@
+// META: title=StorageManager: estimate() should have usage details
+
+promise_test(async t => {
+ const estimate = await navigator.storage.estimate();
+ assert_equals(typeof estimate, 'object');
+ assert_true('usage' in estimate);
+ assert_equals(typeof estimate.usage, 'number');
+ assert_true('quota' in estimate);
+ assert_equals(typeof estimate.quota, 'number');
+ assert_true('usageDetails' in estimate);
+ assert_equals(typeof estimate.usageDetails, 'object');
+}, 'estimate() resolves to dictionary with members, including usageDetails');
diff --git a/testing/web-platform/tests/storage/helpers.js b/testing/web-platform/tests/storage/helpers.js
new file mode 100644
index 0000000000..b524c1b82c
--- /dev/null
+++ b/testing/web-platform/tests/storage/helpers.js
@@ -0,0 +1,46 @@
+/**
+ * @description - Function will create a database with the supplied name
+ * and also create an object store with the specified name.
+ * If a db with the name dbName exists, this will raze the
+ * existing DB beforehand.
+ * @param {string} dbName
+ * @param {string} objectStoreName
+ * @param {testCase} t
+ * @returns {Promise} - A promise that resolves to an indexedDB open request
+ */
+function createDB(dbName, objectStoreName, t) {
+ return new Promise((resolve, reject) => {
+ const openRequest = indexedDB.open(dbName);
+ t.add_cleanup(() => {
+ indexedDB.deleteDatabase(dbName);
+ });
+ openRequest.onerror = () => {
+ reject(openRequest.error);
+ };
+ openRequest.onsuccess = () => {
+ resolve(openRequest.result);
+ };
+ openRequest.onupgradeneeded = (event) => {
+ openRequest.result.createObjectStore(objectStoreName);
+ };
+ });
+}
+
+/**
+ * @description - This function will wrap an IDBTransaction in a promise,
+ * resolving in the oncomplete() method and rejecting with the
+ * transaction error in the onabort() case.
+ * @param {IDBTransaction} transaction - The transaction to wrap in a promise.
+ * @returns {Promise} - A promise that resolves when the transaction is either
+ * aborted or completed.
+ */
+function transactionPromise(transaction) {
+ return new Promise((resolve, reject) => {
+ transaction.onabort = () => {
+ reject(transaction.error);
+ };
+ transaction.oncomplete = () => {
+ resolve();
+ };
+ });
+}
diff --git a/testing/web-platform/tests/storage/idlharness.https.any.js b/testing/web-platform/tests/storage/idlharness.https.any.js
new file mode 100644
index 0000000000..773fac4e4a
--- /dev/null
+++ b/testing/web-platform/tests/storage/idlharness.https.any.js
@@ -0,0 +1,18 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+'use strict';
+
+idl_test(
+ ['storage'],
+ ['html'],
+ idl_array => {
+ idl_array.add_objects({ StorageManager: ['navigator.storage'] });
+ if (self.Window) {
+ idl_array.add_objects({ Navigator: ['navigator'] });
+ } else {
+ idl_array.add_objects({ WorkerNavigator: ['navigator'] });
+ }
+ }
+);
diff --git a/testing/web-platform/tests/storage/opaque-origin.https.window.js b/testing/web-platform/tests/storage/opaque-origin.https.window.js
new file mode 100644
index 0000000000..cc1d31fdf2
--- /dev/null
+++ b/testing/web-platform/tests/storage/opaque-origin.https.window.js
@@ -0,0 +1,80 @@
+// META: title=StorageManager API and opaque origins
+
+function load_iframe(src, sandbox) {
+ return new Promise(resolve => {
+ const iframe = document.createElement('iframe');
+ iframe.onload = () => { resolve(iframe); };
+ if (sandbox)
+ iframe.sandbox = sandbox;
+ iframe.srcdoc = src;
+ 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 && "result" in e.data) {
+ resolve(e.data);
+ self.removeEventListener('message', listener);
+ }
+ });
+ });
+}
+
+function make_script(snippet) {
+ return '<script src="/resources/testharness.js"></script>' +
+ '<script>' +
+ ' window.onmessage = () => {' +
+ ' try {' +
+ ' (' + snippet + ')' +
+ ' .then(' +
+ ' result => {' +
+ ' window.parent.postMessage({result: "no rejection"}, "*");' +
+ ' }, ' +
+ ' error => {' +
+ ' try {' +
+ ' assert_throws_js(TypeError, () => { throw error; });' +
+ ' window.parent.postMessage({result: "correct rejection"}, "*");' +
+ ' } catch (e) {' +
+ ' window.parent.postMessage({result: "incorrect rejection"}, "*");' +
+ ' }' +
+ ' });' +
+ ' } catch (ex) {' +
+ // Report if not implemented/exposed, rather than time out.
+ ' window.parent.postMessage({result: "API access threw"}, "*");' +
+ ' }' +
+ ' };' +
+ '<\/script>';
+}
+
+['navigator.storage.persisted()',
+ 'navigator.storage.estimate()',
+ // persist() can prompt, so make sure we test that last
+ 'navigator.storage.persist()',
+].forEach(snippet => {
+ promise_test(t => {
+ return load_iframe(make_script(snippet))
+ .then(iframe => {
+ iframe.contentWindow.postMessage({}, '*');
+ return wait_for_message(iframe);
+ })
+ .then(message => {
+ assert_equals(message.result, 'no rejection',
+ `${snippet} should not reject`);
+ });
+ }, `${snippet} in non-sandboxed iframe should not reject`);
+
+ promise_test(t => {
+ return load_iframe(make_script(snippet), 'allow-scripts')
+ .then(iframe => {
+ iframe.contentWindow.postMessage({}, '*');
+ return wait_for_message(iframe);
+ })
+ .then(message => {
+ assert_equals(message.result, 'correct rejection',
+ `${snippet} should reject with TypeError`);
+ });
+ }, `${snippet} in sandboxed iframe should reject with TypeError`);
+});
diff --git a/testing/web-platform/tests/storage/partitioned-estimate-usage-details-caches.tentative.https.sub.html b/testing/web-platform/tests/storage/partitioned-estimate-usage-details-caches.tentative.https.sub.html
new file mode 100644
index 0000000000..dc2af7c213
--- /dev/null
+++ b/testing/web-platform/tests/storage/partitioned-estimate-usage-details-caches.tentative.https.sub.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Partitioned estimate() usage details for caches test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+
+<body>
+ <script>
+ const usageDetails = async () =>
+ (await navigator.storage.estimate()).usageDetails.caches || 0;
+
+ const createSomeUsage = async () => {
+ const cache_name = token();
+ const cache_url = `/foo-${cache_name}`;
+ const cache = await caches.open(cache_name);
+ await cache.put(cache_url, new Response('x'.repeat(128)));
+ return [cache, cache_url];
+ }
+
+ const testPath = () => location.pathname.split("/").slice(0, -1).join("/");
+
+ let alt_origin = "https://{{hosts[alt][]}}:{{ports[https][0]}}";
+ let details = {};
+
+ const iframe = document.createElement("iframe");
+ iframe.src = `https://{{host}}:{{ports[https][0]}}${testPath()}` +
+ `/resources/partitioned-estimate-usage-details-caches-helper-frame.html`
+ document.body.appendChild(iframe);
+
+
+ async_test(test => {
+ if (location.origin === alt_origin)
+ return;
+
+
+ let cache, cache_url;
+ window.addEventListener("message", test.step_func(async event => {
+ if (event.data === "iframe-is-ready") {
+ details.init = await usageDetails();
+ [cache, cache_url] = await createSomeUsage(test);
+ details.after = await usageDetails();
+ assert_greater_than(details.after, details.init);
+
+ iframe.contentWindow.postMessage("get-details", iframe.origin);
+ }
+ }));
+
+ window.addEventListener("message", test.step_func(event => {
+ if (event.data.source === "same-site") {
+ details.same_site = event.data;
+
+ const cross_site_window = window
+ .open(`${alt_origin}${location.pathname}`, "", "noopener=false");
+
+ test.add_cleanup(() => cross_site_window.close());
+ }
+ if (event.data.source === "cross-site") {
+ details.cross_site = event.data;
+
+ // Some cleanup
+ test.step(async () => await cache.delete(cache_url));
+
+ test.step(() => {
+ assert_true(details.cross_site.init == 0, "Usage should be 0.");
+ assert_equals(details.same_site.init, details.after);
+ });
+
+ test.done();
+ }
+ }));
+ }, "Partitioned estimate() usage details for caches test.");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/storage/partitioned-estimate-usage-details-indexeddb.tentative.https.sub.html b/testing/web-platform/tests/storage/partitioned-estimate-usage-details-indexeddb.tentative.https.sub.html
new file mode 100644
index 0000000000..98a1ed8da2
--- /dev/null
+++ b/testing/web-platform/tests/storage/partitioned-estimate-usage-details-indexeddb.tentative.https.sub.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Partitioned estimate() usage details for indexeddb test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="./helpers.js"></script>
+<script src="../IndexedDB/resources/support-promises.js"></script>
+
+<body>
+ <script>
+ const usageDetails = async () =>
+ (await navigator.storage.estimate()).usageDetails.indexedDB || 0;
+
+ const createSomeUsage = async (test) => {
+ // We use 100KB here because db compaction usually happens every few MB
+ // 100KB is large enough to avoid a false positive (small amounts of
+ // metadata getting written for some random reason), and small enough to
+ // avoid compaction with a reasonably high probability.
+ const write_size = 1024 * 100;
+ const object_store_name = token();
+ const db_name = self.location.pathname;
+
+ await indexedDB.deleteDatabase(db_name);
+ const db = await createDB(db_name, object_store_name, test);
+ const transaction = db.transaction(object_store_name, 'readwrite');
+ const value_to_store = largeValue(write_size, Math.random() * 255);
+ transaction.objectStore(object_store_name).add(value_to_store, 1);
+
+ await transactionPromise(transaction);
+ return db;
+ }
+
+ const testPath = () => location.pathname.split("/").slice(0, -1).join("/");
+
+ let alt_origin = "https://{{hosts[alt][]}}:{{ports[https][0]}}";
+ let details = {};
+
+ const iframe = document.createElement("iframe");
+ iframe.src = `https://{{host}}:{{ports[https][0]}}${testPath()}/resources` +
+ `/partitioned-estimate-usage-details-indexeddb-helper-frame.html`;
+ document.body.appendChild(iframe);
+
+ async_test(test => {
+ if (location.origin === alt_origin)
+ return;
+
+ let db;
+ window.addEventListener("message", test.step_func(async event => {
+ if (event.data === "iframe-is-ready") {
+ details.init = await usageDetails();
+ db = await createSomeUsage(test);
+ details.after = await usageDetails();
+ assert_greater_than(details.after, details.init);
+
+ iframe.contentWindow.postMessage("get-details", iframe.origin);
+ }
+ }));
+
+ window.addEventListener("message", test.step_func(event => {
+ if (event.data.source === "same-site") {
+ details.same_site = event.data;
+
+ const cross_site_window = window
+ .open(`${alt_origin}${location.pathname}`, "", "noopener=false");
+ test.add_cleanup(() => cross_site_window.close());
+ }
+ if (event.data.source === "cross-site") {
+ details.cross_site = event.data;
+
+ // Some cleanup
+ test.step(async () => await db.close());
+
+ test.step(() => {
+ assert_true(details.cross_site.init == 0, "Usage should be 0.");
+ assert_equals(details.same_site.init, details.after);
+ });
+
+ test.done();
+ }
+ }));
+ }, "Partitioned estimate() usage details for indexeddb test.");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/storage/partitioned-estimate-usage-details-service-workers.tentative.https.sub.html b/testing/web-platform/tests/storage/partitioned-estimate-usage-details-service-workers.tentative.https.sub.html
new file mode 100644
index 0000000000..c52ca34e68
--- /dev/null
+++ b/testing/web-platform/tests/storage/partitioned-estimate-usage-details-service-workers.tentative.https.sub.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Partitioned estimate() usage details for service workers test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+ <script>
+ const usageDetails = async () => {
+ return (await navigator.storage.estimate())
+ .usageDetails.serviceWorkerRegistrations || 0;
+ }
+
+ const createSomeUsage = async () => {
+ const wait_for_active = worker => new Promise(resolve => {
+ if (worker.active) { resolve(worker.active); }
+
+ const listen_for_active = worker => e => {
+ if (e.target.state === 'activated') { resolve(worker.active); }
+ }
+
+ if (worker.waiting) {
+ worker.waiting
+ .addEventListener('statechange', listen_for_active(worker.waiting));
+ }
+ if (worker.installing) {
+ worker.installing.addEventListener('statechange',
+ listen_for_active(worker.installing));
+ }
+ });
+
+ const service_worker_registration =
+ await navigator.serviceWorker.register('resources/worker.js');
+ await wait_for_active(service_worker_registration);
+ return service_worker_registration;
+ }
+
+ const testPath = () => location.pathname.split("/").slice(0, -1).join("/");
+
+ let alt_origin = "https://{{hosts[alt][]}}:{{ports[https][0]}}";
+ let details = {};
+
+ const iframe = document.createElement("iframe");
+ iframe.src = `https://{{host}}:{{ports[https][0]}}${testPath()}/resources` +
+ `/partitioned-estimate-usage-details-service-workers-helper-frame.html`
+ document.body.appendChild(iframe);
+
+ async_test(test => {
+ if (location.origin === alt_origin)
+ return;
+
+ let service_worker_registration;
+ window.addEventListener("message", test.step_func(async event => {
+ if (event.data === "iframe-is-ready") {
+ details.init = await usageDetails();
+ service_worker_registration = await createSomeUsage();
+ details.after = await usageDetails();
+ assert_greater_than(details.after, details.init);
+
+ iframe.contentWindow.postMessage("get-details", iframe.origin);
+ }
+ }));
+
+ window.addEventListener("message", test.step_func(event => {
+ if (event.data.source === "same-site") {
+ details.same_site = event.data;
+
+ const cross_site_window = window
+ .open(`${alt_origin}${location.pathname}`, "", "noopener=false");
+ test.add_cleanup(() => cross_site_window.close());
+ }
+ if (event.data.source === "cross-site") {
+ details.cross_site = event.data;
+
+ // More cleanup.
+ test.step(() => service_worker_registration.unregister());
+
+ test.step(() => {
+ assert_true(details.cross_site.init == 0, "Usage should be 0.");
+ assert_equals(details.same_site.init, details.after);
+ });
+
+ test.done();
+ }
+ }));
+ }, "Partitioned estimate() usage details for service workers test.");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/storage/permission-query.https.any.js b/testing/web-platform/tests/storage/permission-query.https.any.js
new file mode 100644
index 0000000000..9984bdab79
--- /dev/null
+++ b/testing/web-platform/tests/storage/permission-query.https.any.js
@@ -0,0 +1,10 @@
+// META: title=The Permission API registration for "persistent-storage"
+
+promise_test(async t => {
+ const status =
+ await navigator.permissions.query({name: 'persistent-storage'});
+ assert_equals(status.constructor, PermissionStatus,
+ 'query() result should resolve to a PermissionStatus');
+ assert_true(['granted','denied', 'prompt'].includes(status.state),
+ 'state should be a PermissionState');
+}, 'The "persistent-storage" permission is recognized');
diff --git a/testing/web-platform/tests/storage/persist-permission-manual.https.html b/testing/web-platform/tests/storage/persist-permission-manual.https.html
new file mode 100644
index 0000000000..aa49900d69
--- /dev/null
+++ b/testing/web-platform/tests/storage/persist-permission-manual.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>StorageManager: permission state is granted</title>
+ <p>Clear all persistent storage permissions before running this test.</p>
+ <p>Test passes if there is a permission prompt and click allow store persistent data</p>
+ <meta name="help" href="https://storage.spec.whatwg.org/#dom-storagemanager-persist">
+ <meta name="author" title="Mozilla" href="https://www.mozilla.org">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ promise_test(function(t) {
+ return navigator.storage.persist()
+ .then(function(result) {
+ assert_true(result);
+ return navigator.storage.persisted();
+ })
+ .then(function(result) {
+ assert_true(result);
+ })
+ }, 'Expect permission state is granted after calling persist()');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/storage/persisted.https.any.js b/testing/web-platform/tests/storage/persisted.https.any.js
new file mode 100644
index 0000000000..57e15f0e81
--- /dev/null
+++ b/testing/web-platform/tests/storage/persisted.https.any.js
@@ -0,0 +1,14 @@
+// META: title=StorageManager: persisted()
+
+test(function(t) {
+ assert_true('persisted' in navigator.storage);
+ assert_equals(typeof navigator.storage.persisted, 'function');
+ assert_true(navigator.storage.persisted() instanceof Promise);
+}, 'persisted() method exists and returns a Promise');
+
+promise_test(function(t) {
+ return navigator.storage.persisted().then(function(result) {
+ assert_equals(typeof result, 'boolean');
+ assert_equals(result, false);
+ });
+}, 'persisted() returns a promise and resolves as boolean with false');
diff --git a/testing/web-platform/tests/storage/quotachange-in-detached-iframe.tentative.https.html b/testing/web-platform/tests/storage/quotachange-in-detached-iframe.tentative.https.html
new file mode 100644
index 0000000000..123af50e3c
--- /dev/null
+++ b/testing/web-platform/tests/storage/quotachange-in-detached-iframe.tentative.https.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>quotachange event on DOMWindow of detached iframe</title>
+<link rel="author" href="jarrydg@chromium.org" title="Jarryd">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe"></iframe>
+<script>
+'use strict';
+
+test(t => {
+ const iframe = document.getElementById('iframe');
+ const frameWindow = iframe.contentWindow;
+ const storageManager = frameWindow.navigator.storage;
+
+ iframe.parentNode.removeChild(iframe);
+ const emptyListener = () => {};
+ storageManager.addEventListener('quotachange', emptyListener);
+ storageManager.removeEventListener('quotachange', emptyListener);
+}, "Add quotachange listener on detached iframe.");
+</script>
diff --git a/testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html b/testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html
new file mode 100644
index 0000000000..0679c1decf
--- /dev/null
+++ b/testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html
@@ -0,0 +1,30 @@
+<!-- ToDo: Change the virtual suite expected file content once the necessary
+ partitioning implementation is completed -->
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Helper frame</title>
+
+<script>
+ const usageDetails = async () =>
+ (await navigator.storage.estimate()).usageDetails.caches || 0;
+
+ let details = {};
+
+ window.addEventListener("message", async event => {
+ if (event.data === "get-details") {
+ details.source = "same-site";
+ details.init = await usageDetails();
+ event.source.postMessage(details, event.source.origin);
+ }
+ });
+
+ window.addEventListener("load", async () => {
+ if (parent.opener) {
+ details.source = "cross-site";
+ details.init = await usageDetails();
+ parent.opener.postMessage(details, parent.opener.origin);
+ }
+ });
+
+ window.parent.postMessage("iframe-is-ready", window.parent.origin);
+</script>
diff --git a/testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html b/testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html
new file mode 100644
index 0000000000..fd2cfb669b
--- /dev/null
+++ b/testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Helper frame</title>
+
+<script>
+ const usageDetails = async () =>
+ (await navigator.storage.estimate()).usageDetails.indexedDB || 0;
+
+ let details = {};
+
+ window.addEventListener("message", async event => {
+ if (event.data === "get-details") {
+ details.source = "same-site";
+ details.init = await usageDetails();
+ event.source.postMessage(details, event.source.origin);
+ }
+ });
+
+ window.addEventListener("load", async () => {
+ if (parent.opener) {
+ details.source = "cross-site";
+ details.init = await usageDetails();
+ parent.opener.postMessage(details, parent.opener.origin);
+ }
+ });
+
+ window.parent.postMessage("iframe-is-ready", window.parent.origin);
+</script>
diff --git a/testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html b/testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html
new file mode 100644
index 0000000000..25d7554868
--- /dev/null
+++ b/testing/web-platform/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta name=help href="https://privacycg.github.io/storage-partitioning/">
+<title>Helper frame</title>
+
+<script>
+ const usageDetails = async () => {
+ return (await navigator.storage.estimate())
+ .usageDetails.serviceWorkerRegistrations || 0
+ }
+
+ let details = {};
+
+ window.addEventListener("message", async event => {
+ if (event.data === "get-details") {
+ details.source = "same-site";
+ details.init = await usageDetails();
+ event.source.postMessage(details, event.source.origin);
+ }
+ });
+
+ window.addEventListener("load", async () => {
+ if (parent.opener) {
+ details.source = "cross-site";
+ details.init = await usageDetails();
+ parent.opener.postMessage(details, parent.opener.origin);
+ }
+ });
+
+ window.parent.postMessage("iframe-is-ready", window.parent.origin);
+</script>
diff --git a/testing/web-platform/tests/storage/resources/worker.js b/testing/web-platform/tests/storage/resources/worker.js
new file mode 100644
index 0000000000..9271c769b2
--- /dev/null
+++ b/testing/web-platform/tests/storage/resources/worker.js
@@ -0,0 +1,3 @@
+// Dummy service worker to observe some weight when querying the storage usage
+// details from of the service worker from estimate().
+globalThis.oninstall = e => {};
diff --git a/testing/web-platform/tests/storage/storagemanager-estimate.https.any.js b/testing/web-platform/tests/storage/storagemanager-estimate.https.any.js
new file mode 100644
index 0000000000..c2f5c569dc
--- /dev/null
+++ b/testing/web-platform/tests/storage/storagemanager-estimate.https.any.js
@@ -0,0 +1,60 @@
+// META: title=StorageManager: estimate()
+
+test(function(t) {
+ assert_true(navigator.storage.estimate() instanceof Promise);
+}, 'estimate() method returns a Promise');
+
+promise_test(function(t) {
+ return navigator.storage.estimate().then(function(result) {
+ assert_equals(typeof result, 'object');
+ assert_true('usage' in result);
+ assert_equals(typeof result.usage, 'number');
+ assert_true('quota' in result);
+ assert_equals(typeof result.quota, 'number');
+ });
+}, 'estimate() resolves to dictionary with members');
+
+promise_test(function(t) {
+ const large_value = new Uint8Array(1e6);
+ const dbname = `db-${location}-${t.name}`;
+ let db, before, after;
+
+ indexedDB.deleteDatabase(dbname);
+ return new Promise((resolve, reject) => {
+ const open = indexedDB.open(dbname);
+ open.onerror = () => { reject(open.error); };
+ open.onupgradeneeded = () => {
+ const connection = open.result;
+ connection.createObjectStore('store');
+ };
+ open.onsuccess = () => {
+ const connection = open.result;
+ t.add_cleanup(() => {
+ connection.close();
+ indexedDB.deleteDatabase(dbname);
+ });
+ resolve(connection);
+ };
+ })
+ .then(connection => {
+ db = connection;
+ return navigator.storage.estimate();
+ })
+ .then(estimate => {
+ before = estimate.usage;
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction('store', 'readwrite');
+ tx.objectStore('store').put(large_value, 'key');
+ tx.onabort = () => { reject(tx.error); };
+ tx.oncomplete = () => { resolve(); };
+ });
+ })
+ .then(() => {
+ return navigator.storage.estimate();
+ })
+ .then(estimate => {
+ after = estimate.usage;
+ assert_greater_than(after, before,
+ 'estimated usage should increase');
+ });
+}, 'estimate() shows usage increase after 1MB IndexedDB record is stored');
diff --git a/testing/web-platform/tests/storage/storagemanager-persist-persisted-match.https.any.js b/testing/web-platform/tests/storage/storagemanager-persist-persisted-match.https.any.js
new file mode 100644
index 0000000000..edbe67fae2
--- /dev/null
+++ b/testing/web-platform/tests/storage/storagemanager-persist-persisted-match.https.any.js
@@ -0,0 +1,9 @@
+// META: title=StorageManager: result of persist() matches result of persisted()
+
+promise_test(async t => {
+ var persistResult = await navigator.storage.persist();
+ assert_equals(typeof persistResult, 'boolean', persistResult + ' should be boolean');
+ var persistedResult = await navigator.storage.persisted();
+ assert_equals(typeof persistedResult, 'boolean', persistedResult + ' should be boolean');
+ assert_equals(persistResult, persistedResult);
+}, 'navigator.storage.persist() resolves to a value that matches navigator.storage.persisted()');
diff --git a/testing/web-platform/tests/storage/storagemanager-persist.https.window.js b/testing/web-platform/tests/storage/storagemanager-persist.https.window.js
new file mode 100644
index 0000000000..13e17a16e1
--- /dev/null
+++ b/testing/web-platform/tests/storage/storagemanager-persist.https.window.js
@@ -0,0 +1,10 @@
+// META: title=StorageManager: persist()
+
+promise_test(function() {
+ var promise = navigator.storage.persist();
+ assert_true(promise instanceof Promise,
+ 'navigator.storage.persist() returned a Promise.');
+ return promise.then(function(result) {
+ assert_equals(typeof result, 'boolean', result + ' should be boolean');
+ });
+}, 'navigator.storage.persist() returns a promise that resolves.');
diff --git a/testing/web-platform/tests/storage/storagemanager-persist.https.worker.js b/testing/web-platform/tests/storage/storagemanager-persist.https.worker.js
new file mode 100644
index 0000000000..fcf8175f70
--- /dev/null
+++ b/testing/web-platform/tests/storage/storagemanager-persist.https.worker.js
@@ -0,0 +1,8 @@
+// META: title=StorageManager: persist() (worker)
+importScripts("/resources/testharness.js");
+
+test(function() {
+ assert_false('persist' in navigator.storage);
+}, 'navigator.storage.persist should not exist in workers');
+
+done();
diff --git a/testing/web-platform/tests/storage/storagemanager-persisted.https.any.js b/testing/web-platform/tests/storage/storagemanager-persisted.https.any.js
new file mode 100644
index 0000000000..7099940669
--- /dev/null
+++ b/testing/web-platform/tests/storage/storagemanager-persisted.https.any.js
@@ -0,0 +1,10 @@
+// META: title=StorageManager: persisted()
+
+promise_test(function() {
+ var promise = navigator.storage.persisted();
+ assert_true(promise instanceof Promise,
+ 'navigator.storage.persisted() returned a Promise.');
+ return promise.then(function (result) {
+ assert_equals(typeof result, 'boolean', result + ' should be boolean');
+ });
+}, 'navigator.storage.persisted() returns a promise that resolves.');