toolkit/mozapps/extensions/test/browser/head.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:445b2b2da7a3
1 /* Any copyright is dedicated to the Public Domain.
2 * http://creativecommons.org/publicdomain/zero/1.0/
3 */
4
5 Components.utils.import("resource://gre/modules/NetUtil.jsm");
6
7 let tmp = {};
8 Components.utils.import("resource://gre/modules/AddonManager.jsm", tmp);
9 Components.utils.import("resource://gre/modules/Log.jsm", tmp);
10 let AddonManager = tmp.AddonManager;
11 let AddonManagerPrivate = tmp.AddonManagerPrivate;
12 let Log = tmp.Log;
13
14 var pathParts = gTestPath.split("/");
15 // Drop the test filename
16 pathParts.splice(pathParts.length - 1, pathParts.length);
17
18 var gTestInWindow = /-window$/.test(pathParts[pathParts.length - 1]);
19
20 // Drop the UI type
21 if (gTestInWindow) {
22 pathParts.splice(pathParts.length - 1, pathParts.length);
23 }
24
25 const RELATIVE_DIR = pathParts.slice(4).join("/") + "/";
26
27 const TESTROOT = "http://example.com/" + RELATIVE_DIR;
28 const TESTROOT2 = "http://example.org/" + RELATIVE_DIR;
29 const CHROMEROOT = pathParts.join("/") + "/";
30 const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
31 const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
32 const PREF_XPI_ENABLED = "xpinstall.enabled";
33 const PREF_UPDATEURL = "extensions.update.url";
34 const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
35
36 const MANAGER_URI = "about:addons";
37 const INSTALL_URI = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
38 const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
39 const PREF_SEARCH_MAXRESULTS = "extensions.getAddons.maxResults";
40 const PREF_STRICT_COMPAT = "extensions.strictCompatibility";
41
42 var PREF_CHECK_COMPATIBILITY;
43 (function() {
44 var channel = "default";
45 try {
46 channel = Services.prefs.getCharPref("app.update.channel");
47 } catch (e) { }
48 if (channel != "aurora" &&
49 channel != "beta" &&
50 channel != "release") {
51 var version = "nightly";
52 } else {
53 version = Services.appinfo.version.replace(/^([^\.]+\.[0-9]+[a-z]*).*/gi, "$1");
54 }
55 PREF_CHECK_COMPATIBILITY = "extensions.checkCompatibility." + version;
56 })();
57
58 var gPendingTests = [];
59 var gTestsRun = 0;
60 var gTestStart = null;
61
62 var gUseInContentUI = !gTestInWindow && ("switchToTabHavingURI" in window);
63
64 var gRestorePrefs = [{name: PREF_LOGGING_ENABLED},
65 {name: "extensions.webservice.discoverURL"},
66 {name: "extensions.update.url"},
67 {name: "extensions.update.background.url"},
68 {name: "extensions.update.enabled"},
69 {name: "extensions.update.autoUpdateDefault"},
70 {name: "extensions.getAddons.get.url"},
71 {name: "extensions.getAddons.getWithPerformance.url"},
72 {name: "extensions.getAddons.search.browseURL"},
73 {name: "extensions.getAddons.search.url"},
74 {name: "extensions.getAddons.cache.enabled"},
75 {name: "devtools.chrome.enabled"},
76 {name: "devtools.debugger.remote-enabled"},
77 {name: PREF_SEARCH_MAXRESULTS},
78 {name: PREF_STRICT_COMPAT},
79 {name: PREF_CHECK_COMPATIBILITY}];
80
81 for (let pref of gRestorePrefs) {
82 if (!Services.prefs.prefHasUserValue(pref.name)) {
83 pref.type = "clear";
84 continue;
85 }
86 pref.type = Services.prefs.getPrefType(pref.name);
87 if (pref.type == Services.prefs.PREF_BOOL)
88 pref.value = Services.prefs.getBoolPref(pref.name);
89 else if (pref.type == Services.prefs.PREF_INT)
90 pref.value = Services.prefs.getIntPref(pref.name);
91 else if (pref.type == Services.prefs.PREF_STRING)
92 pref.value = Services.prefs.getCharPref(pref.name);
93 }
94
95 // Turn logging on for all tests
96 Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
97
98 // Helper to register test failures and close windows if any are left open
99 function checkOpenWindows(aWindowID) {
100 let windows = Services.wm.getEnumerator(aWindowID);
101 let found = false;
102 while (windows.hasMoreElements()) {
103 let win = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
104 if (!win.closed) {
105 found = true;
106 win.close();
107 }
108 }
109 if (found)
110 ok(false, "Found unexpected " + aWindowID + " window still open");
111 }
112
113 registerCleanupFunction(function() {
114 // Restore prefs
115 for (let pref of gRestorePrefs) {
116 if (pref.type == "clear")
117 Services.prefs.clearUserPref(pref.name);
118 else if (pref.type == Services.prefs.PREF_BOOL)
119 Services.prefs.setBoolPref(pref.name, pref.value);
120 else if (pref.type == Services.prefs.PREF_INT)
121 Services.prefs.setIntPref(pref.name, pref.value);
122 else if (pref.type == Services.prefs.PREF_STRING)
123 Services.prefs.setCharPref(pref.name, pref.value);
124 }
125
126 // Throw an error if the add-ons manager window is open anywhere
127 checkOpenWindows("Addons:Manager");
128 checkOpenWindows("Addons:Compatibility");
129 checkOpenWindows("Addons:Install");
130
131 return new Promise((resolve, reject) => AddonManager.getAllInstalls(resolve))
132 .then(aInstalls => {
133 for (let install of aInstalls) {
134 if (install instanceof MockInstall)
135 continue;
136
137 ok(false, "Should not have seen an install of " + install.sourceURI.spec + " in state " + install.state);
138 install.cancel();
139 }
140 });
141 });
142
143 function log_exceptions(aCallback, ...aArgs) {
144 try {
145 return aCallback.apply(null, aArgs);
146 }
147 catch (e) {
148 info("Exception thrown: " + e);
149 throw e;
150 }
151 }
152
153 function log_callback(aPromise, aCallback) {
154 aPromise.then(aCallback)
155 .then(null, e => info("Exception thrown: " + e));
156 return aPromise;
157 }
158
159 function add_test(test) {
160 gPendingTests.push(test);
161 }
162
163 function run_next_test() {
164 if (gTestsRun > 0)
165 info("Test " + gTestsRun + " took " + (Date.now() - gTestStart) + "ms");
166
167 if (gPendingTests.length == 0) {
168 end_test();
169 return;
170 }
171
172 gTestsRun++;
173 var test = gPendingTests.shift();
174 if (test.name)
175 info("Running test " + gTestsRun + " (" + test.name + ")");
176 else
177 info("Running test " + gTestsRun);
178
179 gTestStart = Date.now();
180 log_exceptions(test);
181 }
182
183 function get_addon_file_url(aFilename) {
184 try {
185 var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].
186 getService(Ci.nsIChromeRegistry);
187 var fileurl = cr.convertChromeURL(makeURI(CHROMEROOT + "addons/" + aFilename));
188 return fileurl.QueryInterface(Ci.nsIFileURL);
189 } catch(ex) {
190 var jar = getJar(CHROMEROOT + "addons/" + aFilename);
191 var tmpDir = extractJarToTmp(jar);
192 tmpDir.append(aFilename);
193
194 return Services.io.newFileURI(tmpDir).QueryInterface(Ci.nsIFileURL);
195 }
196 }
197
198 function get_test_items_in_list(aManager) {
199 var tests = "@tests.mozilla.org";
200
201 let view = aManager.document.getElementById("view-port").selectedPanel;
202 let listid = view.id == "search-view" ? "search-list" : "addon-list";
203 let item = aManager.document.getElementById(listid).firstChild;
204 let items = [];
205
206 while (item) {
207 if (item.localName != "richlistitem") {
208 item = item.nextSibling;
209 continue;
210 }
211
212 if (!item.mAddon || item.mAddon.id.substring(item.mAddon.id.length - tests.length) == tests)
213 items.push(item);
214 item = item.nextSibling;
215 }
216
217 return items;
218 }
219
220 function check_all_in_list(aManager, aIds, aIgnoreExtras) {
221 var doc = aManager.document;
222 var view = doc.getElementById("view-port").selectedPanel;
223 var listid = view.id == "search-view" ? "search-list" : "addon-list";
224 var list = doc.getElementById(listid);
225
226 var inlist = [];
227 var node = list.firstChild;
228 while (node) {
229 if (node.value)
230 inlist.push(node.value);
231 node = node.nextSibling;
232 }
233
234 for (let id of aIds) {
235 if (inlist.indexOf(id) == -1)
236 ok(false, "Should find " + id + " in the list");
237 }
238
239 if (aIgnoreExtras)
240 return;
241
242 for (let inlistItem of inlist) {
243 if (aIds.indexOf(inlistItem) == -1)
244 ok(false, "Shouldn't have seen " + inlistItem + " in the list");
245 }
246 }
247
248 function get_addon_element(aManager, aId) {
249 var doc = aManager.document;
250 var view = doc.getElementById("view-port").selectedPanel;
251 var listid = "addon-list";
252 if (view.id == "search-view")
253 listid = "search-list";
254 else if (view.id == "updates-view")
255 listid = "updates-list";
256 var list = doc.getElementById(listid);
257
258 var node = list.firstChild;
259 while (node) {
260 if (node.value == aId)
261 return node;
262 node = node.nextSibling;
263 }
264 return null;
265 }
266
267 function wait_for_view_load(aManagerWindow, aCallback, aForceWait, aLongerTimeout) {
268 requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);
269
270 if (!aForceWait && !aManagerWindow.gViewController.isLoading) {
271 log_exceptions(aCallback, aManagerWindow);
272 return;
273 }
274
275 aManagerWindow.document.addEventListener("ViewChanged", function() {
276 aManagerWindow.document.removeEventListener("ViewChanged", arguments.callee, false);
277 log_exceptions(aCallback, aManagerWindow);
278 }, false);
279 }
280
281 function wait_for_manager_load(aManagerWindow, aCallback) {
282 if (!aManagerWindow.gIsInitializing) {
283 log_exceptions(aCallback, aManagerWindow);
284 return;
285 }
286
287 info("Waiting for initialization");
288 aManagerWindow.document.addEventListener("Initialized", function() {
289 aManagerWindow.document.removeEventListener("Initialized", arguments.callee, false);
290 log_exceptions(aCallback, aManagerWindow);
291 }, false);
292 }
293
294 function open_manager(aView, aCallback, aLoadCallback, aLongerTimeout) {
295 let p = new Promise((resolve, reject) => {
296
297 function setup_manager(aManagerWindow) {
298 if (aLoadCallback)
299 log_exceptions(aLoadCallback, aManagerWindow);
300
301 if (aView)
302 aManagerWindow.loadView(aView);
303
304 ok(aManagerWindow != null, "Should have an add-ons manager window");
305 is(aManagerWindow.location, MANAGER_URI, "Should be displaying the correct UI");
306
307 waitForFocus(function() {
308 info("window has focus, waiting for manager load");
309 wait_for_manager_load(aManagerWindow, function() {
310 info("Manager waiting for view load");
311 wait_for_view_load(aManagerWindow, function() {
312 resolve(aManagerWindow);
313 }, null, aLongerTimeout);
314 });
315 }, aManagerWindow);
316 }
317
318 if (gUseInContentUI) {
319 info("Loading manager window in tab");
320 Services.obs.addObserver(function (aSubject, aTopic, aData) {
321 Services.obs.removeObserver(arguments.callee, aTopic);
322 if (aSubject.location.href != MANAGER_URI) {
323 info("Ignoring load event for " + aSubject.location.href);
324 return;
325 }
326 setup_manager(aSubject);
327 }, "EM-loaded", false);
328
329 gBrowser.selectedTab = gBrowser.addTab();
330 switchToTabHavingURI(MANAGER_URI, true);
331 } else {
332 info("Loading manager window in dialog");
333 Services.obs.addObserver(function (aSubject, aTopic, aData) {
334 Services.obs.removeObserver(arguments.callee, aTopic);
335 setup_manager(aSubject);
336 }, "EM-loaded", false);
337
338 openDialog(MANAGER_URI);
339 }
340 });
341
342 // The promise resolves with the manager window, so it is passed to the callback
343 return log_callback(p, aCallback);
344 }
345
346 function close_manager(aManagerWindow, aCallback, aLongerTimeout) {
347 let p = new Promise((resolve, reject) => {
348 requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);
349
350 ok(aManagerWindow != null, "Should have an add-ons manager window to close");
351 is(aManagerWindow.location, MANAGER_URI, "Should be closing window with correct URI");
352
353 aManagerWindow.addEventListener("unload", function() {
354 try {
355 dump("Manager window unload handler");
356 this.removeEventListener("unload", arguments.callee, false);
357 resolve();
358 } catch(e) {
359 reject(e);
360 }
361 }, false);
362 });
363
364 info("Telling manager window to close");
365 aManagerWindow.close();
366 info("Manager window close() call returned");
367
368 return log_callback(p, aCallback);
369 }
370
371 function restart_manager(aManagerWindow, aView, aCallback, aLoadCallback) {
372 if (!aManagerWindow) {
373 return open_manager(aView, aCallback, aLoadCallback);
374 }
375
376 return close_manager(aManagerWindow)
377 .then(() => open_manager(aView, aCallback, aLoadCallback));
378 }
379
380 function wait_for_window_open(aCallback) {
381 Services.wm.addListener({
382 onOpenWindow: function(aWindow) {
383 Services.wm.removeListener(this);
384
385 let domwindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
386 .getInterface(Ci.nsIDOMWindow);
387 domwindow.addEventListener("load", function() {
388 domwindow.removeEventListener("load", arguments.callee, false);
389 executeSoon(function() {
390 aCallback(domwindow);
391 });
392 }, false);
393 },
394
395 onCloseWindow: function(aWindow) {
396 },
397
398 onWindowTitleChange: function(aWindow, aTitle) {
399 }
400 });
401 }
402
403 function get_string(aName, ...aArgs) {
404 var bundle = Services.strings.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
405 if (aArgs.length == 0)
406 return bundle.GetStringFromName(aName);
407 return bundle.formatStringFromName(aName, aArgs, aArgs.length);
408 }
409
410 function formatDate(aDate) {
411 return Cc["@mozilla.org/intl/scriptabledateformat;1"]
412 .getService(Ci.nsIScriptableDateFormat)
413 .FormatDate("",
414 Ci.nsIScriptableDateFormat.dateFormatLong,
415 aDate.getFullYear(),
416 aDate.getMonth() + 1,
417 aDate.getDate()
418 );
419 }
420
421 function is_hidden(aElement) {
422 var style = aElement.ownerDocument.defaultView.getComputedStyle(aElement, "");
423 if (style.display == "none")
424 return true;
425 if (style.visibility != "visible")
426 return true;
427
428 // Hiding a parent element will hide all its children
429 if (aElement.parentNode != aElement.ownerDocument)
430 return is_hidden(aElement.parentNode);
431
432 return false;
433 }
434
435 function is_element_visible(aElement, aMsg) {
436 isnot(aElement, null, "Element should not be null, when checking visibility");
437 ok(!is_hidden(aElement), aMsg);
438 }
439
440 function is_element_hidden(aElement, aMsg) {
441 isnot(aElement, null, "Element should not be null, when checking visibility");
442 ok(is_hidden(aElement), aMsg);
443 }
444
445 /**
446 * Install an add-on and call a callback when complete.
447 *
448 * The callback will receive the Addon for the installed add-on.
449 */
450 function install_addon(path, cb, pathPrefix=TESTROOT) {
451 let p = new Promise((resolve, reject) => {
452 AddonManager.getInstallForURL(pathPrefix + path, (install) => {
453 install.addListener({
454 onInstallEnded: () => resolve(install.addon),
455 });
456
457 install.install();
458 }, "application/x-xpinstall");
459 });
460
461 return log_callback(p, cb);
462 }
463
464 function CategoryUtilities(aManagerWindow) {
465 this.window = aManagerWindow;
466
467 var self = this;
468 this.window.addEventListener("unload", function() {
469 self.window.removeEventListener("unload", arguments.callee, false);
470 self.window = null;
471 }, false);
472 }
473
474 CategoryUtilities.prototype = {
475 window: null,
476
477 get selectedCategory() {
478 isnot(this.window, null, "Should not get selected category when manager window is not loaded");
479 var selectedItem = this.window.document.getElementById("categories").selectedItem;
480 isnot(selectedItem, null, "A category should be selected");
481 var view = this.window.gViewController.parseViewId(selectedItem.value);
482 return (view.type == "list") ? view.param : view.type;
483 },
484
485 get: function(aCategoryType, aAllowMissing) {
486 isnot(this.window, null, "Should not get category when manager window is not loaded");
487 var categories = this.window.document.getElementById("categories");
488
489 var viewId = "addons://list/" + aCategoryType;
490 var items = categories.getElementsByAttribute("value", viewId);
491 if (items.length)
492 return items[0];
493
494 viewId = "addons://" + aCategoryType + "/";
495 items = categories.getElementsByAttribute("value", viewId);
496 if (items.length)
497 return items[0];
498
499 if (!aAllowMissing)
500 ok(false, "Should have found a category with type " + aCategoryType);
501 return null;
502 },
503
504 getViewId: function(aCategoryType) {
505 isnot(this.window, null, "Should not get view id when manager window is not loaded");
506 return this.get(aCategoryType).value;
507 },
508
509 isVisible: function(aCategory) {
510 isnot(this.window, null, "Should not check visible state when manager window is not loaded");
511 if (aCategory.hasAttribute("disabled") &&
512 aCategory.getAttribute("disabled") == "true")
513 return false;
514
515 return !is_hidden(aCategory);
516 },
517
518 isTypeVisible: function(aCategoryType) {
519 return this.isVisible(this.get(aCategoryType));
520 },
521
522 open: function(aCategory, aCallback) {
523
524 isnot(this.window, null, "Should not open category when manager window is not loaded");
525 ok(this.isVisible(aCategory), "Category should be visible if attempting to open it");
526
527 EventUtils.synthesizeMouse(aCategory, 2, 2, { }, this.window);
528 let p = new Promise((resolve, reject) => wait_for_view_load(this.window, resolve));
529
530 return log_callback(p, aCallback);
531 },
532
533 openType: function(aCategoryType, aCallback) {
534 return this.open(this.get(aCategoryType), aCallback);
535 }
536 }
537
538 function CertOverrideListener(host, bits) {
539 this.host = host;
540 this.bits = bits;
541 }
542
543 CertOverrideListener.prototype = {
544 host: null,
545 bits: null,
546
547 getInterface: function (aIID) {
548 return this.QueryInterface(aIID);
549 },
550
551 QueryInterface: function(aIID) {
552 if (aIID.equals(Ci.nsIBadCertListener2) ||
553 aIID.equals(Ci.nsIInterfaceRequestor) ||
554 aIID.equals(Ci.nsISupports))
555 return this;
556
557 throw Components.Exception("No interface", Components.results.NS_ERROR_NO_INTERFACE);
558 },
559
560 notifyCertProblem: function (socketInfo, sslStatus, targetHost) {
561 var cert = sslStatus.QueryInterface(Components.interfaces.nsISSLStatus)
562 .serverCert;
563 var cos = Cc["@mozilla.org/security/certoverride;1"].
564 getService(Ci.nsICertOverrideService);
565 cos.rememberValidityOverride(this.host, -1, cert, this.bits, false);
566 return true;
567 }
568 }
569
570 // Add overrides for the bad certificates
571 function addCertOverride(host, bits) {
572 var req = new XMLHttpRequest();
573 try {
574 req.open("GET", "https://" + host + "/", false);
575 req.channel.notificationCallbacks = new CertOverrideListener(host, bits);
576 req.send(null);
577 }
578 catch (e) {
579 // This request will fail since the SSL server is not trusted yet
580 }
581 }
582
583 /***** Mock Provider *****/
584
585 function MockProvider(aUseAsyncCallbacks, aTypes) {
586 this.addons = [];
587 this.installs = [];
588 this.callbackTimers = [];
589 this.timerLocations = new Map();
590 this.useAsyncCallbacks = (aUseAsyncCallbacks === undefined) ? true : aUseAsyncCallbacks;
591 this.types = (aTypes === undefined) ? [{
592 id: "extension",
593 name: "Extensions",
594 uiPriority: 4000,
595 flags: AddonManager.TYPE_UI_VIEW_LIST
596 }] : aTypes;
597
598 var self = this;
599 registerCleanupFunction(function() {
600 if (self.started)
601 self.unregister();
602 });
603
604 this.register();
605 }
606
607 MockProvider.prototype = {
608 addons: null,
609 installs: null,
610 started: null,
611 apiDelay: 10,
612 callbackTimers: null,
613 timerLocations: null,
614 useAsyncCallbacks: null,
615 types: null,
616
617 /***** Utility functions *****/
618
619 /**
620 * Register this provider with the AddonManager
621 */
622 register: function MP_register() {
623 AddonManagerPrivate.registerProvider(this, this.types);
624 },
625
626 /**
627 * Unregister this provider with the AddonManager
628 */
629 unregister: function MP_unregister() {
630 AddonManagerPrivate.unregisterProvider(this);
631 },
632
633 /**
634 * Adds an add-on to the list of add-ons that this provider exposes to the
635 * AddonManager, dispatching appropriate events in the process.
636 *
637 * @param aAddon
638 * The add-on to add
639 */
640 addAddon: function MP_addAddon(aAddon) {
641 var oldAddons = this.addons.filter(function(aOldAddon) aOldAddon.id == aAddon.id);
642 var oldAddon = oldAddons.length > 0 ? oldAddons[0] : null;
643
644 this.addons = this.addons.filter(function(aOldAddon) aOldAddon.id != aAddon.id);
645
646 this.addons.push(aAddon);
647 aAddon._provider = this;
648
649 if (!this.started)
650 return;
651
652 let requiresRestart = (aAddon.operationsRequiringRestart &
653 AddonManager.OP_NEEDS_RESTART_INSTALL) != 0;
654 AddonManagerPrivate.callInstallListeners("onExternalInstall", null, aAddon,
655 oldAddon, requiresRestart)
656 },
657
658 /**
659 * Removes an add-on from the list of add-ons that this provider exposes to
660 * the AddonManager, dispatching the onUninstalled event in the process.
661 *
662 * @param aAddon
663 * The add-on to add
664 */
665 removeAddon: function MP_removeAddon(aAddon) {
666 var pos = this.addons.indexOf(aAddon);
667 if (pos == -1) {
668 ok(false, "Tried to remove an add-on that wasn't registered with the mock provider");
669 return;
670 }
671
672 this.addons.splice(pos, 1);
673
674 if (!this.started)
675 return;
676
677 AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon);
678 },
679
680 /**
681 * Adds an add-on install to the list of installs that this provider exposes
682 * to the AddonManager, dispatching appropriate events in the process.
683 *
684 * @param aInstall
685 * The add-on install to add
686 */
687 addInstall: function MP_addInstall(aInstall) {
688 this.installs.push(aInstall);
689 aInstall._provider = this;
690
691 if (!this.started)
692 return;
693
694 aInstall.callListeners("onNewInstall");
695 },
696
697 removeInstall: function MP_removeInstall(aInstall) {
698 var pos = this.installs.indexOf(aInstall);
699 if (pos == -1) {
700 ok(false, "Tried to remove an install that wasn't registered with the mock provider");
701 return;
702 }
703
704 this.installs.splice(pos, 1);
705 },
706
707 /**
708 * Creates a set of mock add-on objects and adds them to the list of add-ons
709 * managed by this provider.
710 *
711 * @param aAddonProperties
712 * An array of objects containing properties describing the add-ons
713 * @return Array of the new MockAddons
714 */
715 createAddons: function MP_createAddons(aAddonProperties) {
716 var newAddons = [];
717 for (let addonProp of aAddonProperties) {
718 let addon = new MockAddon(addonProp.id);
719 for (let prop in addonProp) {
720 if (prop == "id")
721 continue;
722 if (prop == "applyBackgroundUpdates") {
723 addon._applyBackgroundUpdates = addonProp[prop];
724 continue;
725 }
726 if (prop == "appDisabled") {
727 addon._appDisabled = addonProp[prop];
728 continue;
729 }
730 addon[prop] = addonProp[prop];
731 }
732 if (!addon.optionsType && !!addon.optionsURL)
733 addon.optionsType = AddonManager.OPTIONS_TYPE_DIALOG;
734
735 // Make sure the active state matches the passed in properties
736 addon.isActive = addon.shouldBeActive;
737
738 this.addAddon(addon);
739 newAddons.push(addon);
740 }
741
742 return newAddons;
743 },
744
745 /**
746 * Creates a set of mock add-on install objects and adds them to the list
747 * of installs managed by this provider.
748 *
749 * @param aInstallProperties
750 * An array of objects containing properties describing the installs
751 * @return Array of the new MockInstalls
752 */
753 createInstalls: function MP_createInstalls(aInstallProperties) {
754 var newInstalls = [];
755 for (let installProp of aInstallProperties) {
756 let install = new MockInstall(installProp.name || null,
757 installProp.type || null,
758 null);
759 for (let prop in installProp) {
760 switch (prop) {
761 case "name":
762 case "type":
763 break;
764 case "sourceURI":
765 install[prop] = NetUtil.newURI(installProp[prop]);
766 break;
767 default:
768 install[prop] = installProp[prop];
769 }
770 }
771 this.addInstall(install);
772 newInstalls.push(install);
773 }
774
775 return newInstalls;
776 },
777
778 /***** AddonProvider implementation *****/
779
780 /**
781 * Called to initialize the provider.
782 */
783 startup: function MP_startup() {
784 this.started = true;
785 },
786
787 /**
788 * Called when the provider should shutdown.
789 */
790 shutdown: function MP_shutdown() {
791 if (this.callbackTimers.length) {
792 info("MockProvider: pending callbacks at shutdown(): calling immediately");
793 }
794 while (this.callbackTimers.length > 0) {
795 // When we notify the callback timer, it removes itself from our array
796 let timer = this.callbackTimers[0];
797 try {
798 let setAt = this.timerLocations.get(timer);
799 info("Notifying timer set at " + (setAt || "unknown location"));
800 timer.callback.notify(timer);
801 timer.cancel();
802 } catch(e) {
803 info("Timer notify failed: " + e);
804 }
805 }
806 this.callbackTimers = [];
807 this.timerLocations = null;
808
809 this.started = false;
810 },
811
812 /**
813 * Called to get an Addon with a particular ID.
814 *
815 * @param aId
816 * The ID of the add-on to retrieve
817 * @param aCallback
818 * A callback to pass the Addon to
819 */
820 getAddonByID: function MP_getAddon(aId, aCallback) {
821 for (let addon of this.addons) {
822 if (addon.id == aId) {
823 this._delayCallback(aCallback, addon);
824 return;
825 }
826 }
827
828 aCallback(null);
829 },
830
831 /**
832 * Called to get Addons of a particular type.
833 *
834 * @param aTypes
835 * An array of types to fetch. Can be null to get all types.
836 * @param callback
837 * A callback to pass an array of Addons to
838 */
839 getAddonsByTypes: function MP_getAddonsByTypes(aTypes, aCallback) {
840 var addons = this.addons.filter(function(aAddon) {
841 if (aTypes && aTypes.length > 0 && aTypes.indexOf(aAddon.type) == -1)
842 return false;
843 return true;
844 });
845 this._delayCallback(aCallback, addons);
846 },
847
848 /**
849 * Called to get Addons that have pending operations.
850 *
851 * @param aTypes
852 * An array of types to fetch. Can be null to get all types
853 * @param aCallback
854 * A callback to pass an array of Addons to
855 */
856 getAddonsWithOperationsByTypes: function MP_getAddonsWithOperationsByTypes(aTypes, aCallback) {
857 var addons = this.addons.filter(function(aAddon) {
858 if (aTypes && aTypes.length > 0 && aTypes.indexOf(aAddon.type) == -1)
859 return false;
860 return aAddon.pendingOperations != 0;
861 });
862 this._delayCallback(aCallback, addons);
863 },
864
865 /**
866 * Called to get the current AddonInstalls, optionally restricting by type.
867 *
868 * @param aTypes
869 * An array of types or null to get all types
870 * @param aCallback
871 * A callback to pass the array of AddonInstalls to
872 */
873 getInstallsByTypes: function MP_getInstallsByTypes(aTypes, aCallback) {
874 var installs = this.installs.filter(function(aInstall) {
875 // Appear to have actually removed cancelled installs from the provider
876 if (aInstall.state == AddonManager.STATE_CANCELLED)
877 return false;
878
879 if (aTypes && aTypes.length > 0 && aTypes.indexOf(aInstall.type) == -1)
880 return false;
881
882 return true;
883 });
884 this._delayCallback(aCallback, installs);
885 },
886
887 /**
888 * Called when a new add-on has been enabled when only one add-on of that type
889 * can be enabled.
890 *
891 * @param aId
892 * The ID of the newly enabled add-on
893 * @param aType
894 * The type of the newly enabled add-on
895 * @param aPendingRestart
896 * true if the newly enabled add-on will only become enabled after a
897 * restart
898 */
899 addonChanged: function MP_addonChanged(aId, aType, aPendingRestart) {
900 // Not implemented
901 },
902
903 /**
904 * Update the appDisabled property for all add-ons.
905 */
906 updateAddonAppDisabledStates: function MP_updateAddonAppDisabledStates() {
907 // Not needed
908 },
909
910 /**
911 * Called to get an AddonInstall to download and install an add-on from a URL.
912 *
913 * @param aUrl
914 * The URL to be installed
915 * @param aHash
916 * A hash for the install
917 * @param aName
918 * A name for the install
919 * @param aIconURL
920 * An icon URL for the install
921 * @param aVersion
922 * A version for the install
923 * @param aLoadGroup
924 * An nsILoadGroup to associate requests with
925 * @param aCallback
926 * A callback to pass the AddonInstall to
927 */
928 getInstallForURL: function MP_getInstallForURL(aUrl, aHash, aName, aIconURL,
929 aVersion, aLoadGroup, aCallback) {
930 // Not yet implemented
931 },
932
933 /**
934 * Called to get an AddonInstall to install an add-on from a local file.
935 *
936 * @param aFile
937 * The file to be installed
938 * @param aCallback
939 * A callback to pass the AddonInstall to
940 */
941 getInstallForFile: function MP_getInstallForFile(aFile, aCallback) {
942 // Not yet implemented
943 },
944
945 /**
946 * Called to test whether installing add-ons is enabled.
947 *
948 * @return true if installing is enabled
949 */
950 isInstallEnabled: function MP_isInstallEnabled() {
951 return false;
952 },
953
954 /**
955 * Called to test whether this provider supports installing a particular
956 * mimetype.
957 *
958 * @param aMimetype
959 * The mimetype to check for
960 * @return true if the mimetype is supported
961 */
962 supportsMimetype: function MP_supportsMimetype(aMimetype) {
963 return false;
964 },
965
966 /**
967 * Called to test whether installing add-ons from a URI is allowed.
968 *
969 * @param aUri
970 * The URI being installed from
971 * @return true if installing is allowed
972 */
973 isInstallAllowed: function MP_isInstallAllowed(aUri) {
974 return false;
975 },
976
977
978 /***** Internal functions *****/
979
980 /**
981 * Delay calling a callback to fake a time-consuming async operation.
982 * The delay is specified by the apiDelay property, in milliseconds.
983 * Parameters to send to the callback should be specified as arguments after
984 * the aCallback argument.
985 *
986 * @param aCallback Callback to eventually call
987 */
988 _delayCallback: function MP_delayCallback(aCallback, ...aArgs) {
989 if (!this.useAsyncCallbacks) {
990 aCallback(...aArgs);
991 return;
992 }
993
994 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
995 // Need to keep a reference to the timer, so it doesn't get GC'ed
996 this.callbackTimers.push(timer);
997 // Capture a stack trace where the timer was set
998 // needs the 'new Error' hack until bug 1007656
999 this.timerLocations.set(timer, Log.stackTrace(new Error("dummy")));
1000 timer.initWithCallback(() => {
1001 let idx = this.callbackTimers.indexOf(timer);
1002 if (idx == -1) {
1003 dump("MockProvider._delayCallback lost track of timer set at "
1004 + (this.timerLocations.get(timer) || "unknown location") + "\n");
1005 } else {
1006 this.callbackTimers.splice(idx, 1);
1007 }
1008 this.timerLocations.delete(timer);
1009 aCallback(...aArgs);
1010 }, this.apiDelay, timer.TYPE_ONE_SHOT);
1011 }
1012 };
1013
1014 /***** Mock Addon object for the Mock Provider *****/
1015
1016 function MockAddon(aId, aName, aType, aOperationsRequiringRestart) {
1017 // Only set required attributes.
1018 this.id = aId || "";
1019 this.name = aName || "";
1020 this.type = aType || "extension";
1021 this.version = "";
1022 this.isCompatible = true;
1023 this.isDebuggable = false;
1024 this.providesUpdatesSecurely = true;
1025 this.blocklistState = 0;
1026 this._appDisabled = false;
1027 this._userDisabled = false;
1028 this._applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
1029 this.scope = AddonManager.SCOPE_PROFILE;
1030 this.isActive = true;
1031 this.creator = "";
1032 this.pendingOperations = 0;
1033 this._permissions = AddonManager.PERM_CAN_UNINSTALL |
1034 AddonManager.PERM_CAN_ENABLE |
1035 AddonManager.PERM_CAN_DISABLE |
1036 AddonManager.PERM_CAN_UPGRADE;
1037 this.operationsRequiringRestart = aOperationsRequiringRestart ||
1038 (AddonManager.OP_NEEDS_RESTART_INSTALL |
1039 AddonManager.OP_NEEDS_RESTART_UNINSTALL |
1040 AddonManager.OP_NEEDS_RESTART_ENABLE |
1041 AddonManager.OP_NEEDS_RESTART_DISABLE);
1042 }
1043
1044 MockAddon.prototype = {
1045 get shouldBeActive() {
1046 return !this.appDisabled && !this._userDisabled;
1047 },
1048
1049 get appDisabled() {
1050 return this._appDisabled;
1051 },
1052
1053 set appDisabled(val) {
1054 if (val == this._appDisabled)
1055 return val;
1056
1057 AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["appDisabled"]);
1058
1059 var currentActive = this.shouldBeActive;
1060 this._appDisabled = val;
1061 var newActive = this.shouldBeActive;
1062 this._updateActiveState(currentActive, newActive);
1063
1064 return val;
1065 },
1066
1067 get userDisabled() {
1068 return this._userDisabled;
1069 },
1070
1071 set userDisabled(val) {
1072 if (val == this._userDisabled)
1073 return val;
1074
1075 var currentActive = this.shouldBeActive;
1076 this._userDisabled = val;
1077 var newActive = this.shouldBeActive;
1078 this._updateActiveState(currentActive, newActive);
1079
1080 return val;
1081 },
1082
1083 get permissions() {
1084 let permissions = this._permissions;
1085 if (this.appDisabled || !this._userDisabled)
1086 permissions &= ~AddonManager.PERM_CAN_ENABLE;
1087 if (this.appDisabled || this._userDisabled)
1088 permissions &= ~AddonManager.PERM_CAN_DISABLE;
1089 return permissions;
1090 },
1091
1092 set permissions(val) {
1093 return this._permissions = val;
1094 },
1095
1096 get applyBackgroundUpdates() {
1097 return this._applyBackgroundUpdates;
1098 },
1099
1100 set applyBackgroundUpdates(val) {
1101 if (val != AddonManager.AUTOUPDATE_DEFAULT &&
1102 val != AddonManager.AUTOUPDATE_DISABLE &&
1103 val != AddonManager.AUTOUPDATE_ENABLE) {
1104 ok(false, "addon.applyBackgroundUpdates set to an invalid value: " + val);
1105 }
1106 this._applyBackgroundUpdates = val;
1107 AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]);
1108 },
1109
1110 isCompatibleWith: function(aAppVersion, aPlatformVersion) {
1111 return true;
1112 },
1113
1114 findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
1115 // Tests can implement this if they need to
1116 },
1117
1118 uninstall: function() {
1119 if (this.pendingOperations & AddonManager.PENDING_UNINSTALL)
1120 throw Components.Exception("Add-on is already pending uninstall");
1121
1122 var needsRestart = !!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL);
1123 this.pendingOperations |= AddonManager.PENDING_UNINSTALL;
1124 AddonManagerPrivate.callAddonListeners("onUninstalling", this, needsRestart);
1125 if (!needsRestart) {
1126 this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
1127 this._provider.removeAddon(this);
1128 }
1129 },
1130
1131 cancelUninstall: function() {
1132 if (!(this.pendingOperations & AddonManager.PENDING_UNINSTALL))
1133 throw Components.Exception("Add-on is not pending uninstall");
1134
1135 this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
1136 AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
1137 },
1138
1139 _updateActiveState: function(currentActive, newActive) {
1140 if (currentActive == newActive)
1141 return;
1142
1143 if (newActive == this.isActive) {
1144 this.pendingOperations -= (newActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE);
1145 AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
1146 }
1147 else if (newActive) {
1148 var needsRestart = !!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE);
1149 this.pendingOperations |= AddonManager.PENDING_ENABLE;
1150 AddonManagerPrivate.callAddonListeners("onEnabling", this, needsRestart);
1151 if (!needsRestart) {
1152 this.isActive = newActive;
1153 this.pendingOperations -= AddonManager.PENDING_ENABLE;
1154 AddonManagerPrivate.callAddonListeners("onEnabled", this);
1155 }
1156 }
1157 else {
1158 var needsRestart = !!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE);
1159 this.pendingOperations |= AddonManager.PENDING_DISABLE;
1160 AddonManagerPrivate.callAddonListeners("onDisabling", this, needsRestart);
1161 if (!needsRestart) {
1162 this.isActive = newActive;
1163 this.pendingOperations -= AddonManager.PENDING_DISABLE;
1164 AddonManagerPrivate.callAddonListeners("onDisabled", this);
1165 }
1166 }
1167 }
1168 };
1169
1170 /***** Mock AddonInstall object for the Mock Provider *****/
1171
1172 function MockInstall(aName, aType, aAddonToInstall) {
1173 this.name = aName || "";
1174 // Don't expose type until download completed
1175 this._type = aType || "extension";
1176 this.type = null;
1177 this.version = "1.0";
1178 this.iconURL = "";
1179 this.infoURL = "";
1180 this.state = AddonManager.STATE_AVAILABLE;
1181 this.error = 0;
1182 this.sourceURI = null;
1183 this.file = null;
1184 this.progress = 0;
1185 this.maxProgress = -1;
1186 this.certificate = null;
1187 this.certName = "";
1188 this.existingAddon = null;
1189 this.addon = null;
1190 this._addonToInstall = aAddonToInstall;
1191 this.listeners = [];
1192
1193 // Another type of install listener for tests that want to check the results
1194 // of code run from standard install listeners
1195 this.testListeners = [];
1196 }
1197
1198 MockInstall.prototype = {
1199 install: function() {
1200 switch (this.state) {
1201 case AddonManager.STATE_AVAILABLE:
1202 this.state = AddonManager.STATE_DOWNLOADING;
1203 if (!this.callListeners("onDownloadStarted")) {
1204 this.state = AddonManager.STATE_CANCELLED;
1205 this.callListeners("onDownloadCancelled");
1206 return;
1207 }
1208
1209 this.type = this._type;
1210
1211 // Adding addon to MockProvider to be implemented when needed
1212 if (this._addonToInstall)
1213 this.addon = this._addonToInstall;
1214 else {
1215 this.addon = new MockAddon("", this.name, this.type);
1216 this.addon.version = this.version;
1217 this.addon.pendingOperations = AddonManager.PENDING_INSTALL;
1218 }
1219 this.addon.install = this;
1220 if (this.existingAddon) {
1221 if (!this.addon.id)
1222 this.addon.id = this.existingAddon.id;
1223 this.existingAddon.pendingUpgrade = this.addon;
1224 this.existingAddon.pendingOperations |= AddonManager.PENDING_UPGRADE;
1225 }
1226
1227 this.state = AddonManager.STATE_DOWNLOADED;
1228 this.callListeners("onDownloadEnded");
1229
1230 case AddonManager.STATE_DOWNLOADED:
1231 this.state = AddonManager.STATE_INSTALLING;
1232 if (!this.callListeners("onInstallStarted")) {
1233 this.state = AddonManager.STATE_CANCELLED;
1234 this.callListeners("onInstallCancelled");
1235 return;
1236 }
1237
1238 AddonManagerPrivate.callAddonListeners("onInstalling", this.addon);
1239
1240 this.state = AddonManager.STATE_INSTALLED;
1241 this.callListeners("onInstallEnded");
1242 break;
1243 case AddonManager.STATE_DOWNLOADING:
1244 case AddonManager.STATE_CHECKING:
1245 case AddonManger.STATE_INSTALLING:
1246 // Installation is already running
1247 return;
1248 default:
1249 ok(false, "Cannot start installing when state = " + this.state);
1250 }
1251 },
1252
1253 cancel: function() {
1254 switch (this.state) {
1255 case AddonManager.STATE_AVAILABLE:
1256 this.state = AddonManager.STATE_CANCELLED;
1257 break;
1258 case AddonManager.STATE_INSTALLED:
1259 this.state = AddonManager.STATE_CANCELLED;
1260 this._provider.removeInstall(this);
1261 this.callListeners("onInstallCancelled");
1262 break;
1263 default:
1264 // Handling cancelling when downloading to be implemented when needed
1265 ok(false, "Cannot cancel when state = " + this.state);
1266 }
1267 },
1268
1269
1270 addListener: function(aListener) {
1271 if (!this.listeners.some(function(i) i == aListener))
1272 this.listeners.push(aListener);
1273 },
1274
1275 removeListener: function(aListener) {
1276 this.listeners = this.listeners.filter(function(i) i != aListener);
1277 },
1278
1279 addTestListener: function(aListener) {
1280 if (!this.testListeners.some(function(i) i == aListener))
1281 this.testListeners.push(aListener);
1282 },
1283
1284 removeTestListener: function(aListener) {
1285 this.testListeners = this.testListeners.filter(function(i) i != aListener);
1286 },
1287
1288 callListeners: function(aMethod) {
1289 var result = AddonManagerPrivate.callInstallListeners(aMethod, this.listeners,
1290 this, this.addon);
1291
1292 // Call test listeners after standard listeners to remove race condition
1293 // between standard and test listeners
1294 for (let listener of this.testListeners) {
1295 try {
1296 if (aMethod in listener)
1297 if (listener[aMethod].call(listener, this, this.addon) === false)
1298 result = false;
1299 }
1300 catch (e) {
1301 ok(false, "Test listener threw exception: " + e);
1302 }
1303 }
1304
1305 return result;
1306 }
1307 };
1308
1309 function waitForCondition(condition, nextTest, errorMsg) {
1310 let tries = 0;
1311 let interval = setInterval(function() {
1312 if (tries >= 30) {
1313 ok(false, errorMsg);
1314 moveOn();
1315 }
1316 var conditionPassed;
1317 try {
1318 conditionPassed = condition();
1319 } catch (e) {
1320 ok(false, e + "\n" + e.stack);
1321 conditionPassed = false;
1322 }
1323 if (conditionPassed) {
1324 moveOn();
1325 }
1326 tries++;
1327 }, 100);
1328 let moveOn = function() { clearInterval(interval); nextTest(); };
1329 }
1330
1331 function getTestPluginTag() {
1332 let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
1333 let tags = ph.getPluginTags();
1334
1335 // Find the test plugin
1336 for (let i = 0; i < tags.length; i++) {
1337 if (tags[i].name == "Test Plug-in")
1338 return tags[i];
1339 }
1340 ok(false, "Unable to find plugin");
1341 return null;
1342 }

mercurial