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>