addon-sdk/source/test/test-widget.js

Thu, 15 Jan 2015 15:59:08 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 15:59:08 +0100
branch
TOR_BUG_9701
changeset 10
ac0c01689b40
permissions
-rw-r--r--

Implement a real Private Browsing Mode condition by changing the API/ABI;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     4 'use strict';
     6 module.metadata = {
     7   'engines': {
     8     'Firefox': '*'
     9   }
    10 };
    12 const { Cc, Ci, Cu } = require("chrome");
    13 const { LoaderWithHookedConsole } = require('sdk/test/loader');
    14 const url = require("sdk/url");
    15 const timer = require("sdk/timers");
    16 const self = require("sdk/self");
    17 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
    18 const { close, open, focus } = require("sdk/window/helpers");
    19 const tabs = require("sdk/tabs/utils");
    20 const { merge } = require("sdk/util/object");
    21 const unload = require("sdk/system/unload");
    22 const fixtures = require("./fixtures");
    24 let jetpackID = "testID";
    25 try {
    26   jetpackID = require("sdk/self").id;
    27 } catch(e) {}
    29 function openNewWindowTab(url, options) {
    30   return open('chrome://browser/content/browser.xul', {
    31     features: {
    32       chrome: true,
    33       toolbar: true
    34     }
    35   }).then(focus).then(function(window) {
    36     if (options.onLoad) {
    37       options.onLoad({ target: { defaultView: window } })
    38     }
    40     return newTab;
    41   });
    42 }
    44 exports.testDeprecationMessage = function(assert, done) {
    45   let { loader } = LoaderWithHookedConsole(module, onMessage);
    47   // Intercept all console method calls
    48   let calls = [];
    49   function onMessage(type, msg) {
    50     assert.equal(type, 'error', 'the only message is an error');
    51     assert.ok(/^DEPRECATED:/.test(msg), 'deprecated');
    52     loader.unload();
    53     done();
    54   }
    55   loader.require('sdk/widget');
    56 }
    58 exports.testConstructor = function(assert, done) {
    59   let { loader: loader0 } = LoaderWithHookedConsole(module);
    60   const widgets = loader0.require("sdk/widget");
    61   let browserWindow = getMostRecentBrowserWindow();
    62   let doc = browserWindow.document;
    63   let AddonsMgrListener;
    65   AddonsMgrListener = {
    66     onInstalling: () => {},
    67     onInstalled: () => {},
    68     onUninstalling: () => {},
    69     onUninstalled: () => {}
    70   };
    72   let container = () => doc.getElementById("nav-bar");
    73   let getWidgets = () => container() ? container().querySelectorAll('[id^="widget\:"]') : [];
    74   let widgetCount = () => getWidgets().length;
    75   let widgetStartCount = widgetCount();
    76   let widgetNode = (index) => getWidgets()[index];
    78   // Test basic construct/destroy
    79   AddonsMgrListener.onInstalling();
    80   let w = widgets.Widget({ id: "basic-construct-destroy", label: "foo", content: "bar" });
    81   AddonsMgrListener.onInstalled();
    82   assert.equal(widgetCount(), widgetStartCount + 1, "panel has correct number of child elements after widget construction");
    84   // test widget height
    85   assert.equal(widgetNode(0).firstChild.boxObject.height, 16, "widget has correct default height");
    87   AddonsMgrListener.onUninstalling();
    88   w.destroy();
    89   AddonsMgrListener.onUninstalled();
    90   w.destroy();
    91   assert.pass("Multiple destroys do not cause an error");
    92   assert.equal(widgetCount(), widgetStartCount, "panel has correct number of child elements after destroy");
    94   // Test automatic widget destroy on unload
    95   let { loader } = LoaderWithHookedConsole(module);
    96   let widgetsFromLoader = loader.require("sdk/widget");
    97   let widgetStartCount = widgetCount();
    98   let w = widgetsFromLoader.Widget({ id: "destroy-on-unload", label: "foo", content: "bar" });
    99   assert.equal(widgetCount(), widgetStartCount + 1, "widget has been correctly added");
   100   loader.unload();
   101   assert.equal(widgetCount(), widgetStartCount, "widget has been destroyed on module unload");
   103   // Test nothing
   104   assert.throws(
   105     function() widgets.Widget({}),
   106     /^The widget must have a non-empty label property\.$/,
   107     "throws on no properties");
   109   // Test no label
   110   assert.throws(
   111     function() widgets.Widget({content: "foo"}),
   112     /^The widget must have a non-empty label property\.$/,
   113     "throws on no label");
   115   // Test empty label
   116   assert.throws(
   117     function() widgets.Widget({label: "", content: "foo"}),
   118     /^The widget must have a non-empty label property\.$/,
   119     "throws on empty label");
   121   // Test no content or image
   122   assert.throws(
   123     function() widgets.Widget({id: "no-content-throws", label: "foo"}),
   124     /^No content or contentURL property found\. Widgets must have one or the other\.$/,
   125     "throws on no content");
   127   // Test empty content, no image
   128   assert.throws(
   129     function() widgets.Widget({id:"empty-content-throws", label: "foo", content: ""}),
   130     /^No content or contentURL property found\. Widgets must have one or the other\.$/,
   131     "throws on empty content");
   133   // Test empty image, no content
   134   assert.throws(
   135     function() widgets.Widget({id:"empty-image-throws", label: "foo", image: ""}),
   136     /^No content or contentURL property found\. Widgets must have one or the other\.$/,
   137     "throws on empty content");
   139   // Test empty content, empty image
   140   assert.throws(
   141     function() widgets.Widget({id:"empty-image-and-content-throws", label: "foo", content: "", image: ""}),
   142     /^No content or contentURL property found. Widgets must have one or the other\.$/,
   143     "throws on empty content");
   145   // Test duplicated ID
   146   let duplicateID = widgets.Widget({id: "foo", label: "foo", content: "bar"});
   147   assert.throws(
   148     function() widgets.Widget({id: "foo", label: "bar", content: "bar"}),
   149     /^This widget ID is already used: foo$/,
   150     "throws on duplicated id");
   151   duplicateID.destroy();
   153   // Test Bug 652527
   154   assert.throws(
   155     function() widgets.Widget({id: "", label: "bar", content: "bar"}),
   156     /^You have to specify a unique value for the id property of your widget in order for the application to remember its position\./,
   157     "throws on falsey id");
   159   // Test duplicate label, different ID
   160   let w1 = widgets.Widget({id: "id1", label: "foo", content: "bar"});
   161   let w2 = widgets.Widget({id: "id2", label: "foo", content: "bar"});
   162   w1.destroy();
   163   w2.destroy();
   165   // Test position restore on create/destroy/create
   166   // Create 3 ordered widgets
   167   let w1 = widgets.Widget({id: "position-first", label:"first", content: "bar"});
   168   let w2 = widgets.Widget({id: "position-second", label:"second", content: "bar"});
   169   let w3 = widgets.Widget({id: "position-third", label:"third", content: "bar"});
   170   // Remove the middle widget
   171   assert.equal(widgetNode(1).getAttribute("label"), "second", "second widget is the second widget inserted");
   172   w2.destroy();
   173   assert.equal(widgetNode(1).getAttribute("label"), "third", "second widget is removed, so second widget is now the third one");
   174   w2 = widgets.Widget({id: "position-second", label:"second", content: "bar"});
   175   assert.equal(widgetNode(1).getAttribute("label"), "second", "second widget is created again, at the same location");
   176   // Cleanup this testcase
   177   AddonsMgrListener.onUninstalling();
   178   w1.destroy();
   179   w2.destroy();
   180   w3.destroy();
   181   AddonsMgrListener.onUninstalled();
   183   // Helper for testing a single widget.
   184   // Confirms proper addition and content setup.
   185   function testSingleWidget(widgetOptions) {
   186     // We have to display which test is being run, because here we do not
   187     // use the regular test framework but rather a custom one that iterates
   188     // the `tests` array.
   189     assert.pass("executing: " + widgetOptions.id);
   191     let startCount = widgetCount();
   192     let widget = widgets.Widget(widgetOptions);
   193     let node = widgetNode(startCount);
   194     assert.ok(node, "widget node at index");
   195     assert.equal(node.tagName, "toolbaritem", "widget element is correct");
   196     assert.equal(widget.width + "px", node.style.minWidth, "widget width is correct");
   197     assert.equal(widgetCount(), startCount + 1, "container has correct number of child elements");
   198     let content = node.firstElementChild;
   199     assert.ok(content, "found content");
   200     assert.ok(/iframe|image/.test(content.tagName), "content is iframe or image");
   201     return widget;
   202   }
   204   // Array of widgets to test
   205   // and a function to test them.
   206   let tests = [];
   207   function nextTest() {
   208     assert.equal(widgetCount(), 0, "widget in last test property cleaned itself up");
   209     if (!tests.length) {
   210       loader0.unload();
   211       done();
   212     }
   213     else
   214       timer.setTimeout(tests.shift(), 0);
   215   }
   216   function doneTest() nextTest();
   218   // text widget
   219   tests.push(function testTextWidget() testSingleWidget({
   220     id: "text-single",
   221     label: "text widget",
   222     content: "oh yeah",
   223     contentScript: "self.postMessage(document.body.innerHTML);",
   224     contentScriptWhen: "end",
   225     onMessage: function (message) {
   226       assert.equal(this.content, message, "content matches");
   227       this.destroy();
   228       doneTest();
   229     }
   230   }));
   232   // html widget
   233   tests.push(function testHTMLWidget() testSingleWidget({
   234     id: "html",
   235     label: "html widget",
   236     content: "<div>oh yeah</div>",
   237     contentScript: "self.postMessage(document.body.innerHTML);",
   238     contentScriptWhen: "end",
   239     onMessage: function (message) {
   240       assert.equal(this.content, message, "content matches");
   241       this.destroy();
   242       doneTest();
   243     }
   244   }));
   246   // image url widget
   247   tests.push(function testImageURLWidget() testSingleWidget({
   248     id: "image",
   249     label: "image url widget",
   250     contentURL: fixtures.url("test.html"),
   251     contentScript: "self.postMessage({title: document.title, " +
   252                    "tag: document.body.firstElementChild.tagName, " +
   253                    "content: document.body.firstElementChild.innerHTML});",
   254     contentScriptWhen: "end",
   255     onMessage: function (message) {
   256       assert.equal(message.title, "foo", "title matches");
   257       assert.equal(message.tag, "P", "element matches");
   258       assert.equal(message.content, "bar", "element content matches");
   259       this.destroy();
   260       doneTest();
   261     }
   262   }));
   264   // web uri widget
   265   tests.push(function testWebURIWidget() testSingleWidget({
   266     id: "web",
   267     label: "web uri widget",
   268     contentURL: fixtures.url("test.html"),
   269     contentScript: "self.postMessage({title: document.title, " +
   270                    "tag: document.body.firstElementChild.tagName, " +
   271                    "content: document.body.firstElementChild.innerHTML});",
   272     contentScriptWhen: "end",
   273     onMessage: function (message) {
   274       assert.equal(message.title, "foo", "title matches");
   275       assert.equal(message.tag, "P", "element matches");
   276       assert.equal(message.content, "bar", "element content matches");
   277       this.destroy();
   278       doneTest();
   279     }
   280   }));
   282   // event: onclick + content
   283   tests.push(function testOnclickEventContent() testSingleWidget({
   284     id: "click-content",
   285     label: "click test widget - content",
   286     content: "<div id='me'>foo</div>",
   287     contentScript: "var evt = new MouseEvent('click', {button: 0});" +
   288                    "document.getElementById('me').dispatchEvent(evt);",
   289     contentScriptWhen: "end",
   290     onClick: function() {
   291       assert.pass("onClick called");
   292       this.destroy();
   293       doneTest();
   294     }
   295   }));
   297   // event: onmouseover + content
   298   tests.push(function testOnmouseoverEventContent() testSingleWidget({
   299     id: "mouseover-content",
   300     label: "mouseover test widget - content",
   301     content: "<div id='me'>foo</div>",
   302     contentScript: "var evt = new MouseEvent('mouseover'); " +
   303                    "document.getElementById('me').dispatchEvent(evt);",
   304     contentScriptWhen: "end",
   305     onMouseover: function() {
   306       assert.pass("onMouseover called");
   307       this.destroy();
   308       doneTest();
   309     }
   310   }));
   312   // event: onmouseout + content
   313   tests.push(function testOnmouseoutEventContent() testSingleWidget({
   314     id: "mouseout-content",
   315     label: "mouseout test widget - content",
   316     content: "<div id='me'>foo</div>",
   317     contentScript: "var evt = new MouseEvent('mouseout');" +
   318                    "document.getElementById('me').dispatchEvent(evt);",
   319     contentScriptWhen: "end",
   320     onMouseout: function() {
   321       assert.pass("onMouseout called");
   322       this.destroy();
   323       doneTest();
   324     }
   325   }));
   327   // event: onclick + image
   328   tests.push(function testOnclickEventImage() testSingleWidget({
   329     id: "click-image",
   330     label: "click test widget - image",
   331     contentURL: fixtures.url("moz_favicon.ico"),
   332     contentScript: "var evt = new MouseEvent('click'); " +
   333                    "document.body.firstElementChild.dispatchEvent(evt);",
   334     contentScriptWhen: "end",
   335     onClick: function() {
   336       assert.pass("onClick called");
   337       this.destroy();
   338       doneTest();
   339     }
   340   }));
   342   // event: onmouseover + image
   343   tests.push(function testOnmouseoverEventImage() testSingleWidget({
   344     id: "mouseover-image",
   345     label: "mouseover test widget - image",
   346     contentURL: fixtures.url("moz_favicon.ico"),
   347     contentScript: "var evt = new MouseEvent('mouseover');" +
   348                    "document.body.firstElementChild.dispatchEvent(evt);",
   349     contentScriptWhen: "end",
   350     onMouseover: function() {
   351       assert.pass("onMouseover called");
   352       this.destroy();
   353       doneTest();
   354     }
   355   }));
   357   // event: onmouseout + image
   358   tests.push(function testOnmouseoutEventImage() testSingleWidget({
   359     id: "mouseout-image",
   360     label: "mouseout test widget - image",
   361     contentURL: fixtures.url("moz_favicon.ico"),
   362     contentScript: "var evt = new MouseEvent('mouseout'); " +
   363                    "document.body.firstElementChild.dispatchEvent(evt);",
   364     contentScriptWhen: "end",
   365     onMouseout: function() {
   366       assert.pass("onMouseout called");
   367       this.destroy();
   368       doneTest();
   369     }
   370   }));
   372   // test multiple widgets
   373   tests.push(function testMultipleWidgets() {
   374     let w1 = widgets.Widget({id: "first", label: "first widget", content: "first content"});
   375     let w2 = widgets.Widget({id: "second", label: "second widget", content: "second content"});
   377     w1.destroy();
   378     w2.destroy();
   380     doneTest();
   381   });
   383   // test updating widget content
   384   let loads = 0;
   385   tests.push(function testUpdatingWidgetContent() testSingleWidget({
   386     id: "content-updating",
   387     label: "content update test widget",
   388     content: "<div id='me'>foo</div>",
   389     contentScript: "self.postMessage(1)",
   390     contentScriptWhen: "ready",
   391     onMessage: function(message) {
   392       if (!this.flag) {
   393         this.content = "<div id='me'>bar</div>";
   394         this.flag = 1;
   395       }
   396       else {
   397         assert.equal(this.content, "<div id='me'>bar</div>", 'content is as expected');
   398         this.destroy();
   399         doneTest();
   400       }
   401     }
   402   }));
   404   // test updating widget contentURL
   405   let url1 = "data:text/html;charset=utf-8,<body>foodle</body>";
   406   let url2 = "data:text/html;charset=utf-8,<body>nistel</body>";
   408   tests.push(function testUpdatingContentURL() testSingleWidget({
   409     id: "content-url-updating",
   410     label: "content update test widget",
   411     contentURL: url1,
   412     contentScript: "self.postMessage(document.location.href);",
   413     contentScriptWhen: "end",
   414     onMessage: function(message) {
   415       if (!this.flag) {
   416         assert.equal(this.contentURL.toString(), url1);
   417         assert.equal(message, url1);
   418         this.contentURL = url2;
   419         this.flag = 1;
   420       }
   421       else {
   422         assert.equal(this.contentURL.toString(), url2);
   423         assert.equal(message, url2);
   424         this.destroy();
   425         doneTest();
   426       }
   427     }
   428   }));
   430   // test tooltip
   431   tests.push(function testTooltip() testSingleWidget({
   432     id: "text-with-tooltip",
   433     label: "text widget",
   434     content: "oh yeah",
   435     tooltip: "foo",
   436     contentScript: "self.postMessage(1)",
   437     contentScriptWhen: "ready",
   438     onMessage: function(message) {
   439       assert.equal(this.tooltip, "foo", "tooltip matches");
   440       this.destroy();
   441       doneTest();
   442     }
   443   }));
   445   // test tooltip fallback to label
   446   tests.push(function testTooltipFallback() testSingleWidget({
   447     id: "fallback",
   448     label: "fallback",
   449     content: "oh yeah",
   450     contentScript: "self.postMessage(1)",
   451     contentScriptWhen: "ready",
   452     onMessage: function(message) {
   453       assert.equal(this.tooltip, this.label, "tooltip fallbacks to label");
   454       this.destroy();
   455       doneTest();
   456     }
   457   }));
   459   // test updating widget tooltip
   460   let updated = false;
   461   tests.push(function testUpdatingTooltip() testSingleWidget({
   462     id: "tooltip-updating",
   463     label: "tooltip update test widget",
   464     tooltip: "foo",
   465     content: "<div id='me'>foo</div>",
   466     contentScript: "self.postMessage(1)",
   467     contentScriptWhen: "ready",
   468     onMessage: function(message) {
   469       this.tooltip = "bar";
   470       assert.equal(this.tooltip, "bar", "tooltip gets updated");
   471       this.destroy();
   472       doneTest();
   473     }
   474   }));
   476   // test allow attribute
   477   tests.push(function testDefaultAllow() testSingleWidget({
   478     id: "allow-default",
   479     label: "allow.script attribute",
   480     content: "<script>document.title = 'ok';</script>",
   481     contentScript: "self.postMessage(document.title)",
   482     onMessage: function(message) {
   483       assert.equal(message, "ok", "scripts are evaluated by default");
   484       this.destroy();
   485       doneTest();
   486     }
   487   }));
   489   tests.push(function testExplicitAllow() testSingleWidget({
   490     id: "allow-explicit",
   491     label: "allow.script attribute",
   492     allow: {script: true},
   493     content: "<script>document.title = 'ok';</script>",
   494     contentScript: "self.postMessage(document.title)",
   495     onMessage: function(message) {
   496       assert.equal(message, "ok", "scripts are evaluated when we want to");
   497       this.destroy();
   498       doneTest();
   499     }
   500   }));
   502   tests.push(function testExplicitDisallow() testSingleWidget({
   503     id: "allow-explicit-disallow",
   504     label: "allow.script attribute",
   505     content: "<script>document.title = 'ok';</script>",
   506     allow: {script: false},
   507     contentScript: "self.postMessage(document.title)",
   508     onMessage: function(message) {
   509       assert.notEqual(message, "ok", "scripts aren't evaluated when " +
   510                                          "explicitly blocked it");
   511       this.destroy();
   512       doneTest();
   513     }
   514   }));
   516   // test multiple windows
   517   tests.push(function testMultipleWindows() {
   518     assert.pass('executing test multiple windows');
   519     openNewWindowTab("about:blank", { inNewWindow: true, onLoad: function(e) {
   520       let browserWindow = e.target.defaultView;
   521       assert.ok(browserWindow, 'window was opened');
   522       let doc = browserWindow.document;
   523       let container = () => doc.getElementById("nav-bar");
   524       let widgetCount2 = () => container() ? container().querySelectorAll('[id^="widget\:"]').length : 0;
   525       let widgetStartCount2 = widgetCount2();
   527       let w1Opts = {id:"first-multi-window", label: "first widget", content: "first content"};
   528       let w1 = testSingleWidget(w1Opts);
   529       assert.equal(widgetCount2(), widgetStartCount2 + 1, "2nd window has correct number of child elements after first widget");
   531       let w2Opts = {id:"second-multi-window", label: "second widget", content: "second content"};
   532       let w2 = testSingleWidget(w2Opts);
   533       assert.equal(widgetCount2(), widgetStartCount2 + 2, "2nd window has correct number of child elements after second widget");
   535       w1.destroy();
   536       assert.equal(widgetCount2(), widgetStartCount2 + 1, "2nd window has correct number of child elements after first destroy");
   537       w2.destroy();
   538       assert.equal(widgetCount2(), widgetStartCount2, "2nd window has correct number of child elements after second destroy");
   540       close(browserWindow).then(doneTest);
   541     }});
   542   });
   544   // test window closing
   545   tests.push(function testWindowClosing() {
   546     // 1/ Create a new widget
   547     let w1Opts = {
   548       id:"first-win-closing",
   549       label: "first widget",
   550       content: "first content",
   551       contentScript: "self.port.on('event', function () self.port.emit('event'))"
   552     };
   553     let widget = testSingleWidget(w1Opts);
   554     let windows = loader0.require("sdk/windows").browserWindows;
   556     // 2/ Retrieve a WidgetView for the initial browser window
   557     let acceptDetach = false;
   558     let mainView = widget.getView(windows.activeWindow);
   559     assert.ok(mainView, "Got first widget view");
   560     mainView.on("detach", function () {
   561       // 8/ End of our test. Accept detach event only when it occurs after
   562       // widget.destroy()
   563       if (acceptDetach)
   564         doneTest();
   565       else
   566         assert.fail("View on initial window should not be destroyed");
   567     });
   568     mainView.port.on("event", function () {
   569       // 7/ Receive event sent during 6/ and cleanup our test
   570       acceptDetach = true;
   571       widget.destroy();
   572     });
   574     // 3/ First: open a new browser window
   575     windows.open({
   576       url: "about:blank",
   577       onOpen: function(window) {
   578         // 4/ Retrieve a WidgetView for this new window
   579         let view = widget.getView(window);
   580         assert.ok(view, "Got second widget view");
   581         view.port.on("event", function () {
   582           assert.fail("We should not receive event on the detach view");
   583         });
   584         view.on("detach", function () {
   585           // The related view is destroyed
   586           // 6/ Send a custom event
   587           assert.throws(function () {
   588               view.port.emit("event");
   589             },
   590             /^The widget has been destroyed and can no longer be used.$/,
   591             "emit on a destroyed view should throw");
   592           widget.port.emit("event");
   593         });
   595         // 5/ Destroy this window
   596         window.close();
   597       }
   598     });
   599   });
   601   if (false) {
   602     tests.push(function testAddonBarHide() {
   603       // Hide the addon-bar
   604       browserWindow.setToolbarVisibility(container(), false);
   605       assert.ok(container().collapsed,
   606                 "1st window starts with an hidden addon-bar");
   608       // Then open a browser window and verify that the addon-bar remains hidden
   609       openNewWindowTab("about:blank", { inNewWindow: true, onLoad: function(e) {
   610         let browserWindow2 = e.target.defaultView;
   611         let doc2 = browserWindow2.document;
   612         function container2() doc2.getElementById("addon-bar");
   613         function widgetCount2() container2() ? container2().childNodes.length : 0;
   614         let widgetStartCount2 = widgetCount2();
   615         assert.ok(container2().collapsed,
   616                   "2nd window starts with an hidden addon-bar");
   618         let w1Opts = {id:"first-addonbar-hide", label: "first widget", content: "first content"};
   619         let w1 = testSingleWidget(w1Opts);
   620         assert.equal(widgetCount2(), widgetStartCount2 + 1,
   621                      "2nd window has correct number of child elements after" +
   622                      "widget creation");
   623         assert.ok(!container().collapsed, "1st window has a visible addon-bar");
   624         assert.ok(!container2().collapsed, "2nd window has a visible addon-bar");
   625         w1.destroy();
   626         assert.equal(widgetCount2(), widgetStartCount2,
   627                      "2nd window has correct number of child elements after" +
   628                      "widget destroy");
   630         assert.ok(container().collapsed, "1st window has an hidden addon-bar");
   631         assert.ok(container2().collapsed, "2nd window has an hidden addon-bar");
   633         // Reset addon-bar visibility before exiting this test
   634         browserWindow.setToolbarVisibility(container(), true);
   636         close(browserWindow2).then(doneTest);
   637       }});
   638     });
   639   }
   641   // test widget.width
   642   tests.push(function testWidgetWidth() testSingleWidget({
   643     id: "text-test-width",
   644     label: "test widget.width",
   645     content: "test width",
   646     width: 64,
   647     contentScript: "self.postMessage(1)",
   648     contentScriptWhen: "ready",
   649     onMessage: function(message) {
   650       assert.equal(this.width, 64, 'width is 64');
   652       let node = widgetNode(0);
   653       assert.equal(this.width, node.style.minWidth.replace("px", ""));
   654       assert.equal(this.width, node.firstElementChild.style.width.replace("px", ""));
   655       this.width = 48;
   656       assert.equal(this.width, node.style.minWidth.replace("px", ""));
   657       assert.equal(this.width, node.firstElementChild.style.width.replace("px", ""));
   659       this.destroy();
   660       doneTest();
   661     }
   662   }));
   664   // test click handler not respond to right-click
   665   let clickCount = 0;
   666   tests.push(function testNoRightClick() testSingleWidget({
   667     id: "right-click-content",
   668     label: "click test widget - content",
   669     content: "<div id='me'>foo</div>",
   670     contentScript: // Left click
   671                    "var evt = new MouseEvent('click', {button: 0});" +
   672                    "document.getElementById('me').dispatchEvent(evt); " +
   673                    // Middle click
   674                    "evt = new MouseEvent('click', {button: 1});" +
   675                    "document.getElementById('me').dispatchEvent(evt); " +
   676                    // Right click
   677                    "evt = new MouseEvent('click', {button: 2});" +
   678                    "document.getElementById('me').dispatchEvent(evt); " +
   679                    // Mouseover
   680                    "evt = new MouseEvent('mouseover');" +
   681                    "document.getElementById('me').dispatchEvent(evt);",
   682     contentScriptWhen: "end",
   683     onClick: function() clickCount++,
   684     onMouseover: function() {
   685       assert.equal(clickCount, 1, "only left click was sent to click handler");
   686       this.destroy();
   687       doneTest();
   688     }
   689   }));
   691   // kick off test execution
   692   doneTest();
   693 };
   695 exports.testWidgetWithValidPanel = function(assert, done) {
   696   let { loader } = LoaderWithHookedConsole(module);
   697   const { Widget } = loader.require("sdk/widget");
   698   const { Panel } = loader.require("sdk/panel");
   700   let widget1 = Widget({
   701     id: "testWidgetWithValidPanel",
   702     label: "panel widget 1",
   703     content: "<div id='me'>foo</div>",
   704     contentScript: "var evt = new MouseEvent('click', {button: 0});" +
   705                    "document.body.dispatchEvent(evt);",
   706     contentScriptWhen: "end",
   707     panel: Panel({
   708       contentURL: "data:text/html;charset=utf-8,<body>Look ma, a panel!</body>",
   709       onShow: function() {
   710         let { document } = getMostRecentBrowserWindow();
   711         let widgetEle = document.getElementById("widget:" + jetpackID + "-" + widget1.id);
   712         let panelEle = document.getElementById('mainPopupSet').lastChild;
   713         // See bug https://bugzilla.mozilla.org/show_bug.cgi?id=859592
   714         assert.equal(panelEle.getAttribute("type"), "arrow", 'the panel is a arrow type');
   715         assert.strictEqual(panelEle.anchorNode, widgetEle, 'the panel is properly anchored to the widget');
   717         widget1.destroy();
   718         loader.unload();
   719         assert.pass("panel displayed on click");
   720         done();
   721       }
   722     })
   723   });
   724 };
   726 exports.testWidgetWithInvalidPanel = function(assert) {
   727   let { loader } = LoaderWithHookedConsole(module);
   728   const widgets = loader.require("sdk/widget");
   730   assert.throws(
   731     function() {
   732       widgets.Widget({
   733         id: "panel2",
   734         label: "panel widget 2",
   735         panel: {}
   736       });
   737     },
   738     /^The option \"panel\" must be one of the following types: null, undefined, object$/,
   739     "widget.panel must be a Panel object");
   740   loader.unload();
   741 };
   743 exports.testPanelWidget3 = function testPanelWidget3(assert, done) {
   744   let { loader } = LoaderWithHookedConsole(module);
   745   const widgets = loader.require("sdk/widget");
   746   const { Panel } = loader.require("sdk/panel");
   748   let onClickCalled = false;
   749   let widget3 = widgets.Widget({
   750     id: "panel3",
   751     label: "panel widget 3",
   752     content: "<div id='me'>foo</div>",
   753     contentScript: "var evt = new MouseEvent('click', {button: 0});" +
   754                    "document.body.firstElementChild.dispatchEvent(evt);",
   755     contentScriptWhen: "end",
   756     onClick: function() {
   757       onClickCalled = true;
   758       this.panel.show();
   759     },
   760     panel: Panel({
   761       contentURL: "data:text/html;charset=utf-8,<body>Look ma, a panel!</body>",
   762       onShow: function() {
   763         assert.ok(
   764           onClickCalled,
   765           "onClick called on click for widget with both panel and onClick");
   766         widget3.destroy();
   767         loader.unload();
   768         done();
   769       }
   770     })
   771   });
   772 };
   774 exports.testWidgetWithPanelInMenuPanel = function(assert, done) {
   775   const { CustomizableUI } = Cu.import("resource:///modules/CustomizableUI.jsm", {});
   776   let { loader } = LoaderWithHookedConsole(module);
   777   const widgets = loader.require("sdk/widget");
   778   const { Panel } = loader.require("sdk/panel");
   780   let widget1 = widgets.Widget({
   781     id: "panel1",
   782     label: "panel widget 1",
   783     content: "<div id='me'>foo</div>",
   784     contentScript: "new " + function() {
   785       self.port.on('click', () => {
   786         let evt = new MouseEvent('click', {button: 0});
   787         document.body.dispatchEvent(evt);
   788       });
   789     },
   790     contentScriptWhen: "end",
   791     panel: Panel({
   792       contentURL: "data:text/html;charset=utf-8,<body>Look ma, a panel!</body>",
   793       onShow: function() {
   794         let { document } = getMostRecentBrowserWindow();
   795         let { anchorNode } = document.getElementById('mainPopupSet').lastChild;
   796         let panelButtonNode = document.getElementById("PanelUI-menu-button");
   798         assert.strictEqual(anchorNode, panelButtonNode,
   799           'the panel is anchored to the panel menu button instead of widget');
   801         widget1.destroy();
   802         loader.unload();
   803         done();
   804       }
   805     })
   806   });
   808   let widgetId = "widget:" + jetpackID + "-" + widget1.id;
   810   CustomizableUI.addListener({
   811     onWidgetAdded: function(id) {
   812       if (id !== widgetId) return;
   814       let { document, PanelUI } = getMostRecentBrowserWindow();
   816       PanelUI.panel.addEventListener('popupshowing', function onshow({type}) {
   817         this.removeEventListener(type, onshow);
   818         widget1.port.emit('click');
   819       });
   821       document.getElementById("PanelUI-menu-button").click()
   822     }
   823   });
   825   CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_PANEL);
   826 };
   828 exports.testWidgetMessaging = function testWidgetMessaging(assert, done) {
   829   let { loader } = LoaderWithHookedConsole(module);
   830   const widgets = loader.require("sdk/widget");
   832   let origMessage = "foo";
   833   let widget = widgets.Widget({
   834     id: "widget-messaging",
   835     label: "foo",
   836     content: "<bar>baz</bar>",
   837     contentScriptWhen: "end",
   838     contentScript: "self.on('message', function(data) { self.postMessage(data); }); self.postMessage('ready');",
   839     onMessage: function(message) {
   840       if (message == "ready")
   841         widget.postMessage(origMessage);
   842       else {
   843         assert.equal(origMessage, message);
   844         widget.destroy();
   845         loader.unload();
   846         done();
   847       }
   848     }
   849   });
   850 };
   852 exports.testWidgetViews = function testWidgetViews(assert, done) {
   853   let { loader } = LoaderWithHookedConsole(module);
   854   const widgets = loader.require("sdk/widget");
   856   let widget = widgets.Widget({
   857     id: "widget-views",
   858     label: "foo",
   859     content: "<bar>baz</bar>",
   860     contentScriptWhen: "ready",
   861     contentScript: "self.on('message', function(data) self.postMessage(data)); self.postMessage('ready')",
   862     onAttach: function(view) {
   863       assert.pass("WidgetView created");
   864       view.on("message", function () {
   865         assert.pass("Got message in WidgetView");
   866         widget.destroy();
   867       });
   868       view.on("detach", function () {
   869         assert.pass("WidgetView destroyed");
   870         loader.unload();
   871         done();
   872       });
   873     }
   874   });
   875 };
   877 exports.testWidgetViewsUIEvents = function testWidgetViewsUIEvents(assert, done) {
   878   let { loader } = LoaderWithHookedConsole(module);
   879   const widgets = loader.require("sdk/widget");
   880   const { browserWindows } = loader.require("sdk/windows");
   882   let view = null;
   883   let widget = widgets.Widget({
   884     id: "widget-view-ui-events",
   885     label: "foo",
   886     content: "<div id='me'>foo</div>",
   887     contentScript: "var evt = new MouseEvent('click', {button: 0});" +
   888                    "document.getElementById('me').dispatchEvent(evt);",
   889     contentScriptWhen: "ready",
   890     onAttach: function(attachView) {
   891       view = attachView;
   892       assert.pass("Got attach event");
   893     },
   894     onClick: function (eventView) {
   895       assert.equal(view, eventView,
   896                          "event first argument is equal to the WidgetView");
   897       let view2 = widget.getView(browserWindows.activeWindow);
   898       assert.equal(view, view2,
   899                          "widget.getView return the same WidgetView");
   900       widget.destroy();
   901       loader.unload();
   902       done();
   903     }
   904   });
   905 };
   907 exports.testWidgetViewsCustomEvents = function testWidgetViewsCustomEvents(assert, done) {
   908   let { loader } = LoaderWithHookedConsole(module);
   909   const widgets = loader.require("sdk/widget");
   911   let widget = widgets.Widget({
   912     id: "widget-view-custom-events",
   913     label: "foo",
   914     content: "<div id='me'>foo</div>",
   915     contentScript: "self.port.emit('event', 'ok');",
   916     contentScriptWhen: "ready",
   917     onAttach: function(view) {
   918       view.port.on("event", function (data) {
   919         assert.equal(data, "ok",
   920                          "event argument is valid on WidgetView");
   921       });
   922     },
   923   });
   924   widget.port.on("event", function (data) {
   925     assert.equal(data, "ok", "event argument is valid on Widget");
   926     widget.destroy();
   927     loader.unload();
   928     done();
   929   });
   930 };
   932 exports.testWidgetViewsTooltip = function testWidgetViewsTooltip(assert, done) {
   933   let { loader } = LoaderWithHookedConsole(module);
   934   const widgets = loader.require("sdk/widget");
   935   const { browserWindows } = loader.require("sdk/windows");
   937   let widget = new widgets.Widget({
   938     id: "widget-views-tooltip",
   939     label: "foo",
   940     content: "foo"
   941   });
   942   let view = widget.getView(browserWindows.activeWindow);
   943   widget.tooltip = null;
   944   assert.equal(view.tooltip, "foo",
   945                "view tooltip defaults to base widget label");
   946   assert.equal(widget.tooltip, "foo",
   947                "tooltip defaults to base widget label");
   948   widget.destroy();
   949   loader.unload();
   950   done();
   951 };
   953 exports.testWidgetMove = function testWidgetMove(assert, done) {
   954   let { loader } = LoaderWithHookedConsole(module);
   955   const widgets = loader.require("sdk/widget");
   957   let browserWindow = getMostRecentBrowserWindow();
   958   let doc = browserWindow.document;
   960   let label = "unique-widget-label";
   961   let origMessage = "message after node move";
   962   let gotFirstReady = false;
   964   let widget = widgets.Widget({
   965     id: "widget-move",
   966     label: label,
   967     content: "<bar>baz</bar>",
   968     contentScriptWhen: "ready",
   969     contentScript: "self.on('message', function(data) { self.postMessage(data); }); self.postMessage('ready');",
   970     onMessage: function(message) {
   971       if (message == "ready") {
   972         if (!gotFirstReady) {
   973           assert.pass("Got first ready event");
   974           let widgetNode = doc.querySelector('toolbaritem[label="' + label + '"]');
   975           let parent = widgetNode.parentNode;
   976           parent.insertBefore(widgetNode, parent.firstChild);
   977           gotFirstReady = true;
   978         }
   979         else {
   980           assert.pass("Got second ready event");
   981           widget.postMessage(origMessage);
   982         }
   983       }
   984       else {
   985         assert.equal(origMessage, message, "Got message after node move");
   986         widget.destroy();
   987         loader.unload();
   988         done();
   989       }
   990     }
   991   });
   992 };
   994 /*
   995 The bug is exhibited when a widget with HTML content has it's content
   996 changed to new HTML content with a pound in it. Because the src of HTML
   997 content is converted to a data URI, the underlying iframe doesn't
   998 consider the content change a navigation change, so doesn't load
   999 the new content.
  1000 */
  1001 exports.testWidgetWithPound = function testWidgetWithPound(assert, done) {
  1002   let { loader } = LoaderWithHookedConsole(module);
  1003   const widgets = loader.require("sdk/widget");
  1005   function getWidgetContent(widget) {
  1006     let browserWindow = getMostRecentBrowserWindow();
  1007     let doc = browserWindow.document;
  1008     let widgetNode = doc.querySelector('toolbaritem[label="' + widget.label + '"]');
  1009     assert.ok(widgetNode, 'found widget node in the front-end');
  1010     return widgetNode.firstChild.contentDocument.body.innerHTML;
  1013   let count = 0;
  1014   let widget = widgets.Widget({
  1015     id: "1",
  1016     label: "foo",
  1017     content: "foo",
  1018     contentScript: "window.addEventListener('load', self.postMessage, false);",
  1019     onMessage: function() {
  1020       count++;
  1021       if (count == 1) {
  1022         widget.content = "foo#";
  1024       else {
  1025         assert.equal(getWidgetContent(widget), "foo#", "content updated to pound?");
  1026         widget.destroy();
  1027         loader.unload();
  1028         done();
  1031   });
  1032 };
  1034 exports.testContentScriptOptionsOption = function(assert, done) {
  1035   let { loader } = LoaderWithHookedConsole(module);
  1036   const { Widget } = loader.require("sdk/widget");
  1038   let widget = Widget({
  1039       id: "widget-script-options",
  1040       label: "fooz",
  1041       content: "fooz",
  1042       contentScript: "self.postMessage( [typeof self.options.d, self.options] );",
  1043       contentScriptWhen: "end",
  1044       contentScriptOptions: {a: true, b: [1,2,3], c: "string", d: function(){ return 'test'}},
  1045       onMessage: function(msg) {
  1046         assert.equal( msg[0], 'undefined', 'functions are stripped from contentScriptOptions' );
  1047         assert.equal( typeof msg[1], 'object', 'object as contentScriptOptions' );
  1048         assert.equal( msg[1].a, true, 'boolean in contentScriptOptions' );
  1049         assert.equal( msg[1].b.join(), '1,2,3', 'array and numbers in contentScriptOptions' );
  1050         assert.equal( msg[1].c, 'string', 'string in contentScriptOptions' );
  1051         widget.destroy();
  1052         loader.unload();
  1053         done();
  1055     });
  1056 };
  1058 exports.testOnAttachWithoutContentScript = function(assert, done) {
  1059   let { loader } = LoaderWithHookedConsole(module);
  1060   const { Widget } = loader.require("sdk/widget");
  1062   let widget = Widget({
  1063       id: "onAttachNoCS",
  1064       label: "onAttachNoCS",
  1065       content: "onAttachNoCS",
  1066       onAttach: function (view) {
  1067         assert.pass("received attach event");
  1068         widget.destroy();
  1069         loader.unload();
  1070         done();
  1072     });
  1073 };
  1075 exports.testPostMessageOnAttach = function(assert, done) {
  1076   let { loader } = LoaderWithHookedConsole(module);
  1077   const { Widget } = loader.require("sdk/widget");
  1079   let widget = Widget({
  1080       id: "onAttach",
  1081       label: "onAttach",
  1082       content: "onAttach",
  1083       // 1) Send a message immediatly after `attach` event
  1084       onAttach: function (view) {
  1085         view.postMessage("ok");
  1086       },
  1087       // 2) Listen to it and forward it back to the widget
  1088       contentScript: "self.on('message', self.postMessage);",
  1089       // 3) Listen to this forwarded message
  1090       onMessage: function (msg) {
  1091         assert.equal( msg, "ok", "postMessage works on `attach` event");
  1092         widget.destroy();
  1093         loader.unload();
  1094         done();
  1096     });
  1097 };
  1099 exports.testPostMessageOnLocationChange = function(assert, done) {
  1100   let { loader } = LoaderWithHookedConsole(module);
  1101   const { Widget } = loader.require("sdk/widget");
  1103   let attachEventCount = 0;
  1104   let messagesCount = 0;
  1105   let widget = Widget({
  1106       id: "onLocationChange",
  1107       label: "onLocationChange",
  1108       content: "onLocationChange",
  1109       contentScript: "new " + function ContentScriptScope() {
  1110         // Emit an event when content script is applied in order to know when
  1111         // the first document is loaded so that we can load the 2nd one
  1112         self.postMessage("ready");
  1113         // And forward any incoming message back to the widget to see if
  1114         // messaging is working on 2nd document
  1115         self.on("message", self.postMessage);
  1116       },
  1117       onMessage: function (msg) {
  1118         messagesCount++;
  1119         if (messagesCount == 1) {
  1120           assert.equal(msg, "ready", "First document is loaded");
  1121           widget.content = "location changed";
  1123         else if (messagesCount == 2) {
  1124           assert.equal(msg, "ready", "Second document is loaded");
  1125           widget.postMessage("ok");
  1127         else if (messagesCount == 3) {
  1128           assert.equal(msg, "ok",
  1129                        "We receive the message sent to the 2nd document");
  1130           widget.destroy();
  1131           loader.unload();
  1132           done();
  1135     });
  1136 };
  1138 exports.testSVGWidget = function(assert, done) {
  1139   let { loader } = LoaderWithHookedConsole(module);
  1140   const { Widget } = loader.require("sdk/widget");
  1142   // use of capital SVG here is intended, that was failing..
  1143   let SVG_URL = fixtures.url("mofo_logo.SVG");
  1145   let widget = Widget({
  1146     id: "mozilla-svg-logo",
  1147     label: "moz foundation logo",
  1148     contentURL: SVG_URL,
  1149     contentScript: "self.postMessage({count: window.document.images.length, src: window.document.images[0].src});",
  1150     onMessage: function(data) {
  1151       widget.destroy();
  1152       assert.equal(data.count, 1, 'only one image');
  1153       assert.equal(data.src, SVG_URL, 'only one image');
  1154       loader.unload();
  1155       done();
  1157   });
  1158 };
  1160 exports.testReinsertion = function(assert, done) {
  1161   let { loader } = LoaderWithHookedConsole(module);
  1162   const { Widget } = loader.require("sdk/widget");
  1163   const WIDGETID = "test-reinsertion";
  1164   let browserWindow = getMostRecentBrowserWindow();
  1166   let widget = Widget({
  1167     id: "test-reinsertion",
  1168     label: "test reinsertion",
  1169     content: "Test",
  1170   });
  1171   let realWidgetId = "widget:" + jetpackID + "-" + WIDGETID;
  1172   // Remove the widget:
  1174   browserWindow.CustomizableUI.removeWidgetFromArea(realWidgetId);
  1176   openNewWindowTab("about:blank", { inNewWindow: true, onLoad: function(e) {
  1177     assert.equal(e.target.defaultView.document.getElementById(realWidgetId), null);
  1178     close(e.target.defaultView).then(_ => {
  1179       loader.unload();
  1180       done();
  1181     });
  1182   }});
  1183 };
  1185 exports.testWideWidget = function testWideWidget(assert) {
  1186   let { loader } = LoaderWithHookedConsole(module);
  1187   const widgets = loader.require("sdk/widget");
  1188   const { document, CustomizableUI, gCustomizeMode, setTimeout } = getMostRecentBrowserWindow();
  1190   let wideWidget = widgets.Widget({
  1191     id: "my-wide-widget",
  1192     label: "wide-wdgt",
  1193     content: "foo",
  1194     width: 200
  1195   });
  1197   let widget = widgets.Widget({
  1198     id: "my-regular-widget",
  1199     label: "reg-wdgt",
  1200     content: "foo"
  1201   });
  1203   let wideWidgetNode = document.querySelector("toolbaritem[label=wide-wdgt]");
  1204   let widgetNode = document.querySelector("toolbaritem[label=reg-wdgt]");
  1206   assert.equal(wideWidgetNode, null,
  1207     "Wide Widget are not added to UI");
  1209   assert.notEqual(widgetNode, null,
  1210     "regular size widget are in the UI");
  1211 };
  1213 require("sdk/test").run(exports);

mercurial