aboutsummaryrefslogtreecommitdiff
path: root/tools/addon-sdk-1.7/packages/addon-kit/tests/test-widget.js
blob: 5d3475bcc013fcad944fc9d16b31de376e7518bc (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
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
/* 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 strict";

const {Cc,Ci} = require("chrome");
const { Loader } = require('./helpers');
const widgets = require("widget");
const url = require("url");
const windowUtils = require("window-utils");
const tabBrowser = require("tab-browser");

exports.testConstructor = function(test) {
  test.waitUntilDone(30000);

  let browserWindow = windowUtils.activeBrowserWindow;
  let doc = browserWindow.document;
  let AddonsMgrListener = browserWindow.AddonsMgrListener;

  function container() doc.getElementById("addon-bar");
  function widgetCount() container() ? container().getElementsByTagName("toolbaritem").length : 0;
  let widgetStartCount = widgetCount();
  function widgetNode(index) container() ? container().getElementsByTagName("toolbaritem")[index] : null;

  // Test basic construct/destroy
  AddonsMgrListener.onInstalling();
  let w = widgets.Widget({ id: "fooID", label: "foo", content: "bar" });
  AddonsMgrListener.onInstalled();
  test.assertEqual(widgetCount(), widgetStartCount + 1, "panel has correct number of child elements after widget construction");

  // test widget height
  test.assertEqual(widgetNode(0).firstChild.boxObject.height, 16, "widget has correct default height");

  AddonsMgrListener.onUninstalling();
  w.destroy();
  AddonsMgrListener.onUninstalled();
  w.destroy();
  test.pass("Multiple destroys do not cause an error");
  test.assertEqual(widgetCount(), widgetStartCount, "panel has correct number of child elements after destroy");

  // Test automatic widget destroy on unload
  let loader = Loader(module);
  let widgetsFromLoader = loader.require("widget");
  let widgetStartCount = widgetCount();
  let w = widgetsFromLoader.Widget({ id: "fooID", label: "foo", content: "bar" });
  test.assertEqual(widgetCount(), widgetStartCount + 1, "widget has been correctly added");
  loader.unload();
  test.assertEqual(widgetCount(), widgetStartCount, "widget has been destroyed on module unload");

  // Test nothing
  test.assertRaises(
    function() widgets.Widget({}),
    "The widget must have a non-empty label property.",
    "throws on no properties");

  // Test no label
  test.assertRaises(
    function() widgets.Widget({content: "foo"}),
    "The widget must have a non-empty label property.",
    "throws on no label");

  // Test empty label
  test.assertRaises(
    function() widgets.Widget({label: "", content: "foo"}),
    "The widget must have a non-empty label property.",
    "throws on empty label");

  // Test no content or image
  test.assertRaises(
    function() widgets.Widget({id: "fooID", label: "foo"}),
    "No content or contentURL property found. Widgets must have one or the other.",
    "throws on no content");

  // Test empty content, no image
  test.assertRaises(
    function() widgets.Widget({id:"fooID", label: "foo", content: ""}),
    "No content or contentURL property found. Widgets must have one or the other.",
    "throws on empty content");

  // Test empty image, no content
  test.assertRaises(
    function() widgets.Widget({id:"fooID", label: "foo", image: ""}),
    "No content or contentURL property found. Widgets must have one or the other.",
    "throws on empty content");

  // Test empty content, empty image
  test.assertRaises(
    function() widgets.Widget({id:"fooID", label: "foo", content: "", image: ""}),
    "No content or contentURL property found. Widgets must have one or the other.",
    "throws on empty content");

  // Test duplicated ID
  let duplicateID = widgets.Widget({id: "foo", label: "foo", content: "bar"});
  test.assertRaises(
    function() widgets.Widget({id: "foo", label: "bar", content: "bar"}),
    /This widget ID is already used:/,
    "throws on duplicated id");
  duplicateID.destroy();

  // Test Bug 652527
  test.assertRaises(
    function() widgets.Widget({id: "", label: "bar", content: "bar"}),
    /You have to specify a unique value for the id property of/,
    "throws on falsey id");

  // Test duplicate label, different ID
  let w1 = widgets.Widget({id: "id1", label: "foo", content: "bar"});
  let w2 = widgets.Widget({id: "id2", label: "foo", content: "bar"});
  w1.destroy();
  w2.destroy();

  // Test position restore on create/destroy/create
  // Create 3 ordered widgets
  let w1 = widgets.Widget({id: "first", label:"first", content: "bar"});
  let w2 = widgets.Widget({id: "second", label:"second", content: "bar"});
  let w3 = widgets.Widget({id: "third", label:"third", content: "bar"});
  // Remove the middle widget
  test.assertEqual(widgetNode(1).getAttribute("label"), "second", "second widget is the second widget inserted");
  w2.destroy();
  test.assertEqual(widgetNode(1).getAttribute("label"), "third", "second widget is removed, so second widget is now the third one");
  w2 = widgets.Widget({id: "second", label:"second", content: "bar"});
  test.assertEqual(widgetNode(1).getAttribute("label"), "second", "second widget is created again, at the same location");
  // Cleanup this testcase
  AddonsMgrListener.onUninstalling();
  w1.destroy();
  w2.destroy();
  w3.destroy();
  AddonsMgrListener.onUninstalled();

  // Test concurrent widget module instances on addon-bar hiding
  let loader = Loader(module);
  let anotherWidgetsInstance = loader.require("widget");
  test.assert(container().collapsed, "UI is hidden when no widgets");
  AddonsMgrListener.onInstalling();
  let w1 = widgets.Widget({id: "foo", label: "foo", content: "bar"});
  // Ideally we would let AddonsMgrListener display the addon bar
  // But, for now, addon bar is immediatly displayed by sdk code
  // https://bugzilla.mozilla.org/show_bug.cgi?id=627484
  test.assert(!container().collapsed, "UI is already visible when we just added the widget");
  AddonsMgrListener.onInstalled();
  test.assert(!container().collapsed, "UI become visible when we notify AddonsMgrListener about end of addon installation");
  let w2 = anotherWidgetsInstance.Widget({id: "bar", label: "bar", content: "foo"});
  test.assert(!container().collapsed, "UI still visible when we add a second widget");
  AddonsMgrListener.onUninstalling();
  w1.destroy();
  AddonsMgrListener.onUninstalled();
  test.assert(!container().collapsed, "UI still visible when we remove one of two widgets");
  AddonsMgrListener.onUninstalling();
  w2.destroy();
  test.assert(!container().collapsed, "UI is still visible when we have removed all widget but still not called onUninstalled");
  AddonsMgrListener.onUninstalled();
  test.assert(container().collapsed, "UI is hidden when we have removed all widget and called onUninstalled");

  // Helper for testing a single widget.
  // Confirms proper addition and content setup.
  function testSingleWidget(widgetOptions) {
    // We have to display which test is being run, because here we do not
    // use the regular test framework but rather a custom one that iterates
    // the `tests` array.
    console.info("executing: " + widgetOptions.id);

    let startCount = widgetCount();
    let widget = widgets.Widget(widgetOptions);
    let node = widgetNode(startCount);
    test.assert(node, "widget node at index");
    test.assertEqual(node.tagName, "toolbaritem", "widget element is correct");
    test.assertEqual(widget.width + "px", node.style.minWidth, "widget width is correct");
    test.assertEqual(widgetCount(), startCount + 1, "container has correct number of child elements");
    let content = node.firstElementChild;
    test.assert(content, "found content");
    test.assertMatches(content.tagName, /iframe|image/, "content is iframe or image");
    return widget;
  }

  // Array of widgets to test
  // and a function to test them.
  let tests = [];
  function nextTest() {
    test.assertEqual(widgetCount(), 0, "widget in last test property cleaned itself up");
    if (!tests.length)
      test.done();
    else
      require("timer").setTimeout(tests.shift(), 0);
  }
  function doneTest() nextTest();

  // text widget
  tests.push(function testTextWidget() testSingleWidget({
    id: "text",
    label: "text widget",
    content: "oh yeah",
    contentScript: "self.postMessage(document.body.innerHTML);",
    contentScriptWhen: "end",
    onMessage: function (message) {
      test.assertEqual(this.content, message, "content matches");
      this.destroy();
      doneTest();
    }
  }));

  // html widget
  tests.push(function testHTMLWidget() testSingleWidget({
    id: "html",
    label: "html widget",
    content: "<div>oh yeah</div>",
    contentScript: "self.postMessage(document.body.innerHTML);",
    contentScriptWhen: "end",
    onMessage: function (message) {
      test.assertEqual(this.content, message, "content matches");
      this.destroy();
      doneTest();
    }
  }));

  // image url widget
  tests.push(function testImageURLWidget() testSingleWidget({
    id: "image",
    label: "image url widget",
    contentURL: require("self").data.url("test.html"),
    contentScript: "self.postMessage({title: document.title, " +
                   "tag: document.body.firstElementChild.tagName, " +
                   "content: document.body.firstElementChild.innerHTML});",
    contentScriptWhen: "end",
    onMessage: function (message) {
      test.assertEqual(message.title, "foo", "title matches");
      test.assertEqual(message.tag, "P", "element matches");
      test.assertEqual(message.content, "bar", "element content matches");
      this.destroy();
      doneTest();
    }
  }));

  // web uri widget
  tests.push(function testWebURIWidget() testSingleWidget({
    id: "web",
    label: "web uri widget",
    contentURL: require("self").data.url("test.html"),
    contentScript: "self.postMessage({title: document.title, " +
                   "tag: document.body.firstElementChild.tagName, " +
                   "content: document.body.firstElementChild.innerHTML});",
    contentScriptWhen: "end",
    onMessage: function (message) {
      test.assertEqual(message.title, "foo", "title matches");
      test.assertEqual(message.tag, "P", "element matches");
      test.assertEqual(message.content, "bar", "element content matches");
      this.destroy();
      doneTest();
    }
  }));

  // event: onclick + content
  tests.push(function testOnclickEventContent() testSingleWidget({
    id: "click",
    label: "click test widget - content",
    content: "<div id='me'>foo</div>",
    contentScript: "var evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('click', true, true ); " +
                   "document.getElementById('me').dispatchEvent(evt);",
    contentScriptWhen: "end",
    onClick: function() {
      test.pass("onClick called");
      this.destroy();
      doneTest();
    }
  }));

  // event: onmouseover + content
  tests.push(function testOnmouseoverEventContent() testSingleWidget({
    id: "mouseover",
    label: "mouseover test widget - content",
    content: "<div id='me'>foo</div>",
    contentScript: "var evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('mouseover', true, true ); " +
                   "document.getElementById('me').dispatchEvent(evt);",
    contentScriptWhen: "end",
    onMouseover: function() {
      test.pass("onMouseover called");
      this.destroy();
      doneTest();
    }
  }));

  // event: onmouseout + content
  tests.push(function testOnmouseoutEventContent() testSingleWidget({
    id: "mouseout",
    label: "mouseout test widget - content",
    content: "<div id='me'>foo</div>",
    contentScript: "var evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('mouseout', true, true ); " +
                   "document.getElementById('me').dispatchEvent(evt);",
    contentScriptWhen: "end",
    onMouseout: function() {
      test.pass("onMouseout called");
      this.destroy();
      doneTest();
    }
  }));

  // event: onclick + image
  tests.push(function testOnclickEventImage() testSingleWidget({
    id: "click",
    label: "click test widget - image",
    contentURL: require("self").data.url("moz_favicon.ico"),
    contentScript: "var evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('click', true, true ); " +
                   "document.body.firstElementChild.dispatchEvent(evt);",
    contentScriptWhen: "end",
    onClick: function() {
      test.pass("onClick called");
      this.destroy();
      doneTest();
    }
  }));

  // event: onmouseover + image
  tests.push(function testOnmouseoverEventImage() testSingleWidget({
    id: "mouseover",
    label: "mouseover test widget - image",
    contentURL: require("self").data.url("moz_favicon.ico"),
    contentScript: "var evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('mouseover', true, true ); " +
                   "document.body.firstElementChild.dispatchEvent(evt);",
    contentScriptWhen: "end",
    onMouseover: function() {
      test.pass("onMouseover called");
      this.destroy();
      doneTest();
    }
  }));

  // event: onmouseout + image
  tests.push(function testOnmouseoutEventImage() testSingleWidget({
    id: "mouseout",
    label: "mouseout test widget - image",
    contentURL: require("self").data.url("moz_favicon.ico"),
    contentScript: "var evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('mouseout', true, true ); " +
                   "document.body.firstElementChild.dispatchEvent(evt);",
    contentScriptWhen: "end",
    onMouseout: function() {
      test.pass("onMouseout called");
      this.destroy();
      doneTest();
    }
  }));

  // test multiple widgets
  tests.push(function testMultipleWidgets() {
    let w1 = widgets.Widget({id: "first", label: "first widget", content: "first content"});
    let w2 = widgets.Widget({id: "second", label: "second widget", content: "second content"});

    w1.destroy();
    w2.destroy();

    doneTest();
  });

  // test updating widget content
  let loads = 0;
  tests.push(function testUpdatingWidgetContent() testSingleWidget({
    id: "content",
    label: "content update test widget",
    content: "<div id='me'>foo</div>",
    contentScript: "self.postMessage(1)",
    contentScriptWhen: "ready",
    onMessage: function(message) {
      if (!this.flag) {
        this.content = "<div id='me'>bar</div>";
        this.flag = 1;
      }
      else {
        test.assertEqual(this.content, "<div id='me'>bar</div>");
        this.destroy();
        doneTest();
      }
    }
  }));

  // test updating widget contentURL
  let url1 = "data:text/html,<body>foodle</body>";
  let url2 = "data:text/html,<body>nistel</body>";

  tests.push(function testUpdatingContentURL() testSingleWidget({
    id: "content",
    label: "content update test widget",
    contentURL: url1,
    contentScript: "self.postMessage(document.location.href);",
    contentScriptWhen: "end",
    onMessage: function(message) {
      if (!this.flag) {
        test.assertEqual(this.contentURL.toString(), url1);
        test.assertEqual(message, url1);
        this.contentURL = url2;
        this.flag = 1;
      }
      else {
        test.assertEqual(this.contentURL.toString(), url2);
        test.assertEqual(message, url2);
        this.destroy();
        doneTest();
      }
    }
  }));

  // test tooltip
  tests.push(function testTooltip() testSingleWidget({
    id: "text",
    label: "text widget",
    content: "oh yeah",
    tooltip: "foo",
    contentScript: "self.postMessage(1)",
    contentScriptWhen: "ready",
    onMessage: function(message) {
      test.assertEqual(this.tooltip, "foo", "tooltip matches");
      this.destroy();
      doneTest();
    }
  }));

  // test tooltip fallback to label
  tests.push(function testTooltipFallback() testSingleWidget({
    id: "fallback",
    label: "fallback",
    content: "oh yeah",
    contentScript: "self.postMessage(1)",
    contentScriptWhen: "ready",
    onMessage: function(message) {
      test.assertEqual(this.tooltip, this.label, "tooltip fallbacks to label");
      this.destroy();
      doneTest();
    }
  }));

  // test updating widget tooltip
  let updated = false;
  tests.push(function testUpdatingTooltip() testSingleWidget({
    id: "tooltip",
    label: "tooltip update test widget",
    tooltip: "foo",
    content: "<div id='me'>foo</div>",
    contentScript: "self.postMessage(1)",
    contentScriptWhen: "ready",
    onMessage: function(message) {
      this.tooltip = "bar";
      test.assertEqual(this.tooltip, "bar", "tooltip gets updated");
      this.destroy();
      doneTest();
    }
  }));

  // test allow attribute
  tests.push(function testDefaultAllow() testSingleWidget({
    id: "allow",
    label: "allow.script attribute",
    content: "<script>document.title = 'ok';</script>",
    contentScript: "self.postMessage(document.title)",
    onMessage: function(message) {
      test.assertEqual(message, "ok", "scripts are evaluated by default");
      this.destroy();
      doneTest();
    }
  }));

  tests.push(function testExplicitAllow() testSingleWidget({
    id: "allow",
    label: "allow.script attribute",
    allow: {script: true},
    content: "<script>document.title = 'ok';</script>",
    contentScript: "self.postMessage(document.title)",
    onMessage: function(message) {
      test.assertEqual(message, "ok", "scripts are evaluated when we want to");
      this.destroy();
      doneTest();
    }
  }));

  tests.push(function testExplicitDisallow() testSingleWidget({
    id: "allow",
    label: "allow.script attribute",
    content: "<script>document.title = 'ok';</script>",
    allow: {script: false},
    contentScript: "self.postMessage(document.title)",
    onMessage: function(message) {
      test.assertNotEqual(message, "ok", "scripts aren't evaluated when " +
                                         "explicitly blocked it");
      this.destroy();
      doneTest();
    }
  }));

  // test multiple windows
  tests.push(function testMultipleWindows() {
    tabBrowser.addTab("about:blank", { inNewWindow: true, onLoad: function(e) {
      let browserWindow = e.target.defaultView;
      let doc = browserWindow.document;
      function container() doc.getElementById("addon-bar");
      function widgetCount2() container() ? container().childNodes.length : 0;
      let widgetStartCount2 = widgetCount2();

      let w1Opts = {id:"first", label: "first widget", content: "first content"};
      let w1 = testSingleWidget(w1Opts);
      test.assertEqual(widgetCount2(), widgetStartCount2 + 1, "2nd window has correct number of child elements after first widget");

      let w2Opts = {id:"second", label: "second widget", content: "second content"};
      let w2 = testSingleWidget(w2Opts);
      test.assertEqual(widgetCount2(), widgetStartCount2 + 2, "2nd window has correct number of child elements after second widget");

      w1.destroy();
      test.assertEqual(widgetCount2(), widgetStartCount2 + 1, "2nd window has correct number of child elements after first destroy");
      w2.destroy();
      test.assertEqual(widgetCount2(), widgetStartCount2, "2nd window has correct number of child elements after second destroy");

      closeBrowserWindow(browserWindow, function() {
        doneTest();
      });
    }});
  });

  // test window closing
  tests.push(function testWindowClosing() {
    // 1/ Create a new widget
    let w1Opts = {
      id:"first",
      label: "first widget",
      content: "first content",
      contentScript: "self.port.on('event', function () self.port.emit('event'))"
    };
    let widget = testSingleWidget(w1Opts);
    let windows = require("windows").browserWindows;

    // 2/ Retrieve a WidgetView for the initial browser window
    let acceptDetach = false;
    let mainView = widget.getView(windows.activeWindow);
    test.assert(mainView, "Got first widget view");
    mainView.on("detach", function () {
      // 8/ End of our test. Accept detach event only when it occurs after
      // widget.destroy()
      if (acceptDetach)
        doneTest();
      else
        test.fail("View on initial window should not be destroyed");
    });
    mainView.port.on("event", function () {
      // 7/ Receive event sent during 6/ and cleanup our test
      acceptDetach = true;
      widget.destroy();
    });

    // 3/ First: open a new browser window
    windows.open({
      url: "about:blank",
      onOpen: function(window) {
        // 4/ Retrieve a WidgetView for this new window
        let view = widget.getView(window);
        test.assert(view, "Got second widget view");
        view.port.on("event", function () {
          test.fail("We should not receive event on the detach view");
        });
        view.on("detach", function () {
          // The related view is destroyed
          // 6/ Send a custom event
          test.assertRaises(function () {
              view.port.emit("event");
            },
            /The widget has been destroyed and can no longer be used./,
            "emit on a destroyed view should throw");
          widget.port.emit("event");
        });

        // 5/ Destroy this window
        window.close();
      }
    });
  });

  tests.push(function testAddonBarHide() {
    // Hide the addon-bar
    browserWindow.setToolbarVisibility(container(), false);

    // Then open a browser window and verify that the addon-bar remains hidden
    tabBrowser.addTab("about:blank", { inNewWindow: true, onLoad: function(e) {
      let browserWindow = e.target.defaultView;
      let doc = browserWindow.document;
      function container2() doc.getElementById("addon-bar");
      function widgetCount2() container2() ? container2().childNodes.length : 0;
      let widgetStartCount2 = widgetCount2();

      let w1Opts = {id:"first", label: "first widget", content: "first content"};
      let w1 = testSingleWidget(w1Opts);
      test.assertEqual(widgetCount2(), widgetStartCount2 + 1, "2nd window has correct number of child elements after widget creation");

      w1.destroy();
      test.assertEqual(widgetCount2(), widgetStartCount2, "2nd window has correct number of child elements after widget destroy");

      test.assert(container().collapsed, "1st window has an hidden addon-bar");
      test.assert(container2().collapsed, "2nd window has an hidden addon-bar");

      browserWindow.setToolbarVisibility(container(), true);

      closeBrowserWindow(browserWindow, function() {
        doneTest();
      });
    }});
  });

  // test widget.width
  tests.push(function testWidgetWidth() testSingleWidget({
    id: "text",
    label: "test widget.width",
    content: "test width",
    width: 200,
    contentScript: "self.postMessage(1)",
    contentScriptWhen: "ready",
    onMessage: function(message) {
      test.assertEqual(this.width, 200);

      let node = widgetNode(0);
      test.assertEqual(this.width, node.style.minWidth.replace("px", ""));
      test.assertEqual(this.width, node.firstElementChild.style.width.replace("px", ""));
      this.width = 300;
      test.assertEqual(this.width, node.style.minWidth.replace("px", ""));
      test.assertEqual(this.width, node.firstElementChild.style.width.replace("px", ""));

      this.destroy();
      doneTest();
    }
  }));

  // test click handler not respond to right-click
  let clickCount = 0;
  tests.push(function testNoRightClick() testSingleWidget({
    id: "click-content",
    label: "click test widget - content",
    content: "<div id='me'>foo</div>",
    contentScript: "var evt = document.createEvent('MouseEvents'); " +
                   "evt.initMouseEvent('click', true, true, window, " +
                   "  0, 0, 0, 0, 0, false, false, false, false, 2, null); " +
                   "document.getElementById('me').dispatchEvent(evt); " +
                   "evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('click', true, true ); " +
                   "document.getElementById('me').dispatchEvent(evt); " +
                   "evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('mouseover', true, true ); " +
                   "document.getElementById('me').dispatchEvent(evt);",
    contentScriptWhen: "end",
    onClick: function() clickCount++,
    onMouseover: function() {
      test.assertEqual(clickCount, 1, "right click wasn't sent to click handler");
      this.destroy();
      doneTest();
    }
  }));

  // kick off test execution
  doneTest();
};

exports.testPanelWidget1 = function testPanelWidget1(test) {
  const widgets = require("widget");

  let widget1 = widgets.Widget({
    id: "panel1",
    label: "panel widget 1",
    content: "<div id='me'>foo</div>",
    contentScript: "var evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('click', true, true ); " +
                   "document.body.dispatchEvent(evt);",
    contentScriptWhen: "end",
    panel: require("panel").Panel({
      contentURL: "data:text/html,<body>Look ma, a panel!</body>",
      onShow: function() {
        widget1.destroy();
        test.pass("panel displayed on click");
        test.done();
      }
    })
  });
  test.waitUntilDone();
};

exports.testPanelWidget2 = function testPanelWidget2(test) {
  const widgets = require("widget");
  test.assertRaises(
    function() {
      widgets.Widget({
        id: "panel2",
        label: "panel widget 2",
        panel: {}
      });
    },
    "The option \"panel\" must be one of the following types: null, undefined, object",
    "widget.panel must be a Panel object"
  );
};

exports.testPanelWidget3 = function testPanelWidget3(test) {
  const widgets = require("widget");
  let onClickCalled = false;
  let widget3 = widgets.Widget({
    id: "panel3",
    label: "panel widget 3",
    content: "<div id='me'>foo</div>",
    contentScript: "var evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('click', true, true ); " +
                   "document.body.firstElementChild.dispatchEvent(evt);",
    contentScriptWhen: "end",
    onClick: function() {
      onClickCalled = true;
      this.panel.show();
    },
    panel: require("panel").Panel({
      contentURL: "data:text/html,<body>Look ma, a panel!</body>",
      onShow: function() {
        test.assert(
          onClickCalled,
          "onClick called on click for widget with both panel and onClick"
        );
        widget3.destroy();
        test.done();
      }
    })
  });
  test.waitUntilDone();
};

exports.testWidgetMessaging = function testWidgetMessaging(test) {
  test.waitUntilDone();
  let origMessage = "foo";
  const widgets = require("widget");
  let widget = widgets.Widget({
    id: "foo",
    label: "foo",
    content: "<bar>baz</bar>",
    contentScriptWhen: "end",
    contentScript: "self.on('message', function(data) { self.postMessage(data); }); self.postMessage('ready');",
    onMessage: function(message) {
      if (message == "ready")
        widget.postMessage(origMessage);
      else {
        test.assertEqual(origMessage, message);
        widget.destroy();
        test.done();
      }
    }
  });
};

exports.testWidgetViews = function testWidgetViews(test) {
  test.waitUntilDone();
  const widgets = require("widget");
  let widget = widgets.Widget({
    id: "foo",
    label: "foo",
    content: "<bar>baz</bar>",
    contentScriptWhen: "ready",
    contentScript: "self.on('message', function(data) self.postMessage(data)); self.postMessage('ready')",
    onAttach: function(view) {
      test.pass("WidgetView created");
      view.on("message", function () {
        test.pass("Got message in WidgetView");
        widget.destroy();
      });
      view.on("detach", function () {
        test.pass("WidgetView destroyed");
        test.done();
      });
    }
  });

};

exports.testWidgetViewsUIEvents = function testWidgetViewsUIEvents(test) {
  test.waitUntilDone();
  const widgets = require("widget");
  let view = null;
  let widget = widgets.Widget({
    id: "foo",
    label: "foo",
    content: "<div id='me'>foo</div>",
    contentScript: "var evt = document.createEvent('HTMLEvents'); " +
                   "evt.initEvent('click', true, true ); " +
                   "document.getElementById('me').dispatchEvent(evt);",
    contentScriptWhen: "ready",
    onAttach: function(attachView) {
      view = attachView;
      test.pass("Got attach event");
    },
    onClick: function (eventView) {
      test.assertEqual(view, eventView,
                         "event first argument is equal to the WidgetView");
      let view2 = widget.getView(require("windows").browserWindows.activeWindow);
      test.assertEqual(view, view2,
                         "widget.getView return the same WidgetView");
      widget.destroy();
      test.done();
    }
  });
};

exports.testWidgetViewsCustomEvents = function testWidgetViewsCustomEvents(test) {
  test.waitUntilDone();
  const widgets = require("widget");
  let widget = widgets.Widget({
    id: "foo",
    label: "foo",
    content: "<div id='me'>foo</div>",
    contentScript: "self.port.emit('event', 'ok');",
    contentScriptWhen: "ready",
    onAttach: function(view) {
      view.port.on("event", function (data) {
        test.assertEqual(data, "ok",
                         "event argument is valid on WidgetView");
      });
    },
  });
  widget.port.on("event", function (data) {
    test.assertEqual(data, "ok",
                     "event argument is valid on Widget");
    widget.destroy();
    test.done();
  });
};

exports.testWidgetViewsTooltip = function testWidgetViewsTooltip(test) {
  test.waitUntilDone();
  const widgets = require("widget");

  let widget = new widgets.Widget({
    id: "foo",
    label: "foo",
    content: "foo"
  });
  let view = widget.getView(require("windows").browserWindows.activeWindow);
  widget.tooltip = null;
  test.assertEqual(view.tooltip, "foo",
                   "view tooltip defaults to base widget label");
  test.assertEqual(widget.tooltip, "foo",
                   "tooltip defaults to base widget label");
  widget.destroy();
  test.done();
};

exports.testWidgetMove = function testWidgetMove(test) {
  test.waitUntilDone();

  let windowUtils = require("window-utils");
  let widgets = require("widget");

  let browserWindow = windowUtils.activeBrowserWindow;
  let doc = browserWindow.document;

  let label = "unique-widget-label";
  let origMessage = "message after node move";
  let gotFirstReady = false;

  let widget = widgets.Widget({
    id: "foo",
    label: label,
    content: "<bar>baz</bar>",
    contentScriptWhen: "ready",
    contentScript: "self.on('message', function(data) { self.postMessage(data); }); self.postMessage('ready');",
    onMessage: function(message) {
      if (message == "ready") {
        if (!gotFirstReady) {
          test.pass("Got first ready event");
          let widgetNode = doc.querySelector('toolbaritem[label="' + label + '"]');
          let parent = widgetNode.parentNode;
          parent.insertBefore(widgetNode, parent.firstChild);
          gotFirstReady = true;
        } else {
          test.pass("Got second ready event");
          widget.postMessage(origMessage);
        }
      }
      else {
        test.assertEqual(origMessage, message, "Got message after node move");
        widget.destroy();
        test.done();
      }
    }
  });
};

/*
The bug is exhibited when a widget with HTML content has it's content
changed to new HTML content with a pound in it. Because the src of HTML
content is converted to a data URI, the underlying iframe doesn't
consider the content change a navigation change, so doesn't load
the new content.
*/
exports.testWidgetWithPound = function testWidgetWithPound(test) {
  test.waitUntilDone();

  function getWidgetContent(widget) {
    let windowUtils = require("window-utils");
    let browserWindow = windowUtils.activeBrowserWindow;
    let doc = browserWindow.document;
    let widgetNode = doc.querySelector('toolbaritem[label="' + widget.label + '"]');
    test.assert(widgetNode, 'found widget node in the front-end');
    return widgetNode.firstChild.contentDocument.body.innerHTML;
  }

  let widgets = require("widget");
  let count = 0;
  let widget = widgets.Widget({
    id: "1",
    label: "foo",
    content: "foo",
    contentScript: "window.addEventListener('load', self.postMessage, false);",
    onMessage: function() {
      count++;
      if (count == 1) {
        widget.content = "foo#";
      }
      else {
        test.assertEqual(getWidgetContent(widget), "foo#", "content updated to pound?");
        widget.destroy();
        test.done();
      }
    }
  });
};

exports.testNavigationBarWidgets = function testNavigationBarWidgets(test) {
  test.waitUntilDone();

  let w1 = widgets.Widget({id: "1st", label: "1st widget", content: "1"});
  let w2 = widgets.Widget({id: "2nd", label: "2nd widget", content: "2"});
  let w3 = widgets.Widget({id: "3rd", label: "3rd widget", content: "3"});

  // Hack to move 2nd and 3rd widgets manually to the navigation bar, in 5th
  // position, i.e. after search box. 3rd widget will be in 5th and 2nd in 6th.
  function getWidgetNode(toolbar, position) {
    return toolbar.getElementsByTagName("toolbaritem")[position];
  }
  let browserWindow = windowUtils.activeBrowserWindow;
  let doc = browserWindow.document;
  let addonBar = doc.getElementById("addon-bar");
  let w2Toolbaritem = getWidgetNode(addonBar, 1);
  let w3ToolbarItem = getWidgetNode(addonBar, 2);
  let navBar = doc.getElementById("nav-bar");
  navBar.insertItem(w2Toolbaritem.id, navBar.childNodes[6], null, false);
  navBar.insertItem(w3ToolbarItem.id, navBar.childNodes[6], null, false);
  // Widget and Firefox codes rely on this `currentset` attribute,
  // so ensure it is correctly saved
  navBar.setAttribute("currentset", navBar.currentSet);
  doc.persist(navBar.id, "currentset");
  // Update addonbar too as we removed widget from there.
  // Otherwise, widgets may still be added to this toolbar.
  addonBar.setAttribute("currentset", addonBar.currentSet);
  doc.persist(addonBar.id, "currentset");

  tabBrowser.addTab("about:blank", { inNewWindow: true, onLoad: function(e) {
    let browserWindow = e.target.defaultView;
    let doc = browserWindow.document;
    let navBar = doc.getElementById("nav-bar");
    let addonBar = doc.getElementById("addon-bar");

    // Ensure that 1st is in addon bar
    test.assertEqual(getWidgetNode(addonBar, 0).getAttribute("label"), w1.label);
    // And that 2nd and 3rd keep their original positions in navigation bar
    test.assertEqual(navBar.childNodes[5].getAttribute("label"), w3.label);
    test.assertEqual(navBar.childNodes[6].getAttribute("label"), w2.label);

    w1.destroy();
    w2.destroy();
    w3.destroy();

    closeBrowserWindow(browserWindow, function() {
      test.done();
    });
  }});
};

/******************* helpers *********************/

// Helper for calling code at window close
function closeBrowserWindow(window, callback) {
  require("timer").setTimeout(function() {
    window.addEventListener("unload", function onUnload() {
      window.removeEventListener("unload", onUnload, false);
      callback();
    }, false);
    window.close();
  }, 0);
}

// ADD NO TESTS BELOW THIS LINE! ///////////////////////////////////////////////

// If the module doesn't support the app we're being run in, require() will
// throw.  In that case, remove all tests above from exports, and add one dummy
// test that passes.
try {
  require("widget");
}
catch (err) {
  // This bug should be mentioned in the error message.
  let bug = "https://bugzilla.mozilla.org/show_bug.cgi?id=560716";
  if (err.message.indexOf(bug) < 0)
    throw err;
  for (let [prop, val] in Iterator(exports)) {
    if (/^test/.test(prop) && typeof(val) === "function")
      delete exports[prop];
  }
  exports.testAppNotSupported = function (test) {
    test.pass("widget does not support this application.");
  };
}