michael@0: // Utility functions for offline tests. michael@0: var Cc = SpecialPowers.Cc; michael@0: var Ci = SpecialPowers.Ci; michael@0: var Cu = SpecialPowers.Cu; michael@0: var LoadContextInfo = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {}).LoadContextInfo; michael@0: michael@0: const kNetBase = 2152398848; // 0x804B0000 michael@0: var NS_ERROR_CACHE_KEY_NOT_FOUND = kNetBase + 61; michael@0: var NS_ERROR_CACHE_KEY_WAIT_FOR_VALIDATION = kNetBase + 64; michael@0: michael@0: // Reading the contents of multiple cache entries asynchronously michael@0: function OfflineCacheContents(urls) { michael@0: this.urls = urls; michael@0: this.contents = {}; michael@0: } michael@0: michael@0: OfflineCacheContents.prototype = { michael@0: QueryInterface: function(iid) { michael@0: if (!iid.equals(Ci.nsISupports) && michael@0: !iid.equals(Ci.nsICacheListener)) { michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: return this; michael@0: }, michael@0: onCacheEntryAvailable: function(desc, accessGranted, status) { michael@0: if (!desc) { michael@0: this.fetch(this.callback); michael@0: return; michael@0: } michael@0: michael@0: var stream = desc.QueryInterface(Ci.nsICacheEntryDescriptor).openInputStream(0); michael@0: var sstream = Cc["@mozilla.org/scriptableinputstream;1"] michael@0: .createInstance(SpecialPowers.Ci.nsIScriptableInputStream); michael@0: sstream.init(stream); michael@0: this.contents[desc.key] = sstream.read(sstream.available()); michael@0: sstream.close(); michael@0: desc.close(); michael@0: this.fetch(this.callback); michael@0: }, michael@0: michael@0: fetch: function(callback) michael@0: { michael@0: this.callback = callback; michael@0: if (this.urls.length == 0) { michael@0: callback(this.contents); michael@0: return; michael@0: } michael@0: michael@0: var url = this.urls.shift(); michael@0: var self = this; michael@0: michael@0: var cacheSession = OfflineTest.getActiveSession(); michael@0: cacheSession.asyncOpenCacheEntry(url, Ci.nsICache.ACCESS_READ, this); michael@0: } michael@0: }; michael@0: michael@0: var OfflineTest = { michael@0: michael@0: _allowedByDefault: false, michael@0: michael@0: _hasSlave: false, michael@0: michael@0: // The window where test results should be sent. michael@0: _masterWindow: null, michael@0: michael@0: // Array of all PUT overrides on the server michael@0: _pathOverrides: [], michael@0: michael@0: // SJSs whom state was changed to be reverted on teardown michael@0: _SJSsStated: [], michael@0: michael@0: setupChild: function() michael@0: { michael@0: if (this._allowedByDefault) { michael@0: this._masterWindow = window; michael@0: return true; michael@0: } michael@0: michael@0: if (window.parent.OfflineTest._hasSlave) { michael@0: return false; michael@0: } michael@0: michael@0: this._masterWindow = window.top; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Setup the tests. This will reload the current page in a new window michael@0: * if necessary. michael@0: * michael@0: * @return boolean Whether this window is the slave window michael@0: * to actually run the test in. michael@0: */ michael@0: setup: function() michael@0: { michael@0: netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); michael@0: michael@0: try { michael@0: this._allowedByDefault = SpecialPowers.getBoolPref("offline-apps.allow_by_default"); michael@0: } catch (e) {} michael@0: michael@0: if (this._allowedByDefault) { michael@0: this._masterWindow = window; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: if (!window.opener || !window.opener.OfflineTest || michael@0: !window.opener.OfflineTest._hasSlave) { michael@0: // Offline applications must be toplevel windows and have the michael@0: // offline-app permission. Because we were loaded without the michael@0: // offline-app permission and (probably) in an iframe, we need to michael@0: // enable the pref and spawn a new window to perform the actual michael@0: // tests. It will use this window to report successes and michael@0: // failures. michael@0: michael@0: if (SpecialPowers.testPermission("offline-app", Ci.nsIPermissionManager.ALLOW_ACTION, document)) { michael@0: ok(false, "Previous test failed to clear offline-app permission! Expect failures."); michael@0: } michael@0: SpecialPowers.addPermission("offline-app", Ci.nsIPermissionManager.ALLOW_ACTION, document); michael@0: michael@0: // Tests must run as toplevel windows. Open a slave window to run michael@0: // the test. michael@0: this._hasSlave = true; michael@0: window.open(window.location, "offlinetest"); michael@0: michael@0: return false; michael@0: } michael@0: michael@0: this._masterWindow = window.opener; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: teardownAndFinish: function() michael@0: { michael@0: this.teardown(function(self) { self.finish(); }); michael@0: }, michael@0: michael@0: teardown: function(callback) michael@0: { michael@0: // First wait for any pending scheduled updates to finish michael@0: this.waitForUpdates(function(self) { michael@0: // Remove the offline-app permission we gave ourselves. michael@0: michael@0: SpecialPowers.removePermission("offline-app", window.document); michael@0: michael@0: // Clear all overrides on the server michael@0: for (override in self._pathOverrides) michael@0: self.deleteData(self._pathOverrides[override]); michael@0: for (statedSJS in self._SJSsStated) michael@0: self.setSJSState(self._SJSsStated[statedSJS], ""); michael@0: michael@0: self.clear(); michael@0: callback(self); michael@0: }); michael@0: }, michael@0: michael@0: finish: function() michael@0: { michael@0: if (this._allowedByDefault) { michael@0: SimpleTest.executeSoon(SimpleTest.finish); michael@0: } else if (this._masterWindow) { michael@0: // Slave window: pass control back to master window, close itself. michael@0: this._masterWindow.SimpleTest.executeSoon(this._masterWindow.OfflineTest.finish); michael@0: window.close(); michael@0: } else { michael@0: // Master window: finish test. michael@0: SimpleTest.finish(); michael@0: } michael@0: }, michael@0: michael@0: // michael@0: // Mochitest wrappers - These forward tests to the proper mochitest window. michael@0: // michael@0: ok: function(condition, name, diag) michael@0: { michael@0: return this._masterWindow.SimpleTest.ok(condition, name, diag); michael@0: }, michael@0: michael@0: is: function(a, b, name) michael@0: { michael@0: return this._masterWindow.SimpleTest.is(a, b, name); michael@0: }, michael@0: michael@0: isnot: function(a, b, name) michael@0: { michael@0: return this._masterWindow.SimpleTest.isnot(a, b, name); michael@0: }, michael@0: michael@0: todo: function(a, name) michael@0: { michael@0: return this._masterWindow.SimpleTest.todo(a, name); michael@0: }, michael@0: michael@0: clear: function() michael@0: { michael@0: // XXX: maybe we should just wipe out the entire disk cache. michael@0: var applicationCache = this.getActiveCache(); michael@0: if (applicationCache) { michael@0: applicationCache.discard(); michael@0: } michael@0: }, michael@0: michael@0: waitForUpdates: function(callback) michael@0: { michael@0: var self = this; michael@0: var observer = { michael@0: notified: false, michael@0: observe: function(subject, topic, data) { michael@0: if (subject) { michael@0: subject.QueryInterface(SpecialPowers.Ci.nsIOfflineCacheUpdate); michael@0: dump("Update of " + subject.manifestURI.spec + " finished\n"); michael@0: } michael@0: michael@0: SimpleTest.executeSoon(function() { michael@0: if (observer.notified) { michael@0: return; michael@0: } michael@0: michael@0: var updateservice = Cc["@mozilla.org/offlinecacheupdate-service;1"] michael@0: .getService(SpecialPowers.Ci.nsIOfflineCacheUpdateService); michael@0: var updatesPending = updateservice.numUpdates; michael@0: if (updatesPending == 0) { michael@0: try { michael@0: SpecialPowers.removeObserver(observer, "offline-cache-update-completed"); michael@0: } catch(ex) {} michael@0: dump("All pending updates done\n"); michael@0: observer.notified = true; michael@0: callback(self); michael@0: return; michael@0: } michael@0: michael@0: dump("Waiting for " + updateservice.numUpdates + " update(s) to finish\n"); michael@0: }); michael@0: } michael@0: } michael@0: michael@0: SpecialPowers.addObserver(observer, "offline-cache-update-completed", false); michael@0: michael@0: // Call now to check whether there are some updates scheduled michael@0: observer.observe(); michael@0: }, michael@0: michael@0: failEvent: function(e) michael@0: { michael@0: OfflineTest.ok(false, "Unexpected event: " + e.type); michael@0: }, michael@0: michael@0: // The offline API as specified has no way to watch the load of a resource michael@0: // added with applicationCache.mozAdd(). michael@0: waitForAdd: function(url, onFinished) { michael@0: // Check every half second for ten seconds. michael@0: var numChecks = 20; michael@0: michael@0: var waitForAddListener = { michael@0: onCacheEntryAvailable: function(entry, access, status) { michael@0: if (entry) { michael@0: entry.close(); michael@0: onFinished(); michael@0: return; michael@0: } michael@0: michael@0: if (--numChecks == 0) { michael@0: onFinished(); michael@0: return; michael@0: } michael@0: michael@0: setTimeout(OfflineTest.priv(waitFunc), 500); michael@0: } michael@0: }; michael@0: michael@0: var waitFunc = function() { michael@0: var cacheSession = OfflineTest.getActiveSession(); michael@0: cacheSession.asyncOpenCacheEntry(url, michael@0: Ci.nsICache.ACCESS_READ, michael@0: waitForAddListener); michael@0: } michael@0: michael@0: setTimeout(this.priv(waitFunc), 500); michael@0: }, michael@0: michael@0: manifestURL: function(overload) michael@0: { michael@0: var manifestURLspec; michael@0: if (overload) { michael@0: manifestURLspec = overload; michael@0: } else { michael@0: var win = window; michael@0: while (win && !win.document.documentElement.getAttribute("manifest")) { michael@0: if (win == win.parent) michael@0: break; michael@0: win = win.parent; michael@0: } michael@0: if (win) michael@0: manifestURLspec = win.document.documentElement.getAttribute("manifest"); michael@0: } michael@0: michael@0: var ios = Cc["@mozilla.org/network/io-service;1"] michael@0: .getService(Ci.nsIIOService) michael@0: michael@0: var baseURI = ios.newURI(window.location.href, null, null); michael@0: return ios.newURI(manifestURLspec, null, baseURI); michael@0: }, michael@0: michael@0: loadContext: function() michael@0: { michael@0: return SpecialPowers.wrap(window).QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) michael@0: .getInterface(SpecialPowers.Ci.nsIWebNavigation) michael@0: .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) michael@0: .getInterface(SpecialPowers.Ci.nsILoadContext); michael@0: }, michael@0: michael@0: loadContextInfo: function() michael@0: { michael@0: return LoadContextInfo.fromLoadContext(this.loadContext(), false); michael@0: }, michael@0: michael@0: getActiveCache: function(overload) michael@0: { michael@0: // Note that this is the current active cache in the cache stack, not the michael@0: // one associated with this window. michael@0: var serv = Cc["@mozilla.org/network/application-cache-service;1"] michael@0: .getService(Ci.nsIApplicationCacheService); michael@0: var groupID = serv.buildGroupID(this.manifestURL(overload), this.loadContextInfo()); michael@0: return serv.getActiveCache(groupID); michael@0: }, michael@0: michael@0: getActiveSession: function() michael@0: { michael@0: var cache = this.getActiveCache(); michael@0: if (!cache) { michael@0: return null; michael@0: } michael@0: michael@0: var cacheService = Cc["@mozilla.org/network/cache-service;1"] michael@0: .getService(Ci.nsICacheService); michael@0: return cacheService.createSession(cache.clientID, michael@0: Ci.nsICache.STORE_OFFLINE, michael@0: true); michael@0: }, michael@0: michael@0: priv: function(func) michael@0: { michael@0: var self = this; michael@0: return function() { michael@0: func(arguments); michael@0: } michael@0: }, michael@0: michael@0: checkCacheEntries: function(entries, callback) michael@0: { michael@0: var checkNextEntry = function() { michael@0: if (entries.length == 0) { michael@0: setTimeout(OfflineTest.priv(callback), 0); michael@0: } else { michael@0: OfflineTest.checkCache(entries[0][0], entries[0][1], checkNextEntry); michael@0: entries.shift(); michael@0: } michael@0: } michael@0: michael@0: checkNextEntry(); michael@0: }, michael@0: michael@0: checkCache: function(url, expectEntry, callback) michael@0: { michael@0: var cacheSession = this.getActiveSession(); michael@0: this._checkCache(cacheSession, url, expectEntry, callback); michael@0: }, michael@0: michael@0: _checkCache: function(cacheSession, url, expectEntry, callback) michael@0: { michael@0: if (!cacheSession) { michael@0: if (expectEntry) { michael@0: this.ok(false, url + " should exist in the offline cache (no session)"); michael@0: } else { michael@0: this.ok(true, url + " should not exist in the offline cache (no session)"); michael@0: } michael@0: if (callback) setTimeout(this.priv(callback), 0); michael@0: return; michael@0: } michael@0: michael@0: var _checkCacheListener = { michael@0: onCacheEntryAvailable: function(entry, access, status) { michael@0: if (entry) { michael@0: if (expectEntry) { michael@0: OfflineTest.ok(true, url + " should exist in the offline cache"); michael@0: } else { michael@0: OfflineTest.ok(false, url + " should not exist in the offline cache"); michael@0: } michael@0: entry.close(); michael@0: } else { michael@0: if (status == NS_ERROR_CACHE_KEY_NOT_FOUND) { michael@0: if (expectEntry) { michael@0: OfflineTest.ok(false, url + " should exist in the offline cache"); michael@0: } else { michael@0: OfflineTest.ok(true, url + " should not exist in the offline cache"); michael@0: } michael@0: } else if (status == NS_ERROR_CACHE_KEY_WAIT_FOR_VALIDATION) { michael@0: // There was a cache key that we couldn't access yet, that's good enough. michael@0: if (expectEntry) { michael@0: OfflineTest.ok(!mustBeValid, url + " should exist in the offline cache"); michael@0: } else { michael@0: OfflineTest.ok(mustBeValid, url + " should not exist in the offline cache"); michael@0: } michael@0: } else { michael@0: OfflineTest.ok(false, "got invalid error for " + url); michael@0: } michael@0: } michael@0: if (callback) setTimeout(OfflineTest.priv(callback), 0); michael@0: } michael@0: }; michael@0: michael@0: cacheSession.asyncOpenCacheEntry(url, michael@0: Ci.nsICache.ACCESS_READ, michael@0: _checkCacheListener, michael@0: false); michael@0: }, michael@0: michael@0: setSJSState: function(sjsPath, stateQuery) michael@0: { michael@0: var client = new XMLHttpRequest(); michael@0: client.open("GET", sjsPath + "?state=" + stateQuery, false); michael@0: michael@0: var appcachechannel = SpecialPowers.wrap(client).channel.QueryInterface(Ci.nsIApplicationCacheChannel); michael@0: appcachechannel.chooseApplicationCache = false; michael@0: appcachechannel.inheritApplicationCache = false; michael@0: appcachechannel.applicationCache = null; michael@0: michael@0: client.send(); michael@0: michael@0: if (stateQuery == "") michael@0: delete this._SJSsStated[sjsPath]; michael@0: else michael@0: this._SJSsStated.push(sjsPath); michael@0: } michael@0: michael@0: };