summaryrefslogtreecommitdiff
path: root/toolkit/components/addoncompat/tests/addon/bootstrap.js
blob: 5e69fee22cf9ba910f7575af926b33a5f42d4528 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/BrowserUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

const baseURL = "http://mochi.test:8888/browser/" +
  "toolkit/components/addoncompat/tests/browser/";

var contentSecManager = Cc["@mozilla.org/contentsecuritymanager;1"]
                          .getService(Ci.nsIContentSecurityManager);

function forEachWindow(f)
{
  let wins = Services.wm.getEnumerator("navigator:browser");
  while (wins.hasMoreElements()) {
    let win = wins.getNext();
    f(win);
  }
}

function addLoadListener(target, listener)
{
  target.addEventListener("load", function handler(event) {
    target.removeEventListener("load", handler, true);
    return listener(event);
  }, true);
}

var gWin;
var gBrowser;
var ok, is, info;

function removeTab(tab, done)
{
  // Remove the tab in a different turn of the event loop. This way
  // the nested event loop in removeTab doesn't conflict with the
  // event listener shims.
  gWin.setTimeout(() => {
    gBrowser.removeTab(tab);
    done();
  }, 0);
}

// Make sure that the shims for window.content, browser.contentWindow,
// and browser.contentDocument are working.
function testContentWindow()
{
  return new Promise(function(resolve, reject) {
    const url = baseURL + "browser_addonShims_testpage.html";
    let tab = gBrowser.addTab(url);
    gBrowser.selectedTab = tab;
    let browser = tab.linkedBrowser;
    addLoadListener(browser, function handler() {
      ok(gWin.content, "content is defined on chrome window");
      ok(browser.contentWindow, "contentWindow is defined");
      ok(browser.contentDocument, "contentWindow is defined");
      is(gWin.content, browser.contentWindow, "content === contentWindow");
      ok(browser.webNavigation.sessionHistory, "sessionHistory is defined");

      ok(browser.contentDocument.getElementById("link"), "link present in document");

      // FIXME: Waiting on bug 1073631.
      // is(browser.contentWindow.wrappedJSObject.global, 3, "global available on document");

      removeTab(tab, resolve);
    });
  });
}

// Test for bug 1060046 and bug 1072607. We want to make sure that
// adding and removing listeners works as expected.
function testListeners()
{
  return new Promise(function(resolve, reject) {
    const url1 = baseURL + "browser_addonShims_testpage.html";
    const url2 = baseURL + "browser_addonShims_testpage2.html";

    let tab = gBrowser.addTab(url2);
    let browser = tab.linkedBrowser;
    addLoadListener(browser, function handler() {
      function dummyHandler() {}

      // Test that a removed listener stays removed (bug
      // 1072607). We're looking to make sure that adding and removing
      // a listener here doesn't cause later listeners to fire more
      // than once.
      for (let i = 0; i < 5; i++) {
        gBrowser.addEventListener("load", dummyHandler, true);
        gBrowser.removeEventListener("load", dummyHandler, true);
      }

      // We also want to make sure that this listener doesn't fire
      // after it's removed.
      let loadWithRemoveCount = 0;
      addLoadListener(browser, function handler1(event) {
        loadWithRemoveCount++;
        is(event.target.documentURI, url1, "only fire for first url");
      });

      // Load url1 and then url2. We want to check that:
      // 1. handler1 only fires for url1.
      // 2. handler2 only fires once for url1 (so the second time it
      //    fires should be for url2).
      let loadCount = 0;
      browser.addEventListener("load", function handler2(event) {
        loadCount++;
        if (loadCount == 1) {
          is(event.target.documentURI, url1, "first load is for first page loaded");
          browser.loadURI(url2);
        } else {
          gBrowser.removeEventListener("load", handler2, true);

          is(event.target.documentURI, url2, "second load is for second page loaded");
          is(loadWithRemoveCount, 1, "load handler is only called once");

          removeTab(tab, resolve);
        }
      }, true);

      browser.loadURI(url1);
    });
  });
}

// Test for bug 1059207. We want to make sure that adding a capturing
// listener and a non-capturing listener to the same element works as
// expected.
function testCapturing()
{
  return new Promise(function(resolve, reject) {
    let capturingCount = 0;
    let nonCapturingCount = 0;

    function capturingHandler(event) {
      is(capturingCount, 0, "capturing handler called once");
      is(nonCapturingCount, 0, "capturing handler called before bubbling handler");
      capturingCount++;
    }

    function nonCapturingHandler(event) {
      is(capturingCount, 1, "bubbling handler called after capturing handler");
      is(nonCapturingCount, 0, "bubbling handler called once");
      nonCapturingCount++;
    }

    gBrowser.addEventListener("mousedown", capturingHandler, true);
    gBrowser.addEventListener("mousedown", nonCapturingHandler, false);

    const url = baseURL + "browser_addonShims_testpage.html";
    let tab = gBrowser.addTab(url);
    let browser = tab.linkedBrowser;
    addLoadListener(browser, function handler() {
      let win = browser.contentWindow;
      let event = win.document.createEvent("MouseEvents");
      event.initMouseEvent("mousedown", true, false, win, 1,
                           1, 0, 0, 0, // screenX, screenY, clientX, clientY
                           false, false, false, false, // ctrlKey, altKey, shiftKey, metaKey
                           0, null); // buttonCode, relatedTarget

      let element = win.document.getElementById("output");
      element.dispatchEvent(event);

      is(capturingCount, 1, "capturing handler fired");
      is(nonCapturingCount, 1, "bubbling handler fired");

      gBrowser.removeEventListener("mousedown", capturingHandler, true);
      gBrowser.removeEventListener("mousedown", nonCapturingHandler, false);

      removeTab(tab, resolve);
    });
  });
}

// Make sure we get observer notifications that normally fire in the
// child.
function testObserver()
{
  return new Promise(function(resolve, reject) {
    let observerFired = 0;

    function observer(subject, topic, data) {
      Services.obs.removeObserver(observer, "document-element-inserted");
      observerFired++;
    }
    Services.obs.addObserver(observer, "document-element-inserted", false);

    let count = 0;
    const url = baseURL + "browser_addonShims_testpage.html";
    let tab = gBrowser.addTab(url);
    let browser = tab.linkedBrowser;
    browser.addEventListener("load", function handler() {
      count++;
      if (count == 1) {
        browser.reload();
      } else {
        browser.removeEventListener("load", handler);

        is(observerFired, 1, "got observer notification");

        removeTab(tab, resolve);
      }
    }, true);
  });
}

// Test for bug 1072472. Make sure that creating a sandbox to run code
// in the content window works. This is essentially a test for
// Greasemonkey.
function testSandbox()
{
  return new Promise(function(resolve, reject) {
    const url = baseURL + "browser_addonShims_testpage.html";
    let tab = gBrowser.addTab(url);
    let browser = tab.linkedBrowser;
    browser.addEventListener("load", function handler() {
      browser.removeEventListener("load", handler);

      let sandbox = Cu.Sandbox(browser.contentWindow,
                               {sandboxPrototype: browser.contentWindow,
                                wantXrays: false});
      Cu.evalInSandbox("const unsafeWindow = window;", sandbox);
      Cu.evalInSandbox("document.getElementById('output').innerHTML = 'hello';", sandbox);

      is(browser.contentDocument.getElementById("output").innerHTML, "hello",
         "sandbox code ran successfully");

      // Now try a sandbox with expanded principals.
      sandbox = Cu.Sandbox([browser.contentWindow],
                           {sandboxPrototype: browser.contentWindow,
                            wantXrays: false});
      Cu.evalInSandbox("const unsafeWindow = window;", sandbox);
      Cu.evalInSandbox("document.getElementById('output').innerHTML = 'hello2';", sandbox);

      is(browser.contentDocument.getElementById("output").innerHTML, "hello2",
         "EP sandbox code ran successfully");

      removeTab(tab, resolve);
    }, true);
  });
}

// Test for bug 1095305. We just want to make sure that loading some
// unprivileged content from an add-on package doesn't crash.
function testAddonContent()
{
  let chromeRegistry = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
    .getService(Components.interfaces.nsIChromeRegistry);
  let base = chromeRegistry.convertChromeURL(BrowserUtils.makeURI("chrome://addonshim1/content/"));

  let res = Services.io.getProtocolHandler("resource")
    .QueryInterface(Ci.nsIResProtocolHandler);
  res.setSubstitution("addonshim1", base);

  return new Promise(function(resolve, reject) {
    const url = "resource://addonshim1/page.html";
    let tab = gBrowser.addTab(url);
    let browser = tab.linkedBrowser;
    addLoadListener(browser, function handler() {
      res.setSubstitution("addonshim1", null);
      removeTab(tab, resolve);
    });
  });
}


// Test for bug 1102410. We check that multiple nsIAboutModule's can be
// registered in the parent, and that the child can browse to each of
// the registered about: pages.
function testAboutModuleRegistration()
{
  let Registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);

  let modulesToUnregister = new Map();

  function TestChannel(uri, aLoadInfo, aboutName) {
    this.aboutName = aboutName;
    this.loadInfo = aLoadInfo;
    this.URI = this.originalURI = uri;
  }

  TestChannel.prototype = {
    asyncOpen: function(listener, context) {
      let stream = this.open();
      let runnable = {
        run: () => {
          try {
            listener.onStartRequest(this, context);
          } catch (e) {}
          try {
            listener.onDataAvailable(this, context, stream, 0, stream.available());
          } catch (e) {}
          try {
            listener.onStopRequest(this, context, Cr.NS_OK);
          } catch (e) {}
        }
      };
      Services.tm.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
    },

    asyncOpen2: function(listener) {
      // throws an error if security checks fail
      var outListener = contentSecManager.performSecurityCheck(this, listener);
      return this.asyncOpen(outListener, null);
    },

    open: function() {
      function getWindow(channel) {
        try
        {
          if (channel.notificationCallbacks)
            return channel.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
        } catch (e) {}

        try
        {
          if (channel.loadGroup && channel.loadGroup.notificationCallbacks)
            return channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
        } catch (e) {}

        return null;
      }

      let data = `<html><h1>${this.aboutName}</h1></html>`;
      let wnd = getWindow(this);
      if (!wnd)
        throw Cr.NS_ERROR_UNEXPECTED;

      let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
      stream.setData(data, data.length);
      return stream;
    },

    open2: function() {
      // throws an error if security checks fail
      contentSecManager.performSecurityCheck(this, null);
      return this.open();
    },

    isPending: function() {
      return false;
    },
    cancel: function() {
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
    },
    suspend: function() {
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
    },
    resume: function() {
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
    },

    QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
  };

  /**
   * This function creates a new nsIAboutModule and registers it. Callers
   * should also call unregisterModules after using this function to clean
   * up the nsIAboutModules at the end of this test.
   *
   * @param aboutName
   *        This will be the string after about: used to refer to this module.
   *        For example, if aboutName is foo, you can refer to this module by
   *        browsing to about:foo.
   *
   * @param uuid
   *        A unique identifer string for this module. For example,
   *        "5f3a921b-250f-4ac5-a61c-8f79372e6063"
   */
  let createAndRegisterAboutModule = function(aboutName, uuid) {

    let AboutModule = function() {};

    AboutModule.prototype = {
      classID: Components.ID(uuid),
      classDescription: `Testing About Module for about:${aboutName}`,
      contractID: `@mozilla.org/network/protocol/about;1?what=${aboutName}`,
      QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),

      newChannel: (aURI, aLoadInfo) => {
        return new TestChannel(aURI, aLoadInfo, aboutName);
      },

      getURIFlags: (aURI) => {
        return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
               Ci.nsIAboutModule.ALLOW_SCRIPT;
      },
    };

    let factory = {
      createInstance: function(outer, iid) {
        if (outer) {
          throw Cr.NS_ERROR_NO_AGGREGATION;
        }
        return new AboutModule();
      },
    };

    Registrar.registerFactory(AboutModule.prototype.classID,
                              AboutModule.prototype.classDescription,
                              AboutModule.prototype.contractID,
                              factory);

    modulesToUnregister.set(AboutModule.prototype.classID,
                            factory);
  };

  /**
   * Unregisters any nsIAboutModules registered with
   * createAndRegisterAboutModule.
   */
  let unregisterModules = () => {
    for (let [classID, factory] of modulesToUnregister) {
      Registrar.unregisterFactory(classID, factory);
    }
  };

  /**
   * Takes a browser, and sends it a framescript to attempt to
   * load some about: pages. The frame script will send a test:result
   * message on completion, passing back a data object with:
   *
   * {
   *   pass: true
   * }
   *
   * on success, and:
   *
   * {
   *   pass: false,
   *   errorMsg: message,
   * }
   *
   * on failure.
   *
   * @param browser
   *        The browser to send the framescript to.
   */
  let testAboutModulesWork = (browser) => {
    let testConnection = () => {
      let request = new content.XMLHttpRequest();
      try {
        request.open("GET", "about:test1", false);
        request.send(null);
        if (request.status != 200) {
          throw (`about:test1 response had status ${request.status} - expected 200`);
        }
        if (request.responseText.indexOf("test1") == -1) {
          throw (`about:test1 response had result ${request.responseText}`);
        }

        request = new content.XMLHttpRequest();
        request.open("GET", "about:test2", false);
        request.send(null);

        if (request.status != 200) {
          throw (`about:test2 response had status ${request.status} - expected 200`);
        }
        if (request.responseText.indexOf("test2") == -1) {
          throw (`about:test2 response had result ${request.responseText}`);
        }

        sendAsyncMessage("test:result", {
          pass: true,
        });
      } catch (e) {
        sendAsyncMessage("test:result", {
          pass: false,
          errorMsg: e.toString(),
        });
      }
    };

    return new Promise((resolve, reject) => {
      let mm = browser.messageManager;
      mm.addMessageListener("test:result", function onTestResult(message) {
        mm.removeMessageListener("test:result", onTestResult);
        if (message.data.pass) {
          ok(true, "Connections to about: pages were successful");
        } else {
          ok(false, message.data.errorMsg);
        }
        resolve();
      });
      mm.loadFrameScript("data:,(" + testConnection.toString() + ")();", false);
    });
  }

  // Here's where the actual test is performed.
  return new Promise((resolve, reject) => {
    createAndRegisterAboutModule("test1", "5f3a921b-250f-4ac5-a61c-8f79372e6063");
    createAndRegisterAboutModule("test2", "d7ec0389-1d49-40fa-b55c-a1fc3a6dbf6f");

    // This needs to be a chrome-privileged page that loads in the
    // content process. It needs chrome privs because otherwise the
    // XHRs for about:test[12] will fail with a privilege error
    // despite the presence of URI_SAFE_FOR_UNTRUSTED_CONTENT.
    let newTab = gBrowser.addTab("chrome://addonshim1/content/page.html");
    gBrowser.selectedTab = newTab;
    let browser = newTab.linkedBrowser;

    addLoadListener(browser, function() {
      testAboutModulesWork(browser).then(() => {
        unregisterModules();
        removeTab(newTab, resolve);
      });
    });
  });
}

function testProgressListener()
{
  const url = baseURL + "browser_addonShims_testpage.html";

  let sawGlobalLocChange = false;
  let sawTabsLocChange = false;

  let globalListener = {
    onLocationChange: function(webProgress, request, uri) {
      if (uri.spec == url) {
        sawGlobalLocChange = true;
        ok(request instanceof Ci.nsIHttpChannel, "Global listener channel is an HTTP channel");
      }
    },
  };

  let tabsListener = {
    onLocationChange: function(browser, webProgress, request, uri) {
      if (uri.spec == url) {
        sawTabsLocChange = true;
        ok(request instanceof Ci.nsIHttpChannel, "Tab listener channel is an HTTP channel");
      }
    },
  };

  gBrowser.addProgressListener(globalListener);
  gBrowser.addTabsProgressListener(tabsListener);
  info("Added progress listeners");

  return new Promise(function(resolve, reject) {
    let tab = gBrowser.addTab(url);
    gBrowser.selectedTab = tab;
    addLoadListener(tab.linkedBrowser, function handler() {
      ok(sawGlobalLocChange, "Saw global onLocationChange");
      ok(sawTabsLocChange, "Saw tabs onLocationChange");

      gBrowser.removeProgressListener(globalListener);
      gBrowser.removeTabsProgressListener(tabsListener);
      removeTab(tab, resolve);
    });
  });
}

function testRootTreeItem()
{
  return new Promise(function(resolve, reject) {
    const url = baseURL + "browser_addonShims_testpage.html";
    let tab = gBrowser.addTab(url);
    gBrowser.selectedTab = tab;
    let browser = tab.linkedBrowser;
    addLoadListener(browser, function handler() {
      let win = browser.contentWindow;

      // Add-ons love this crap.
      let root = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                    .getInterface(Components.interfaces.nsIWebNavigation)
                    .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
                    .rootTreeItem
                    .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                    .getInterface(Components.interfaces.nsIDOMWindow);
      is(root, gWin, "got correct chrome window");

      removeTab(tab, resolve);
    });
  });
}

function testImportNode()
{
  return new Promise(function(resolve, reject) {
    const url = baseURL + "browser_addonShims_testpage.html";
    let tab = gBrowser.addTab(url);
    gBrowser.selectedTab = tab;
    let browser = tab.linkedBrowser;
    addLoadListener(browser, function handler() {
      let node = gWin.document.createElement("div");
      let doc = browser.contentDocument;
      let result;
      try {
        result = doc.importNode(node, false);
      } catch (e) {
        ok(false, "importing threw an exception");
      }
      if (browser.isRemoteBrowser) {
        is(result, node, "got expected import result");
      }

      removeTab(tab, resolve);
    });
  });
}

function runTests(win, funcs)
{
  ok = funcs.ok;
  is = funcs.is;
  info = funcs.info;

  gWin = win;
  gBrowser = win.gBrowser;

  return testContentWindow().
    then(testListeners).
    then(testCapturing).
    then(testObserver).
    then(testSandbox).
    then(testAddonContent).
    then(testAboutModuleRegistration).
    then(testProgressListener).
    then(testRootTreeItem).
    then(testImportNode).
    then(Promise.resolve());
}

/*
 bootstrap.js API
*/

function startup(aData, aReason)
{
  forEachWindow(win => {
    win.runAddonShimTests = (funcs) => runTests(win, funcs);
  });
}

function shutdown(aData, aReason)
{
  forEachWindow(win => {
    delete win.runAddonShimTests;
  });
}

function install(aData, aReason)
{
}

function uninstall(aData, aReason)
{
}