Bug 871445 - patch 4 - DatAStore: permissions, owned/access, r=mounir, r=ehsan

--HG--
rename : dom/datastore/tests/file_app.template.webapp => dom/datastore/tests/file_app2.template.webapp
rename : dom/datastore/tests/test_revision.html => dom/datastore/tests/file_revision.html
This commit is contained in:
Andrea Marchesini 2013-09-11 15:47:53 +02:00
parent f16a67e2fa
commit 907c386bbd
17 changed files with 771 additions and 368 deletions

View File

@ -541,16 +541,26 @@ this.DOMApplicationRegistry = {
}, },
updateDataStore: function(aId, aManifestURL, aManifest) { updateDataStore: function(aId, aManifestURL, aManifest) {
if (!("datastores" in aManifest)) { if ('datastores-owned' in aManifest) {
return; for (let name in aManifest['datastores-owned']) {
let readonly = "access" in aManifest['datastores-owned'][name]
? aManifest['datastores-owned'][name].access == 'readonly'
: false;
dataStoreService.installDataStore(aId, name, aManifestURL, readonly);
}
} }
for (let name in aManifest.datastores) { if ('datastores-access' in aManifest) {
let readonly = ("readonly" in aManifest.datastores[name]) && for (let name in aManifest['datastores-access']) {
!aManifest.datastores[name].readonly let readonly = ("readonly" in aManifest['datastores-access'][name]) &&
? false : true; !aManifest['datastores-access'][name].readonly
? false : true;
dataStoreService.installDataStore(aId, name, aManifestURL, readonly); // The first release is always in readonly mode.
dataStoreService.installAccessDataStore(aId, name, aManifestURL,
/* readonly */ true);
}
} }
}, },

View File

@ -6,7 +6,7 @@
'use strict' 'use strict'
var EXPORTED_SYMBOLS = ["DataStore"]; var EXPORTED_SYMBOLS = ["DataStore", "DataStoreAccess"];
function debug(s) { function debug(s) {
// dump('DEBUG DataStore: ' + s + '\n'); // dump('DEBUG DataStore: ' + s + '\n');
@ -204,7 +204,7 @@ DataStore.prototype = {
new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode")); new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode"));
}, },
exposeObject: function(aWindow) { exposeObject: function(aWindow, aReadOnly) {
let self = this; let self = this;
let object = { let object = {
@ -219,7 +219,7 @@ DataStore.prototype = {
}, },
get readOnly() { get readOnly() {
return self.readOnly; return aReadOnly;
}, },
get: function DS_get(aId) { get: function DS_get(aId) {
@ -242,7 +242,7 @@ DataStore.prototype = {
return self.throwInvalidArg(aWindow); return self.throwInvalidArg(aWindow);
} }
if (self.readOnly) { if (aReadOnly) {
return self.throwReadOnly(aWindow); return self.throwReadOnly(aWindow);
} }
@ -255,7 +255,7 @@ DataStore.prototype = {
}, },
add: function DS_add(aObj) { add: function DS_add(aObj) {
if (self.readOnly) { if (aReadOnly) {
return self.throwReadOnly(aWindow); return self.throwReadOnly(aWindow);
} }
@ -273,7 +273,7 @@ DataStore.prototype = {
return self.throwInvalidArg(aWindow); return self.throwInvalidArg(aWindow);
} }
if (self.readOnly) { if (aReadOnly) {
return self.throwReadOnly(aWindow); return self.throwReadOnly(aWindow);
} }
@ -286,7 +286,7 @@ DataStore.prototype = {
}, },
clear: function DS_clear() { clear: function DS_clear() {
if (self.readOnly) { if (aReadOnly) {
return self.throwReadOnly(aWindow); return self.throwReadOnly(aWindow);
} }
@ -424,3 +424,15 @@ DataStore.prototype = {
this.db.delete(); this.db.delete();
} }
}; };
/* DataStoreAccess */
function DataStoreAccess(aAppId, aName, aOrigin, aReadOnly) {
this.appId = aAppId;
this.name = aName;
this.origin = aOrigin;
this.readOnly = aReadOnly;
}
DataStoreAccess.prototype = {};

View File

@ -47,6 +47,7 @@ function DataStoreService() {
DataStoreService.prototype = { DataStoreService.prototype = {
// Hash of DataStores // Hash of DataStores
stores: {}, stores: {},
accessStores: {},
installDataStore: function(aAppId, aName, aOwner, aReadOnly) { installDataStore: function(aAppId, aName, aOwner, aReadOnly) {
debug('installDataStore - appId: ' + aAppId + ', aName: ' + debug('installDataStore - appId: ' + aAppId + ', aName: ' +
@ -67,15 +68,52 @@ DataStoreService.prototype = {
this.stores[aName][aAppId] = store; this.stores[aName][aAppId] = store;
}, },
installAccessDataStore: function(aAppId, aName, aOwner, aReadOnly) {
debug('installDataStore - appId: ' + aAppId + ', aName: ' +
aName + ', aOwner:' + aOwner + ', aReadOnly: ' +
aReadOnly);
if (aName in this.accessStores && aAppId in this.accessStores[aName]) {
debug('This should not happen');
return;
}
let accessStore = new DataStoreAccess(aAppId, aName, aOwner, aReadOnly);
if (!(aName in this.accessStores)) {
this.accessStores[aName] = {};
}
this.accessStores[aName][aAppId] = accessStore;
},
getDataStores: function(aWindow, aName) { getDataStores: function(aWindow, aName) {
debug('getDataStores - aName: ' + aName); debug('getDataStores - aName: ' + aName);
let appId = aWindow.document.nodePrincipal.appId;
let self = this; let self = this;
return new aWindow.Promise(function(resolver) { return new aWindow.Promise(function(resolver) {
let matchingStores = []; let matchingStores = [];
if (aName in self.stores) { if (aName in self.stores) {
for (let appId in self.stores[aName]) { if (appId in self.stores[aName]) {
matchingStores.push(self.stores[aName][appId]); matchingStores.push({ store: self.stores[aName][appId],
readonly: false });
}
for (var i in self.stores[aName]) {
if (i == appId) {
continue;
}
let access = self.getDataStoreAccess(self.stores[aName][i], appId);
if (!access) {
continue;
}
let readOnly = self.stores[aName][i].readOnly || access.readOnly;
matchingStores.push({ store: self.stores[aName][i],
readonly: readOnly });
} }
} }
@ -88,10 +126,11 @@ DataStoreService.prototype = {
} }
for (let i = 0; i < matchingStores.length; ++i) { for (let i = 0; i < matchingStores.length; ++i) {
let obj = matchingStores[i].exposeObject(aWindow); let obj = matchingStores[i].store.exposeObject(aWindow,
matchingStores[i].readonly);
results.push(obj); results.push(obj);
matchingStores[i].retrieveRevisionId( matchingStores[i].store.retrieveRevisionId(
function() { function() {
--callbackPending; --callbackPending;
if (!callbackPending) { if (!callbackPending) {
@ -106,6 +145,15 @@ DataStoreService.prototype = {
}); });
}, },
getDataStoreAccess: function(aStore, aAppId) {
if (!(aStore.name in this.accessStores) ||
!(aAppId in this.accessStores[aStore.name])) {
return null;
}
return this.accessStores[aStore.name][aAppId];
},
observe: function observe(aSubject, aTopic, aData) { observe: function observe(aSubject, aTopic, aData) {
debug('getDataStores - aTopic: ' + aTopic); debug('getDataStores - aTopic: ' + aTopic);
if (aTopic != 'webapps-clear-data') { if (aTopic != 'webapps-clear-data') {

View File

@ -15,6 +15,11 @@ interface nsIDataStoreService : nsISupports
in DOMString manifestURL, in DOMString manifestURL,
in boolean readOnly); in boolean readOnly);
void installAccessDataStore(in unsigned long appId,
in DOMString name,
in DOMString manifestURL,
in boolean readOnly);
nsISupports getDataStores(in nsIDOMWindow window, nsISupports getDataStores(in nsIDOMWindow window,
in DOMString name); in DOMString name);
}; };

View File

@ -13,11 +13,17 @@ include $(DEPTH)/config/autoconf.mk
MOCHITEST_FILES = \ MOCHITEST_FILES = \
test_app_install.html \ test_app_install.html \
file_app_install.html \
test_readonly.html \ test_readonly.html \
file_readonly.html \
test_basic.html \ test_basic.html \
file_basic.html \
test_revision.html \ test_revision.html \
file_revision.html \
file_app.sjs \ file_app.sjs \
file_app.template.webapp \ file_app.template.webapp \
file_app2.template.webapp \
file_app2.template.webapp^headers^ \
$(NULL) $(NULL)
include $(topsrcdir)/config/rules.mk include $(topsrcdir)/config/rules.mk

View File

@ -2,9 +2,16 @@ var gBasePath = "tests/dom/datastore/tests/";
var gAppTemplatePath = "tests/dom/datastore/tests/file_app.template.webapp"; var gAppTemplatePath = "tests/dom/datastore/tests/file_app.template.webapp";
function handleRequest(request, response) { function handleRequest(request, response) {
var query = getQuery(request);
var testToken = '';
if ('testToken' in query) {
testToken = query.testToken;
}
var template = gBasePath + 'file_app.template.webapp'; var template = gBasePath + 'file_app.template.webapp';
response.setHeader("Content-Type", "application/x-web-app-manifest+json", false); response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
response.write(readTemplate(template)); response.write(readTemplate(template).replace(/TESTTOKEN/g, testToken));
} }
// Copy-pasted incantations. There ought to be a better way to synchronously read // Copy-pasted incantations. There ought to be a better way to synchronously read
@ -35,3 +42,12 @@ function readTemplate(path) {
cis.close(); cis.close();
return data; return data;
} }
function getQuery(request) {
var query = {};
request.queryString.split('&').forEach(function (val) {
var [name, value] = val.split('=');
query[name] = unescape(value);
});
return query;
}

View File

@ -1,9 +1,13 @@
{ {
"name": "Really Rapid Release (hosted)", "name": "Really Rapid Release (hosted)",
"description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.", "description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.",
"launch_path": "/tests/dom/datastore/tests/file_app.sjs", "launch_path": "/tests/dom/datastore/tests/TESTTOKEN",
"icons": { "128": "default_icon" }, "icons": { "128": "default_icon" },
"datastores" : { "datastores-owned" : {
"foo" : { "access": "readwrite", "description" : "This store is called foo" },
"bar" : { "access": "readonly", "description" : "This store is called bar" }
},
"datastores-access" : {
"foo" : { "readonly": false, "description" : "This store is called foo" }, "foo" : { "readonly": false, "description" : "This store is called foo" },
"bar" : { "readonly": true, "description" : "This store is called bar" } "bar" : { "readonly": true, "description" : "This store is called bar" }
} }

View File

@ -0,0 +1,10 @@
{
"name": "Really Rapid Release (hosted) - app 2",
"description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.",
"launch_path": "/tests/dom/datastore/tests/file_readonly.html",
"icons": { "128": "default_icon" },
"datastores-access" : {
"foo" : { "readonly": false, "description" : "This store is called foo" },
"bar" : { "readonly": true, "description" : "This store is called bar" }
}
}

View File

@ -0,0 +1 @@
Content-Type: application/x-web-app-manifest+json

View File

@ -0,0 +1,40 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test for DataStore - install/uninstall apps</title>
<body>
<script type="application/javascript;version=1.7">
function is(a, b, msg) {
alert((a === b ? 'OK' : 'KO') + ' ' + msg)
}
function ok(a, msg) {
alert((a ? 'OK' : 'KO')+ ' ' + msg)
}
function cbError() {
alert('KO error');
}
function finish() {
alert('DONE');
}
navigator.getDataStores('foo').then(function(stores) {
is(stores.length, 1, "getDataStores('foo') returns 1 element");
is(stores[0].name, 'foo', 'The dataStore.name is foo');
ok(stores[0].owner, 'The dataStore.owner exists');
is(stores[0].readOnly, false, 'The dataStore foo is not in readonly');
navigator.getDataStores('bar').then(function(stores) {
is(stores.length, 1, "getDataStores('bar') returns 1 element");
is(stores[0].name, 'bar', 'The dataStore.name is bar');
ok(stores[0].owner, 'The dataStore.owner exists');
is(stores[0].readOnly, false, 'The dataStore bar is in readonly');
finish();
}, cbError);
}, cbError);
</script>
</html>

View File

@ -0,0 +1,185 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test for DataStore - basic operation on a readonly db</title>
</head>
<body>
<div id="container"></div>
<script type="application/javascript;version=1.7">
var gStore;
function is(a, b, msg) {
alert((a === b ? 'OK' : 'KO') + ' ' + msg)
}
function ok(a, msg) {
alert((a ? 'OK' : 'KO')+ ' ' + msg)
}
function cbError() {
alert('KO error');
}
function finish() {
alert('DONE');
}
function testGetDataStores() {
navigator.getDataStores('foo').then(function(stores) {
is(stores.length, 1, "getDataStores('foo') returns 1 element");
is(stores[0].name, 'foo', 'The dataStore.name is foo');
is(stores[0].readOnly, false, 'The dataStore foo is not in readonly');
var store = stores[0];
ok("get" in store, "store.get exists");
ok("update" in store, "store.update exists");
ok("add" in store, "store.add exists");
ok("remove" in store, "store.remove exists");
ok("clear" in store, "store.clear exists");
gStore = stores[0];
runTest();
}, cbError);
}
function testStoreErrorGet(id) {
gStore.get(id).then(function(what) {
ok(false, "store.get(" + id + ") retrieves data");
}, function(error) {
ok(true, "store.get() failed properly because the id is non-valid");
ok(error instanceof DOMError, "error is a DOMError");
is(error.name, "SyntaxError", "Error is a syntax error");
}).then(runTest, cbError);
}
function testStoreErrorUpdate(id) {
gStore.update(id, "foo").then(function(what) {
ok(false, "store.update(" + id + ") retrieves data");
}, function(error) {
ok(true, "store.update() failed properly because the id is non-valid");
ok(error instanceof DOMError, "error is a DOMError");
is(error.name, "SyntaxError", "Error is a syntax error");
}).then(runTest, cbError);
}
function testStoreErrorRemove(id) {
gStore.remove(id).then(function(what) {
ok(false, "store.remove(" + id + ") retrieves data");
}, function(error) {
ok(true, "store.remove() failed properly because the id is non-valid");
ok(error instanceof DOMError, "error is a DOMError");
is(error.name, "SyntaxError", "Error is a syntax error");
}).then(runTest, cbError);
}
function testStoreGet(id, value) {
gStore.get(id).then(function(what) {
ok(true, "store.get() retrieves data");
is(what, value, "store.get(" + id + ") returns " + value);
}, function() {
ok(false, "store.get(" + id + ") retrieves data");
}).then(runTest, cbError);
}
function testStoreAdd(value) {
return gStore.add(value).then(function(what) {
ok(true, "store.add() is called");
ok(what > 0, "store.add() returns something");
return what;
}, cbError);
}
function testStoreUpdate(id, value) {
return gStore.update(id, value).then(function() {
ok(true, "store.update() is called");
}, cbError);
}
function testStoreRemove(id) {
return gStore.remove(id).then(function() {
ok(true, "store.remove() is called");
}, cbError);
}
function testStoreClear() {
return gStore.clear().then(function() {
ok(true, "store.clear() is called");
}, cbError);
}
var tests = [
// Test for GetDataStore
testGetDataStores,
// Broken ID
function() { testStoreErrorGet('hello world'); },
function() { testStoreErrorGet(true); },
function() { testStoreErrorGet(null); },
// Unknown ID
function() { testStoreGet(42, undefined); },
function() { testStoreGet(42, undefined); }, // twice
// Add + Get - number
function() { testStoreAdd(42).then(function(id) {
gId = id; runTest(); }, cbError); },
function() { testStoreGet(gId, 42); },
function() { testStoreGet(gId + "", 42); },
// Add + Get - boolean
function() { testStoreAdd(true).then(function(id) {
gId = id; runTest(); }, cbError); },
function() { testStoreGet(gId, true); },
// Add + Get - string
function() { testStoreAdd("hello world").then(function(id) {
gId = id; runTest(); }, cbError); },
function() { testStoreGet(gId, "hello world"); },
// Broken update
function() { testStoreErrorUpdate('hello world'); },
function() { testStoreErrorUpdate(true); },
function() { testStoreErrorUpdate(null); },
// Update + Get - string
function() { testStoreUpdate(gId, "hello world 2").then(function() {
runTest(); }, cbError); },
function() { testStoreGet(gId, "hello world 2"); },
// Broken remove
function() { testStoreErrorRemove('hello world'); },
function() { testStoreErrorRemove(true); },
function() { testStoreErrorRemove(null); },
// Remove
function() { testStoreRemove(gId).then(function(what) {
runTest(); }, cbError); },
function() { testStoreGet(gId).catch(function() {
runTest(); }); },
// Remove - wrong ID
function() { testStoreRemove(gId).then(function(what) {
runTest(); }, cbError); },
// Clear
function() { testStoreClear().then(function(what) {
runTest(); }, cbError); },
];
function runTest() {
if (!tests.length) {
finish();
return;
}
var test = tests.shift();
test();
}
runTest();
</script>
</body>
</html>

View File

@ -0,0 +1,72 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test for DataStore - basic operation on a readonly db</title>
</head>
<body>
<div id="container"></div>
<script type="application/javascript;version=1.7">
function is(a, b, msg) {
dump((a === b ? 'OK' : 'KO') + ' ' + msg + "\n")
alert((a === b ? 'OK' : 'KO') + ' ' + msg)
}
function ok(a, msg) {
alert((a ? 'OK' : 'KO')+ ' ' + msg)
}
function cbError() {
alert('KO error');
}
function finish() {
alert('DONE');
}
function runTest() {
navigator.getDataStores('bar').then(function(stores) {
is(stores.length, 1, "getDataStores('bar') returns 1 element");
is(stores[0].name, 'bar', 'The dataStore.name is bar');
is(stores[0].readOnly, true, 'The dataStore bar is eadonly');
var store = stores[0];
ok("get" in store, "store.get exists");
ok("update" in store, "store.update exists");
ok("add" in store, "store.add exists");
ok("remove" in store, "store.remove exists");
ok("clear" in store, "store.clear exists");
var f = store.clear();
f = f.then(cbError, function() {
ok(true, "store.clear() fails because the db is readonly");
return store.remove(123);
});
f = f.then(cbError, function() {
ok(true, "store.remove() fails because the db is readonly");
return store.add(123, true);
});
f = f.then(cbError, function() {
ok(true, "store.add() fails because the db is readonly");
return store.update(123, {});
})
f = f.then(cbError, function() {
ok(true, "store.update() fails because the db is readonly");
})
f.then(function() {
// All done.
ok(true, "All done");
finish();
});
}, cbError);
}
runTest();
</script>
</body>
</html>

View File

@ -0,0 +1,196 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test for DataStore - basic operation on a readonly db</title>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
<script type="application/javascript;version=1.7">
var gStore;
var gPreviousRevisionId = '';
function is(a, b, msg) {
alert((a === b ? 'OK' : 'KO') + ' ' + msg)
}
function isnot(a, b, msg) {
alert((a !== b ? 'OK' : 'KO') + ' ' + msg)
}
function ok(a, msg) {
alert((a ? 'OK' : 'KO')+ ' ' + msg)
}
function cbError() {
alert('KO error');
}
function finish() {
alert('DONE');
}
function testGetDataStores() {
navigator.getDataStores('foo').then(function(stores) {
is(stores.length, 1, "getDataStores('foo') returns 1 element");
is(stores[0].name, 'foo', 'The dataStore.name is foo');
is(stores[0].readOnly, false, 'The dataStore foo is not in readonly');
gStore = stores[0];
runTest();
}, cbError);
}
function testStoreAdd(value, expectedId) {
return gStore.add(value).then(function(id) {
is(id, expectedId, "store.add() is called");
runTest();
}, cbError);
}
function testStoreUpdate(id, value) {
return gStore.update(id, value).then(function(retId) {
is(id, retId, "store.update() is called with the right id");
runTest();
}, cbError);
}
function testStoreRemove(id, expectedSuccess) {
return gStore.remove(id).then(function(success) {
is(success, expectedSuccess, "store.remove() returns the right value");
runTest();
}, cbError);
}
function testStoreRevisionId() {
is(/[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}/.test(gStore.revisionId), true, "store.revisionId returns something");
runTest();
}
function testStoreWrongRevisions(id) {
return gStore.getChanges(id).then(
function(what) {
is(what, undefined, "Wrong revisionId == undefined object");
runTest();
}, cbError);
}
function testStoreRevisions(id, changes) {
return gStore.getChanges(id).then(function(what) {
is(JSON.stringify(changes.addedIds),
JSON.stringify(what.addedIds), "store.revisions - addedIds: " +
JSON.stringify(what.addedIds) + " | " + JSON.stringify(changes.addedIds));
is(JSON.stringify(changes.updatedIds),
JSON.stringify(what.updatedIds), "store.revisions - updatedIds: " +
JSON.stringify(what.updatedIds) + " | " + JSON.stringify(changes.updatedIds));
is(JSON.stringify(changes.removedIds),
JSON.stringify(what.removedIds), "store.revisions - removedIds: " +
JSON.stringify(what.removedIds) + " | " + JSON.stringify(changes.removedIds));
runTest();
}, cbError);
}
function testStoreRevisionIdChanged() {
isnot(gStore.revisionId, gPreviousRevisionId, "Revision changed");
gPreviousRevisionId = gStore.revisionId;
runTest();
}
function testStoreRevisionIdNotChanged() {
is(gStore.revisionId, gPreviousRevisionId, "Revision changed");
runTest();
}
var revisions = [];
var tests = [
// Test for GetDataStore
testGetDataStores,
// The first revision is not empty
testStoreRevisionIdChanged,
// wrong revision ID
function() { testStoreWrongRevisions('foobar'); },
// Add
function() { testStoreAdd({ number: 42 }, 1); },
function() { revisions.push(gStore.revisionId); testStoreRevisionId(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Add
function() { testStoreAdd({ number: 42 }, 2); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Add
function() { testStoreAdd({ number: 42 }, 3); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2,3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Update
function() { testStoreUpdate(3, { number: 43 }); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2,3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [3], removedIds: [] }); },
function() { testStoreRevisions(revisions[3], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Update
function() { testStoreUpdate(3, { number: 42 }); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2,3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [3], removedIds: [] }); },
function() { testStoreRevisions(revisions[3], { addedIds: [], updatedIds: [3], removedIds: [] }); },
function() { testStoreRevisions(revisions[4], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Remove
function() { testStoreRemove(3, true); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [], removedIds: [3] }); },
function() { testStoreRevisions(revisions[3], { addedIds: [], updatedIds: [], removedIds: [3] }); },
function() { testStoreRevisions(revisions[4], { addedIds: [], updatedIds: [], removedIds: [3] }); },
function() { testStoreRevisions(revisions[5], { addedIds: [], updatedIds: [], removedIds: [] }); },
function() { testStoreRemove(3, false); },
testStoreRevisionIdNotChanged,
// Remove
function() { testStoreRemove(42, false); },
testStoreRevisionIdNotChanged,
];
function runTest() {
if (!tests.length) {
finish();
return;
}
var test = tests.shift();
test();
}
runTest();
</script>
</pre>
</body>
</html>

View File

@ -13,7 +13,7 @@
SimpleTest.waitForExplicitFinish(); SimpleTest.waitForExplicitFinish();
var gBaseURL = 'http://test/tests/dom/datastore/tests/'; var gBaseURL = 'http://test/tests/dom/datastore/tests/';
var gHostedManifestURL = gBaseURL + 'file_app.sjs'; var gHostedManifestURL = gBaseURL + 'file_app.sjs?testToken=file_app_install.html';
var gGenerator = runTest(); var gGenerator = runTest();
SpecialPowers.pushPermissions( SpecialPowers.pushPermissions(
@ -23,7 +23,8 @@
function() { gGenerator.next() }); function() { gGenerator.next() });
function continueTest() { function continueTest() {
gGenerator.next(); try { gGenerator.next(); }
catch(e) { dump("Got exception: " + e + "\n"); }
} }
function cbError() { function cbError() {
@ -40,6 +41,8 @@
}, cbError); }, cbError);
yield undefined; yield undefined;
SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true);
SpecialPowers.autoConfirmAppInstall(continueTest); SpecialPowers.autoConfirmAppInstall(continueTest);
yield undefined; yield undefined;
@ -53,42 +56,43 @@
is(app.manifest.description, "Updated even faster than Firefox, just to annoy slashdotters.", is(app.manifest.description, "Updated even faster than Firefox, just to annoy slashdotters.",
"Manifest is HTML-sanitized"); "Manifest is HTML-sanitized");
navigator.getDataStores('foo').then(function(stores) { var ifr = document.createElement('iframe');
is(stores.length, 1, "getDataStores('foo') returns 1 element"); ifr.setAttribute('mozbrowser', 'true');
is(stores[0].name, 'foo', 'The dataStore.name is foo'); ifr.setAttribute('mozapp', app.manifestURL);
is(stores[0].owner, 'http://test/tests/dom/datastore/tests/file_app.sjs', 'The dataStore.owner exists'); ifr.setAttribute('src', app.manifest.launch_path);
is(stores[0].readOnly, false, 'The dataStore foo is not in readonly'); var domParent = document.getElementById('container');
continueTest();
}, cbError);
yield undefined;
navigator.getDataStores('bar').then(function(stores) { // Set us up to listen for messages from the app.
is(stores.length, 1, "getDataStores('bar') returns 1 element"); var listener = function(e) {
is(stores[0].name, 'bar', 'The dataStore.name is bar'); var message = e.detail.message;
is(stores[0].owner, 'http://test/tests/dom/datastore/tests/file_app.sjs', 'The dataStore.owner exists'); if (/^OK/.exec(message)) {
is(stores[0].readOnly, true, 'The dataStore bar is in readonly'); ok(true, "Message from app: " + message);
continueTest(); } else if (/KO/.exec(message)) {
}, cbError); ok(false, "Message from app: " + message);
yield undefined; } else if (/DONE/.exec(message)) {
ok(true, "Messaging from app complete");
ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
domParent.removeChild(ifr);
// Uninstall the app. // Uninstall the app.
request = navigator.mozApps.mgmt.uninstall(app); request = navigator.mozApps.mgmt.uninstall(app);
request.onerror = cbError; request.onerror = cbError;
request.onsuccess = continueTest; request.onsuccess = function() {
yield undefined; // All done.
ok(true, "All done");
finish();
}
}
}
navigator.getDataStores('foo').then(function(stores) { // This event is triggered when the app calls "alert".
is(stores.length, 0, "getDataStores('foo') returns 0 elements"); ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
continueTest();
}, cbError);
yield undefined;
// All done. domParent.appendChild(ifr);
info("All done");
finish();
} }
function finish() { function finish() {
SpecialPowers.clearUserPref("dom.mozBrowserFramesEnabled");
SimpleTest.finish(); SimpleTest.finish();
} }

View File

@ -11,9 +11,8 @@
<script type="application/javascript;version=1.7"> <script type="application/javascript;version=1.7">
var gBaseURL = 'http://test/tests/dom/datastore/tests/'; var gBaseURL = 'http://test/tests/dom/datastore/tests/';
var gHostedManifestURL = gBaseURL + 'file_app.sjs'; var gHostedManifestURL = gBaseURL + 'file_app.sjs?testToken=file_basic.html';
var gApp; var gApp;
var gStore;
function cbError() { function cbError() {
ok(false, "Error callback invoked"); ok(false, "Error callback invoked");
@ -29,94 +28,9 @@
} }
} }
function testGetDataStores() {
navigator.getDataStores('foo').then(function(stores) {
is(stores.length, 1, "getDataStores('foo') returns 1 element");
is(stores[0].name, 'foo', 'The dataStore.name is foo');
is(stores[0].readOnly, false, 'The dataStore foo is not in readonly');
var store = stores[0];
ok("get" in store, "store.get exists");
ok("update" in store, "store.update exists");
ok("add" in store, "store.add exists");
ok("remove" in store, "store.remove exists");
ok("clear" in store, "store.clear exists");
gStore = stores[0];
runTest();
}, cbError);
}
function testStoreErrorGet(id) {
gStore.get(id).then(function(what) {
ok(false, "store.get(" + id + ") retrieves data");
}, function(error) {
ok(true, "store.get() failed properly because the id is non-valid");
ok(error instanceof DOMError, "error is a DOMError");
is(error.name, "SyntaxError", "Error is a syntax error");
}).then(runTest, cbError);
}
function testStoreErrorUpdate(id) {
gStore.update(id, "foo").then(function(what) {
ok(false, "store.update(" + id + ") retrieves data");
}, function(error) {
ok(true, "store.update() failed properly because the id is non-valid");
ok(error instanceof DOMError, "error is a DOMError");
is(error.name, "SyntaxError", "Error is a syntax error");
}).then(runTest, cbError);
}
function testStoreErrorRemove(id) {
gStore.remove(id).then(function(what) {
ok(false, "store.remove(" + id + ") retrieves data");
}, function(error) {
ok(true, "store.remove() failed properly because the id is non-valid");
ok(error instanceof DOMError, "error is a DOMError");
is(error.name, "SyntaxError", "Error is a syntax error");
}).then(runTest, cbError);
}
function testStoreGet(id, value) {
gStore.get(id).then(function(what) {
ok(true, "store.get() retrieves data");
is(what, value, "store.get(" + id + ") returns " + value);
}, function() {
ok(false, "store.get(" + id + ") retrieves data");
}).then(runTest, cbError);
}
function testStoreAdd(value) {
return gStore.add(value).then(function(what) {
ok(true, "store.add() is called");
ok(what > 0, "store.add() returns something");
return what;
}, cbError);
}
function testStoreUpdate(id, value) {
return gStore.update(id, value).then(function(what) {
ok(true, "store.update() is called");
is(id, what, "store.update(" + id + ") updates the correct id");
}, cbError);
}
function testStoreRemove(id) {
return gStore.remove(id).then(function() {
ok(true, "store.remove() is called");
}, cbError);
}
function testStoreClear() {
return gStore.clear().then(function() {
ok(true, "store.clear() is called");
}, cbError);
}
function uninstallApp() { function uninstallApp() {
// Uninstall the app. // Uninstall the app.
request = navigator.mozApps.mgmt.uninstall(gApp); var request = navigator.mozApps.mgmt.uninstall(gApp);
request.onerror = cbError; request.onerror = cbError;
request.onsuccess = function() { request.onsuccess = function() {
// All done. // All done.
@ -125,6 +39,33 @@
} }
} }
function testApp() {
var ifr = document.createElement('iframe');
ifr.setAttribute('mozbrowser', 'true');
ifr.setAttribute('mozapp', gApp.manifestURL);
ifr.setAttribute('src', gApp.manifest.launch_path);
var domParent = document.getElementById('container');
// Set us up to listen for messages from the app.
var listener = function(e) {
var message = e.detail.message;
if (/^OK/.exec(message)) {
ok(true, "Message from app: " + message);
} else if (/KO/.exec(message)) {
ok(false, "Message from app: " + message);
} else if (/DONE/.exec(message)) {
ok(true, "Messaging from app complete");
ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
domParent.removeChild(ifr);
runTest();
}
}
// This event is triggered when the app calls "alert".
ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
domParent.appendChild(ifr);
}
var tests = [ var tests = [
// Permissions // Permissions
function() { function() {
@ -139,6 +80,11 @@
SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true]]}, runTest); SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true]]}, runTest);
}, },
function() {
SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true);
runTest();
},
// No confirmation needed when an app is installed // No confirmation needed when an app is installed
function() { function() {
SpecialPowers.autoConfirmAppInstall(runTest); SpecialPowers.autoConfirmAppInstall(runTest);
@ -147,62 +93,8 @@
// Installing the app // Installing the app
installApp, installApp,
// Test for GetDataStore // Run tests in app
testGetDataStores, testApp,
// Broken ID
function() { testStoreErrorGet('hello world'); },
function() { testStoreErrorGet(true); },
function() { testStoreErrorGet(null); },
// Unknown ID
function() { testStoreGet(42, undefined); },
function() { testStoreGet(42, undefined); }, // twice
// Add + Get - number
function() { testStoreAdd(42).then(function(id) {
gId = id; runTest(); }, cbError); },
function() { testStoreGet(gId, 42); },
function() { testStoreGet(gId+"", 42); },
// Add + Get - boolean
function() { testStoreAdd(true).then(function(id) {
gId = id; runTest(); }, cbError); },
function() { testStoreGet(gId, true); },
// Add + Get - string
function() { testStoreAdd("hello world").then(function(id) {
gId = id; runTest(); }, cbError); },
function() { testStoreGet(gId, "hello world"); },
// Broken update
function() { testStoreErrorUpdate('hello world'); },
function() { testStoreErrorUpdate(true); },
function() { testStoreErrorUpdate(null); },
// Update + Get - string
function() { testStoreUpdate(gId, "hello world 2").then(function() {
runTest(); }, cbError); },
function() { testStoreGet(gId, "hello world 2"); },
// Broken remove
function() { testStoreErrorRemove('hello world'); },
function() { testStoreErrorRemove(true); },
function() { testStoreErrorRemove(null); },
// Remove
function() { testStoreRemove(gId).then(function(what) {
runTest(); }, cbError); },
function() { testStoreGet(gId).catch(function() {
runTest(); }); },
// Remove - wrong ID
function() { testStoreRemove(gId).then(function(what) {
runTest(); }, cbError); },
// Clear
function() { testStoreClear().then(function(what) {
runTest(); }, cbError); },
// Uninstall the app // Uninstall the app
uninstallApp uninstallApp

View File

@ -10,7 +10,8 @@
<div id="container"></div> <div id="container"></div>
<script type="application/javascript;version=1.7"> <script type="application/javascript;version=1.7">
var gBaseURL = 'http://test/tests/dom/datastore/tests/'; var gBaseURL = 'http://test/tests/dom/datastore/tests/';
var gHostedManifestURL = gBaseURL + 'file_app.sjs'; var gHostedManifestURL = gBaseURL + 'file_app.sjs?testToken=file_readonly.html';
var gHostedManifestURL2 = 'http://example.com/tests/dom/datastore/tests/file_app2.template.webapp';
var gGenerator = runTest(); var gGenerator = runTest();
SpecialPowers.pushPermissions( SpecialPowers.pushPermissions(
@ -30,6 +31,8 @@
} }
function runTest() { function runTest() {
SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true);
SpecialPowers.autoConfirmAppInstall(continueTest); SpecialPowers.autoConfirmAppInstall(continueTest);
yield undefined; yield undefined;
@ -40,52 +43,55 @@
var app = request.result; var app = request.result;
navigator.getDataStores('bar').then(function(stores) { request = navigator.mozApps.install(gHostedManifestURL2);
is(stores.length, 1, "getDataStores('bar') returns 1 element"); request.onerror = cbError;
is(stores[0].name, 'bar', 'The dataStore.name is bar'); request.onsuccess = continueTest;
is(stores[0].readOnly, true, 'The dataStore bar is readonly'); yield undefined;
var store = stores[0]; var app2 = request.result;
ok("get" in store, "store.get exists");
ok("update" in store, "store.update exists");
ok("add" in store, "store.add exists");
ok("remove" in store, "store.remove exists");
ok("clear" in store, "store.clear exists");
var f = store.clear(); var ifr = document.createElement('iframe');
f = f.then(cbError, function() { ifr.setAttribute('mozbrowser', 'true');
ok(true, "store.clear() fails because the db is readonly"); ifr.setAttribute('mozapp', app2.manifestURL);
return store.remove(123); ifr.setAttribute('src', 'http://example.com/tests/dom/datastore/tests/file_readonly.html');
}); var domParent = document.getElementById('container');
f = f.then(cbError, function() { // Set us up to listen for messages from the app.
ok(true, "store.remove() fails because the db is readonly"); var listener = function(e) {
return store.add(123, true); var message = e.detail.message;
}); if (/^OK/.exec(message)) {
ok(true, "Message from app: " + message);
} else if (/KO/.exec(message)) {
ok(false, "Message from app: " + message);
} else if (/DONE/.exec(message)) {
ok(true, "Messaging from app complete");
ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
domParent.removeChild(ifr);
f = f.then(cbError, function() {
ok(true, "store.add() fails because the db is readonly");
return store.update(123, {});
})
f = f.then(cbError, function() {
ok(true, "store.update() fails because the db is readonly");
})
f.then(function() {
// Uninstall the app. // Uninstall the app.
request = navigator.mozApps.mgmt.uninstall(app); request = navigator.mozApps.mgmt.uninstall(app);
request.onerror = cbError; request.onerror = cbError;
request.onsuccess = function() { request.onsuccess = function() {
// All done. // Uninstall app2
info("All done"); request = navigator.mozApps.mgmt.uninstall(app2);
finish(); request.onerror = cbError;
request.onsuccess = function() {
// All done.
info("All done");
finish();
}
} }
}); }
}, cbError); }
// This event is triggered when the app calls "alert".
ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
domParent.appendChild(ifr);
} }
function finish() { function finish() {
SpecialPowers.clearUserPref("dom.mozBrowserFramesEnabled");
SimpleTest.finish(); SimpleTest.finish();
} }

View File

@ -15,7 +15,7 @@
<script type="application/javascript;version=1.7"> <script type="application/javascript;version=1.7">
var gBaseURL = 'http://test/tests/dom/datastore/tests/'; var gBaseURL = 'http://test/tests/dom/datastore/tests/';
var gHostedManifestURL = gBaseURL + 'file_app.sjs'; var gHostedManifestURL = gBaseURL + 'file_app.sjs?testToken=file_revision.html';
var gApp; var gApp;
var gStore; var gStore;
var gPreviousRevisionId = ''; var gPreviousRevisionId = '';
@ -34,70 +34,9 @@
} }
} }
function testGetDataStores() {
navigator.getDataStores('foo').then(function(stores) {
is(stores.length, 1, "getDataStores('foo') returns 1 element");
is(stores[0].name, 'foo', 'The dataStore.name is foo');
is(stores[0].readOnly, false, 'The dataStore foo is not in readonly');
gStore = stores[0];
runTest();
}, cbError);
}
function testStoreAdd(value, expectedId) {
return gStore.add(value).then(function(id) {
is(id, expectedId, "store.add() is called");
runTest();
}, cbError);
}
function testStoreUpdate(id, value) {
return gStore.update(id, value).then(function(retId) {
is(id, retId, "store.update() is called with the right id");
runTest();
}, cbError);
}
function testStoreRemove(id, expectedSuccess) {
return gStore.remove(id).then(function(success) {
is(success, expectedSuccess, "store.remove() returns the right value");
runTest();
}, cbError);
}
function testStoreRevisionId() {
is(/[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}/.test(gStore.revisionId), true, "store.revisionId returns something");
runTest();
}
function testStoreWrongRevisions(id) {
return gStore.getChanges(id).then(
function(what) {
is(what, undefined, "Wrong revisionId == undefined object");
runTest();
}, cbError);
}
function testStoreRevisions(id, changes) {
return gStore.getChanges(id).then(function(what) {
is(JSON.stringify(changes.addedIds),
JSON.stringify(what.addedIds), "store.revisions - addedIds: " +
JSON.stringify(what.addedIds) + " | " + JSON.stringify(changes.addedIds));
is(JSON.stringify(changes.updatedIds),
JSON.stringify(what.updatedIds), "store.revisions - updatedIds: " +
JSON.stringify(what.updatedIds) + " | " + JSON.stringify(changes.updatedIds));
is(JSON.stringify(changes.removedIds),
JSON.stringify(what.removedIds), "store.revisions - removedIds: " +
JSON.stringify(what.removedIds) + " | " + JSON.stringify(changes.removedIds));
runTest();
}, cbError);
}
function uninstallApp() { function uninstallApp() {
// Uninstall the app. // Uninstall the app.
request = navigator.mozApps.mgmt.uninstall(gApp); var request = navigator.mozApps.mgmt.uninstall(gApp);
request.onerror = cbError; request.onerror = cbError;
request.onsuccess = function() { request.onsuccess = function() {
// All done. // All done.
@ -106,15 +45,31 @@
} }
} }
function testStoreRevisionIdChanged() { function testApp() {
isnot(gStore.revisionId, gPreviousRevisionId, "Revision changed"); var ifr = document.createElement('iframe');
gPreviousRevisionId = gStore.revisionId; ifr.setAttribute('mozbrowser', 'true');
runTest(); ifr.setAttribute('mozapp', gApp.manifestURL);
} ifr.setAttribute('src', gApp.manifest.launch_path);
var domParent = document.getElementById('content');
function testStoreRevisionIdNotChanged() { // Set us up to listen for messages from the app.
is(gStore.revisionId, gPreviousRevisionId, "Revision changed"); var listener = function(e) {
runTest(); var message = e.detail.message;
if (/^OK/.exec(message)) {
ok(true, "Message from app: " + message);
} else if (/KO/.exec(message)) {
ok(false, "Message from app: " + message);
} else if (/DONE/.exec(message)) {
ok(true, "Messaging from app complete");
ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
domParent.removeChild(ifr);
runTest();
}
}
// This event is triggered when the app calls "alert".
ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
domParent.appendChild(ifr);
} }
var revisions = []; var revisions = [];
@ -133,6 +88,11 @@
SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true]]}, runTest); SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true]]}, runTest);
}, },
// Enabling mozBrowser
function() {
SpecialPowers.pushPrefEnv({"set": [["dom.mozBrowserFramesEnabled", true]]}, runTest);
},
// No confirmation needed when an app is installed // No confirmation needed when an app is installed
function() { function() {
SpecialPowers.autoConfirmAppInstall(runTest); SpecialPowers.autoConfirmAppInstall(runTest);
@ -141,72 +101,8 @@
// Installing the app // Installing the app
installApp, installApp,
// Test for GetDataStore // Run tests in app
testGetDataStores, testApp,
// The first revision is not empty
testStoreRevisionIdChanged,
// wrong revision ID
function() { testStoreWrongRevisions('foobar'); },
// Add
function() { testStoreAdd({ number: 42 }, 1); },
function() { revisions.push(gStore.revisionId); testStoreRevisionId(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Add
function() { testStoreAdd({ number: 42 }, 2); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Add
function() { testStoreAdd({ number: 42 }, 3); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2,3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Update
function() { testStoreUpdate(3, { number: 43 }); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2,3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [3], removedIds: [] }); },
function() { testStoreRevisions(revisions[3], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Update
function() { testStoreUpdate(3, { number: 42 }); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2,3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [3], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [3], removedIds: [] }); },
function() { testStoreRevisions(revisions[3], { addedIds: [], updatedIds: [3], removedIds: [] }); },
function() { testStoreRevisions(revisions[4], { addedIds: [], updatedIds: [], removedIds: [] }); },
// Remove
function() { testStoreRemove(3, true); },
function() { revisions.push(gStore.revisionId); runTest(); },
testStoreRevisionIdChanged,
function() { testStoreRevisions(revisions[0], { addedIds: [2], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[1], { addedIds: [], updatedIds: [], removedIds: [] }); },
function() { testStoreRevisions(revisions[2], { addedIds: [], updatedIds: [], removedIds: [3] }); },
function() { testStoreRevisions(revisions[3], { addedIds: [], updatedIds: [], removedIds: [3] }); },
function() { testStoreRevisions(revisions[4], { addedIds: [], updatedIds: [], removedIds: [3] }); },
function() { testStoreRevisions(revisions[5], { addedIds: [], updatedIds: [], removedIds: [] }); },
function() { testStoreRemove(3, false); },
testStoreRevisionIdNotChanged,
// Remove
function() { testStoreRemove(42, false); },
testStoreRevisionIdNotChanged,
// Uninstall the app // Uninstall the app
uninstallApp uninstallApp