Bug 871445 - patch 2 - DataStore: basic functions, r=mounir, r=bent
☠☠ backed out by db83498e31f0 ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 11 Sep 2013 15:47:49 +0200
changeset 154541 51c0d52303062436f39440ccee6fc28d244c8f82
parent 154540 76c9069bdb56ce4619e8607fc3e5f596291185a4
child 154542 ec3382ceef99f527916f270e95ba0a58af8ddbe2
push id4254
push userakeybl@mozilla.com
push dateTue, 17 Sep 2013 14:18:33 +0000
treeherdermozilla-aurora@9edd56e694b0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmounir, bent
bugs871445
milestone26.0a1
Bug 871445 - patch 2 - DataStore: basic functions, r=mounir, r=bent
dom/datastore/DataStore.jsm
dom/datastore/DataStoreDB.jsm
dom/datastore/DataStoreService.js
dom/datastore/moz.build
dom/datastore/tests/Makefile.in
dom/datastore/tests/test_app_install.html
dom/datastore/tests/test_basic.html
dom/datastore/tests/test_readonly.html
--- a/dom/datastore/DataStore.jsm
+++ b/dom/datastore/DataStore.jsm
@@ -3,61 +3,257 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 'use strict'
 
 var EXPORTED_SYMBOLS = ["DataStore"];
 
+function debug(s) {
+  // dump('DEBUG DataStore: ' + s + '\n');
+}
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/DataStoreDB.jsm");
+Cu.import("resource://gre/modules/ObjectWrapper.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+
 /* DataStore object */
 
-function DataStore(aAppId, aName, aOwner, aReadOnly) {
+function DataStore(aAppId, aName, aOwner, aReadOnly, aGlobalScope) {
   this.appId = aAppId;
   this.name = aName;
   this.owner = aOwner;
   this.readOnly = aReadOnly;
+
+  this.db = new DataStoreDB();
+  this.db.init(aOwner, aName, aGlobalScope);
 }
 
 DataStore.prototype = {
   appId: null,
   name: null,
   owner: null,
   readOnly: null,
 
+  newDBPromise: function(aWindow, aTxnType, aFunction) {
+    let db = this.db;
+    return new aWindow.Promise(function(resolver) {
+      debug("DBPromise started");
+      db.txn(
+        aTxnType,
+        function(aTxn, aStore) {
+          debug("DBPromise success");
+          aFunction(resolver, aTxn, aStore);
+        },
+        function() {
+          debug("DBPromise error");
+          resolver.reject(new aWindow.DOMError("InvalidStateError"));
+        }
+      );
+    });
+  },
+
+  getInternal: function(aWindow, aResolver, aStore, aId) {
+    debug("GetInternal " + aId);
+
+    let request = aStore.get(aId);
+    request.onsuccess = function(aEvent) {
+      debug("GetInternal success. Record: " + aEvent.target.result);
+      aResolver.resolve(ObjectWrapper.wrap(aEvent.target.result, aWindow));
+    };
+
+    request.onerror = function(aEvent) {
+      debug("GetInternal error");
+      aResolver.reject(new aWindow.DOMError(aEvent.target.error.name));
+    };
+  },
+
+  updateInternal: function(aWindow, aResolver, aStore, aId, aObj) {
+    debug("UpdateInternal " + aId);
+
+    let request = aStore.put(aObj, aId);
+    request.onsuccess = function(aEvent) {
+      debug("UpdateInternal success");
+      // No wrap here because the result is always a int.
+      aResolver.resolve(aEvent.target.result);
+    };
+    request.onerror = function(aEvent) {
+      debug("UpdateInternal error");
+      aResolver.reject(new aWindow.DOMError(aEvent.target.error.name));
+    };
+  },
+
+  addInternal: function(aWindow, aResolver, aStore, aObj) {
+    debug("AddInternal");
+
+    let request = aStore.put(aObj);
+    request.onsuccess = function(aEvent) {
+      debug("Request successful. Id: " + aEvent.target.result);
+      // No wrap here because the result is always a int.
+      aResolver.resolve(aEvent.target.result);
+    };
+    request.onerror = function(aEvent) {
+      debug("AddInternal error");
+      aResolver.reject(new aWindow.DOMError(aEvent.target.error.name));
+    };
+  },
+
+  removeInternal: function(aResolver, aStore, aId) {
+    debug("RemoveInternal");
+
+    let request = aStore.delete(aId);
+    request.onsuccess = function() {
+      debug("RemoveInternal success");
+      aResolver.resolve();
+    };
+    request.onerror = function(aEvent) {
+      debug("RemoveInternal error");
+      aResolver.reject(new aWindow.DOMError(aEvent.target.error.name));
+    };
+  },
+
+  clearInternal: function(aResolver, aStore) {
+    debug("ClearInternal");
+
+    let request = aStore.clear();
+    request.onsuccess = function() {
+      debug("ClearInternal success");
+      aResolver.resolve();
+    };
+    request.onerror = function(aEvent) {
+      debug("ClearInternal error");
+      aResolver.reject(new aWindow.DOMError(aEvent.target.error.name));
+    };
+  },
+
+  throwInvalidArg: function(aWindow) {
+    return aWindow.Promise.reject(
+      new aWindow.DOMError("SyntaxError", "Non-numeric or invalid id"));
+  },
+
+  throwReadOnly: function(aWindow) {
+    return aWindow.Promise.reject(
+      new aWindow.DOMError("ReadOnlyError", "DataStore in readonly mode"));
+  },
+
   exposeObject: function(aWindow) {
     let self = this;
-    let chromeObject = {
+    let object = {
+
+      // Public interface :
+
       get name() {
         return self.name;
       },
 
       get owner() {
         return self.owner;
       },
 
       get readOnly() {
         return self.readOnly;
       },
 
+      get: function DS_get(aId) {
+        aId = parseInt(aId);
+        if (isNaN(aId) || aId <= 0) {
+          return self.throwInvalidArg(aWindow);
+        }
+
+        // Promise<Object>
+        return self.newDBPromise(aWindow, "readonly",
+          function(aResolver, aTxn, aStore) {
+            self.getInternal(aWindow, aResolver, aStore, aId);
+          }
+        );
+      },
+
+      update: function DS_update(aId, aObj) {
+        aId = parseInt(aId);
+        if (isNaN(aId) || aId <= 0) {
+          return self.throwInvalidArg(aWindow);
+        }
+
+        if (self.readOnly) {
+          return self.throwReadOnly(aWindow);
+        }
+
+        // Promise<void>
+        return self.newDBPromise(aWindow, "readwrite",
+          function(aResolver, aTxn, aStore) {
+            self.updateInternal(aWindow, aResolver, aStore, aId, aObj);
+          }
+        );
+      },
+
+      add: function DS_add(aObj) {
+        if (self.readOnly) {
+          return self.throwReadOnly(aWindow);
+        }
+
+        // Promise<int>
+        return self.newDBPromise(aWindow, "readwrite",
+          function(aResolver, aTxn, aStore) {
+            self.addInternal(aWindow, aResolver, aStore, aObj);
+          }
+        );
+      },
+
+      remove: function DS_remove(aId) {
+        aId = parseInt(aId);
+        if (isNaN(aId) || aId <= 0) {
+          return self.throwInvalidArg(aWindow);
+        }
+
+        if (self.readOnly) {
+          return self.throwReadOnly(aWindow);
+        }
+
+        // Promise<void>
+        return self.newDBPromise(aWindow, "readwrite",
+          function(aResolver, aTxn, aStore) {
+            self.removeInternal(aResolver, aStore, aId);
+          }
+        );
+      },
+
+      clear: function DS_clear() {
+        if (self.readOnly) {
+          return self.throwReadOnly(aWindow);
+        }
+
+        // Promise<void>
+        return self.newDBPromise(aWindow, "readwrite",
+          function(aResolver, aTxn, aStore) {
+            self.clearInternal(aResolver, aStore);
+          }
+        );
+      },
+
       /* TODO:
-         Promise<Object> get(unsigned long id);
-         Promise<void> update(unsigned long id, any obj);
-         Promise<int> add(any obj)
-         Promise<boolean> remove(unsigned long id)
-         Promise<void> clear();
-
          readonly attribute DOMString revisionId
          attribute EventHandler onchange;
          Promise<DataStoreChanges> getChanges(DOMString revisionId)
          getAll(), getLength()
        */
 
       __exposedProps__: {
         name: 'r',
         owner: 'r',
-        readOnly: 'r'
+        readOnly: 'r',
+        get: 'r',
+        update: 'r',
+        add: 'r',
+        remove: 'r',
+        clear: 'r'
       }
     };
 
-    return chromeObject;
+    return object;
+  },
+
+  delete: function() {
+    this.db.delete();
   }
 };
new file mode 100644
--- /dev/null
+++ b/dom/datastore/DataStoreDB.jsm
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+this.EXPORTED_SYMBOLS = ['DataStoreDB'];
+
+function debug(s) {
+  // dump('DEBUG DataStoreDB: ' + s + '\n');
+}
+
+const DATASTOREDB_VERSION = 1;
+const DATASTOREDB_OBJECTSTORE_NAME = 'DataStoreDB';
+
+Cu.import('resource://gre/modules/IndexedDBHelper.jsm');
+
+this.DataStoreDB = function DataStoreDB() {}
+
+DataStoreDB.prototype = {
+
+  __proto__: IndexedDBHelper.prototype,
+
+  upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
+    debug('updateSchema');
+    aDb.createObjectStore(DATASTOREDB_OBJECTSTORE_NAME, { autoIncrement: true });
+  },
+
+  init: function(aOrigin, aName, aGlobal) {
+    let dbName = aOrigin + '_' + aName;
+    this.initDBHelper(dbName, DATASTOREDB_VERSION,
+                      [DATASTOREDB_OBJECTSTORE_NAME], aGlobal);
+  },
+
+  txn: function(aType, aCallback, aErrorCb) {
+    debug('Transaction request');
+    this.newTxn(
+      aType,
+      DATASTOREDB_OBJECTSTORE_NAME,
+      aCallback,
+      function() {},
+      aErrorCb
+    );
+  },
+
+  delete: function() {
+    debug('delete');
+    this.close();
+    this.dbGlobal.indexedDB.deleteDatabase(this.dbName);
+    debug('database deleted');
+  }
+}
--- a/dom/datastore/DataStoreService.js
+++ b/dom/datastore/DataStoreService.js
@@ -3,59 +3,67 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 'use strict'
 
 /* static functions */
 
-let DEBUG = 0;
-let debug;
-if (DEBUG)
-  debug = function (s) { dump('DEBUG DataStore: ' + s + '\n'); }
-else
-  debug = function (s) {}
+function debug(s) {
+  // dump('DEBUG DataStoreService: ' + s + '\n');
+}
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import('resource://gre/modules/DataStore.jsm');
 
+const GLOBAL_SCOPE = this;
+
 /* DataStoreService */
 
 const DATASTORESERVICE_CID = Components.ID('{d193d0e2-c677-4a7b-bb0a-19155b470f2e}');
 
 function DataStoreService() {
   debug('DataStoreService Constructor');
 
   let obs = Services.obs;
   if (!obs) {
     debug("DataStore Error: observer-service is null!");
     return;
   }
 
   obs.addObserver(this, 'webapps-clear-data', false);
+
+  let idbManager = Cc["@mozilla.org/dom/indexeddb/manager;1"]
+                     .getService(Ci.nsIIndexedDatabaseManager);
+  if (!idbManager) {
+    debug("DataStore Error: indexedDb Manager is null!");
+  }
+
+  idbManager.initWindowless(GLOBAL_SCOPE);
 }
 
 DataStoreService.prototype = {
   // Hash of DataStores
   stores: {},
 
   installDataStore: function(aAppId, aName, aOwner, aReadOnly) {
-    debug('installDataStore - appId: ' + aAppId + ', aName: ' + aName +
-          ', aOwner:' + aOwner + ', aReadOnly: ' + aReadOnly);
+    debug('installDataStore - appId: ' + aAppId + ', aName: ' +
+          aName + ', aOwner:' + aOwner + ', aReadOnly: ' +
+          aReadOnly);
 
     if (aName in this.stores && aAppId in this.stores[aName]) {
       debug('This should not happen');
       return;
     }
 
-    let store = new DataStore(aAppId, aName, aOwner, aReadOnly);
+    let store = new DataStore(aAppId, aName, aOwner, aReadOnly, GLOBAL_SCOPE);
 
     if (!(aName in this.stores)) {
       this.stores[aName] = {};
     }
 
     this.stores[aName][aAppId] = store;
   },
 
@@ -87,16 +95,17 @@ DataStoreService.prototype = {
 
     // DataStore is explosed to apps, not browser content.
     if (params.browserOnly) {
       return;
     }
 
     for (let key in this.stores) {
       if (params.appId in this.stores[key]) {
+        this.stores[key][params.appId].delete();
         delete this.stores[key][params.appId];
       }
 
       if (!this.stores[key].length) {
         delete this.stores[key];
       }
     }
   },
--- a/dom/datastore/moz.build
+++ b/dom/datastore/moz.build
@@ -16,9 +16,10 @@ MODULE = 'dom'
 
 EXTRA_COMPONENTS += [
     'DataStore.manifest',
     'DataStoreService.js',
 ]
 
 EXTRA_JS_MODULES += [
     'DataStore.jsm',
+    'DataStoreDB.jsm',
 ]
--- a/dom/datastore/tests/Makefile.in
+++ b/dom/datastore/tests/Makefile.in
@@ -8,13 +8,15 @@ srcdir           = @srcdir@
 VPATH            = @srcdir@
 
 relativesrcdir   = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_FILES = \
   test_app_install.html \
+  test_readonly.html \
+  test_basic.html \
   file_app.sjs \
   file_app.template.webapp \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/dom/datastore/tests/test_app_install.html
+++ b/dom/datastore/tests/test_app_install.html
@@ -1,30 +1,31 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test for DataStore - install/uninstall apps</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="container"></div>
   <script type="application/javascript;version=1.7">
 
   SimpleTest.waitForExplicitFinish();
 
   var gBaseURL = 'http://test/tests/dom/datastore/tests/';
   var gHostedManifestURL = gBaseURL + 'file_app.sjs';
   var gGenerator = runTest();
 
-  function go() {
-    SpecialPowers.pushPermissions(
-      [{ "type": "browser", "allow": 1, "context": document },
-       { "type": "embed-apps", "allow": 1, "context": document },
-       { "type": "webapps-manage", "allow": 1, "context": document }],
-      function() { gGenerator.next() });
-  }
+  SpecialPowers.pushPermissions(
+    [{ "type": "browser", "allow": 1, "context": document },
+     { "type": "embed-apps", "allow": 1, "context": document },
+     { "type": "webapps-manage", "allow": 1, "context": document }],
+    function() { gGenerator.next() });
 
   function continueTest() {
     gGenerator.next();
   }
 
   function cbError() {
     ok(false, "Error callback invoked");
     finish();
@@ -87,19 +88,10 @@
     finish();
   }
 
   function finish() {
     SimpleTest.finish();
   }
 
   </script>
-</head>
-<body onload="go()">
-<p id="display"></p>
-<div id="content" style="display: none">
-
-</div>
-<pre id="test">
-</pre>
-<div id="container"></div>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/test_basic.html
@@ -0,0 +1,229 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for DataStore - basic operation on a readonly db</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="container"></div>
+  <script type="application/javascript;version=1.7">
+
+  var gBaseURL = 'http://test/tests/dom/datastore/tests/';
+  var gHostedManifestURL = gBaseURL + 'file_app.sjs';
+  var gApp;
+  var gStore;
+
+  function cbError() {
+    ok(false, "Error callback invoked");
+    finish();
+  }
+
+  function installApp() {
+    var request = navigator.mozApps.install(gHostedManifestURL);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      gApp = request.result;
+      runTest();
+    }
+  }
+
+  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() {
+    // Uninstall the app.
+    request = navigator.mozApps.mgmt.uninstall(gApp);
+    request.onerror = cbError;
+    request.onsuccess = function() {
+      // All done.
+      info("All done");
+      runTest();
+    }
+  }
+
+  var tests = [
+    // Permissions
+    function() {
+      SpecialPowers.pushPermissions(
+        [{ "type": "browser", "allow": 1, "context": document },
+         { "type": "embed-apps", "allow": 1, "context": document },
+         { "type": "webapps-manage", "allow": 1, "context": document }], runTest);
+    },
+
+    // Preferences
+    function() {
+      SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true]]}, runTest);
+    },
+
+    // No confirmation needed when an app is installed
+    function() {
+      SpecialPowers.autoConfirmAppInstall(runTest);
+    },
+
+    // Installing the app
+    installApp,
+
+    // 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); },
+
+    // Uninstall the app
+    uninstallApp
+  ];
+
+  function runTest() {
+    if (!tests.length) {
+      finish();
+      return;
+    }
+
+    var test = tests.shift();
+    test();
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  runTest();
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/datastore/tests/test_readonly.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for DataStore - basic operation on a readonly db</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <div id="container"></div>
+  <script type="application/javascript;version=1.7">
+  var gBaseURL = 'http://test/tests/dom/datastore/tests/';
+  var gHostedManifestURL = gBaseURL + 'file_app.sjs';
+  var gGenerator = runTest();
+
+  SpecialPowers.pushPermissions(
+    [{ "type": "browser", "allow": 1, "context": document },
+     { "type": "embed-apps", "allow": 1, "context": document },
+     { "type": "webapps-manage", "allow": 1, "context": document }],
+    function() { gGenerator.next() });
+
+  function continueTest() {
+    try { gGenerator.next(); }
+    catch(e) {}
+  }
+
+  function cbError() {
+    ok(false, "Error callback invoked");
+    finish();
+  }
+
+  function runTest() {
+    SpecialPowers.autoConfirmAppInstall(continueTest);
+    yield undefined;
+
+    var request = navigator.mozApps.install(gHostedManifestURL);
+    request.onerror = cbError;
+    request.onsuccess = continueTest;
+    yield undefined;
+
+    var app = request.result;
+
+    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 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");
+
+      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() {
+        // Uninstall the app.
+        request = navigator.mozApps.mgmt.uninstall(app);
+        request.onerror = cbError;
+        request.onsuccess = function() {
+          // All done.
+          info("All done");
+          finish();
+        }
+      });
+    }, cbError);
+  }
+
+  function finish() {
+    SimpleTest.finish();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  SpecialPowers.pushPrefEnv({"set": [["dom.promise.enabled", true]]}, runTest);
+  </script>
+</body>
+</html>