|
1 const RELATIVE_DIR = "toolkit/mozapps/extensions/test/xpinstall/"; |
|
2 |
|
3 const TESTROOT = "http://example.com/browser/" + RELATIVE_DIR; |
|
4 const TESTROOT2 = "http://example.org/browser/" + RELATIVE_DIR; |
|
5 const XPINSTALL_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; |
|
6 const PROMPT_URL = "chrome://global/content/commonDialog.xul"; |
|
7 const ADDONS_URL = "chrome://mozapps/content/extensions/extensions.xul"; |
|
8 const PREF_LOGGING_ENABLED = "extensions.logging.enabled"; |
|
9 const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts"; |
|
10 const CHROME_NAME = "mochikit"; |
|
11 |
|
12 function getChromeRoot(path) { |
|
13 if (path === undefined) { |
|
14 return "chrome://" + CHROME_NAME + "/content/browser/" + RELATIVE_DIR |
|
15 } |
|
16 return getRootDirectory(path); |
|
17 } |
|
18 |
|
19 function extractChromeRoot(path) { |
|
20 var chromeRootPath = getChromeRoot(path); |
|
21 var jar = getJar(chromeRootPath); |
|
22 if (jar) { |
|
23 var tmpdir = extractJarToTmp(jar); |
|
24 return "file://" + tmpdir.path + "/"; |
|
25 } |
|
26 return chromeRootPath; |
|
27 } |
|
28 |
|
29 /** |
|
30 * This is a test harness designed to handle responding to UI during the process |
|
31 * of installing an XPI. A test can set callbacks to hear about specific parts |
|
32 * of the sequence. |
|
33 * Before use setup must be called and finish must be called afterwards. |
|
34 */ |
|
35 var Harness = { |
|
36 // If set then the callback is called when an install is attempted and |
|
37 // software installation is disabled. |
|
38 installDisabledCallback: null, |
|
39 // If set then the callback is called when an install is attempted and |
|
40 // then canceled. |
|
41 installCancelledCallback: null, |
|
42 // If set then the callback will be called when an install is blocked by the |
|
43 // whitelist. The callback should return true to continue with the install |
|
44 // anyway. |
|
45 installBlockedCallback: null, |
|
46 // If set will be called in the event of authentication being needed to get |
|
47 // the xpi. Should return a 2 element array of username and password, or |
|
48 // null to not authenticate. |
|
49 authenticationCallback: null, |
|
50 // If set this will be called to allow checking the contents of the xpinstall |
|
51 // confirmation dialog. The callback should return true to continue the install. |
|
52 installConfirmCallback: null, |
|
53 // If set will be called when downloading of an item has begun. |
|
54 downloadStartedCallback: null, |
|
55 // If set will be called during the download of an item. |
|
56 downloadProgressCallback: null, |
|
57 // If set will be called when an xpi fails to download. |
|
58 downloadFailedCallback: null, |
|
59 // If set will be called when an xpi download is cancelled. |
|
60 downloadCancelledCallback: null, |
|
61 // If set will be called when downloading of an item has ended. |
|
62 downloadEndedCallback: null, |
|
63 // If set will be called when installation by the extension manager of an xpi |
|
64 // item starts |
|
65 installStartedCallback: null, |
|
66 // If set will be called when an xpi fails to install. |
|
67 installFailedCallback: null, |
|
68 // If set will be called when each xpi item to be installed completes |
|
69 // installation. |
|
70 installEndedCallback: null, |
|
71 // If set will be called when all triggered items are installed or the install |
|
72 // is canceled. |
|
73 installsCompletedCallback: null, |
|
74 |
|
75 pendingCount: null, |
|
76 installCount: null, |
|
77 runningInstalls: null, |
|
78 |
|
79 waitingForFinish: false, |
|
80 |
|
81 // Setup and tear down functions |
|
82 setup: function() { |
|
83 if (!this.waitingForFinish) { |
|
84 waitForExplicitFinish(); |
|
85 this.waitingForFinish = true; |
|
86 |
|
87 Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true); |
|
88 Services.obs.addObserver(this, "addon-install-started", false); |
|
89 Services.obs.addObserver(this, "addon-install-disabled", false); |
|
90 Services.obs.addObserver(this, "addon-install-blocked", false); |
|
91 Services.obs.addObserver(this, "addon-install-failed", false); |
|
92 Services.obs.addObserver(this, "addon-install-complete", false); |
|
93 |
|
94 AddonManager.addInstallListener(this); |
|
95 |
|
96 Services.wm.addListener(this); |
|
97 |
|
98 var self = this; |
|
99 registerCleanupFunction(function() { |
|
100 Services.prefs.clearUserPref(PREF_LOGGING_ENABLED); |
|
101 Services.obs.removeObserver(self, "addon-install-started"); |
|
102 Services.obs.removeObserver(self, "addon-install-disabled"); |
|
103 Services.obs.removeObserver(self, "addon-install-blocked"); |
|
104 Services.obs.removeObserver(self, "addon-install-failed"); |
|
105 Services.obs.removeObserver(self, "addon-install-complete"); |
|
106 |
|
107 AddonManager.removeInstallListener(self); |
|
108 |
|
109 Services.wm.removeListener(self); |
|
110 |
|
111 AddonManager.getAllInstalls(function(aInstalls) { |
|
112 is(aInstalls.length, 0, "Should be no active installs at the end of the test"); |
|
113 aInstalls.forEach(function(aInstall) { |
|
114 info("Install for " + aInstall.sourceURI + " is in state " + aInstall.state); |
|
115 aInstall.cancel(); |
|
116 }); |
|
117 }); |
|
118 }); |
|
119 } |
|
120 |
|
121 this.installCount = 0; |
|
122 this.pendingCount = 0; |
|
123 this.runningInstalls = []; |
|
124 }, |
|
125 |
|
126 finish: function() { |
|
127 finish(); |
|
128 }, |
|
129 |
|
130 endTest: function() { |
|
131 // Defer the final notification to allow things like the InstallTrigger |
|
132 // callback to complete |
|
133 var self = this; |
|
134 executeSoon(function() { |
|
135 let callback = self.installsCompletedCallback; |
|
136 let count = self.installCount; |
|
137 |
|
138 is(self.runningInstalls.length, 0, "Should be no running installs left"); |
|
139 self.runningInstalls.forEach(function(aInstall) { |
|
140 info("Install for " + aInstall.sourceURI + " is in state " + aInstall.state); |
|
141 }); |
|
142 |
|
143 self.installBlockedCallback = null; |
|
144 self.authenticationCallback = null; |
|
145 self.installConfirmCallback = null; |
|
146 self.downloadStartedCallback = null; |
|
147 self.downloadProgressCallback = null; |
|
148 self.downloadCancelledCallback = null; |
|
149 self.downloadFailedCallback = null; |
|
150 self.downloadEndedCallback = null; |
|
151 self.installStartedCallback = null; |
|
152 self.installFailedCallback = null; |
|
153 self.installEndedCallback = null; |
|
154 self.installsCompletedCallback = null; |
|
155 self.runningInstalls = null; |
|
156 |
|
157 if (callback) |
|
158 callback(count); |
|
159 }); |
|
160 }, |
|
161 |
|
162 // Window open handling |
|
163 windowReady: function(window) { |
|
164 if (window.document.location.href == XPINSTALL_URL) { |
|
165 if (this.installBlockedCallback) |
|
166 ok(false, "Should have been blocked by the whitelist"); |
|
167 this.pendingCount = window.document.getElementById("itemList").childNodes.length; |
|
168 |
|
169 // If there is a confirm callback then its return status determines whether |
|
170 // to install the items or not. If not the test is over. |
|
171 if (this.installConfirmCallback && !this.installConfirmCallback(window)) { |
|
172 window.document.documentElement.cancelDialog(); |
|
173 } |
|
174 else { |
|
175 // Initially the accept button is disabled on a countdown timer |
|
176 var button = window.document.documentElement.getButton("accept"); |
|
177 button.disabled = false; |
|
178 window.document.documentElement.acceptDialog(); |
|
179 } |
|
180 } |
|
181 else if (window.document.location.href == PROMPT_URL) { |
|
182 var promptType = window.args.promptType; |
|
183 switch (promptType) { |
|
184 case "alert": |
|
185 case "alertCheck": |
|
186 case "confirmCheck": |
|
187 case "confirm": |
|
188 case "confirmEx": |
|
189 window.document.documentElement.acceptDialog(); |
|
190 break; |
|
191 case "promptUserAndPass": |
|
192 // This is a login dialog, hopefully an authentication prompt |
|
193 // for the xpi. |
|
194 if (this.authenticationCallback) { |
|
195 var auth = this.authenticationCallback(); |
|
196 if (auth && auth.length == 2) { |
|
197 window.document.getElementById("loginTextbox").value = auth[0]; |
|
198 window.document.getElementById("password1Textbox").value = auth[1]; |
|
199 window.document.documentElement.acceptDialog(); |
|
200 } |
|
201 else { |
|
202 window.document.documentElement.cancelDialog(); |
|
203 } |
|
204 } |
|
205 else { |
|
206 window.document.documentElement.cancelDialog(); |
|
207 } |
|
208 break; |
|
209 default: |
|
210 ok(false, "prompt type " + promptType + " not handled in test."); |
|
211 break; |
|
212 } |
|
213 } |
|
214 }, |
|
215 |
|
216 // Install blocked handling |
|
217 |
|
218 installDisabled: function(installInfo) { |
|
219 ok(!!this.installDisabledCallback, "Installation shouldn't have been disabled"); |
|
220 if (this.installDisabledCallback) |
|
221 this.installDisabledCallback(installInfo); |
|
222 this.expectingCancelled = true; |
|
223 installInfo.installs.forEach(function(install) { |
|
224 install.cancel(); |
|
225 }); |
|
226 this.expectingCancelled = false; |
|
227 this.endTest(); |
|
228 }, |
|
229 |
|
230 installCancelled: function(installInfo) { |
|
231 if (this.expectingCancelled) |
|
232 return; |
|
233 |
|
234 ok(!!this.installCancelledCallback, "Installation shouldn't have been cancelled"); |
|
235 if (this.installCancelledCallback) |
|
236 this.installCancelledCallback(installInfo); |
|
237 this.endTest(); |
|
238 }, |
|
239 |
|
240 installBlocked: function(installInfo) { |
|
241 ok(!!this.installBlockedCallback, "Shouldn't have been blocked by the whitelist"); |
|
242 if (this.installBlockedCallback && this.installBlockedCallback(installInfo)) { |
|
243 this.installBlockedCallback = null; |
|
244 installInfo.install(); |
|
245 } |
|
246 else { |
|
247 this.expectingCancelled = true; |
|
248 installInfo.installs.forEach(function(install) { |
|
249 install.cancel(); |
|
250 }); |
|
251 this.expectingCancelled = false; |
|
252 this.endTest(); |
|
253 } |
|
254 }, |
|
255 |
|
256 // nsIWindowMediatorListener |
|
257 |
|
258 onWindowTitleChange: function(window, title) { |
|
259 }, |
|
260 |
|
261 onOpenWindow: function(window) { |
|
262 var domwindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
|
263 .getInterface(Components.interfaces.nsIDOMWindow); |
|
264 var self = this; |
|
265 waitForFocus(function() { |
|
266 self.windowReady(domwindow); |
|
267 }, domwindow); |
|
268 }, |
|
269 |
|
270 onCloseWindow: function(window) { |
|
271 }, |
|
272 |
|
273 // Addon Install Listener |
|
274 |
|
275 onNewInstall: function(install) { |
|
276 this.runningInstalls.push(install); |
|
277 }, |
|
278 |
|
279 onDownloadStarted: function(install) { |
|
280 this.pendingCount++; |
|
281 if (this.downloadStartedCallback) |
|
282 this.downloadStartedCallback(install); |
|
283 }, |
|
284 |
|
285 onDownloadProgress: function(install) { |
|
286 if (this.downloadProgressCallback) |
|
287 this.downloadProgressCallback(install); |
|
288 }, |
|
289 |
|
290 onDownloadEnded: function(install) { |
|
291 if (this.downloadEndedCallback) |
|
292 this.downloadEndedCallback(install); |
|
293 }, |
|
294 |
|
295 onDownloadCancelled: function(install) { |
|
296 isnot(this.runningInstalls.indexOf(install), -1, |
|
297 "Should only see cancelations for started installs"); |
|
298 this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1); |
|
299 |
|
300 if (this.downloadCancelledCallback) |
|
301 this.downloadCancelledCallback(install); |
|
302 this.checkTestEnded(); |
|
303 }, |
|
304 |
|
305 onDownloadFailed: function(install) { |
|
306 if (this.downloadFailedCallback) |
|
307 this.downloadFailedCallback(install); |
|
308 this.checkTestEnded(); |
|
309 }, |
|
310 |
|
311 onInstallStarted: function(install) { |
|
312 if (this.installStartedCallback) |
|
313 this.installStartedCallback(install); |
|
314 }, |
|
315 |
|
316 onInstallEnded: function(install, addon) { |
|
317 if (this.installEndedCallback) |
|
318 this.installEndedCallback(install, addon); |
|
319 this.installCount++; |
|
320 this.checkTestEnded(); |
|
321 }, |
|
322 |
|
323 onInstallFailed: function(install) { |
|
324 if (this.installFailedCallback) |
|
325 this.installFailedCallback(install); |
|
326 this.checkTestEnded(); |
|
327 }, |
|
328 |
|
329 checkTestEnded: function() { |
|
330 if (--this.pendingCount == 0) |
|
331 this.endTest(); |
|
332 }, |
|
333 |
|
334 // nsIObserver |
|
335 |
|
336 observe: function(subject, topic, data) { |
|
337 var installInfo = subject.QueryInterface(Components.interfaces.amIWebInstallInfo); |
|
338 switch (topic) { |
|
339 case "addon-install-started": |
|
340 is(this.runningInstalls.length, installInfo.installs.length, |
|
341 "Should have seen the expected number of installs started"); |
|
342 break; |
|
343 case "addon-install-disabled": |
|
344 this.installDisabled(installInfo); |
|
345 break; |
|
346 case "addon-install-cancelled": |
|
347 this.installCancelled(installInfo); |
|
348 break; |
|
349 case "addon-install-blocked": |
|
350 this.installBlocked(installInfo); |
|
351 break; |
|
352 case "addon-install-failed": |
|
353 installInfo.installs.forEach(function(aInstall) { |
|
354 isnot(this.runningInstalls.indexOf(aInstall), -1, |
|
355 "Should only see failures for started installs"); |
|
356 |
|
357 ok(aInstall.error != 0 || aInstall.addon.appDisabled, |
|
358 "Failed installs should have an error or be appDisabled"); |
|
359 |
|
360 this.runningInstalls.splice(this.runningInstalls.indexOf(aInstall), 1); |
|
361 }, this); |
|
362 break; |
|
363 case "addon-install-complete": |
|
364 installInfo.installs.forEach(function(aInstall) { |
|
365 isnot(this.runningInstalls.indexOf(aInstall), -1, |
|
366 "Should only see completed events for started installs"); |
|
367 |
|
368 is(aInstall.error, 0, "Completed installs should have no error"); |
|
369 ok(!aInstall.appDisabled, "Completed installs should not be appDisabled"); |
|
370 |
|
371 // Complete installs are either in the INSTALLED or CANCELLED state |
|
372 // since the test may cancel installs the moment they complete. |
|
373 ok(aInstall.state == AddonManager.STATE_INSTALLED || |
|
374 aInstall.state == AddonManager.STATE_CANCELLED, |
|
375 "Completed installs should be in the right state"); |
|
376 |
|
377 this.runningInstalls.splice(this.runningInstalls.indexOf(aInstall), 1); |
|
378 }, this); |
|
379 break; |
|
380 } |
|
381 }, |
|
382 |
|
383 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, |
|
384 Ci.nsIWindowMediatorListener, |
|
385 Ci.nsISupports]) |
|
386 } |