next batch of test cases and fixes

This commit is contained in:
Florian Dold 2021-02-16 13:46:51 +01:00
parent db59275b6b
commit 987f22de02
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 509 additions and 27 deletions

View File

@ -27,10 +27,7 @@ import {
StoreLevel,
RecordStoreResponse,
} from "./backend-interface";
import {
structuredClone,
structuredRevive,
} from "./util/structuredClone";
import { structuredClone, structuredRevive } from "./util/structuredClone";
import {
InvalidStateError,
InvalidAccessError,
@ -42,11 +39,7 @@ import { compareKeys } from "./util/cmp";
import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue";
import { getIndexKeys } from "./util/getIndexKeys";
import { openPromise } from "./util/openPromise";
import {
IDBKeyRange,
IDBTransactionMode,
IDBValidKey,
} from "./idbtypes";
import { IDBKeyRange, IDBTransactionMode, IDBValidKey } from "./idbtypes";
import { BridgeIDBKeyRange } from "./bridge-idb";
type Key = IDBValidKey;
@ -488,10 +481,10 @@ export class MemoryBackend implements Backend {
objectStores: string[],
mode: IDBTransactionMode,
): Promise<DatabaseTransaction> {
if (this.enableTracing) {
console.log(`TRACING: beginTransaction`);
}
const transactionCookie = `tx-${this.transactionIdCounter++}`;
if (this.enableTracing) {
console.log(`TRACING: beginTransaction ${transactionCookie}`);
}
const myConn = this.connections[conn.connectionCookie];
if (!myConn) {
throw Error("connection not found");
@ -556,7 +549,7 @@ export class MemoryBackend implements Backend {
async close(conn: DatabaseConnection): Promise<void> {
if (this.enableTracing) {
console.log(`TRACING: close`);
console.log(`TRACING: close (${conn.connectionCookie})`);
}
const myConn = this.connections[conn.connectionCookie];
if (!myConn) {
@ -640,7 +633,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) {
console.log(`TRACING: deleteIndex(${indexName})`);
}
const myConn = this.connections[btx.transactionCookie];
const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
@ -670,9 +663,11 @@ export class MemoryBackend implements Backend {
deleteObjectStore(btx: DatabaseTransaction, name: string): void {
if (this.enableTracing) {
console.log(`TRACING: deleteObjectStore(${name})`);
console.log(
`TRACING: deleteObjectStore(${name}) in ${btx.transactionCookie}`,
);
}
const myConn = this.connections[btx.transactionCookie];
const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
@ -714,7 +709,7 @@ export class MemoryBackend implements Backend {
console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`);
}
const myConn = this.connections[btx.transactionCookie];
const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
@ -846,7 +841,15 @@ export class MemoryBackend implements Backend {
objectStoreMapEntry.store.originalData;
storeData.forEach((v, k) => {
this.insertIntoIndex(newIndex, k, v.value, indexProperties);
try {
this.insertIntoIndex(newIndex, k, v.value, indexProperties);
} catch (e) {
if (e instanceof DataError) {
// We don't propagate this error here.
return;
}
throw e;
}
});
}
@ -1404,6 +1407,16 @@ export class MemoryBackend implements Backend {
const autoIncrement =
schema.objectStores[storeReq.objectStoreName].autoIncrement;
const keyPath = schema.objectStores[storeReq.objectStoreName].keyPath;
if (
keyPath !== null &&
keyPath !== undefined &&
storeReq.key !== undefined
) {
// If in-line keys are used, a key can't be explicitly specified.
throw new DataError();
}
let storeKeyResult: StoreKeyResult;
const revivedValue = structuredRevive(storeReq.value);
try {
@ -1463,7 +1476,16 @@ export class MemoryBackend implements Backend {
}
const indexProperties =
schema.objectStores[storeReq.objectStoreName].indexes[indexName];
this.insertIntoIndex(index, key, value, indexProperties);
try {
this.insertIntoIndex(index, key, value, indexProperties);
} catch (e) {
if (e instanceof DataError) {
// https://www.w3.org/TR/IndexedDB-2/#object-store-storage-operation
// Do nothing
} else {
throw e;
}
}
}
return { key };

View File

@ -58,6 +58,7 @@ import {
import { fakeDOMStringList } from "./util/fakeDOMStringList";
import FakeEvent from "./util/FakeEvent";
import FakeEventTarget from "./util/FakeEventTarget";
import { makeStoreKeyValue } from "./util/makeStoreKeyValue";
import { normalizeKeyPath } from "./util/normalizeKeyPath";
import { openPromise } from "./util/openPromise";
import queueTask from "./util/queueTask";
@ -605,7 +606,17 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
throw new TypeError();
}
const transaction = confirmActiveVersionchangeTransaction(this);
transaction._objectStoresCache.delete(name);
const backendTx = transaction._backendTransaction;
if (!backendTx) {
throw Error("invariant violated");
}
this._backend.deleteObjectStore(backendTx, name);
const os = transaction._objectStoresCache.get(name);
if (os) {
os._deleted = true;
transaction._objectStoresCache.delete(name);
}
}
public _internalTransaction(
@ -866,7 +877,9 @@ export class BridgeIDBFactory {
event2.eventPath = [request];
request.dispatchEvent(event2);
} else {
console.log(`dispatching success event, _active=${transaction._active}`);
console.log(
`dispatching success event, _active=${transaction._active}`,
);
const event2 = new FakeEvent("success", {
bubbles: false,
cancelable: false,
@ -1064,8 +1077,23 @@ export class BridgeIDBIndex implements IDBIndex {
});
}
public get(key: BridgeIDBKeyRange | IDBValidKey) {
private _confirmIndexExists() {
const storeSchema = this._schema.objectStores[this._objectStore._name];
if (!storeSchema) {
throw new InvalidStateError();
}
if (!storeSchema.indexes[this._name]) {
throw new InvalidStateError();
}
}
get(key: BridgeIDBKeyRange | IDBValidKey) {
confirmActiveTransaction(this);
this._confirmIndexExists();
if (this._deleted) {
throw new InvalidStateError();
}
if (!(key instanceof BridgeIDBKeyRange)) {
key = BridgeIDBKeyRange._valueToKeyRange(key);
@ -1384,6 +1412,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
);
}
public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) {
if (BridgeIDBFactory.enableTracing) {
console.log(`TRACE: IDBObjectStore._store`);
@ -1391,6 +1420,22 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
if (this._transaction.mode === "readonly") {
throw new ReadOnlyError();
}
const { keyPath, autoIncrement } = this._schema.objectStores[this._name];
if (key !== null && key !== undefined) {
valueToKey(key);
}
// We only call this to synchronously verify the request.
makeStoreKeyValue(
value,
key,
1,
autoIncrement,
keyPath,
);
const operation = async () => {
const { btx } = this._confirmActiveTransaction();
const result = await this._backend.storeRecord(btx, {
@ -1411,6 +1456,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
if (arguments.length === 0) {
throw new TypeError();
}
if (this._deleted) {
throw new InvalidStateError("tried to call 'put' on a deleted object store");
}
return this._store(value, key, true);
}
@ -1418,6 +1466,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
if (arguments.length === 0) {
throw new TypeError();
}
if (!this._schema.objectStores[this._name]) {
throw new InvalidStateError("object store does not exist");
}
return this._store(value, key, false);
}
@ -1425,7 +1476,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
if (arguments.length === 0) {
throw new TypeError();
}
if (this._deleted) {
throw new InvalidStateError("tried to call 'delete' on a deleted object store");
}
if (this._transaction.mode === "readonly") {
throw new ReadOnlyError();
}
@ -1458,6 +1511,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new TypeError();
}
if (this._deleted) {
throw new InvalidStateError("tried to call 'delete' on a deleted object store");
}
let keyRange: BridgeIDBKeyRange;
if (key instanceof BridgeIDBKeyRange) {
@ -1541,6 +1598,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
range?: IDBKeyRange | IDBValidKey,
direction: IDBCursorDirection = "next",
) {
if (this._deleted) {
throw new InvalidStateError("tried to call 'openCursor' on a deleted object store");
}
if (range === null) {
range = undefined;
}
@ -1572,6 +1632,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
range?: BridgeIDBKeyRange | IDBValidKey,
direction?: IDBCursorDirection,
) {
if (this._deleted) {
throw new InvalidStateError("tried to call 'openKeyCursor' on a deleted object store");
}
if (range === null) {
range = undefined;
}
@ -2091,7 +2154,9 @@ export class BridgeIDBTransaction
request.dispatchEvent(event);
} catch (err) {
if (BridgeIDBFactory.enableTracing) {
console.log("TRACING: caught error in transaction success event handler");
console.log(
"TRACING: caught error in transaction success event handler",
);
}
this._abort("AbortError");
this._active = false;

View File

@ -0,0 +1,177 @@
import test from "ava";
import { BridgeIDBKeyRange, BridgeIDBRequest } from "..";
import { IDBDatabase } from "../idbtypes";
import { createdb } from "./wptsupport";
// IDBIndex.get() - returns the record
test("WPT idbindex_get.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any, index: any;
const record = { key: 1, indexedProperty: "data" };
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var objStore = db.createObjectStore("store", { keyPath: "key" });
index = objStore.createIndex("index", "indexedProperty");
objStore.add(record);
};
open_rq.onsuccess = function (e) {
var rq = db
.transaction("store")
.objectStore("store")
.index("index")
.get(record.indexedProperty);
rq.onsuccess = function (e: any) {
t.deepEqual(e.target.result.key, record.key);
resolve();
};
};
});
t.pass();
});
// IDBIndex.get() - returns the record where the index contains duplicate values
test("WPT idbindex_get2.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
const records = [
{ key: 1, indexedProperty: "data" },
{ key: 2, indexedProperty: "data" },
{ key: 3, indexedProperty: "data" },
];
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var objStore = db.createObjectStore("test", { keyPath: "key" });
objStore.createIndex("index", "indexedProperty");
for (var i = 0; i < records.length; i++) objStore.add(records[i]);
};
open_rq.onsuccess = function (e) {
var rq = db
.transaction("test")
.objectStore("test")
.index("index")
.get("data");
rq.onsuccess = function (e: any) {
t.deepEqual(e.target.result.key, records[0].key);
resolve();
};
};
});
t.pass();
});
// IDBIndex.get() - attempt to retrieve a record that doesn't exist
test("WPT idbindex_get3.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var rq = db
.createObjectStore("test", { keyPath: "key" })
.createIndex("index", "indexedProperty")
.get(1);
rq.onsuccess = function (e: any) {
t.deepEqual(e.target.result, undefined);
resolve();
};
};
});
t.pass();
});
// IDBIndex.get() - returns the record with the first key in the range
test("WPT idbindex_get4.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var store = db.createObjectStore("store", { keyPath: "key" });
store.createIndex("index", "indexedProperty");
for (var i = 0; i < 10; i++) {
store.add({ key: i, indexedProperty: "data" + i });
}
};
open_rq.onsuccess = function (e) {
var rq = db
.transaction("store")
.objectStore("store")
.index("index")
.get(BridgeIDBKeyRange.bound("data4", "data7"));
rq.onsuccess = function (e: any) {
t.deepEqual(e.target.result.key, 4);
t.deepEqual(e.target.result.indexedProperty, "data4");
setTimeout(function () {
resolve();
}, 4);
};
};
});
t.pass();
});
// IDBIndex.get() - throw DataError when using invalid key
test("WPT idbindex_get5.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var index = db
.createObjectStore("test", { keyPath: "key" })
.createIndex("index", "indexedProperty");
t.throws(
function () {
index.get(NaN);
},
{ name: "DataError" },
);
resolve();
};
});
t.pass();
});
// IDBIndex.get() - throw InvalidStateError when the index is deleted
test("WPT idbindex_get6.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var store = db.createObjectStore("store", { keyPath: "key" });
var index = store.createIndex("index", "indexedProperty");
store.add({ key: 1, indexedProperty: "data" });
store.deleteIndex("index");
t.throws(
function () {
index.get("data");
},
{ name: "InvalidStateError" },
);
resolve();
};
});
t.pass();
});

View File

@ -1,4 +1,5 @@
import test from "ava";
import { BridgeIDBRequest } from "..";
import { IDBDatabase } from "../idbtypes";
import { createdb } from "./wptsupport";
@ -228,7 +229,8 @@ test("WPT idbobjectstore_add7.htm", async (t) => {
t.pass();
});
// IDBObjectStore.add() - object store has autoIncrement:true and the key path is an object attribute
// IDBObjectStore.add() - object store has autoIncrement:true and the key path
// is an object attribute
test("WPT idbobjectstore_add8.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
@ -268,3 +270,217 @@ test("WPT idbobjectstore_add8.htm", async (t) => {
});
t.pass();
});
// IDBObjectStore.add() - Attempt to add a record that does not meet the
// constraints of an object store's inline key requirements
test("WPT idbobjectstore_add9.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
const record = { key: 1, property: "data" };
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
var rq,
db = e.target.result,
objStore = db.createObjectStore("store", { keyPath: "key" });
t.throws(
function () {
rq = objStore.add(record, 1);
},
{ name: "DataError" },
);
t.deepEqual(rq, undefined);
resolve();
};
});
t.pass();
});
// IDBObjectStore.add() - Attempt to call 'add' without an key parameter when the
// object store uses out-of-line keys.
test("WPT idbobjectstore_add10.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
const record = { property: "data" };
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var rq,
objStore = db.createObjectStore("store");
t.throws(
function () {
rq = objStore.add(record);
},
{ name: "DataError" },
);
t.deepEqual(rq, undefined);
resolve();
};
});
t.pass();
});
// IDBObjectStore.add() - Attempt to add a record where the record's key
// does not meet the constraints of a valid key
test("WPT idbobjectstore_add11.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
const record = { key: { value: 1 }, property: "data" };
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var rq,
objStore = db.createObjectStore("store", { keyPath: "key" });
t.throws(
function () {
rq = objStore.add(record);
},
{ name: "DataError" },
);
t.deepEqual(rq, undefined);
resolve();
};
});
t.pass();
});
// IDBObjectStore.add() - Attempt to add a record where the
// record's in-line key is not defined
test("WPT idbobjectstore_add12.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
const record = { property: "data" };
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var rq,
objStore = db.createObjectStore("store", { keyPath: "key" });
t.throws(
function () {
rq = objStore.add(record);
},
{ name: "DataError" },
);
t.deepEqual(rq, undefined);
resolve();
};
});
t.pass();
});
// IDBObjectStore.add() - Attempt to add a record where the out of line
// key provided does not meet the constraints of a valid key
test("WPT idbobjectstore_add13.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
const record = { property: "data" };
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var rq,
objStore = db.createObjectStore("store");
t.throws(
function () {
rq = objStore.add(record, { value: 1 });
},
{ name: "DataError" },
);
t.deepEqual(rq, undefined);
resolve();
};
});
t.pass();
});
// IDBObjectStore.add() - Add a record where a value
// being indexed does not meet the constraints of a valid key
test("WPT idbobjectstore_add14.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
const record = { key: 1, indexedProperty: { property: "data" } };
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
var rq,
objStore = db.createObjectStore("store", { keyPath: "key" });
objStore.createIndex("index", "indexedProperty");
rq = objStore.add(record);
t.assert(rq instanceof BridgeIDBRequest);
rq.onsuccess = function () {
resolve();
};
};
});
t.pass();
});
// IDBObjectStore.add() - If the transaction this IDBObjectStore belongs
// to has its mode set to readonly, throw ReadOnlyError
test("WPT idbobjectstore_add15.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (event: any) {
db = event.target.result;
db.createObjectStore("store", { keyPath: "pKey" });
};
open_rq.onsuccess = function (event) {
var txn = db.transaction("store");
var ostore = txn.objectStore("store");
t.throws(
function () {
ostore.add({ pKey: "primaryKey_0" });
},
{ name: "ReadOnlyError" },
);
resolve();
};
});
t.pass();
});
// IDBObjectStore.add() - If the object store has been
// deleted, the implementation must throw a DOMException of type InvalidStateError
test("WPT idbobjectstore_add16.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
let ostore: any;
var open_rq = createdb(t);
open_rq.onupgradeneeded = function (event: any) {
db = event.target.result;
ostore = db.createObjectStore("store", { keyPath: "pKey" });
db.deleteObjectStore("store");
t.throws(
function () {
ostore.add({ pKey: "primaryKey_0" });
},
{ name: "InvalidStateError" },
);
resolve();
};
});
t.pass();
});

View File

@ -5,7 +5,9 @@ import { MemoryBackend } from "../MemoryBackend";
import { compareKeys } from "../util/cmp";
BridgeIDBFactory.enableTracing = true;
const idbFactory = new BridgeIDBFactory(new MemoryBackend());
const backend = new MemoryBackend();
backend.enableTracing = true;
const idbFactory = new BridgeIDBFactory(backend);
const self = {
indexedDB: idbFactory,

View File

@ -17,7 +17,7 @@
import { IDBValidKey } from "..";
import { DataError } from "./errors";
// https://w3c.github.io/IndexedDB/#convert-a-value-to-a-input
// https://www.w3.org/TR/IndexedDB-2/#convert-a-value-to-a-key
export function valueToKey(
input: any,
seen?: Set<object>,