aboutsummaryrefslogtreecommitdiff
path: root/tools/addon-sdk-1.12/test/test-content-worker.js
blob: f1c8983ededb58cdb052591ee5892440e793847c (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
654
655
656
657
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use stirct";

const { Cc, Ci } = require("chrome");
const { setTimeout } = require("sdk/timers");
const { Loader, Require, override } = require("sdk/test/loader");
const { Worker } = require("sdk/content/worker");

const DEFAULT_CONTENT_URL = "data:text/html;charset=utf-8,foo";

function makeWindow(contentURL) {
  let content =
    "<?xml version=\"1.0\"?>" +
    "<window " +
    "xmlns=\"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul\">" +
    "<script>var documentValue=true;</script>" +
    "</window>";
  var url = "data:application/vnd.mozilla.xul+xml;charset=utf-8," +
            encodeURIComponent(content);
  var features = ["chrome", "width=10", "height=10"];

  return Cc["@mozilla.org/embedcomp/window-watcher;1"].
         getService(Ci.nsIWindowWatcher).
         openWindow(null, url, null, features.join(","), null);
}

// Listen for only first one occurence of DOM event
function listenOnce(node, eventName, callback) {
  node.addEventListener(eventName, function onevent(event) {
    node.removeEventListener(eventName, onevent, true);
    callback(node);
  }, true);
}

// Load a given url in a given browser and fires the callback when it is loaded
function loadAndWait(browser, url, callback) {
  listenOnce(browser, "load", callback);
  // We have to wait before calling `loadURI` otherwise, if we call
  // `loadAndWait` during browser load event, the history will be broken
  setTimeout(function () {
    browser.loadURI(url);
  }, 0);
}

// Returns a test function that will automatically open a new chrome window
// with a <browser> element loaded on a given content URL
// The callback receive 3 arguments:
// - test: reference to the jetpack test object
// - browser: a reference to the <browser> xul node
// - done: a callback to call when test is over
function WorkerTest(url, callback) {
  return function testFunction(test) {
    test.waitUntilDone();
    let chromeWindow = makeWindow();
    chromeWindow.addEventListener("load", function onload() {
      chromeWindow.removeEventListener("load", onload, true);
      let browser = chromeWindow.document.createElement("browser");
      browser.setAttribute("type", "content");
      chromeWindow.document.documentElement.appendChild(browser);
      // Wait for about:blank load event ...
      listenOnce(browser, "load", function onAboutBlankLoad() {
        // ... before loading the expected doc and waiting for its load event
        loadAndWait(browser, url, function onDocumentLoaded() {
          callback(test, browser, function onTestDone() {
            chromeWindow.close();
            test.done();
          });
        });
      });
    }, true);
  };
}

exports["test:sample"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {
    
    test.assertNotEqual(browser.contentWindow.location.href, "about:blank",
                        "window is now on the right document");

    let window = browser.contentWindow
    let worker =  Worker({
      window: window,
      contentScript: "new " + function WorkerScope() {
        // window is accessible
        let myLocation = window.location.toString();
        self.on("message", function(data) {
          if (data == "hi!")
            self.postMessage("bye!");
        });
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        test.assertEqual("bye!", msg);
        test.assertEqual(worker.url, window.location.href,
                         "worker.url still works");
        done();
      }
    });

    test.assertEqual(worker.url, window.location.href,
                     "worker.url works");
    worker.postMessage("hi!");
  }
);

exports["test:emit"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {

    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Validate self.on and self.emit
          self.port.on("addon-to-content", function (data) {
            self.port.emit("content-to-addon", data);
          });

          // Check for global pollution
          //if (typeof on != "undefined")
          //  self.postMessage("`on` is in globals");
          if (typeof once != "undefined")
            self.postMessage("`once` is in globals");
          if (typeof emit != "undefined")
            self.postMessage("`emit` is in globals");

        },
        onMessage: function(msg) {
          test.fail("Got an unexpected message : "+msg);
        }
      });

    // Validate worker.port
    worker.port.on("content-to-addon", function (data) {
      test.assertEqual(data, "event data");
      done();
    });
    worker.port.emit("addon-to-content", "event data");
  }
);

exports["test:emit hack message"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {
    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Validate self.port
          self.port.on("message", function (data) {
            self.port.emit("message", data);
          });
          // We should not receive message on self, but only on self.port
          self.on("message", function (data) {
            self.postMessage("message", data);
          });
        },
        onError: function(e) {
          test.fail("Got exception: "+e);
        }
      });

    worker.port.on("message", function (data) {
      test.assertEqual(data, "event data");
      done();
    });
    worker.on("message", function (data) {
      test.fail("Got an unexpected message : "+msg);
    });
    worker.port.emit("message", "event data");
  }
);

exports["test:n-arguments emit"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {
    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Validate self.on and self.emit
          self.port.on("addon-to-content", function (a1, a2, a3) {
            self.port.emit("content-to-addon", a1, a2, a3);
          });
        }
      });

    // Validate worker.port
    worker.port.on("content-to-addon", function (arg1, arg2, arg3) {
      test.assertEqual(arg1, "first argument");
      test.assertEqual(arg2, "second");
      test.assertEqual(arg3, "third");
      done();
    });
    worker.port.emit("addon-to-content", "first argument", "second", "third");
  }
);

exports["test:post-json-values-only"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {

    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          self.on("message", function (message) {
            self.postMessage([ message.fun === undefined,
                               typeof message.w,
                               message.w && "port" in message.w,
                               message.w.url,
                               Array.isArray(message.array),
                               JSON.stringify(message.array)]);
          });
        }
      });

    // Validate worker.onMessage
    let array = [1, 2, 3];
    worker.on("message", function (message) {
      test.assert(message[0], "function becomes undefined");
      test.assertEqual(message[1], "object", "object stays object");
      test.assert(message[2], "object's attributes are enumerable");
      test.assertEqual(message[3], DEFAULT_CONTENT_URL,
                       "jsonable attributes are accessible");
      // See bug 714891, Arrays may be broken over compartements:
      test.assert(message[4], "Array keeps being an array");
      test.assertEqual(message[5], JSON.stringify(array),
                       "Array is correctly serialized");
      done();
    });
    worker.postMessage({ fun: function () {}, w: worker, array: array });
  }
);

exports["test:emit-json-values-only"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {
  
    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Validate self.on and self.emit
          self.port.on("addon-to-content", function (fun, w, obj, array) {
            self.port.emit("content-to-addon", [
                            fun === null,
                            typeof w,
                            "port" in w,
                            w.url,
                            "fun" in obj,
                            Object.keys(obj.dom).length,
                            Array.isArray(array),
                            JSON.stringify(array)
                          ]);
          });
        }
      });

    // Validate worker.port
    let array = [1, 2, 3];
    worker.port.on("content-to-addon", function (result) {
      test.assert(result[0], "functions become null");
      test.assertEqual(result[1], "object", "objects stay objects");
      test.assert(result[2], "object's attributes are enumerable");
      test.assertEqual(result[3], DEFAULT_CONTENT_URL,
                       "json attribute is accessible");
      test.assert(!result[4], "function as object attribute is removed");
      test.assertEqual(result[5], 0, "DOM nodes are converted into empty object");
      // See bug 714891, Arrays may be broken over compartments:
      test.assert(result[6], "Array keeps being an array");
      test.assertEqual(result[7], JSON.stringify(array),
                       "Array is correctly serialized");
      done();
    });

    let obj = {
      fun: function () {},
      dom: browser.contentWindow.document.createElement("div")
    };
    worker.port.emit("addon-to-content", function () {}, worker, obj, array);
  }
);

exports["test:content is wrapped"] = WorkerTest(
  "data:text/html;charset=utf-8,<script>var documentValue=true;</script>",
  function(test, browser, done) {

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        self.postMessage(!window.documentValue);
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        test.assert(msg,
          "content script has a wrapped access to content document");
        done();
      }
    });
  }
);

exports["test:chrome is unwrapped"] = function(test) {
  let window = makeWindow();
  test.waitUntilDone();

  listenOnce(window, "load", function onload() {

    let worker =  Worker({
      window: window,
      contentScript: "new " + function WorkerScope() {
        self.postMessage(window.documentValue);
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        test.assert(msg,
          "content script has an unwrapped access to chrome document");
        window.close();
        test.done();
      }
    });

  });
}

exports["test:nothing is leaked to content script"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        self.postMessage([
          "ContentWorker" in window,
          "UNWRAP_ACCESS_KEY" in window,
          "getProxyForObject" in window
        ]);
      },
      contentScriptWhen: "ready",
      onMessage: function(list) {
        test.assert(!list[0], "worker API contrustor isn't leaked");
        test.assert(!list[1], "Proxy API stuff isn't leaked 1/2");
        test.assert(!list[2], "Proxy API stuff isn't leaked 2/2");
        done();
      }
    });
  }
);

exports["test:ensure console.xxx works in cs"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {

    // Create a new module loader in order to be able to create a `console`
    // module mockup:
    let loader = Loader(module, {
      console: {
        log: hook.bind("log"),
        info: hook.bind("info"),
        warn: hook.bind("warn"),
        error: hook.bind("error"),
        debug: hook.bind("debug"),
        exception: hook.bind("exception")
      }
    });

    // Intercept all console method calls
    let calls = [];
    function hook(msg) {
      test.assertEqual(this, msg,
                       "console.xxx(\"xxx\"), i.e. message is equal to the " +
                       "console method name we are calling");
      calls.push(msg);
    }

    // Finally, create a worker that will call all console methods
    let worker =  loader.require("sdk/content/worker").Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        console.log("log");
        console.info("info");
        console.warn("warn");
        console.error("error");
        console.debug("debug");
        console.exception("exception");
        self.postMessage();
      },
      onMessage: function() {
        // Ensure that console methods are called in the same execution order
        test.assertEqual(JSON.stringify(calls),
                         JSON.stringify(["log", "info", "warn", "error", "debug", "exception"]),
                         "console has been called successfully, in the expected order");
        done();
      }
    });
  }
);


exports["test:setTimeout can\"t be cancelled by content"] = WorkerTest(
  "data:text/html;charset=utf-8,<script>var documentValue=true;</script>",
  function(test, browser, done) {

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        let id = setTimeout(function () {
          self.postMessage("timeout");
        }, 100);
        unsafeWindow.eval("clearTimeout("+id+");");
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        test.assert(msg,
          "content didn't managed to cancel our setTimeout");
        done();
      }
    });
  }
);

exports["test:clearTimeout"] = WorkerTest(
  "data:text/html;charset=utf-8,clear timeout",
  function(test, browser, done) {
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        let id1 = setTimeout(function() {
          self.postMessage("failed");
        }, 10);
        let id2 = setTimeout(function() {
          self.postMessage("done");
        }, 100);
        clearTimeout(id1);
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        if (msg === "failed") {
          test.fail("failed to cancel timer");
        } else {
          test.pass("timer cancelled");
          done();
        }
      }
    });
  }
);

exports["test:clearInterval"] = WorkerTest(
  "data:text/html;charset=utf-8,clear timeout",
  function(test, browser, done) {
    let called = 0;
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        let id = setInterval(function() {
          self.postMessage("intreval")
          clearInterval(id)
          setTimeout(function() {
            self.postMessage("done")
          }, 100)
        }, 10);
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        if (msg === "intreval") {
          called = called + 1;
          if (called > 1) test.fail("failed to cancel timer");
        } else {
          test.pass("interval cancelled");
          done();
        }
      }
    });
  }
)

exports["test:setTimeout are unregistered on content unload"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {

    let originalWindow = browser.contentWindow;
    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        document.title = "ok";
        let i = 0;
        setInterval(function () {
          document.title = i++;
        }, 10);
      },
      contentScriptWhen: "ready"
    });

    // Change location so that content script is destroyed,
    // and all setTimeout/setInterval should be unregistered.
    // Wait some cycles in order to execute some intervals.
    setTimeout(function () {
      // Bug 689621: Wait for the new document load so that we are sure that
      // previous document cancelled its intervals
      let url2 = "data:text/html;charset=utf-8,<title>final</title>";
      loadAndWait(browser, url2, function onload() {
        let titleAfterLoad = originalWindow.document.title;
        // Wait additional cycles to verify that intervals are really cancelled
        setTimeout(function () {
          test.assertEqual(browser.contentDocument.title, "final",
                           "New document has not been modified");
          test.assertEqual(originalWindow.document.title, titleAfterLoad,
                           "Nor previous one");

          done();
        }, 100);
      });
    }, 100);
  }
);

exports['test:check window attribute in iframes'] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {

    // Create a first iframe and wait for its loading
    let contentWin = browser.contentWindow;
    let contentDoc = contentWin.document;
    let iframe = contentDoc.createElement("iframe");
    contentDoc.body.appendChild(iframe);

    listenOnce(iframe, "load", function onload() {

      // Create a second iframe inside the first one and wait for its loading
      let iframeDoc = iframe.contentWindow.document;
      let subIframe = iframeDoc.createElement("iframe");
      iframeDoc.body.appendChild(subIframe);

      listenOnce(subIframe, "load", function onload() {
        subIframe.removeEventListener("load", onload, true);

        // And finally create a worker against this second iframe
        let worker =  Worker({
          window: subIframe.contentWindow,
          contentScript: 'new ' + function WorkerScope() {
            self.postMessage([
              window.top !== window,
              frameElement,
              window.parent !== window,
              top.location.href,
              parent.location.href,
            ]);
          },
          onMessage: function(msg) {
            test.assert(msg[0], "window.top != window");
            test.assert(msg[1], "window.frameElement is defined");
            test.assert(msg[2], "window.parent != window");
            test.assertEqual(msg[3], contentWin.location.href,
                             "top.location refers to the toplevel content doc");
            test.assertEqual(msg[4], iframe.contentWindow.location.href,
                             "parent.location refers to the first iframe doc");
            done();
          }
        });

      });
      subIframe.setAttribute("src", "data:text/html;charset=utf-8,bar");

    });
    iframe.setAttribute("src", "data:text/html;charset=utf-8,foo");
  }
);

exports['test:check window attribute in toplevel documents'] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: 'new ' + function WorkerScope() {
        self.postMessage([
          window.top === window,
          frameElement,
          window.parent === window
        ]);
      },
      onMessage: function(msg) {
        test.assert(msg[0], "window.top == window");
        test.assert(!msg[1], "window.frameElement is null");
        test.assert(msg[2], "window.parent == window");
        done();
      }
    });
  }
);

exports["test:check worker API with page history"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(test, browser, done) {
    let url2 = "data:text/html;charset=utf-8,bar";

    loadAndWait(browser, url2, function () {
      let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Just before the content script is disable, we register a timeout
          // that will be disable until the page gets visible again
          self.on("pagehide", function () {
            setTimeout(function () {
              self.postMessage("timeout restored");
            }, 0);
          });
        },
        contentScriptWhen: "start"
      });

      // postMessage works correctly when the page is visible
      worker.postMessage("ok");

      // We have to wait before going back into history,
      // otherwise `goBack` won't do anything.
      setTimeout(function () {
        browser.goBack();
      }, 0);

      // Wait for the document to be hidden
      browser.addEventListener("pagehide", function onpagehide() {
        browser.removeEventListener("pagehide", onpagehide, false);
        // Now any event sent to this worker should throw
        test.assertRaises(
            function () { worker.postMessage("data"); },
            "The page is currently hidden and can no longer be used until it" +
            " is visible again.",
            "postMessage should throw when the page is hidden in history"
            );
        test.assertRaises(
            function () { worker.port.emit("event"); },
            "The page is currently hidden and can no longer be used until it" +
            " is visible again.",
            "port.emit should throw when the page is hidden in history"
            );

        // Display the page with attached content script back in order to resume
        // its timeout and receive the expected message.
        // We have to delay this in order to not break the history.
        // We delay for a non-zero amount of time in order to ensure that we
        // do not receive the message immediatly, so that the timeout is
        // actually disabled
        setTimeout(function () {
          worker.on("message", function (data) {
            test.assert(data, "timeout restored");
            done();
          });
          browser.goForward();
        }, 500);

      }, false);
    });

  }
);