Skip to content

Commit c1d40be

Browse files
pwnallinexorabletash
authored andcommitted
IndexedDB upgrade transaction lifecycle tests. (web-platform-tests#4509)
These tests are upstreamed from Blink - https://crrev.com/2556373003
1 parent 1dd5361 commit c1d40be

4 files changed

+377
-50
lines changed

IndexedDB/support-promises.js

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,46 @@
1-
// Returns an IndexedDB database name likely to be unique to the test case.
2-
const databaseName = (testCase) => {
1+
'use strict';
2+
3+
// Returns an IndexedDB database name that is unique to the test case.
4+
function databaseName(testCase) {
35
return 'db' + self.location.pathname + '-' + testCase.name;
4-
};
6+
}
57

68
// Creates an EventWatcher covering all the events that can be issued by
79
// IndexedDB requests and transactions.
8-
const requestWatcher = (testCase, request) => {
10+
function requestWatcher(testCase, request) {
911
return new EventWatcher(testCase, request,
10-
['error', 'success', 'upgradeneeded']);
11-
};
12+
['abort', 'blocked', 'complete', 'error', 'success', 'upgradeneeded']);
13+
}
1214

1315
// Migrates an IndexedDB database whose name is unique for the test case.
1416
//
1517
// newVersion must be greater than the database's current version.
1618
//
1719
// migrationCallback will be called during a versionchange transaction and will
18-
// be given the created database and the versionchange transaction.
20+
// given the created database, the versionchange transaction, and the database
21+
// open request.
1922
//
2023
// Returns a promise. If the versionchange transaction goes through, the promise
2124
// resolves to an IndexedDB database that must be closed by the caller. If the
2225
// versionchange transaction is aborted, the promise resolves to an error.
23-
const migrateDatabase = (testCase, newVersion, migrationCallback) => {
26+
function migrateDatabase(testCase, newVersion, migrationCallback) {
2427
return migrateNamedDatabase(
2528
testCase, databaseName(testCase), newVersion, migrationCallback);
26-
};
29+
}
2730

2831
// Migrates an IndexedDB database.
2932
//
3033
// newVersion must be greater than the database's current version.
3134
//
3235
// migrationCallback will be called during a versionchange transaction and will
33-
// be given the created database and the versionchange transaction.
36+
// given the created database, the versionchange transaction, and the database
37+
// open request.
3438
//
3539
// Returns a promise. If the versionchange transaction goes through, the promise
3640
// resolves to an IndexedDB database that must be closed by the caller. If the
3741
// versionchange transaction is aborted, the promise resolves to an error.
38-
const migrateNamedDatabase =
39-
(testCase, databaseName, newVersion, migrationCallback) => {
42+
function migrateNamedDatabase(
43+
testCase, databaseName, newVersion, migrationCallback) {
4044
// We cannot use eventWatcher.wait_for('upgradeneeded') here, because
4145
// the versionchange transaction auto-commits before the Promise's then
4246
// callback gets called.
@@ -45,70 +49,86 @@ const migrateNamedDatabase =
4549
request.onupgradeneeded = testCase.step_func(event => {
4650
const database = event.target.result;
4751
const transaction = event.target.transaction;
48-
let abortCalled = false;
52+
let shouldBeAborted = false;
53+
let requestEventPromise = null;
4954

5055
// We wrap IDBTransaction.abort so we can set up the correct event
5156
// listeners and expectations if the test chooses to abort the
5257
// versionchange transaction.
5358
const transactionAbort = transaction.abort.bind(transaction);
5459
transaction.abort = () => {
55-
request.onerror = event => {
56-
event.preventDefault();
57-
resolve(event);
58-
};
59-
request.onsuccess = () => reject(new Error(
60-
'indexedDB.open should not succeed after the versionchange ' +
61-
'transaction is aborted'));
60+
transaction._willBeAborted();
6261
transactionAbort();
63-
abortCalled = true;
62+
}
63+
transaction._willBeAborted = () => {
64+
requestEventPromise = new Promise((resolve, reject) => {
65+
request.onerror = event => {
66+
event.preventDefault();
67+
resolve(event);
68+
};
69+
request.onsuccess = () => reject(new Error(
70+
'indexedDB.open should not succeed for an aborted ' +
71+
'versionchange transaction'));
72+
});
73+
shouldBeAborted = true;
6474
}
6575

66-
migrationCallback(database, transaction);
67-
if (!abortCalled) {
76+
// If migration callback returns a promise, we'll wait for it to resolve.
77+
// This simplifies some tests.
78+
const callbackResult = migrationCallback(database, transaction, request);
79+
if (!shouldBeAborted) {
80+
request.onerror = null;
6881
request.onsuccess = null;
69-
resolve(requestWatcher(testCase, request).wait_for('success'));
82+
requestEventPromise =
83+
requestWatcher(testCase, request).wait_for('success');
7084
}
85+
86+
// requestEventPromise needs to be the last promise in the chain, because
87+
// we want the event that it resolves to.
88+
resolve(Promise.resolve(callbackResult).then(() => requestEventPromise));
7189
});
7290
request.onerror = event => reject(event.target.error);
7391
request.onsuccess = () => reject(new Error(
7492
'indexedDB.open should not succeed without creating a ' +
7593
'versionchange transaction'));
7694
}).then(event => event.target.result || event.target.error);
77-
};
95+
}
7896

7997
// Creates an IndexedDB database whose name is unique for the test case.
8098
//
8199
// setupCallback will be called during a versionchange transaction, and will be
82-
// given the created database and the versionchange transaction.
100+
// given the created database, the versionchange transaction, and the database
101+
// open request.
83102
//
84103
// Returns a promise that resolves to an IndexedDB database. The caller must
85104
// close the database.
86-
const createDatabase = (testCase, setupCallback) => {
105+
function createDatabase(testCase, setupCallback) {
87106
return createNamedDatabase(testCase, databaseName(testCase), setupCallback);
88-
};
107+
}
89108

90109
// Creates an IndexedDB database.
91110
//
92111
// setupCallback will be called during a versionchange transaction, and will be
93-
// given the created database and the versionchange transaction.
112+
// given the created database, the versionchange transaction, and the database
113+
// open request.
94114
//
95115
// Returns a promise that resolves to an IndexedDB database. The caller must
96116
// close the database.
97-
const createNamedDatabase = (testCase, databaseName, setupCallback) => {
117+
function createNamedDatabase(testCase, databaseName, setupCallback) {
98118
const request = indexedDB.deleteDatabase(databaseName);
99119
const eventWatcher = requestWatcher(testCase, request);
100120

101121
return eventWatcher.wait_for('success').then(event =>
102122
migrateNamedDatabase(testCase, databaseName, 1, setupCallback));
103-
};
123+
}
104124

105125
// Opens an IndexedDB database without performing schema changes.
106126
//
107127
// The given version number must match the database's current version.
108128
//
109129
// Returns a promise that resolves to an IndexedDB database. The caller must
110130
// close the database.
111-
const openDatabase = (testCase, version) => {
131+
function openDatabase(testCase, version) {
112132
return openNamedDatabase(testCase, databaseName(testCase), version);
113133
}
114134

@@ -118,7 +138,7 @@ const openDatabase = (testCase, version) => {
118138
//
119139
// Returns a promise that resolves to an IndexedDB database. The caller must
120140
// close the database.
121-
const openNamedDatabase = (testCase, databaseName, version) => {
141+
function openNamedDatabase(testCase, databaseName, version) {
122142
const request = indexedDB.open(databaseName, version);
123143
const eventWatcher = requestWatcher(testCase, request);
124144
return eventWatcher.wait_for('success').then(event => event.target.result);
@@ -142,24 +162,24 @@ const createBooksStore = (testCase, database) => {
142162
for (let record of BOOKS_RECORD_DATA)
143163
store.put(record);
144164
return store;
145-
};
165+
}
146166

147167
// Creates a 'not_books' object store used to test renaming into existing or
148168
// deleted store names.
149-
const createNotBooksStore = (testCase, database) => {
150-
const store = database.createObjectStore('not_books');
151-
store.createIndex('not_by_author', 'author');
152-
store.createIndex('not_by_title', 'title', { unique: true });
153-
return store;
154-
};
169+
function createNotBooksStore(testCase, database) {
170+
const store = database.createObjectStore('not_books');
171+
store.createIndex('not_by_author', 'author');
172+
store.createIndex('not_by_title', 'title', { unique: true });
173+
return store;
174+
}
155175

156176
// Verifies that an object store's indexes match the indexes used to create the
157177
// books store in the test database's version 1.
158178
//
159179
// The errorMessage is used if the assertions fail. It can state that the
160180
// IndexedDB implementation being tested is incorrect, or that the testing code
161181
// is using it incorrectly.
162-
const checkStoreIndexes = (testCase, store, errorMessage) => {
182+
function checkStoreIndexes (testCase, store, errorMessage) {
163183
assert_array_equals(
164184
store.indexNames, ['by_author', 'by_title'], errorMessage);
165185
const authorIndex = store.index('by_author');
@@ -168,31 +188,31 @@ const checkStoreIndexes = (testCase, store, errorMessage) => {
168188
checkAuthorIndexContents(testCase, authorIndex, errorMessage),
169189
checkTitleIndexContents(testCase, titleIndex, errorMessage),
170190
]);
171-
};
191+
}
172192

173193
// Verifies that an object store's key generator is in the same state as the
174194
// key generator created for the books store in the test database's version 1.
175195
//
176196
// The errorMessage is used if the assertions fail. It can state that the
177197
// IndexedDB implementation being tested is incorrect, or that the testing code
178198
// is using it incorrectly.
179-
const checkStoreGenerator = (testCase, store, expectedKey, errorMessage) => {
199+
function checkStoreGenerator(testCase, store, expectedKey, errorMessage) {
180200
const request = store.put(
181201
{ title: 'Bedrock Nights ' + expectedKey, author: 'Barney' });
182202
const eventWatcher = requestWatcher(testCase, request);
183203
return eventWatcher.wait_for('success').then(() => {
184204
const result = request.result;
185205
assert_equals(result, expectedKey, errorMessage);
186206
});
187-
};
207+
}
188208

189209
// Verifies that an object store's contents matches the contents used to create
190210
// the books store in the test database's version 1.
191211
//
192212
// The errorMessage is used if the assertions fail. It can state that the
193213
// IndexedDB implementation being tested is incorrect, or that the testing code
194214
// is using it incorrectly.
195-
const checkStoreContents = (testCase, store, errorMessage) => {
215+
function checkStoreContents(testCase, store, errorMessage) {
196216
const request = store.get(123456);
197217
const eventWatcher = requestWatcher(testCase, request);
198218
return eventWatcher.wait_for('success').then(() => {
@@ -201,36 +221,36 @@ const checkStoreContents = (testCase, store, errorMessage) => {
201221
assert_equals(result.author, BOOKS_RECORD_DATA[0].author, errorMessage);
202222
assert_equals(result.title, BOOKS_RECORD_DATA[0].title, errorMessage);
203223
});
204-
};
224+
}
205225

206226
// Verifies that index matches the 'by_author' index used to create the
207227
// by_author books store in the test database's version 1.
208228
//
209229
// The errorMessage is used if the assertions fail. It can state that the
210230
// IndexedDB implementation being tested is incorrect, or that the testing code
211231
// is using it incorrectly.
212-
const checkAuthorIndexContents = (testCase, index, errorMessage) => {
232+
function checkAuthorIndexContents(testCase, index, errorMessage) {
213233
const request = index.get(BOOKS_RECORD_DATA[2].author);
214234
const eventWatcher = requestWatcher(testCase, request);
215235
return eventWatcher.wait_for('success').then(() => {
216236
const result = request.result;
217237
assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage);
218238
assert_equals(result.title, BOOKS_RECORD_DATA[2].title, errorMessage);
219239
});
220-
};
240+
}
221241

222242
// Verifies that an index matches the 'by_title' index used to create the books
223243
// store in the test database's version 1.
224244
//
225245
// The errorMessage is used if the assertions fail. It can state that the
226246
// IndexedDB implementation being tested is incorrect, or that the testing code
227247
// is using it incorrectly.
228-
const checkTitleIndexContents = (testCase, index, errorMessage) => {
248+
function checkTitleIndexContents(testCase, index, errorMessage) {
229249
const request = index.get(BOOKS_RECORD_DATA[2].title);
230250
const eventWatcher = requestWatcher(testCase, request);
231251
return eventWatcher.wait_for('success').then(() => {
232252
const result = request.result;
233253
assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage);
234254
assert_equals(result.author, BOOKS_RECORD_DATA[2].author, errorMessage);
235255
});
236-
};
256+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!doctype html>
2+
<meta charset="utf8">
3+
<title>IndexedDB: backend-aborted versionchange transaction lifecycle</title>
4+
<link rel="help"
5+
href="https://w3c.github.io/IndexedDB/#upgrade-transaction-steps">
6+
<link rel="help"
7+
href="https://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore">
8+
<link rel="help"
9+
href="https://w3c.github.io/IndexedDB/#dom-idbdatabase-deleteobjectstore">
10+
<link rel="author" href="pwnall@chromium.org" title="Victor Costan">
11+
<script src="/resources/testharness.js"></script>
12+
<script src="/resources/testharnessreport.js"></script>
13+
<script src="support-promises.js"></script>
14+
<script>
15+
'use strict';
16+
17+
promise_test(t => {
18+
return createDatabase(t, database => {
19+
createBooksStore(t, database);
20+
}).then(database => {
21+
database.close();
22+
}).then(() => migrateDatabase(t, 2, (database, transaction, request) => {
23+
return new Promise((resolve, reject) => {
24+
transaction.addEventListener('abort', () => {
25+
resolve(new Promise((resolve, reject) => {
26+
assert_equals(
27+
request.transaction, transaction,
28+
"The open request's transaction should be reset after onabort");
29+
assert_throws(
30+
'InvalidStateError',
31+
() => { database.createObjectStore('books2'); },
32+
'createObjectStore exception should reflect that the ' +
33+
'transaction is no longer running');
34+
assert_throws(
35+
'InvalidStateError',
36+
() => { database.deleteObjectStore('books'); },
37+
'deleteObjectStore exception should reflect that the ' +
38+
'transaction is no longer running');
39+
resolve();
40+
}));
41+
}, false);
42+
transaction.objectStore('books').add(BOOKS_RECORD_DATA[0]);
43+
transaction._willBeAborted();
44+
});
45+
}));
46+
}, 'in the abort event handler for a transaction aborted due to an unhandled ' +
47+
'request error');
48+
49+
promise_test(t => {
50+
return createDatabase(t, database => {
51+
createBooksStore(t, database);
52+
}).then(database => {
53+
database.close();
54+
}).then(() => migrateDatabase(t, 2, (database, transaction, request) => {
55+
return new Promise((resolve, reject) => {
56+
transaction.addEventListener('abort', () => {
57+
setTimeout(() => {
58+
resolve(new Promise((resolve, reject) => {
59+
assert_equals(
60+
request.transaction, null,
61+
"The open request's transaction should be reset after " +
62+
'onabort microtasks');
63+
assert_throws(
64+
'InvalidStateError',
65+
() => { database.createObjectStore('books2'); },
66+
'createObjectStore exception should reflect that the ' +
67+
'transaction is no longer running');
68+
assert_throws(
69+
'InvalidStateError',
70+
() => { database.deleteObjectStore('books'); },
71+
'deleteObjectStore exception should reflect that the ' +
72+
'transaction is no longer running');
73+
resolve();
74+
}));
75+
}, 0);
76+
}, false);
77+
transaction.objectStore('books').add(BOOKS_RECORD_DATA[0]);
78+
transaction._willBeAborted();
79+
});
80+
}));
81+
}, 'in a setTimeout(0) callback after the abort event is fired for a ' +
82+
'transaction aborted due to an unhandled request failure');
83+
84+
</script>

0 commit comments

Comments
 (0)