KV Storage: make keys()/values()/entries() async iterators
Follows https://github.com/WICG/kv-storage/commit/2432509c487b30b1e35a25414a8b51557bfd6380.
Binary-Size: needed to implement the feature. Enabling gzip is tracked in crbug.com/920264.
Change-Id: If5262a32d42eb1c7abaad4fb2a029ce44e94834e
Reviewed-on: https://chromium-review.googlesource.com/c/1396453
Commit-Queue: Domenic Denicola <domenic@chromium.org>
Reviewed-by: Joshua Bell <jsbell@chromium.org>
Reviewed-by: Kinuko Yasuda <kinuko@chromium.org>
Cr-Commit-Position: refs/heads/master@{#624378}
diff --git a/kv-storage/api-surface.https.html b/kv-storage/api-surface.https.html
index 65452f5..90e7058 100644
--- a/kv-storage/api-surface.https.html
+++ b/kv-storage/api-surface.https.html
@@ -23,7 +23,9 @@
classAssert.propertyKeys(StorageArea.prototype, [
"constructor", "set", "get", "delete", "clear",
"keys", "values", "entries", "backingStore"
- ], []);
+ ], [
+ Symbol.asyncIterator
+ ]);
classAssert.methods(StorageArea.prototype, {
set: 2,
@@ -40,6 +42,10 @@
});
}, "StorageArea.prototype methods and properties");
+test(() => {
+ assert_equals(StorageArea.prototype[Symbol.asyncIterator], StorageArea.prototype.entries);
+}, "[Symbol.asyncIterator]() and entries() must be the same function");
+
testWithArea(async area => {
classAssert.propertyKeys(area, [], []);
}, "Instances don't have any properties");
diff --git a/kv-storage/cause-errors-via-idb.https.html b/kv-storage/cause-errors-via-idb.https.html
new file mode 100644
index 0000000..21fe36b
--- /dev/null
+++ b/kv-storage/cause-errors-via-idb.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>KV Storage: causing errors by directly manipulating the IDB</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/IndexedDB/support-promises.js"></script>
+
+<script type="module">
+import { testWithArea } from "./helpers/kvs-tests.js";
+
+const mustFail = {
+ "set()": area => area.set(1, "value 1"),
+ "get()": area => area.get(1),
+ "delete()": area => area.delete(1),
+ "keys()": area => {
+ const iter = area.keys();
+ return iter.next();
+ },
+ "values()": area => {
+ const iter = area.values();
+ return iter.next();
+ },
+ "entries()": area => {
+ const iter = area.entries();
+ return iter.next();
+ }
+};
+
+for (const [method, testFn] of Object.entries(mustFail)) {
+ testWithArea(async (area, t) => {
+ const { database, store, version } = area.backingStore;
+ const db = await migrateNamedDatabase(t, database, version + 1, () => {});
+
+ const result = testFn(area);
+
+ await promise_rejects(t, "VersionError", result);
+ }, `${method}: upgrading the database must cause a "VersionError" DOMException`);
+
+ testWithArea(async (area, t) => {
+ const { database, store } = area.backingStore;
+
+ // Set up a new database with that name, but with no object stores!
+ // NB: this depends on the fact that createNameDatabase sets the initial version to 1, which is
+ // the same as the database version used/expected by KV Storage.
+ const db = await createNamedDatabase(t, database, () => {});
+
+ const result = testFn(area);
+
+ await promise_rejects(t, "NotFoundError", result);
+ }, `${method}: creating a same-named database with no object store must cause a "NotFoundError" DOMException`);
+}
+</script>
diff --git a/kv-storage/entries.https.html b/kv-storage/entries.https.html
new file mode 100644
index 0000000..0d1ab84
--- /dev/null
+++ b/kv-storage/entries.https.html
@@ -0,0 +1,290 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>KV Storage: entries() trickier tests</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script type="module">
+import { testWithArea } from "./helpers/kvs-tests.js";
+import * as classAssert from "./helpers/class-assert.js";
+import {
+ assertAsyncIteratorEquals,
+ assertAsyncIteratorCustomEquals,
+ assertArrayCustomEquals,
+ assertEqualPostKeyRoundtripping
+} from "./helpers/equality-asserters.js";
+
+function assertEqualsArrayOrUndefined(actual, expected, label) {
+ if (expected === undefined) {
+ return assert_equals(actual, expected, label);
+ }
+ return assert_array_equals(actual, expected, label);
+}
+
+testWithArea(async area => {
+ await area.set(1, "value 1");
+ await area.set(2, "value 2");
+ await area.set(3, "value 3");
+
+ await assertAsyncIteratorCustomEquals(
+ area.entries(),
+ [[1, "value 1"], [2, "value 2"], [3, "value 3"]],
+ assert_array_equals
+ );
+}, "Using for-await-of to collect the results works");
+
+testWithArea(async area => {
+ // We're not testing every key type since this isn't a test of IndexedDB.
+ await area.set(1, "value 1");
+ await area.set(new Date(500), "value date 500");
+ await area.set(-1, "value -1");
+ await area.set(new Date(-20), "value date -20");
+ await area.set("aaa", "value aaa");
+ await area.set("a", "value a");
+ await area.set(-Infinity, "value -Infinity");
+
+ await assertAsyncIteratorCustomEquals(
+ area.entries(),
+ [
+ [-Infinity, "value -Infinity"],
+ [-1, "value -1"],
+ [1, "value 1"],
+ [new Date(-20), "value date -20"],
+ [new Date(500), "value date 500"],
+ ["a", "value a"],
+ ["aaa", "value aaa"]
+ ],
+ (actual, expected, label) => {
+ return assertArrayCustomEquals(actual, expected, assertEqualPostKeyRoundtripping, label);
+ }
+ );
+}, "Results are returned in IndexedDB order");
+
+testWithArea(async area => {
+ await area.set(1, "value 1");
+ await area.set(2, "value 2");
+ await area.set(3, "value 3");
+
+ const iter = area.entries();
+ const iterResults = [
+ await iter.next(),
+ await iter.next(),
+ await iter.next(),
+ await iter.next(),
+ await iter.next(),
+ await iter.next()
+ ];
+
+ classAssert.iterResultsCustom(
+ iterResults,
+ [
+ [[1, "value 1"], false],
+ [[2, "value 2"], false],
+ [[3, "value 3"], false],
+ [undefined, true],
+ [undefined, true],
+ [undefined, true]
+ ],
+ assertEqualsArrayOrUndefined
+ );
+}, "Manual testing of .next() calls, with awaiting");
+
+testWithArea(async area => {
+ area.set(1, "value 1");
+ area.set(2, "value 2");
+ area.set(3, "value 3");
+
+ const iter = area.entries();
+ const promises = [
+ iter.next(),
+ iter.next(),
+ iter.next(),
+ iter.next(),
+ iter.next(),
+ iter.next()
+ ];
+ const iterResults = await Promise.all(promises);
+
+ classAssert.iterResultsCustom(
+ iterResults,
+ [
+ [[1, "value 1"], false],
+ [[2, "value 2"], false],
+ [[3, "value 3"], false],
+ [undefined, true],
+ [undefined, true],
+ [undefined, true]
+ ],
+ assertEqualsArrayOrUndefined
+ );
+}, "Manual testing of .next() calls, no awaiting");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const entry of area.entries()) {
+ seen.push(entry);
+ if (entry[0] === 20) {
+ await area.set(15, "value 15");
+ }
+ }
+
+ assertArrayCustomEquals(
+ seen,
+ [[10, "value 10"], [20, "value 20"], [30, "value 30"], [40, "value 40"]],
+ assert_array_equals
+ );
+}, "Inserting an entry before the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const entry of area.entries()) {
+ seen.push(entry);
+ if (entry[0] === 20) {
+ await area.set(25, "value 25");
+ }
+ }
+
+ assertArrayCustomEquals(
+ seen,
+ [[10, "value 10"], [20, "value 20"], [25, "value 25"], [30, "value 30"], [40, "value 40"]],
+ assert_array_equals
+ );
+}, "Inserting an entry after the current entry must show up in iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const entry of area.entries()) {
+ seen.push(entry);
+ if (entry[0] === 20) {
+ await area.delete(10);
+ }
+ }
+
+ assertArrayCustomEquals(
+ seen,
+ [[10, "value 10"], [20, "value 20"], [30, "value 30"], [40, "value 40"]],
+ assert_array_equals
+ );
+}, "Deleting an entry before the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const entry of area.entries()) {
+ seen.push(entry);
+ if (entry[0] === 20) {
+ await area.delete(20);
+ }
+ }
+
+ assertArrayCustomEquals(
+ seen,
+ [[10, "value 10"], [20, "value 20"], [30, "value 30"], [40, "value 40"]],
+ assert_array_equals
+ );
+}, "Deleting the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const entry of area.entries()) {
+ seen.push(entry);
+ if (entry[0] === 20) {
+ await area.delete(30);
+ }
+ }
+
+ assertArrayCustomEquals(
+ seen,
+ [[10, "value 10"], [20, "value 20"], [40, "value 40"]],
+ assert_array_equals
+ );
+}, "Deleting an entry after the current entry must show up in iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const entry of area.entries()) {
+ seen.push(entry);
+ if (entry[0] === 20) {
+ await area.set(10, "value 10, but changed!!");
+ }
+ }
+
+ assertArrayCustomEquals(
+ seen,
+ [[10, "value 10"], [20, "value 20"], [30, "value 30"], [40, "value 40"]],
+ assert_array_equals
+ );
+}, "Modifying a value before the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const entry of area.entries()) {
+ seen.push(entry);
+ if (entry[0] === 20) {
+ await area.set(20, "value 20, but changed!!");
+ }
+ }
+
+ assertArrayCustomEquals(
+ seen,
+ [[10, "value 10"], [20, "value 20"], [30, "value 30"], [40, "value 40"]],
+ assert_array_equals
+ );
+}, "Modifying a value at the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const entry of area.entries()) {
+ seen.push(entry);
+ if (entry[0] === 20) {
+ await area.set(30, "value 30, but changed!!");
+ }
+ }
+
+ assertArrayCustomEquals(
+ seen,
+ [[10, "value 10"], [20, "value 20"], [30, "value 30, but changed!!"], [40, "value 40"]],
+ assert_array_equals
+ );
+}, "Modifying a value after the current entry must show up in iteration");
+</script>
diff --git a/kv-storage/helpers/class-assert.js b/kv-storage/helpers/class-assert.js
index 31b25ca..89f0889 100644
--- a/kv-storage/helpers/class-assert.js
+++ b/kv-storage/helpers/class-assert.js
@@ -38,6 +38,38 @@
`${label}property symbols`);
}
+export function iterResultCustom(o, expectedValue, expectedDone, valueAsserter, label) {
+ label = formatLabel(label);
+
+ assert_equals(typeof expectedDone, "boolean",
+ `${label} iterResult assert usage check: expectedDone must be a boolean`);
+
+ propertyKeys(o, ["value", "done"], [], label);
+ assert_equals(Object.getPrototypeOf(o), Object.prototype, `${label}prototype must be Object.prototype`);
+ valueAsserter(o.value, expectedValue, `${label}value`);
+ assert_equals(o.done, expectedDone, `${label}done`);
+}
+
+export function iterResult(o, expectedValue, expectedDone, label) {
+ return iterResultCustom(o, expectedValue, expectedDone, assert_equals, label);
+}
+
+export function iterResultsCustom(actualArray, expectedArrayOfArrays, valueAsserter, label) {
+ label = formatLabel(label);
+
+ assert_equals(actualArray.length, expectedArrayOfArrays.length,
+ `${label} iterResults assert usage check: actual and expected must have the same length`);
+
+ for (let i = 0; i < actualArray.length; ++i) {
+ const [expectedValue, expectedDone] = expectedArrayOfArrays[i];
+ iterResultCustom(actualArray[i], expectedValue, expectedDone, valueAsserter, `${label}iter result ${i}`);
+ }
+}
+
+export function iterResults(actualArray, expectedArrayOfArrays, label) {
+ return iterResultsCustom(actualArray, expectedArrayOfArrays, assert_equals, label);
+}
+
export function methods(o, expectedMethods) {
for (const [name, length] of Object.entries(expectedMethods)) {
method(o, name, length);
@@ -103,5 +135,5 @@
}
function formatLabel(label) {
- return label !== undefined ? ` ${label}` : "";
+ return label !== undefined ? `${label} ` : "";
}
diff --git a/kv-storage/helpers/equality-asserters.js b/kv-storage/helpers/equality-asserters.js
index ad4623c..448ab31 100644
--- a/kv-storage/helpers/equality-asserters.js
+++ b/kv-storage/helpers/equality-asserters.js
@@ -1,37 +1,91 @@
export function assertEqualDates(actual, expected, label) {
- assert_equals(expected.constructor, Date,
- "assertEqualDates usage check: expected must be a Date");
+ label = formatLabel(label);
- const labelPart = label === undefined ? "" : `${label}: `;
- assert_equals(actual.constructor, Date, `${labelPart}must be a Date`);
- assert_equals(actual.valueOf(), expected.valueOf(), `${labelPart}timestamps must match`);
+ assert_equals(expected.constructor, Date,
+ `${label}assertEqualDates usage check: expected must be a Date`);
+
+ assert_equals(actual.constructor, Date, `${label}must be a Date`);
+ assert_equals(actual.valueOf(), expected.valueOf(), `${label}timestamps must match`);
+}
+
+export function assertEqualPostKeyRoundtripping(actual, expected, label) {
+ label = formatLabel(label);
+
+ // Please extend this to support other types as needed!
+ assert_true(
+ typeof expected === "number" || typeof expected === "string" || expected.constructor === Date,
+ `${label}assertEqualPostKeyRoundtripping usage check: currently only supports numbers, strings, and dates`
+ );
+
+ if (expected.constructor === Date) {
+ assert_equals(actual.constructor, Date, `${label}comparing to Date(${Number(expected)}) (actual = ${actual})`);
+ actual = Number(actual);
+ expected = Number(expected);
+ }
+
+ assert_equals(actual, expected, label);
}
export function assertEqualArrayBuffers(actual, expected, label) {
- assert_equals(expected.constructor, ArrayBuffer,
- "assertEqualArrayBuffers usage check: expected must be an ArrayBuffer");
+ label = formatLabel(label);
- const labelPart = label === undefined ? "" : `${label}: `;
- assert_equals(actual.constructor, ArrayBuffer, `${labelPart}must be an ArrayBuffer`);
- assert_array_equals(new Uint8Array(actual), new Uint8Array(expected), `${labelPart}must match`);
+ assert_equals(expected.constructor, ArrayBuffer,
+ `${label}assertEqualArrayBuffers usage check: expected must be an ArrayBuffer`);
+
+ assert_equals(actual.constructor, ArrayBuffer, `${label}must be an ArrayBuffer`);
+ assert_array_equals(new Uint8Array(actual), new Uint8Array(expected), `${label}must match`);
}
export function assertArrayBufferEqualsABView(actual, expected, label) {
+ label = formatLabel(label);
+
assert_true(ArrayBuffer.isView(expected),
- "assertArrayBufferEqualsABView usage check: expected must be an ArrayBuffer view");
+ `${label}assertArrayBufferEqualsABView usage check: expected must be an ArrayBuffer view`);
assertEqualArrayBuffers(actual, expected.buffer, label);
}
-export function assertArrayCustomEquals(actual, expected, equalityAsserter, label) {
- assert_true(Array.isArray(expected),
- "assertArrayCustomEquals usage check: expected must be an Array");
+export function assertAsyncIteratorEquals(actual, expected, label) {
+ return assertAsyncIteratorCustomEquals(actual, expected, Object.is, label);
+}
- const labelPart = label === undefined ? "" : `${label}: `;
- assert_true(Array.isArray(actual), `${labelPart}must be an array`);
- assert_equals(actual.length, expected.length, `${labelPart}length must be as expected`);
+export function assertArrayCustomEquals(actual, expected, equalityAsserter, label) {
+ label = formatLabel(label);
+
+ assert_true(Array.isArray(expected),
+ `${label} assertArrayCustomEquals usage check: expected must be an Array`);
+
+ assert_true(Array.isArray(actual), `${label}must be an array`);
+ assert_equals(actual.length, expected.length, `${label}length must be as expected`);
for (let i = 0; i < actual.length; ++i) {
- equalityAsserter(actual[i], expected[i], `${labelPart}index ${i}`);
+ equalityAsserter(actual[i], expected[i], `${label}index ${i}`);
}
}
+
+export async function assertAsyncIteratorCustomEquals(actual, expected, equalityAsserter, label) {
+ label = formatLabel(label);
+
+ assert_true(Array.isArray(expected),
+ `${label} assertAsyncIteratorCustomEquals usage check: expected must be an Array`);
+
+ const collected = await collectAsyncIterator(actual);
+ assert_equals(collected.length, expected.length, `${label}length must be as expected`);
+
+ for (let i = 0; i < collected.length; ++i) {
+ equalityAsserter(collected[i], expected[i], `${label}index ${i}`);
+ }
+}
+
+async function collectAsyncIterator(asyncIterator) {
+ const array = [];
+ for await (const entry of asyncIterator) {
+ array.push(entry);
+ }
+
+ return array;
+}
+
+function formatLabel(label) {
+ return label !== undefined ? `${label} ` : "";
+}
diff --git a/kv-storage/helpers/kvs-tests.js b/kv-storage/helpers/kvs-tests.js
index 0ffe71f..a6c4d58 100644
--- a/kv-storage/helpers/kvs-tests.js
+++ b/kv-storage/helpers/kvs-tests.js
@@ -1,5 +1,5 @@
import { StorageArea, storage as defaultArea } from "std:kv-storage";
-import { assertArrayCustomEquals } from "./equality-asserters.js";
+import { assertAsyncIteratorEquals, assertAsyncIteratorCustomEquals } from "./equality-asserters.js";
export function testWithArea(testFn, description) {
promise_test(t => {
@@ -36,23 +36,24 @@
await assertPromiseEquals(area.get(key), value, "get()", "the set value");
- const keysPromise = area.keys();
- assertIsPromise(keysPromise, "keys()");
- assertArrayCustomEquals(await keysPromise, [key], keyEqualityAsserter, "keys() must have the key");
+ const keysIter = area.keys();
+ await assertAsyncIteratorCustomEquals(keysIter, [key], keyEqualityAsserter, "keys() must have the key");
- const valuesPromise = area.values();
- assertIsPromise(valuesPromise);
- assert_array_equals(await valuesPromise, [value], "values() must have the value");
+ const valuesIter = area.values();
+ await assertAsyncIteratorEquals(valuesIter, [value], "values() must have the value");
- const entriesPromise = area.entries();
- assertIsPromise(entriesPromise, "entries()");
- const entries = await entriesPromise;
- assert_true(Array.isArray(entries), "entries() must give an array");
- assert_equals(entries.length, 1, "entries() must have only one value");
- assert_true(Array.isArray(entries[0]), "entries() 0th element must be an array");
- assert_equals(entries[0].length, 2, "entries() 0th element must have 2 elements");
- keyEqualityAsserter(entries[0][0], key, "entries() 0th element's 0th element must be the key");
- assert_equals(entries[0][1], value, "entries() 0th element's 1st element must be the value");
+ const entriesIter = area.entries();
+
+ const entry0 = await entriesIter.next();
+ assert_false(entry0.done, "entries() 0th iter-result must not be done");
+ assert_true(Array.isArray(entry0.value), "entries() 0th iter-result value must be an array");
+ assert_equals(entry0.value.length, 2, "entries() 0th iter-result value must have 2 elements");
+ keyEqualityAsserter(entry0.value[0], key, "entries() 0th iter-result value's 0th element must be the key");
+ assert_equals(entry0.value[1], value, "entries() 0th iter-result value's 1st element must be the value");
+
+ const entry1 = await entriesIter.next();
+ assert_true(entry1.done, "entries() 1st iter-result must be done");
+ assert_equals(entry1.value, undefined, "entries() 1st iter-result must have undefined value");
await assertPromiseEquals(area.delete(key), undefined, "delete()", "undefined");
diff --git a/kv-storage/keys-values-entries.https.html b/kv-storage/keys-values-entries.https.html
new file mode 100644
index 0000000..b263238
--- /dev/null
+++ b/kv-storage/keys-values-entries.https.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>KV Storage: keys()/values()/entries()</title>
+<!--
+ This file contains tests that are easy to generalize over all three methods.
+ See sibling files for more complicated tests which are not worth generalizing.
+-->
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/IndexedDB/support-promises.js"></script>
+
+<script type="module">
+import { testWithArea } from "./helpers/kvs-tests.js";
+import * as classAssert from "./helpers/class-assert.js";
+import { assertAsyncIteratorEquals } from "./helpers/equality-asserters.js";
+// Also uses some global functions included via support-promises.js.
+
+const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function*() {}).prototype);
+
+testWithArea(async area => {
+ const keysProto = Object.getPrototypeOf(area.keys());
+ const valuesProto = Object.getPrototypeOf(area.values());
+ const entriesProto = Object.getPrototypeOf(area.entries());
+
+ assert_equals(keysProto, valuesProto, "keys() and values() return values' must have the same [[Prototype]]");
+ assert_equals(valuesProto, entriesProto, "values() and entries () return values' must have the same [[Prototype]]");
+}, "keys()/values()/entries() all use the same prototype object");
+
+for (const method of ["keys", "values", "entries"]) {
+ testWithArea(async area => {
+ const iter = area[method]();
+ const proto = Object.getPrototypeOf(iter);
+ assert_equals(Object.getPrototypeOf(proto), AsyncIteratorPrototype,
+ "[[Prototype]] must be the async iterator prototype");
+ classAssert.propertyKeys(proto, ["next"], [], "must only have a next() method");
+ }, `${method}() return value is an async iterator of the expected shape`);
+
+ testWithArea(async area => {
+ const iter = area[method]();
+ const promise = iter.next();
+
+ await area.set(1, "value 1");
+
+ const iterResults = [
+ await promise,
+ await iter.next()
+ ];
+
+ classAssert.iterResults(iterResults, [
+ [undefined, true],
+ [undefined, true]
+ ]);
+ }, `${method}(): .next() on empty means forever done, even if you set more`);
+
+ testWithArea(async area => {
+ for await (const key of area[method]()) {
+ assert_unreached("Loop body must not be entered");
+ }
+ }, `${method}(): for-await-of loop body never executes`);
+
+ testWithArea(async (area, t) => {
+ await area.set(1, "value 1");
+
+ const iter = area[method]();
+
+ const { database, store, version } = area.backingStore;
+ await migrateNamedDatabase(t, database, version + 1, () => {});
+
+ const iterResultPromise1 = iter.next();
+ const iterResultPromise2 = iter.next();
+
+ await promise_rejects(t, "VersionError", iterResultPromise1, "first next()");
+ await promise_rejects(t, "VersionError", iterResultPromise2, "second next()");
+
+ const iterResultPromise3 = iter.next();
+
+ assert_not_equals(iterResultPromise1, iterResultPromise2,
+ "Two promises retrieved from synchronous next() calls must be different (1 vs 2)");
+ assert_not_equals(iterResultPromise1, iterResultPromise3,
+ "Two promises, one retrieved after waiting for the other, must be different (1 vs 3)");
+ assert_not_equals(iterResultPromise2, iterResultPromise3,
+ "Two promises, one retrieved after waiting for the other, must be different (2 vs 3)");
+
+ await promise_rejects(t, "VersionError", iterResultPromise2, "third next()");
+
+ const reason1 = await iterResultPromise1.catch(r => r);
+ const reason2 = await iterResultPromise2.catch(r => r);
+ const reason3 = await iterResultPromise3.catch(r => r);
+
+ assert_equals(reason1, reason2, "reasons must be the same (1 vs 2)");
+ assert_equals(reason2, reason3, "reasons must be the same (2 vs 3)");
+ }, `${method}(): error path: returns new rejected promises, each with the same reason`);
+}
+</script>
diff --git a/kv-storage/keys.https.html b/kv-storage/keys.https.html
new file mode 100644
index 0000000..a6be297
--- /dev/null
+++ b/kv-storage/keys.https.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>KV Storage: keys() trickier tests</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script type="module">
+import { testWithArea } from "./helpers/kvs-tests.js";
+import * as classAssert from "./helpers/class-assert.js";
+import {
+ assertAsyncIteratorEquals,
+ assertAsyncIteratorCustomEquals,
+ assertEqualPostKeyRoundtripping
+} from "./helpers/equality-asserters.js";
+
+testWithArea(async area => {
+ await area.set(1, "value 1");
+ await area.set(2, "value 2");
+ await area.set(3, "value 3");
+
+ await assertAsyncIteratorEquals(area.keys(), [1, 2, 3]);
+}, "Using for-await-of to collect the results works");
+
+testWithArea(async area => {
+ // We're not testing every key type since this isn't a test of IndexedDB.
+ await area.set(1, "value 1");
+ await area.set(new Date(500), "value date 500");
+ await area.set(-1, "value -1");
+ await area.set(new Date(-20), "value date -20");
+ await area.set("aaa", "value aaa");
+ await area.set("a", "value a");
+ await area.set(-Infinity, "value -Infinity");
+
+ await assertAsyncIteratorCustomEquals(
+ area.keys(),
+ [
+ -Infinity,
+ -1,
+ 1,
+ new Date(-20),
+ new Date(500),
+ "a",
+ "aaa"
+ ],
+ assertEqualPostKeyRoundtripping
+ );
+}, "Results are returned in IndexedDB order");
+
+testWithArea(async area => {
+ await area.set(1, "value 1");
+ await area.set(2, "value 2");
+ await area.set(3, "value 3");
+
+ const iter = area.keys();
+ const iterResults = [
+ await iter.next(),
+ await iter.next(),
+ await iter.next(),
+ await iter.next(),
+ await iter.next(),
+ await iter.next()
+ ];
+
+ classAssert.iterResults(iterResults, [
+ [1, false],
+ [2, false],
+ [3, false],
+ [undefined, true],
+ [undefined, true],
+ [undefined, true]
+ ]);
+}, "Manual testing of .next() calls, with awaiting");
+
+testWithArea(async area => {
+ area.set(1, "value 1");
+ area.set(2, "value 2");
+ area.set(3, "value 3");
+
+ const iter = area.keys();
+ const promises = [
+ iter.next(),
+ iter.next(),
+ iter.next(),
+ iter.next(),
+ iter.next(),
+ iter.next()
+ ];
+ const iterResults = await Promise.all(promises);
+
+ classAssert.iterResults(iterResults, [
+ [1, false],
+ [2, false],
+ [3, false],
+ [undefined, true],
+ [undefined, true],
+ [undefined, true]
+ ]);
+}, "Manual testing of .next() calls, no awaiting");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const key of area.keys()) {
+ seen.push(key);
+ if (key === 20) {
+ await area.set(15, "value 15");
+ }
+ }
+
+ assert_array_equals(seen, [10, 20, 30, 40]);
+}, "Inserting an entry before the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const key of area.keys()) {
+ seen.push(key);
+ if (key === 20) {
+ await area.set(25, "value 25");
+ }
+ }
+
+ assert_array_equals(seen, [10, 20, 25, 30, 40]);
+}, "Inserting an entry after the current entry must show up in iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const key of area.keys()) {
+ seen.push(key);
+ if (key === 20) {
+ await area.delete(10);
+ }
+ }
+
+ assert_array_equals(seen, [10, 20, 30, 40]);
+}, "Deleting an entry before the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const key of area.keys()) {
+ seen.push(key);
+ if (key === 20) {
+ await area.delete(20);
+ }
+ }
+
+ assert_array_equals(seen, [10, 20, 30, 40]);
+}, "Deleting the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const key of area.keys()) {
+ seen.push(key);
+ if (key === 20) {
+ await area.delete(30);
+ }
+ }
+
+ assert_array_equals(seen, [10, 20, 40]);
+}, "Deleting an entry after the current entry must show up in iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const key of area.keys()) {
+ seen.push(key);
+ if (key === 20) {
+ await area.set(10, "value 10, but changed!!");
+ }
+ }
+
+ assert_array_equals(seen, [10, 20, 30, 40]);
+}, "Modifying a value before the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const key of area.keys()) {
+ seen.push(key);
+ if (key === 20) {
+ await area.set(20, "value 20, but changed!!");
+ }
+ }
+
+ assert_array_equals(seen, [10, 20, 30, 40]);
+}, "Modifying a value at the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const key of area.keys()) {
+ seen.push(key);
+ if (key === 20) {
+ await area.set(30, "value 30, but changed!!");
+ }
+ }
+
+ assert_array_equals(seen, [10, 20, 30, 40]);
+}, "Modifying a value after the current entry must have no effect on iteration (since we're iterating keys)");
+</script>
diff --git a/kv-storage/undefined-value.https.html b/kv-storage/undefined-value.https.html
index 89da5d5..4cb483a 100644
--- a/kv-storage/undefined-value.https.html
+++ b/kv-storage/undefined-value.https.html
@@ -9,6 +9,7 @@
<script type="module">
import { StorageArea } from "std:kv-storage";
import { testWithArea } from "./helpers/kvs-tests.js";
+import { assertAsyncIteratorEquals } from "./helpers/equality-asserters.js";
testWithArea(async (area) => {
assert_equals(await area.get("key"), undefined);
@@ -18,9 +19,9 @@
await area.set("key", undefined);
assert_equals(await area.get("key"), undefined);
- assert_equals((await area.keys()).length, 0, "number of keys");
- assert_equals((await area.values()).length, 0, "number of values");
- assert_equals((await area.entries()).length, 0, "number of entries");
+ await assertAsyncIteratorEquals(area.keys(), [], "keys");
+ await assertAsyncIteratorEquals(area.values(), [], "values");
+ await assertAsyncIteratorEquals(area.entries(), [], "entries");
}, "Setting undefined as a value when nothing was present is a no-op");
testWithArea(async (area) => {
@@ -29,8 +30,8 @@
assert_equals(await area.get("key"), undefined);
- assert_equals((await area.keys()).length, 0, "number of keys");
- assert_equals((await area.values()).length, 0, "number of values");
- assert_equals((await area.entries()).length, 0, "number of entries");
+ await assertAsyncIteratorEquals(area.keys(), [], "keys");
+ await assertAsyncIteratorEquals(area.values(), [], "values");
+ await assertAsyncIteratorEquals(area.entries(), [], "entries");
}, "Setting undefined as a value deletes what was previously there");
</script>
diff --git a/kv-storage/values.https.html b/kv-storage/values.https.html
new file mode 100644
index 0000000..64756bf
--- /dev/null
+++ b/kv-storage/values.https.html
@@ -0,0 +1,231 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>KV Storage: values() trickier tests</title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script type="module">
+import { testWithArea } from "./helpers/kvs-tests.js";
+import * as classAssert from "./helpers/class-assert.js";
+import { assertAsyncIteratorEquals } from "./helpers/equality-asserters.js";
+
+testWithArea(async area => {
+ await area.set(1, "value 1");
+ await area.set(2, "value 2");
+ await area.set(3, "value 3");
+
+ await assertAsyncIteratorEquals(area.values(), ["value 1", "value 2", "value 3"]);
+}, "Using for-await-of to collect the results works");
+
+testWithArea(async area => {
+ // We're not testing every key type since this isn't a test of IndexedDB.
+ await area.set(1, "value 1");
+ await area.set(new Date(500), "value date 500");
+ await area.set(-1, "value -1");
+ await area.set(new Date(-20), "value date -20");
+ await area.set("aaa", "value aaa");
+ await area.set("a", "value a");
+ await area.set(-Infinity, "value -Infinity");
+
+ await assertAsyncIteratorEquals(
+ area.values(),
+ [
+ "value -Infinity",
+ "value -1",
+ "value 1",
+ "value date -20",
+ "value date 500",
+ "value a",
+ "value aaa"
+ ]
+ );
+}, "Results are returned in IndexedDB key order");
+
+testWithArea(async area => {
+ await area.set(1, "value 1");
+ await area.set(2, "value 2");
+ await area.set(3, "value 3");
+
+ const iter = area.values();
+ const iterResults = [
+ await iter.next(),
+ await iter.next(),
+ await iter.next(),
+ await iter.next(),
+ await iter.next(),
+ await iter.next()
+ ];
+
+ classAssert.iterResults(iterResults, [
+ ["value 1", false],
+ ["value 2", false],
+ ["value 3", false],
+ [undefined, true],
+ [undefined, true],
+ [undefined, true]
+ ]);
+}, "Manual testing of .next() calls, with awaiting");
+
+testWithArea(async area => {
+ area.set(1, "value 1");
+ area.set(2, "value 2");
+ area.set(3, "value 3");
+
+ const iter = area.values();
+ const promises = [
+ iter.next(),
+ iter.next(),
+ iter.next(),
+ iter.next(),
+ iter.next(),
+ iter.next()
+ ];
+ const iterResults = await Promise.all(promises);
+
+ classAssert.iterResults(iterResults, [
+ ["value 1", false],
+ ["value 2", false],
+ ["value 3", false],
+ [undefined, true],
+ [undefined, true],
+ [undefined, true]
+ ]);
+}, "Manual testing of .next() calls, no awaiting");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const value of area.values()) {
+ seen.push(value);
+ if (value === "value 20") {
+ await area.set(15, "value 15");
+ }
+ }
+
+ assert_array_equals(seen, ["value 10", "value 20", "value 30", "value 40"]);
+}, "Inserting an entry before the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const value of area.values()) {
+ seen.push(value);
+ if (value === "value 20") {
+ await area.set(25, "value 25");
+ }
+ }
+
+ assert_array_equals(seen, ["value 10", "value 20", "value 25", "value 30", "value 40"]);
+}, "Inserting an entry after the current entry must show up in iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const value of area.values()) {
+ seen.push(value);
+ if (value === "value 20") {
+ await area.delete(10);
+ }
+ }
+
+ assert_array_equals(seen, ["value 10", "value 20", "value 30", "value 40"]);
+}, "Deleting an entry before the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const value of area.values()) {
+ seen.push(value);
+ if (value === "value 20") {
+ await area.delete(20);
+ }
+ }
+
+ assert_array_equals(seen, ["value 10", "value 20", "value 30", "value 40"]);
+}, "Deleting the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const value of area.values()) {
+ seen.push(value);
+ if (value === "value 20") {
+ await area.delete(30);
+ }
+ }
+
+ assert_array_equals(seen, ["value 10", "value 20", "value 40"]);
+}, "Deleting an entry after the current entry must show up in iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const value of area.values()) {
+ seen.push(value);
+ if (value === "value 20") {
+ await area.set(10, "value 10, but changed!!");
+ }
+ }
+
+ assert_array_equals(seen, ["value 10", "value 20", "value 30", "value 40"]);
+}, "Modifying a value before the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const value of area.values()) {
+ seen.push(value);
+ if (value === "value 20") {
+ await area.set(20, "value 20, but changed!!");
+ }
+ }
+
+ assert_array_equals(seen, ["value 10", "value 20", "value 30", "value 40"]);
+}, "Modifying a value at the current entry must have no effect on iteration");
+
+testWithArea(async area => {
+ await area.set(10, "value 10");
+ await area.set(20, "value 20");
+ await area.set(30, "value 30");
+ await area.set(40, "value 40");
+
+ let seen = [];
+ for await (const value of area.values()) {
+ seen.push(value);
+ if (value === "value 20") {
+ await area.set(30, "value 30, but changed!!");
+ }
+ }
+
+ assert_array_equals(seen, ["value 10", "value 20", "value 30, but changed!!", "value 40"]);
+}, "Modifying a value after the current entry must show up in iteration");
+</script>