var TestUtils = (function() { function randomString() { var result = ""; for (var i = 0; i < 5; i++) result += String.fromCharCode(97 + Math.floor(Math.random() * 26)); return result; }; /** * Representation of one datatype. * @typedef Datatype * @type{object} * @property{string} name Name of the datatype. * @property{function():boolean} supported * Whether this datatype is supported by this user agent. * @method{function():Void} add A function to add an instance of the datatype. * @method{function():boolean} isEmpty A function that tests whether * the datatype's storage backend is empty. */ var Datatype; var TestUtils = {}; /** * Various storage backends that are part of the 'storage' datatype. * @param{Array.} */ TestUtils.STORAGE = [ { "name": "local storage", "supported": function() { return !!window.localStorage; }, "add": function() { return new Promise(function(resolve, reject) { localStorage.setItem(randomString(), randomString()); resolve(); }); }, "isEmpty": function() { return new Promise(function(resolve, reject) { resolve(!localStorage.length); }); } }, { "name": "Indexed DB", "supported": function() { return !!window.indexedDB; }, "add": function() { return new Promise(function(resolve, reject) { var request = window.indexedDB.open("database"); request.onupgradeneeded = function() { request.result.createObjectStore("store"); }; request.onsuccess = function() { request.result.close(); resolve(); } }); }, "isEmpty": function() { return new Promise(function(resolve, reject) { var request = window.indexedDB.open("database"); request.onsuccess = function() { var database = request.result; try { var transaction = database.transaction(["store"]); resolve(false); } catch(error) { // The database is empty. However, by testing that, we have also // created it, which means that |onupgradeneeded| in the "add" // method will not run the next time. Delete the database before // reporting that it was empty. var deletion = window.indexedDB.deleteDatabase("database"); deletion.onsuccess = resolve.bind(this, true); } finally { database.close(); } }; }); } }, { // TODO(@msramek): We should also test the PERSISTENT filesystem, however, // that might require storage permissions. "name": "filesystems", "supported": function() { return window.requestFileSystem || window.webkitRequestFileSystem; }, "add": function() { return new Promise(function(resolve, reject) { var onSuccess = function(fileSystem) { fileSystem.root.getFile('file', {"create": true}, resolve, resolve); } var onFailure = resolve; var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; requestFileSystem(window.TEMPORARY, 1 /* 1B */, onSuccess, onFailure); }); }, "isEmpty": function() { return new Promise(function(resolve, reject) { var onSuccess = function(fileSystem) { fileSystem.root.getFile( 'file', {}, resolve.bind(this, false) /* opened successfully */, resolve.bind(this, true) /* failed to open */); } var onFailure = resolve.bind(this, true); var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; requestFileSystem(window.TEMPORARY, 1 /* 1B */, onSuccess, onFailure); }); } }, { "name": "service workers", "supported": function() { return !!navigator.serviceWorker; }, "add": function() { return navigator.serviceWorker.register( "support/service_worker.js", { scope: "support/page_using_service_worker.html"}); }, "isEmpty": function() { return new Promise(function(resolve, reject) { navigator.serviceWorker.getRegistrations() .then(function(registrations) { resolve(!registrations.length); }); }); } }, { "name": "WebSQL", "supported": function() { return !!window.openDatabase; }, "add": function() { return new Promise(function(resolve, reject) { var database = window.openDatabase( "database", "1.0", "database", 1024 /* 1 kB */); database.transaction(function(context) { context.executeSql("CREATE TABLE IF NOT EXISTS data (column)"); context.executeSql( "INSERT INTO data (column) VALUES (1)", [], resolve); }); }); }, "isEmpty": function() { return new Promise(function(resolve, reject) { var database = window.openDatabase( "database", "1.0", "database", 1024 /* 1 kB */); database.transaction(function(context) { context.executeSql("CREATE TABLE IF NOT EXISTS data (column)"); context.executeSql( "SELECT * FROM data", [], function(transaction, result) { resolve(!result.rows.length); }); }); }); } } ].filter(function(backend) { return backend.supported(); }); /** * All datatypes supported by Clear-Site-Data. * @param{Array.} */ TestUtils.DATATYPES = [ { "name": "cookies", "supported": function() { return typeof document.cookie == "string"; }, "add": function() { return new Promise(function(resolve, reject) { document.cookie = randomString() + "=" + randomString(); resolve(); }); }, "isEmpty": function() { return new Promise(function(resolve, reject) { resolve(!document.cookie); }); } }, { "name": "storage", "supported": TestUtils.STORAGE[0].supported, "add": TestUtils.STORAGE[0].add, "isEmpty": TestUtils.STORAGE[0].isEmpty, } ].filter(function(datatype) { return datatype.supported(); }); /** * All possible combinations of datatypes. * @property {Array.>} */ TestUtils.COMBINATIONS = (function() { var combinations = []; for (var mask = 0; mask < (1 << TestUtils.DATATYPES.length); mask++) { var combination = []; for (var datatype = 0; datatype < TestUtils.DATATYPES.length; datatype++) { if (mask & (1 << datatype)) combination.push(TestUtils.DATATYPES[datatype]); } combinations.push(combination); } return combinations; })(); /** * Populates |datatypes| by calling the "add" method on each of them, * and verifies that they are nonempty. * @param {Array.} datatypes to be populated. * @private */ function populate(datatypes) { return Promise.all(datatypes.map(function(datatype) { return new Promise(function(resolve, reject) { datatype.add().then(function() { datatype.isEmpty().then(function(isEmpty) { assert_false( isEmpty, datatype.name + " has to be nonempty before the test starts."); resolve(); }); }); }); })); }; /** * Ensures that all datatypes are nonempty. Should be called in the test * setup phase. */ TestUtils.populateDatatypes = populate.bind(this, TestUtils.DATATYPES); /** * Ensures that all backends of the "storage" datatype are nonempty. Should * be called in the test setup phase. */ TestUtils.populateStorage = populate.bind(this, TestUtils.STORAGE); /** * Get the support server URL that returns a Clear-Site-Data header * to clear |datatypes|. * @param{Array.} datatypes The list of datatypes to be deleted. * @return string The URL to be queried. */ TestUtils.getClearSiteDataUrl = function(datatypes) { names = datatypes.map(function(e) { return e.name }); return "support/echo-clear-site-data.py?" + names.join("&"); } /** * @param{string} page_scheme Scheme of the page. "http" or "https". * @param{string} resource_scheme Scheme of the resource. "http" or "https". * @return The URL of a page that contains a resource requesting the deletion * of storage. */ TestUtils.getPageWithResourceUrl = function(page_scheme, resource_scheme) { if (page_scheme != "https" && page_scheme != "http") throw "Unsupported scheme: " + page_scheme; if (resource_scheme != "https" && resource_scheme != "http") throw "Unsupported scheme: " + resource_scheme; return page_scheme + "://{{domains[]}}:" + (page_scheme == "https" ? {{ports[https][0]}} : {{ports[http][0]}}) + "/clear-site-data/support/page_with_resource.sub.html?scheme=" + resource_scheme; } return TestUtils; })();