|
1 // Utility functions for offline tests. |
|
2 var Cc = SpecialPowers.Cc; |
|
3 var Ci = SpecialPowers.Ci; |
|
4 var Cu = SpecialPowers.Cu; |
|
5 var LoadContextInfo = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {}).LoadContextInfo; |
|
6 |
|
7 const kNetBase = 2152398848; // 0x804B0000 |
|
8 var NS_ERROR_CACHE_KEY_NOT_FOUND = kNetBase + 61; |
|
9 var NS_ERROR_CACHE_KEY_WAIT_FOR_VALIDATION = kNetBase + 64; |
|
10 |
|
11 // Reading the contents of multiple cache entries asynchronously |
|
12 function OfflineCacheContents(urls) { |
|
13 this.urls = urls; |
|
14 this.contents = {}; |
|
15 } |
|
16 |
|
17 OfflineCacheContents.prototype = { |
|
18 QueryInterface: function(iid) { |
|
19 if (!iid.equals(Ci.nsISupports) && |
|
20 !iid.equals(Ci.nsICacheListener)) { |
|
21 throw Cr.NS_ERROR_NO_INTERFACE; |
|
22 } |
|
23 return this; |
|
24 }, |
|
25 onCacheEntryAvailable: function(desc, accessGranted, status) { |
|
26 if (!desc) { |
|
27 this.fetch(this.callback); |
|
28 return; |
|
29 } |
|
30 |
|
31 var stream = desc.QueryInterface(Ci.nsICacheEntryDescriptor).openInputStream(0); |
|
32 var sstream = Cc["@mozilla.org/scriptableinputstream;1"] |
|
33 .createInstance(SpecialPowers.Ci.nsIScriptableInputStream); |
|
34 sstream.init(stream); |
|
35 this.contents[desc.key] = sstream.read(sstream.available()); |
|
36 sstream.close(); |
|
37 desc.close(); |
|
38 this.fetch(this.callback); |
|
39 }, |
|
40 |
|
41 fetch: function(callback) |
|
42 { |
|
43 this.callback = callback; |
|
44 if (this.urls.length == 0) { |
|
45 callback(this.contents); |
|
46 return; |
|
47 } |
|
48 |
|
49 var url = this.urls.shift(); |
|
50 var self = this; |
|
51 |
|
52 var cacheSession = OfflineTest.getActiveSession(); |
|
53 cacheSession.asyncOpenCacheEntry(url, Ci.nsICache.ACCESS_READ, this); |
|
54 } |
|
55 }; |
|
56 |
|
57 var OfflineTest = { |
|
58 |
|
59 _allowedByDefault: false, |
|
60 |
|
61 _hasSlave: false, |
|
62 |
|
63 // The window where test results should be sent. |
|
64 _masterWindow: null, |
|
65 |
|
66 // Array of all PUT overrides on the server |
|
67 _pathOverrides: [], |
|
68 |
|
69 // SJSs whom state was changed to be reverted on teardown |
|
70 _SJSsStated: [], |
|
71 |
|
72 setupChild: function() |
|
73 { |
|
74 if (this._allowedByDefault) { |
|
75 this._masterWindow = window; |
|
76 return true; |
|
77 } |
|
78 |
|
79 if (window.parent.OfflineTest._hasSlave) { |
|
80 return false; |
|
81 } |
|
82 |
|
83 this._masterWindow = window.top; |
|
84 |
|
85 return true; |
|
86 }, |
|
87 |
|
88 /** |
|
89 * Setup the tests. This will reload the current page in a new window |
|
90 * if necessary. |
|
91 * |
|
92 * @return boolean Whether this window is the slave window |
|
93 * to actually run the test in. |
|
94 */ |
|
95 setup: function() |
|
96 { |
|
97 netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); |
|
98 |
|
99 try { |
|
100 this._allowedByDefault = SpecialPowers.getBoolPref("offline-apps.allow_by_default"); |
|
101 } catch (e) {} |
|
102 |
|
103 if (this._allowedByDefault) { |
|
104 this._masterWindow = window; |
|
105 |
|
106 return true; |
|
107 } |
|
108 |
|
109 if (!window.opener || !window.opener.OfflineTest || |
|
110 !window.opener.OfflineTest._hasSlave) { |
|
111 // Offline applications must be toplevel windows and have the |
|
112 // offline-app permission. Because we were loaded without the |
|
113 // offline-app permission and (probably) in an iframe, we need to |
|
114 // enable the pref and spawn a new window to perform the actual |
|
115 // tests. It will use this window to report successes and |
|
116 // failures. |
|
117 |
|
118 if (SpecialPowers.testPermission("offline-app", Ci.nsIPermissionManager.ALLOW_ACTION, document)) { |
|
119 ok(false, "Previous test failed to clear offline-app permission! Expect failures."); |
|
120 } |
|
121 SpecialPowers.addPermission("offline-app", Ci.nsIPermissionManager.ALLOW_ACTION, document); |
|
122 |
|
123 // Tests must run as toplevel windows. Open a slave window to run |
|
124 // the test. |
|
125 this._hasSlave = true; |
|
126 window.open(window.location, "offlinetest"); |
|
127 |
|
128 return false; |
|
129 } |
|
130 |
|
131 this._masterWindow = window.opener; |
|
132 |
|
133 return true; |
|
134 }, |
|
135 |
|
136 teardownAndFinish: function() |
|
137 { |
|
138 this.teardown(function(self) { self.finish(); }); |
|
139 }, |
|
140 |
|
141 teardown: function(callback) |
|
142 { |
|
143 // First wait for any pending scheduled updates to finish |
|
144 this.waitForUpdates(function(self) { |
|
145 // Remove the offline-app permission we gave ourselves. |
|
146 |
|
147 SpecialPowers.removePermission("offline-app", window.document); |
|
148 |
|
149 // Clear all overrides on the server |
|
150 for (override in self._pathOverrides) |
|
151 self.deleteData(self._pathOverrides[override]); |
|
152 for (statedSJS in self._SJSsStated) |
|
153 self.setSJSState(self._SJSsStated[statedSJS], ""); |
|
154 |
|
155 self.clear(); |
|
156 callback(self); |
|
157 }); |
|
158 }, |
|
159 |
|
160 finish: function() |
|
161 { |
|
162 if (this._allowedByDefault) { |
|
163 SimpleTest.executeSoon(SimpleTest.finish); |
|
164 } else if (this._masterWindow) { |
|
165 // Slave window: pass control back to master window, close itself. |
|
166 this._masterWindow.SimpleTest.executeSoon(this._masterWindow.OfflineTest.finish); |
|
167 window.close(); |
|
168 } else { |
|
169 // Master window: finish test. |
|
170 SimpleTest.finish(); |
|
171 } |
|
172 }, |
|
173 |
|
174 // |
|
175 // Mochitest wrappers - These forward tests to the proper mochitest window. |
|
176 // |
|
177 ok: function(condition, name, diag) |
|
178 { |
|
179 return this._masterWindow.SimpleTest.ok(condition, name, diag); |
|
180 }, |
|
181 |
|
182 is: function(a, b, name) |
|
183 { |
|
184 return this._masterWindow.SimpleTest.is(a, b, name); |
|
185 }, |
|
186 |
|
187 isnot: function(a, b, name) |
|
188 { |
|
189 return this._masterWindow.SimpleTest.isnot(a, b, name); |
|
190 }, |
|
191 |
|
192 todo: function(a, name) |
|
193 { |
|
194 return this._masterWindow.SimpleTest.todo(a, name); |
|
195 }, |
|
196 |
|
197 clear: function() |
|
198 { |
|
199 // XXX: maybe we should just wipe out the entire disk cache. |
|
200 var applicationCache = this.getActiveCache(); |
|
201 if (applicationCache) { |
|
202 applicationCache.discard(); |
|
203 } |
|
204 }, |
|
205 |
|
206 waitForUpdates: function(callback) |
|
207 { |
|
208 var self = this; |
|
209 var observer = { |
|
210 notified: false, |
|
211 observe: function(subject, topic, data) { |
|
212 if (subject) { |
|
213 subject.QueryInterface(SpecialPowers.Ci.nsIOfflineCacheUpdate); |
|
214 dump("Update of " + subject.manifestURI.spec + " finished\n"); |
|
215 } |
|
216 |
|
217 SimpleTest.executeSoon(function() { |
|
218 if (observer.notified) { |
|
219 return; |
|
220 } |
|
221 |
|
222 var updateservice = Cc["@mozilla.org/offlinecacheupdate-service;1"] |
|
223 .getService(SpecialPowers.Ci.nsIOfflineCacheUpdateService); |
|
224 var updatesPending = updateservice.numUpdates; |
|
225 if (updatesPending == 0) { |
|
226 try { |
|
227 SpecialPowers.removeObserver(observer, "offline-cache-update-completed"); |
|
228 } catch(ex) {} |
|
229 dump("All pending updates done\n"); |
|
230 observer.notified = true; |
|
231 callback(self); |
|
232 return; |
|
233 } |
|
234 |
|
235 dump("Waiting for " + updateservice.numUpdates + " update(s) to finish\n"); |
|
236 }); |
|
237 } |
|
238 } |
|
239 |
|
240 SpecialPowers.addObserver(observer, "offline-cache-update-completed", false); |
|
241 |
|
242 // Call now to check whether there are some updates scheduled |
|
243 observer.observe(); |
|
244 }, |
|
245 |
|
246 failEvent: function(e) |
|
247 { |
|
248 OfflineTest.ok(false, "Unexpected event: " + e.type); |
|
249 }, |
|
250 |
|
251 // The offline API as specified has no way to watch the load of a resource |
|
252 // added with applicationCache.mozAdd(). |
|
253 waitForAdd: function(url, onFinished) { |
|
254 // Check every half second for ten seconds. |
|
255 var numChecks = 20; |
|
256 |
|
257 var waitForAddListener = { |
|
258 onCacheEntryAvailable: function(entry, access, status) { |
|
259 if (entry) { |
|
260 entry.close(); |
|
261 onFinished(); |
|
262 return; |
|
263 } |
|
264 |
|
265 if (--numChecks == 0) { |
|
266 onFinished(); |
|
267 return; |
|
268 } |
|
269 |
|
270 setTimeout(OfflineTest.priv(waitFunc), 500); |
|
271 } |
|
272 }; |
|
273 |
|
274 var waitFunc = function() { |
|
275 var cacheSession = OfflineTest.getActiveSession(); |
|
276 cacheSession.asyncOpenCacheEntry(url, |
|
277 Ci.nsICache.ACCESS_READ, |
|
278 waitForAddListener); |
|
279 } |
|
280 |
|
281 setTimeout(this.priv(waitFunc), 500); |
|
282 }, |
|
283 |
|
284 manifestURL: function(overload) |
|
285 { |
|
286 var manifestURLspec; |
|
287 if (overload) { |
|
288 manifestURLspec = overload; |
|
289 } else { |
|
290 var win = window; |
|
291 while (win && !win.document.documentElement.getAttribute("manifest")) { |
|
292 if (win == win.parent) |
|
293 break; |
|
294 win = win.parent; |
|
295 } |
|
296 if (win) |
|
297 manifestURLspec = win.document.documentElement.getAttribute("manifest"); |
|
298 } |
|
299 |
|
300 var ios = Cc["@mozilla.org/network/io-service;1"] |
|
301 .getService(Ci.nsIIOService) |
|
302 |
|
303 var baseURI = ios.newURI(window.location.href, null, null); |
|
304 return ios.newURI(manifestURLspec, null, baseURI); |
|
305 }, |
|
306 |
|
307 loadContext: function() |
|
308 { |
|
309 return SpecialPowers.wrap(window).QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) |
|
310 .getInterface(SpecialPowers.Ci.nsIWebNavigation) |
|
311 .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) |
|
312 .getInterface(SpecialPowers.Ci.nsILoadContext); |
|
313 }, |
|
314 |
|
315 loadContextInfo: function() |
|
316 { |
|
317 return LoadContextInfo.fromLoadContext(this.loadContext(), false); |
|
318 }, |
|
319 |
|
320 getActiveCache: function(overload) |
|
321 { |
|
322 // Note that this is the current active cache in the cache stack, not the |
|
323 // one associated with this window. |
|
324 var serv = Cc["@mozilla.org/network/application-cache-service;1"] |
|
325 .getService(Ci.nsIApplicationCacheService); |
|
326 var groupID = serv.buildGroupID(this.manifestURL(overload), this.loadContextInfo()); |
|
327 return serv.getActiveCache(groupID); |
|
328 }, |
|
329 |
|
330 getActiveSession: function() |
|
331 { |
|
332 var cache = this.getActiveCache(); |
|
333 if (!cache) { |
|
334 return null; |
|
335 } |
|
336 |
|
337 var cacheService = Cc["@mozilla.org/network/cache-service;1"] |
|
338 .getService(Ci.nsICacheService); |
|
339 return cacheService.createSession(cache.clientID, |
|
340 Ci.nsICache.STORE_OFFLINE, |
|
341 true); |
|
342 }, |
|
343 |
|
344 priv: function(func) |
|
345 { |
|
346 var self = this; |
|
347 return function() { |
|
348 func(arguments); |
|
349 } |
|
350 }, |
|
351 |
|
352 checkCacheEntries: function(entries, callback) |
|
353 { |
|
354 var checkNextEntry = function() { |
|
355 if (entries.length == 0) { |
|
356 setTimeout(OfflineTest.priv(callback), 0); |
|
357 } else { |
|
358 OfflineTest.checkCache(entries[0][0], entries[0][1], checkNextEntry); |
|
359 entries.shift(); |
|
360 } |
|
361 } |
|
362 |
|
363 checkNextEntry(); |
|
364 }, |
|
365 |
|
366 checkCache: function(url, expectEntry, callback) |
|
367 { |
|
368 var cacheSession = this.getActiveSession(); |
|
369 this._checkCache(cacheSession, url, expectEntry, callback); |
|
370 }, |
|
371 |
|
372 _checkCache: function(cacheSession, url, expectEntry, callback) |
|
373 { |
|
374 if (!cacheSession) { |
|
375 if (expectEntry) { |
|
376 this.ok(false, url + " should exist in the offline cache (no session)"); |
|
377 } else { |
|
378 this.ok(true, url + " should not exist in the offline cache (no session)"); |
|
379 } |
|
380 if (callback) setTimeout(this.priv(callback), 0); |
|
381 return; |
|
382 } |
|
383 |
|
384 var _checkCacheListener = { |
|
385 onCacheEntryAvailable: function(entry, access, status) { |
|
386 if (entry) { |
|
387 if (expectEntry) { |
|
388 OfflineTest.ok(true, url + " should exist in the offline cache"); |
|
389 } else { |
|
390 OfflineTest.ok(false, url + " should not exist in the offline cache"); |
|
391 } |
|
392 entry.close(); |
|
393 } else { |
|
394 if (status == NS_ERROR_CACHE_KEY_NOT_FOUND) { |
|
395 if (expectEntry) { |
|
396 OfflineTest.ok(false, url + " should exist in the offline cache"); |
|
397 } else { |
|
398 OfflineTest.ok(true, url + " should not exist in the offline cache"); |
|
399 } |
|
400 } else if (status == NS_ERROR_CACHE_KEY_WAIT_FOR_VALIDATION) { |
|
401 // There was a cache key that we couldn't access yet, that's good enough. |
|
402 if (expectEntry) { |
|
403 OfflineTest.ok(!mustBeValid, url + " should exist in the offline cache"); |
|
404 } else { |
|
405 OfflineTest.ok(mustBeValid, url + " should not exist in the offline cache"); |
|
406 } |
|
407 } else { |
|
408 OfflineTest.ok(false, "got invalid error for " + url); |
|
409 } |
|
410 } |
|
411 if (callback) setTimeout(OfflineTest.priv(callback), 0); |
|
412 } |
|
413 }; |
|
414 |
|
415 cacheSession.asyncOpenCacheEntry(url, |
|
416 Ci.nsICache.ACCESS_READ, |
|
417 _checkCacheListener, |
|
418 false); |
|
419 }, |
|
420 |
|
421 setSJSState: function(sjsPath, stateQuery) |
|
422 { |
|
423 var client = new XMLHttpRequest(); |
|
424 client.open("GET", sjsPath + "?state=" + stateQuery, false); |
|
425 |
|
426 var appcachechannel = SpecialPowers.wrap(client).channel.QueryInterface(Ci.nsIApplicationCacheChannel); |
|
427 appcachechannel.chooseApplicationCache = false; |
|
428 appcachechannel.inheritApplicationCache = false; |
|
429 appcachechannel.applicationCache = null; |
|
430 |
|
431 client.send(); |
|
432 |
|
433 if (stateQuery == "") |
|
434 delete this._SJSsStated[sjsPath]; |
|
435 else |
|
436 this._SJSsStated.push(sjsPath); |
|
437 } |
|
438 |
|
439 }; |