diff options
Diffstat (limited to 'tools/addon-sdk-1.7/packages')
321 files changed, 54122 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.7/packages/addon-kit/README.md b/tools/addon-sdk-1.7/packages/addon-kit/README.md new file mode 100644 index 0000000..cfbb4df --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/README.md @@ -0,0 +1,12 @@ +<!-- 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/. --> + +The addon-kit package provides high-level APIs for add-on developers. +Most of the needs of most add-on developers should be served by the modules +found here. Modules in this packages don't require any special privileges to +run. + +The modules in the addon-kit package are relatively stable. We intend to add +new APIs here and extend existing ones, but will avoid making incompatible +changes to them unless absolutely necessary. diff --git a/tools/addon-sdk-1.7/packages/addon-kit/data/index.html b/tools/addon-sdk-1.7/packages/addon-kit/data/index.html new file mode 100644 index 0000000..7095e7d --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/data/index.html @@ -0,0 +1,12 @@ +<!-- 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/. --> + +<html> + <head> + <title>Add-on Page</title> + </head> + <body> + <p>This is an add-on page test!</p> + </body> +</html> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/data/moz_favicon.ico b/tools/addon-sdk-1.7/packages/addon-kit/data/moz_favicon.ico Binary files differnew file mode 100644 index 0000000..d444389 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/data/moz_favicon.ico diff --git a/tools/addon-sdk-1.7/packages/addon-kit/data/pagemod-css-include-file.css b/tools/addon-sdk-1.7/packages/addon-kit/data/pagemod-css-include-file.css new file mode 100644 index 0000000..91d8e25 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/data/pagemod-css-include-file.css @@ -0,0 +1 @@ +div { border: 10px solid black; } diff --git a/tools/addon-sdk-1.7/packages/addon-kit/data/test-context-menu.js b/tools/addon-sdk-1.7/packages/addon-kit/data/test-context-menu.js new file mode 100644 index 0000000..13c4eb2 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/data/test-context-menu.js @@ -0,0 +1,5 @@ +/* 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/. */ + +self.on("context", function () true); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/data/test-page-mod.html b/tools/addon-sdk-1.7/packages/addon-kit/data/test-page-mod.html new file mode 100644 index 0000000..da3ec99 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/data/test-page-mod.html @@ -0,0 +1,12 @@ +<!-- 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/. --> + +<html> +<head> + <title>Page Mod test</title> +</head> +<body> + <p id="paragraph">Lorem ipsum dolor sit amet.</p> +</body> +</html> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/data/test-page-worker.html b/tools/addon-sdk-1.7/packages/addon-kit/data/test-page-worker.html new file mode 100644 index 0000000..aabe1df --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/data/test-page-worker.html @@ -0,0 +1,12 @@ +<!-- 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/. --> + +<html> +<head> + <title>Page Worker test</title> +</head> +<body> + <p id="paragraph">Lorem ipsum dolor sit amet.</p> +</body> +</html> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/data/test-page-worker.js b/tools/addon-sdk-1.7/packages/addon-kit/data/test-page-worker.js new file mode 100644 index 0000000..d59ccae --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/data/test-page-worker.js @@ -0,0 +1,29 @@ +/* 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/. */ + + +// get title directly +self.postMessage(["assertEqual", document.title, "Page Worker test", + "Correct page title accessed directly"]); + +// get <p> directly +let p = document.getElementById("paragraph"); +self.postMessage(["assert", !!p, "<p> can be accessed directly"]); +self.postMessage(["assertEqual", p.firstChild.nodeValue, + "Lorem ipsum dolor sit amet.", + "Correct text node expected"]); + +// Modify page +let div = document.createElement("div"); +div.setAttribute("id", "block"); +div.appendChild(document.createTextNode("Test text created")); +document.body.appendChild(div); + +// Check back the modification +div = document.getElementById("block"); +self.postMessage(["assert", !!div, "<div> can be accessed directly"]); +self.postMessage(["assertEqual", div.firstChild.nodeValue, + "Test text created", "Correct text node expected"]); +self.postMessage(["done"]); + diff --git a/tools/addon-sdk-1.7/packages/addon-kit/data/test.html b/tools/addon-sdk-1.7/packages/addon-kit/data/test.html new file mode 100644 index 0000000..0c7cf24 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/data/test.html @@ -0,0 +1,12 @@ +<!-- 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/. --> + +<html> + <head> + <title>foo</title> + </head> + <body> + <p>bar</p> + </body> +</html> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/clipboard.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/clipboard.md new file mode 100644 index 0000000..387766e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/clipboard.md @@ -0,0 +1,62 @@ +<!-- 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/. --> + +<!-- contributed by Dietrich Ayala [dietrich@mozilla.com] --> + +The `clipboard` module allows callers to interact with the system clipboard, +setting and retrieving its contents. + +You can optionally specify the type of the data to set and retrieve. +The following types are supported: + +* `text` (plain text) +* `html` (a string of HTML) + +If no data type is provided, then the module will detect it for you. + +Examples +-------- + +Set and get the contents of the clipboard. + + let clipboard = require("clipboard"); + clipboard.set("Lorem ipsum dolor sit amet"); + let contents = clipboard.get(); + +Set the clipboard contents to some HTML. + + let clipboard = require("clipboard"); + clipboard.set("<blink>Lorem ipsum dolor sit amet</blink>", "html"); + +If the clipboard contains HTML content, open it in a new tab. + + let clipboard = require("clipboard"); + if (clipboard.currentFlavors.indexOf("html") != -1) + require("tabs").open("data:text/html," + clipboard.get("html")); + +<api name="set"> +@function + Replace the contents of the user's clipboard with the provided data. +@param data {string} + The data to put on the clipboard. +@param [datatype] {string} + The type of the data (optional). +</api> + +<api name="get"> +@function + Get the contents of the user's clipboard. +@param [datatype] {string} + Retrieve the clipboard contents only if matching this type (optional). + The function will return null if the contents of the clipboard do not match + the supplied type. +</api> + +<api name="currentFlavors"> +@property {array} + Data on the clipboard is sometimes available in multiple types. For example, + HTML data might be available as both a string of HTML (the `html` type) + and a string of plain text (the `text` type). This function returns an array + of all types in which the data currently on the clipboard is available. +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/context-menu.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/context-menu.md new file mode 100644 index 0000000..28fb35a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/context-menu.md @@ -0,0 +1,719 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `context-menu` module lets you add items to Firefox's page context menu. + + +Introduction +------------ + +The `context-menu` API provides a simple, declarative way to add items to the +page's context menu. You can add items that perform an action when clicked, +submenus, and menu separators. + +Instead of manually adding items when particular contexts occur and then +removing them when those contexts go away, you *bind* items to contexts, and the +adding and removing is automatically handled for you. Items are bound to +contexts in much the same way that event listeners are bound to events. When +the user invokes the context menu, all of the items bound to the current context +are automatically added to the menu. If no items are bound, none are added. +Likewise, any items that were previously in the menu but are not bound to the +current context are automatically removed from the menu. You never need to +manually remove your items from the menu unless you want them to never appear +again. + +For example, if your add-on needs to add a context menu item whenever the +user visits a certain page, don't create the item when that page loads, and +don't remove it when the page unloads. Rather, create your item only once and +supply a context that matches the target URL. + + +Specifying Contexts +------------------- + +As its name implies, the context menu should be reserved for the occurrence of +specific contexts. Contexts can be related to page content or the page itself, +but they should never be external to the page. + +For example, a good use of the menu would be to show an "Edit Image" item when +the user right-clicks an image in the page. A bad use would be to show a +submenu that listed all the user's tabs, since tabs aren't related to the page +or the node the user clicked to open the menu. + +### The Page Context + +First of all, you may not need to specify a context at all. When an item does +not specify a context, the page context applies. + +The *page context* occurs when the user invokes the context menu on a +non-interactive portion of the page. Try right-clicking a blank spot in this +page, or on text. Make sure that no text is selected. The menu that appears +should contain the items "Back", "Forward", "Reload", "Stop", and so on. This +is the page context. + +The page context is appropriate when your item acts on the page as a whole. It +does not occur when the user invokes the context menu on a link, image, or other +non-text node, or while a selection exists. + +### Declarative Contexts + +You can specify some simple, declarative contexts when you create a menu item by +setting the `context` property of the options object passed to its constructor, +like this: + + var cm = require("context-menu"); + cm.Item({ + label: "My Menu Item", + context: cm.URLContext("*.mozilla.org") + }); + +These contexts may be specified by calling the following constructors. Each is +exported by the `context-menu` module. + +<table> + <tr> + <th>Constructor</th> + <th>Description</th> + </tr> + <tr> + <td><code> + PageContext() + </code></td> + <td> + The page context. + </td> + </tr> + <tr> + <td><code> + SelectionContext() + </code></td> + <td> + This context occurs when the menu is invoked on a page in which the user + has made a selection. + </td> + </tr> + <tr> + <td><code> + SelectorContext(selector) + </code></td> + <td> + This context occurs when the menu is invoked on a node that either matches + <code>selector</code>, a CSS selector, or has an ancestor that matches. + <code>selector</code> may include multiple selectors separated by commas, + e.g., <code>"a[href], img"</code>. + </td> + </tr> + <tr> + <td><code> + URLContext(matchPattern) + </code></td> + <td> + This context occurs when the menu is invoked on pages with particular + URLs. <code>matchPattern</code> is a match pattern string or an array of + match pattern strings. When <code>matchPattern</code> is an array, the + context occurs when the menu is invoked on a page whose URL matches any of + the patterns. These are the same match pattern strings that you use with + the <a href="packages/addon-kit/page-mod.html"><code>page-mod</code></a> + <code>include</code> property. + <a href="packages/api-utils/match-pattern.html">Read more about patterns</a>. + </td> + </tr> + <tr> + <td> + array + </td> + <td> + An array of any of the other types. This context occurs when all contexts + in the array occur. + </td> + </tr> +</table> + +Menu items also have a `context` property that can be used to add and remove +declarative contexts after construction. For example: + + var context = require("context-menu").SelectorContext("img"); + myMenuItem.context.add(context); + myMenuItem.context.remove(context); + +When a menu item is bound to more than one context, it appears in the menu when +all of those contexts occur. + +### In Content Scripts + +The declarative contexts are handy but not very powerful. For instance, you +might want your menu item to appear for any page that has at least one image, +but declarative contexts won't help you there. + +When you need more control control over the context in which your menu items are +shown, you can use content scripts. Like other APIs in the SDK, the +`context-menu` API uses +[content scripts](dev-guide/guides/content-scripts/index.html) to let your +add-on interact with pages in the browser. Each menu item you create in the +top-level context menu can have a content script. + +A special event named `"context"` is emitted in your content scripts whenever +the context menu is about to be shown. If you register a listener function for +this event and it returns true, the menu item associated with the listener's +content script is shown in the menu. + +For example, this item appears whenever the context menu is invoked on a page +that contains at least one image: + + require("context-menu").Item({ + label: "This Page Has Images", + contentScript: 'self.on("context", function (node) {' + + ' return !!document.querySelector("img");' + + '});' + }); + +Note that the listener function has a parameter called `node`. This is the node +in the page that the user context-clicked to invoke the menu. You can use it to +determine whether your item should be shown. + +You can both specify declarative contexts and listen for contexts in a content +script. In that case, the declarative contexts are evaluated first. If they +are not current, then your context listener is never called. + +This example takes advantage of that fact. The listener can be assured that +`node` will always be an image: + + var cm = require("context-menu"); + cm.Item({ + label: "A Mozilla Image", + context: cm.SelectorContext("img"), + contentScript: 'self.on("context", function (node) {' + + ' return /mozilla/.test(node.src);' + + '});' + }); + +Your item is shown only when all declarative contexts are current and your +context listener returns true. + + +Handling Menu Item Clicks +------------------------- + +In addition to using content scripts to listen for the `"context"` event as +described above, you can use content scripts to handle item clicks. When the +user clicks your menu item, an event named `"click"` is emitted in the item's +content script. + +Therefore, to handle an item click, listen for the `"click"` event in that +item's content script like so: + + require("context-menu").Item({ + label: "My Item", + contentScript: 'self.on("click", function (node, data) {' + + ' console.log("Item clicked!");' + + '});' + }); + +Note that the listener function has parameters called `node` and `data`. `node` +is the node that the user context-clicked to invoke the menu. You can use it +when performing some action. `data` is the `data` property of the menu item +that was clicked. Since only top-level menu items have content scripts, this +comes in handy for determining which item in a `Menu` was clicked: + + var cm = require("context-menu"); + cm.Menu({ + label: "My Menu", + contentScript: 'self.on("click", function (node, data) {' + + ' console.log("You clicked " + data);' + + '});', + items: [ + cm.Item({ label: "Item 1", data: "item1" }), + cm.Item({ label: "Item 2", data: "item2" }), + cm.Item({ label: "Item 3", data: "item3" }) + ] + }); + +Often you will need to collect some kind of information in the click listener +and perform an action unrelated to content. To communicate to the menu item +associated with the content script, the content script can call the +`postMessage` function attached to the global `self` object, passing it some +JSON-able data. The menu item's `"message"` event listener will be called with +that data. + + var cm = require("context-menu"); + cm.Item({ + label: "Edit Image", + context: cm.SelectorContext("img"), + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage(node.src);' + + '});', + onMessage: function (imgSrc) { + openImageEditor(imgSrc); + } + }); + + +Updating a Menu Item's Label +---------------------------- + +Each menu item must be created with a label, but you can change its label later +using a couple of methods. + +The simplest method is to set the menu item's `label` property. This example +updates the item's label based on the number of times it's been clicked: + + var numClicks = 0; + var myItem = require("context-menu").Item({ + label: "Click Me: " + numClicks, + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + numClicks++; + this.label = "Click Me: " + numClicks; + // Setting myItem.label is equivalent. + } + }); + +Sometimes you might want to update the label based on the context. For +instance, if your item performs a search with the user's selected text, it would +be nice to display the text in the item to provide feedback to the user. In +these cases you can use the second method. Recall that your content scripts can +listen for the `"context"` event and if your listeners return true, the items +associated with the content scripts are shown in the menu. In addition to +returning true, your `"context"` listeners can also return strings. When a +`"context"` listener returns a string, it becomes the item's new label. + +This item implements the aforementioned search example: + + var cm = require("context-menu"); + cm.Item({ + label: "Search Google", + context: cm.SelectionContext(), + contentScript: 'self.on("context", function () {' + + ' var text = window.getSelection().toString();' + + ' if (text.length > 20)' + + ' text = text.substr(0, 20) + "...";' + + ' return "Search Google for " + text;' + + '});' + }); + +The `"context"` listener gets the window's current selection, truncating it if +it's too long, and includes it in the returned string. When the item is shown, +its label will be "Search Google for `text`", where `text` is the truncated +selection. + + +More Examples +------------- + +For conciseness, these examples create their content scripts as strings and use +the `contentScript` property. In your own add-on, you will probably want to +create your content scripts in separate files and pass their URLs using the +`contentScriptFile` property. See +[Working with Content Scripts](dev-guide/guides/content-scripts/index.html) +for more information. + +<div class="warning"> +<p>Unless your content script is extremely simple and consists only of a +static string, don't use <code>contentScript</code>: if you do, you may +have problems getting your add-on approved on AMO.</p> +<p>Instead, keep the script in a separate file and load it using +<code>contentScriptFile</code>. This makes your code easier to maintain, +secure, debug and review.</p> +</div> + +Show an "Edit Page Source" item when the user right-clicks a non-interactive +part of the page: + + require("context-menu").Item({ + label: "Edit Page Source", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage(document.URL);' + + '});', + onMessage: function (pageURL) { + editSource(pageURL); + } + }); + +Show an "Edit Image" item when the menu is invoked on an image: + + var cm = require("context-menu"); + cm.Item({ + label: "Edit Image", + context: cm.SelectorContext("img"), + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage(node.src);' + + '});', + onMessage: function (imgSrc) { + openImageEditor(imgSrc); + } + }); + +Show an "Edit Mozilla Image" item when the menu is invoked on an image in a +mozilla.org or mozilla.com page: + + var cm = require("context-menu"); + cm.Item({ + label: "Edit Mozilla Image", + context: [ + cm.URLContext(["*.mozilla.org", "*.mozilla.com"]), + cm.SelectorContext("img") + ], + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage(node.src);' + + '});', + onMessage: function (imgSrc) { + openImageEditor(imgSrc); + } + }); + +Show an "Edit Page Images" item when the page contains at least one image: + + var cm = require("context-menu"); + cm.Item({ + label: "Edit Page Images", + // This ensures the item only appears during the page context. + context: cm.PageContext(), + contentScript: 'self.on("context", function (node) {' + + ' var pageHasImgs = !!document.querySelector("img");' + + ' return pageHasImgs;' + + '});' + + 'self.on("click", function (node, data) {' + + ' var imgs = document.querySelectorAll("img");' + + ' var imgSrcs = [];' + + ' for (var i = 0 ; i < imgs.length; i++)' + + ' imgSrcs.push(imgs[i].src);' + + ' self.postMessage(imgSrcs);' + + '});', + onMessage: function (imgSrcs) { + openImageEditor(imgSrcs); + } + }); + +Show a "Search With" menu when the user right-clicks an anchor that searches +Google or Wikipedia with the text contained in the anchor: + + var cm = require("context-menu"); + var googleItem = cm.Item({ + label: "Google", + data: "http://www.google.com/search?q=" + }); + var wikipediaItem = cm.Item({ + label: "Wikipedia", + data: "http://en.wikipedia.org/wiki/Special:Search?search=" + }); + var searchMenu = cm.Menu({ + label: "Search With", + context: cm.SelectorContext("a[href]"), + contentScript: 'self.on("click", function (node, data) {' + + ' var searchURL = data + node.textContent;' + + ' window.location.href = searchURL;' + + '});', + items: [googleItem, wikipediaItem] + }); + + +<api name="Item"> +@class +A labeled menu item that can perform an action when clicked. +<api name="Item"> +@constructor + Creates a labeled menu item that can perform an action when clicked. +@param options {object} + An object with the following keys: + @prop label {string} + The item's label. It must either be a string or an object that implements + `toString()`. + @prop [image] {string} + The item's icon, a string URL. The URL can be remote, a reference to an + image in the add-on's `data` directory, or a data URI. + @prop [data] {string} + An optional arbitrary value to associate with the item. It must be either a + string or an object that implements `toString()`. It will be passed to + click listeners. + @prop [context] {value} + If the item is contained in the top-level context menu, this declaratively + specifies the context under which the item will appear; see Specifying + Contexts above. Ignored if the item is contained in a submenu. + @prop [contentScript] {string,array} + If the item is contained in the top-level context menu, this is the content + script or an array of content scripts that the item can use to interact with + the page. Ignored if the item is contained in a submenu. + @prop [contentScriptFile] {string,array} + If the item is contained in the top-level context menu, this is the local + file URL of the content script or an array of such URLs that the item can + use to interact with the page. Ignored if the item is contained in a + submenu. + @prop [onMessage] {function} + If the item is contained in the top-level context menu, this function will + be called when the content script calls `self.postMessage`. It will be + passed the data that was passed to `postMessage`. Ignored if the item is + contained in a submenu. +</api> + +<api name="label"> +@property {string} + The menu item's label. You can set this after creating the item to update its + label later. +</api> + +<api name="image"> +@property {string} + The item's icon, a string URL. The URL can be remote, a reference to an image + in the add-on's `data` directory, or a data URI. You can set this after + creating the item to update its image later. To remove the item's image, set + it to `null`. +</api> + +<api name="data"> +@property {string} + An optional arbitrary value to associate with the item. It must be either a + string or an object that implements `toString()`. It will be passed to + click listeners. You can set this after creating the item to update its data + later. +</api> + +<api name="context"> +@property {list} + A list of declarative contexts for which the menu item will appear in the + context menu. Contexts can be added by calling `context.add()` and removed by + called `context.remove()`. This property is meaningful only for items + contained in the top-level context menu. +</api> + +<api name="parentMenu"> +@property {Menu} + The item's parent `Menu`, or `null` if the item is contained in the top-level + context menu. This property is read-only. To add the item to a new menu, + call that menu's `addItem()` method. +</api> + +<api name="contentScript"> +@property {string,array} + The content script or the array of content scripts associated with the menu + item during creation. This property is meaningful only for items contained in + the top-level context menu. +</api> + +<api name="contentScriptFile"> +@property {string,array} + The URL of a content script or the array of such URLs associated with the menu + item during creation. This property is meaningful only for items contained in + the top-level context menu. +</api> + +<api name="destroy"> +@method + Permanently removes the item from its parent menu and frees its resources. + The item must not be used afterward. If you need to remove the item from its + parent menu but use it afterward, call `removeItem()` on the parent menu + instead. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this menu item. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the menu item's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/guides/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +</api> + +<api name="Menu"> +@class +A labeled menu item that expands into a submenu. + +<api name="Menu"> +@constructor + Creates a labeled menu item that expands into a submenu. +@param options {object} + An object with the following keys: + @prop label {string} + The item's label. It must either be a string or an object that implements + `toString()`. + @prop items {array} + An array of menu items that the menu will contain. Each must be an `Item`, + `Menu`, or `Separator`. + @prop [image] {string} + The menu's icon, a string URL. The URL can be remote, a reference to an + image in the add-on's `data` directory, or a data URI. + @prop [context] {value} + If the menu is contained in the top-level context menu, this declaratively + specifies the context under which the menu will appear; see Specifying + Contexts above. Ignored if the menu is contained in a submenu. + @prop [contentScript] {string,array} + If the menu is contained in the top-level context menu, this is the content + script or an array of content scripts that the menu can use to interact with + the page. Ignored if the menu is contained in a submenu. + @prop [contentScriptFile] {string,array} + If the menu is contained in the top-level context menu, this is the local + file URL of the content script or an array of such URLs that the menu can + use to interact with the page. Ignored if the menu is contained in a + submenu. + @prop [onMessage] {function} + If the menu is contained in the top-level context menu, this function will + be called when the content script calls `self.postMessage`. It will be + passed the data that was passed to `postMessage`. Ignored if the item is + contained in a submenu. +</api> + +<api name="label"> +@property {string} + The menu's label. You can set this after creating the menu to update its + label later. +</api> + +<api name="items"> +@property {array} + An array containing the items in the menu. The array is read-only, meaning + that modifications to it will not affect the menu. However, setting this + property to a new array will replace all the items currently in the menu with + the items in the new array. +</api> + +<api name="image"> +@property {string} + The menu's icon, a string URL. The URL can be remote, a reference to an image + in the add-on's `data` directory, or a data URI. You can set this after + creating the menu to update its image later. To remove the menu's image, set + it to `null`. +</api> + +<api name="context"> +@property {list} + A list of declarative contexts for which the menu will appear in the context + menu. Contexts can be added by calling `context.add()` and removed by called + `context.remove()`. This property is meaningful only for menus contained in + the top-level context menu. +</api> + +<api name="parentMenu"> +@property {Menu} + The menu's parent `Menu`, or `null` if the menu is contained in the top-level + context menu. This property is read-only. To add the menu to a new menu, + call that menu's `addItem()` method. +</api> + +<api name="contentScript"> +@property {string,array} + The content script or the array of content scripts associated with the menu + during creation. This property is meaningful only for menus contained in the + top-level context menu. +</api> + +<api name="contentScriptFile"> +@property {string,array} + The URL of a content script or the array of such URLs associated with the menu + during creation. This property is meaningful only for menus contained in the + top-level context menu. +</api> + +<api name="addItem"> +@method + Appends a menu item to the end of the menu. If the item is already contained + in another menu or in the top-level context menu, it's automatically removed + first. +@param item {Item,Menu,Separator} + The `Item`, `Menu`, or `Separator` to add to the menu. +</api> + +<api name="removeItem"> +@method + Removes the given menu item from the menu. If the menu does not contain the + item, this method does nothing. +@param item {Item,Menu,Separator} + The menu item to remove from the menu. +</api> + +<api name="destroy"> +@method + Permanently removes the menu from its parent menu and frees its resources. + The menu must not be used afterward. If you need to remove the menu from its + parent menu but use it afterward, call `removeItem()` on the parent menu + instead. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this menu item. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the menu item's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/guides/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +</api> + +<api name="Separator"> +@class +A menu separator. Separators can be contained only in `Menu`s, not in the +top-level context menu. + +<api name="Separator"> +@constructor + Creates a menu separator. +</api> + +<api name="parentMenu"> +@property {Menu} + The separator's parent `Menu`. This property is read-only. To add the + separator to a new menu, call that menu's `addItem()` method. +</api> + +<api name="destroy"> +@method + Permanently removes the separator from its parent menu and frees its + resources. The separator must not be used afterward. If you need to remove + the separator from its parent menu but use it afterward, call `removeItem()` + on the parent menu instead. +</api> + +</api> + +<api name="PageContext"> +@class +<api name="PageContext"> +@constructor + Creates a page context. See Specifying Contexts above. +</api> +</api> + +<api name="SelectionContext"> +@class +<api name="SelectionContext"> +@constructor + Creates a context that occurs when a page contains a selection. See + Specifying Contexts above. +</api> +</api> + +<api name="SelectorContext"> +@class +<api name="SelectorContext"> +@constructor + Creates a context that matches a given CSS selector. See Specifying Contexts + above. +@param selector {string} + A CSS selector. +</api> +</api> + +<api name="URLContext"> +@class +<api name="URLContext"> +@constructor + Creates a context that matches pages with particular URLs. See Specifying + Contexts above. +@param matchPattern {string,array} + A [match pattern](packages/api-utils/match-pattern.html) string or an array of + match pattern strings. +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/hotkeys.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/hotkeys.md new file mode 100644 index 0000000..b12791c --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/hotkeys.md @@ -0,0 +1,78 @@ +<!-- 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/. --> + +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +Some add-ons may wish to define keyboard shortcuts for certain operations. This +module exposes an API to create those. + +<api name="Hotkey"> +@class + +Module exports `Hotkey` constructor allowing users to create a `hotkey` for the +host application. + +<api name="Hotkey"> +@constructor +Creates a hotkey who's `onPress` listener method is invoked when key combination +defined by `hotkey` is pressed. + +Please note: If more than one `hotkey` is created for the same key +combination, the listener is executed only on the last one created + +@param options {Object} + Options for the hotkey, with the following keys: + +@prop combo {String} +Any function key: `"f1, f2, ..., f24"` or key combination in the format +of `'modifier-key'`: + + "accel-s" + "meta-shift-i" + "control-alt-d" + +Modifier keynames: + +- **shift**: The Shift key. +- **alt**: The Alt key. On the Macintosh, this is the Option key. On + Macintosh this can only be used in conjunction with another modifier, + since `Alt-Letter` combinations are reserved for entering special + characters in text. +- **meta**: The Meta key. On the Macintosh, this is the Command key. +- **control**: The Control key. +- **accel**: The key used for keyboard shortcuts on the user's platform, + which is Control on Windows and Linux, and Command on Mac. Usually, this + would be the value you would use. + +@prop onPress {Function} +Function that is invoked when the key combination `hotkey` is pressed. + +</api> +<api name="destroy"> +@method +Stops this instance of `Hotkey` from reacting on the key combinations. Once +destroyed it can no longer be used. +</api> +</api> + +## Example ## + + // Define keyboard shortcuts for showing and hiding a custom panel. + var { Hotkey } = require("hotkeys"); + + var showHotKey = Hotkey({ + combo: "accel-shift-o", + onPress: function() { + showMyPanel(); + } + }); + var hideHotKey = Hotkey({ + combo: "accel-alt-shift-o", + onPress: function() { + hideMyPanel(); + } + }); + +[Mozilla keyboard planning FAQ]:http://www.mozilla.org/access/keyboard/ +[keyboard shortcuts]:https://developer.mozilla.org/en/XUL_Tutorial/Keyboard_Shortcuts diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/notifications.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/notifications.md new file mode 100644 index 0000000..ca120ab --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/notifications.md @@ -0,0 +1,64 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> + +The `notifications` module allows you to display transient, +[toaster](http://en.wikipedia.org/wiki/Toast_%28computing%29)-style +desktop messages to the user. + +This API supports desktop notifications on Windows, OS X using +[Growl](http://growl.info/), and Linux using libnotify. If the user's system +does not support desktop notifications or if its notifications service is not +running, then notifications made with this API are logged to Firefox's error +console and, if the user launched Firefox from the command line, the terminal. + +Examples +-------- + +Here's a typical example. When the message is clicked, a string is logged to +the console. + + var notifications = require("notifications"); + notifications.notify({ + title: "Jabberwocky", + text: "'Twas brillig, and the slithy toves", + data: "did gyre and gimble in the wabe", + onClick: function (data) { + console.log(data); + // console.log(this.data) would produce the same result. + } + }); + +This one displays an icon that's stored in the add-on's `data` directory. (See +the [`self`](packages/addon-kit/self.html) module documentation for more information.) + + var notifications = require("notifications"); + var self = require("self"); + var myIconURL = self.data.url("myIcon.png"); + notifications.notify({ + text: "I have an icon!", + iconURL: myIconURL + }); + + +<api name="notify"> +@function + Displays a transient notification to the user. +@param options {object} + An object with the following keys. Each is optional. + @prop [title] {string} + A string to display as the message's title. + @prop [text] {string} + A string to display as the body of the message. + @prop [iconURL] {string} + The URL of an icon to display inside the message. It may be a remote URL, + a data URI, or a URL returned by the [`self`](packages/addon-kit/self.html) + module. + @prop [onClick] {function} + A function to be called when the user clicks the message. It will be passed + the value of `data`. + @prop [data] {string} + A string that will be passed to `onClick`. +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/page-mod.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/page-mod.md new file mode 100644 index 0000000..3b6ef6b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/page-mod.md @@ -0,0 +1,412 @@ +<!-- 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/. --> + +<!-- contributed by Nickolay Ponomarev [asqueella@gmail.com] --> +<!-- contributed by Myk Melez [myk@mozilla.org] --> +<!-- contributed by Irakli Gozalishvil [gozala@mozilla.com] --> + +Overview +-------- +The page-mod module enables add-on developers to execute scripts in the context +of specific web pages. Most obviously you could use page-mod to dynamically +modify the content of certain pages. + +The module exports a constructor function `PageMod` which creates a new page +modification (or "mod" for short). + +A page mod does not modify its pages until those pages are loaded or reloaded. +In other words, if your add-on is loaded while the user's browser is open, the +user will have to reload any open pages that match the mod for the mod to affect +them. + +To stop a page mod from making any more modifications, call its `destroy` +method. + +Like all modules that interact with web content, page-mod uses content +scripts that execute in the content process and defines a messaging API to +communicate between the content scripts and the main add-on script. For more +details on content scripting see the tutorial on [interacting with web +content](dev-guide/guides/content-scripts/index.html). + +To create a PageMod the add-on developer supplies: + +* a set of rules to select the desired subset of web pages based on their URL. +Each rule is specified using the +[match-pattern](packages/api-utils/match-pattern.html) syntax. + +* a set of content scripts to execute in the context of the desired pages. + +* a value for the onAttach option: this value is a function which will be +called when a page is loaded that matches the ruleset. This is used to set up a +communication channel between the add-on code and the content script. + +All these parameters are optional except for the ruleset, which must include +at least one rule. + +The following add-on displays an alert whenever a page matching the ruleset is +loaded: + + var pageMod = require("page-mod"); + pageMod.PageMod({ + include: "*.org", + contentScript: 'window.alert("Page matches ruleset");' + }); + +If you specify a value of "ready" or "end" for `contentScriptWhen`, +as opposed to "start", +then the content script can interact with the DOM itself: + + var pageMod = require("page-mod"); + pageMod.PageMod({ + include: "*.org", + contentScriptWhen: 'end', + contentScript: 'document.body.innerHTML = ' + + ' "<h1>Page matches ruleset</h1>";' + }); + +### Using `contentScriptFile` ### + +Most of the examples in this page define content scripts as strings, +and use the `contentScript` option to assign them to page mods. + +Alternatively, you can create content scripts in separate files +under your add-on's `data` directory. Then you can use the +[`self`](packages/addon-kit/self.html) module to retrieve a URL pointing +to the file, and assign this to the page-mod's `contentScriptFile` +property. + +For example, if you save the content script +file in your `data` directory as "myScript.js", you would assign it using +code like: + + var data = require("self").data; + + var pageMod = require("page-mod"); + pageMod.PageMod({ + include: "*.org", + contentScriptWhen: 'end', + contentScriptFile: data.url("myScript.js") + }); + +<div class="warning"> +<p>Unless your content script is extremely simple and consists only of a +static string, don't use <code>contentScript</code>: if you do, you may +have problems getting your add-on approved on AMO.</p> +<p>Instead, keep the script in a separate file and load it using +<code>contentScriptFile</code>. This makes your code easier to maintain, +secure, debug and review.</p> +</div> + +### Styling web pages ### + +Sometimes adding a script to web pages is not enough, you also want to styling +them. `PageMod` provides an easy way to do that through options' `contentStyle` +and `contentStyleFile` properties: + + var data = require("self").data; + var pageMod = require("page-mod"); + + pageMod.PageMod({ + include: "*.org", + + contentStyleFile: data.url("my-page-mod.css"), + contentStyle: [ + "div { padding: 10px; border: 1px solid silver}", + "img { display: none}" + ] + }) + +It's important to note that `PageMod` will add these styles as +[user style sheet](https://developer.mozilla.org/en/CSS/Getting_Started/Cascading_and_inheritance). + +## Communicating With Content Scripts ## + +When a matching page is loaded the `PageMod` will call the function that the +add-on code supplied to `onAttach`. The `PageMod` supplies one argument to +this function: a `worker` object. + +The worker can be thought of as the add-on's end of +a communication channel between the add-on code and the content scripts that +have been attached to this page. + +Thus the add-on can pass messages to the content scripts by calling the +worker's `postMessage` function and can receive messages from the content +scripts by registering a function as a listener to the worker's `on` function. + +Note that if multiple matching pages are loaded simultaneously then each page +is loaded into its own execution context with its own copy of the content +scripts. In this case `onAttach` is called once for each loaded page, and the +add-on code will have a separate worker for each page: + +![Multiple workers](static-files/media/multiple-workers.jpg) + +This is demonstrated in the following example: + + var pageMod = require("page-mod"); + var tabs = require("tabs"); + + var workers = []; + + pageMod.PageMod({ + include: ["http://www.mozilla*"], + contentScriptWhen: 'end', + contentScript: "onMessage = function onMessage(message) {" + + " window.alert(message);};", + onAttach: function onAttach(worker) { + if (workers.push(worker) == 3) { + workers[0].postMessage("The first worker!"); + workers[1].postMessage("The second worker!"); + workers[2].postMessage("The third worker!"); + } + } + }); + + tabs.open("http://www.mozilla.com"); + tabs.open("http://www.mozilla.org"); + tabs.open("http://www.mozilla-europe.org"); + +Here we specify a ruleset to match any URLs starting with +"http://www.mozilla". When a page matches we add the supplied worker to +an array, and when we have three workers in the array we send a message to +each worker in turn, telling it the order in which it was attached. The +worker just displays the message in an alert box. + +This shows that separate pages execute in separate contexts and that each +context has its own communication channel with the add-on script. + +Note though that while there is a separate worker for each execution context, +the worker is shared across all the content scripts associated with a single +execution context. In the following example we pass two content scripts into +the `PageMod`: these content scripts will share a worker instance. + +In the example each content script identifies itself to the add-on script +by sending it a message using the global `postMessage` function. In the +`onAttach` function the add-on code logs the fact that a new page is +attached and registers a listener function that simply logs the message: + + + var pageMod = require("page-mod"); + var data = require("self").data; + var tabs = require("tabs"); + + pageMod.PageMod({ + include: ["http://www.mozilla*"], + contentScriptWhen: 'end', + contentScript: ["postMessage('Content script 1 is attached to '+ " + + "document.URL);", + "postMessage('Content script 2 is attached to '+ " + + "document.URL);"], + onAttach: function onAttach(worker) { + console.log("Attaching content scripts") + worker.on('message', function(data) { + console.log(data); + }); + } + }); + + tabs.open("http://www.mozilla.com"); + +The console output of this add-on is: + +<pre> + info: Attaching content scripts + info: Content script 1 is attached to http://www.mozilla.com/en-US/ + info: Content script 2 is attached to http://www.mozilla.com/en-US/ +</pre> + +### Mapping workers to tabs ### + +The [`worker`](packages/api-utils/content/worker.html) has a `tab` +property which returns the tab associated with this worker. You can use this +to access the [`tabs API`](packages/addon-kit/tabs.html) for the tab +associated with a specific page: + + var pageMod = require("page-mod"); + var tabs = require("tabs"); + + pageMod.PageMod({ + include: ["*"], + onAttach: function onAttach(worker) { + console.log(worker.tab.title); + } + }); + +### Attaching content scripts to tabs ### + +We've seen that the page mod API attaches content scripts to pages based on +their URL. Sometimes, though, we don't care about the URL: we just want +to execute a script on demand in the context of a particular tab. + +For example, we might want to run a script in the context of the currently +active tab when the user clicks a widget: to block certain content, to +change the font style, or to display the page's DOM structure. + +Using the `attach` method of the [`tab`](packages/addon-kit/tabs.html) +object, you can attach a set of content scripts to a particular tab. The +scripts are executed immediately. + +The following add-on creates a widget which, when clicked, highlights all the +`div` elements in the page loaded into the active tab: + + var widgets = require("widget"); + var tabs = require("tabs"); + + var widget = widgets.Widget({ + id: "div-show", + label: "Show divs", + contentURL: "http://www.mozilla.org/favicon.ico", + onClick: function() { + tabs.activeTab.attach({ + contentScript: + 'var divs = document.getElementsByTagName("div");' + + 'for (var i = 0; i < divs.length; ++i) {' + + 'divs[i].setAttribute("style", "border: solid red 1px;");' + + '}' + }); + } + }); + +## Destroying Workers ## + +Workers generate a `detach` event when their associated page is closed: that +is, when the tab is closed or the tab's location changes. If +you are maintaining a list of workers belonging to a page mod, you can use +this event to remove workers that are no longer valid. + +For example, if you maintain a list of workers attached to a page mod: + + var workers = []; + + var pageMod = require("page-mod").PageMod({ + include: ['*'], + contentScriptWhen: 'ready', + contentScriptFile: data.url('pagemod.js'), + onAttach: function(worker) { + workers.push(worker); + } + }); + +You can remove workers when they are no longer valid by listening to `detach`: + + var workers = []; + + function detachWorker(worker, workerArray) { + var index = workerArray.indexOf(worker); + if(index != -1) { + workerArray.splice(index, 1); + } + } + + var pageMod = require("page-mod").PageMod({ + include: ['*'], + contentScriptWhen: 'ready', + contentScriptFile: data.url('pagemod.js'), + onAttach: function(worker) { + workers.push(worker); + worker.on('detach', function () { + detachWorker(this, workers); + }); + } + }); + +<api name="PageMod"> +@class +A PageMod object. Once activated a page mod will execute the supplied content +scripts in the context of any pages matching the pattern specified by the +'include' property. +<api name="PageMod"> +@constructor +Creates a PageMod. +@param options {object} + Options for the PageMod, with the following keys: + @prop include {string,array} + A match pattern string or an array of match pattern strings. These define + the pages to which the PageMod applies. See the + [match-pattern](packages/api-utils/match-pattern.html) module for + a description of match pattern syntax. + At least one match pattern must be supplied. + + @prop [contentScriptFile] {string,array} + The local file URLs of content scripts to load. Content scripts specified + by this option are loaded *before* those specified by the `contentScript` + option. Optional. + @prop [contentScript] {string,array} + The texts of content scripts to load. Content scripts specified by this + option are loaded *after* those specified by the `contentScriptFile` option. + Optional. + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the page is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the page has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + + @prop [contentStyleFile] {string,array} + The local file URLs of stylesheet to load. Content style specified by this + option are loaded *before* those specified by the `contentStyle` option. + Optional. + @prop [contentStyle] {string,array} + The texts of stylesheet rules to add. Content styles specified by this + option are loaded *after* those specified by the `contentStyleFile` option. + Optional. + + @prop [onAttach] {function} +A function to call when the PageMod attaches content scripts to +a matching page. The function will be called with one argument, a `worker` +object which the add-on script can use to communicate with the content scripts +attached to the page in question. + +</api> + +<api name="include"> +@property {List} +A [list](packages/api-utils/list.html) of match pattern strings. These +define the pages to which the page mod applies. See the +[match-pattern](packages/api-utils/match-pattern.html) module for a +description of match patterns. Rules can be added to the list by calling its +`add` method and removed by calling its `remove` method. + +</api> + +<api name="destroy"> +@method +Stops the page mod from making any more modifications. Once destroyed the page +mod can no longer be used. Note that modifications already made to open pages +will not be undone, except for any stylesheet added by `contentStyle` or +`contentStyleFile`, that are unregistered immediately. +</api> + +<api name="attach"> +@event +This event is emitted this event when the page-mod's content scripts are +attached to a page whose URL matches the page-mod's `include` filter. + +@argument {Worker} +The listener function is passed a [`Worker`](packages/api-utils/content/worker.html) object that can be used to communicate +with any content scripts attached to this page. +</api> + +<api name="error"> +@event +This event is emitted when an uncaught runtime error occurs in one of the page +mod's content scripts. + +@argument {Error} +Listeners are passed a single argument, the +[Error](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error) +object. +</api> + +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/page-worker.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/page-worker.md new file mode 100644 index 0000000..5539c25 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/page-worker.md @@ -0,0 +1,325 @@ +<!-- 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/. --> + +<!-- contributed by Felipe Gomes [felipc@gmail.com] --> + +The `page-worker` module provides a way to create a permanent, invisible page +and access its DOM. + +The module exports a constructor function `Page`, which constructs a new page +worker. A page worker may be destroyed, after which its memory is freed, and +you must create a new instance to load another page. + +You specify the page to load using the `contentURL` option to the +[`Page()` constructor](packages/addon-kit/page-worker.html#Page(options)). +This can point to a remote file: + + pageWorker = require("page-worker").Page({ + contentScript: "console.log(document.body.innerHTML);", + contentURL: "http://en.wikipedia.org/wiki/Internet" + }); + +It can also point to an HTML file which you've packaged with your add-on. +To do this, save the file in your add-on's `data` directory and create the +URL using the `data.url()` method of the +[`self`](packages/addon-kit/self.html) module: + + pageWorker = require("page-worker").Page({ + contentScript: "console.log(document.body.innerHTML);", + contentURL: require("self").data.url("myFile.html") + }); + +You can load a new page by setting the page worker's `contentURL` property. +In this example we fetch the first paragraph of a page from Wikipedia, then +the first paragraph of a different page: + + var getFirstParagraph = "var paras = document.getElementsByTagName('p');" + + "console.log(paras[0].textContent);" + + "self.port.emit('loaded');" + + pageWorker = require("page-worker").Page({ + contentScript: getFirstParagraph, + contentURL: "http://en.wikipedia.org/wiki/Chalk" + }); + + pageWorker.port.on("loaded", function() { + pageWorker.contentURL = "http://en.wikipedia.org/wiki/Cheese" + }); + +## Scripting Page-Worker Content ## + +To access the page's DOM you need to attach a script to it. In the SDK these +scripts are called "content scripts" because they're explicitly used for +interacting with web content. + +You can specify one or more content scripts to load into the page using the +`contentScript` or `contentScriptFile` options to the +[`Page()` constructor](packages/addon-kit/page-worker.html#Page(options)). +With `contentScript` you pass the script as a string, as in the examples +above. With `contentScriptFile` you pass a URL which points to a script +saved under your add-on's `data` directory. You construct the URL using +the `data.url()` method of the +[`self`](packages/addon-kit/self.html) module. + +While content scripts can access DOM content, they can't access any of the SDK +APIs, so in many cases you'll need to exchange messages between the content +script and your main add-on code for a complete solution. + +For example, the content script might read some content and send it back to +the main add-on, which could store it using the +[`simple-storage`](packages/addon-kit/simple-storage.html) API. You can +communicate with the script using either the +[`postMessage()`](dev-guide/guides/content-scripts/using-postmessage.html) +API or (preferably, usually) the +[`port`](dev-guide/guides/content-scripts/using-port.html) API. + +For example, this add-on loads a page from Wikipedia, and runs a content script +in it to send all the headers back to the main add-on code: + + var pageWorkers = require("page-worker"); + + // This content script sends header titles from the page to the add-on: + var script = "var elements = document.querySelectorAll('h2 > span'); " + + "for (var i = 0; i < elements.length; i++) { " + + " postMessage(elements[i].textContent) " + + "}"; + + // Create a page worker that loads Wikipedia: + pageWorkers.Page({ + contentURL: "http://en.wikipedia.org/wiki/Internet", + contentScript: script, + contentScriptWhen: "ready", + onMessage: function(message) { + console.log(message); + } + }); + +For conciseness, this example creates the content script as a string and uses +the `contentScript` property. In your own add-ons, you will probably want to +create your content scripts in separate files and pass their URLs using the +`contentScriptFile` property. + +<div class="warning"> +<p>Unless your content script is extremely simple and consists only of a +static string, don't use <code>contentScript</code>: if you do, you may +have problems getting your add-on approved on AMO.</p> +<p>Instead, keep the script in a separate file and load it using +<code>contentScriptFile</code>. This makes your code easier to maintain, +secure, debug and review.</p> +</div> + +To learn much more about content scripts, see the +[Working with Content Scripts](dev-guide/guides/content-scripts/index.html) +guide. + +<div class="experimental"> +<h3>Scripting Trusted Page Content</h3> + +**Note that the feature described in this section is experimental: we'll +very probably continue to support it, but the name of the `addon` +property might change in a future release.** + +We've already seen that you can package HTML files in your add-on's `data` +directory and load them using `page-worker`. We can call this "trusted" +content, because unlike content loaded from a source outside the +add-on, the add-on author knows exactly what it's doing. To +interact with trusted content you don't need to use content scripts: +you can just include a script from the HTML file in the normal way, using +`<script>` tags. + +Like a content script, these scripts can communicate with the add-on code +using the +[`postMessage()`](dev-guide/guides/content-scripts/using-postmessage.html) +API or the +[`port`](dev-guide/guides/content-scripts/using-port.html) API. +The crucial difference is that these scripts access the `postMessage` +and `port` objects through the `addon` object, whereas content scripts +access them through the `self` object. + +So given an add-on that loads trusted content and uses content scripts +to access it, there are typically three changes you have to make, if you +want to use normal page scripts instead: + +* **in the content script**: change occurrences of `self` to `addon`. +For example, `self.port.emit("my-event")` becomes +`addon.port.emit("my-event")`. + +* **in the HTML page itself**: add a `<script>` tag to load the script. So +if your content script is saved under `data` as "my-script.js", you need +a line like `<script src="my-script.js"></script>` in the page header. + +* **in the "main.js" file**: remove the `contentScriptFile` option in +the `Page()` constructor. + +</div> + +<api name="Page"> +@class +A `Page` object loads the page specified by its `contentURL` option and +executes any content scripts that have been supplied to it in the +`contentScript` and `contentScriptFile` options. + +The page is not displayed to the user. + +The page worker is loaded as soon as the `Page` object is created and stays +loaded until its `destroy` method is called or the add-on is unloaded. + +<api name="Page"> +@constructor + Creates an uninitialized page worker instance. +@param [options] {object} + The *`options`* parameter is optional, and if given it should be an object + with any of the following keys: + @prop [contentURL] {string} + The URL of the content to load in the panel. + @prop [allow] {object} + An object with keys to configure the permissions on the page worker. The + boolean key `script` controls if scripts from the page are allowed to run. + `script` defaults to true. + @prop [contentScriptFile] {string,array} + A local file URL or an array of local file URLs of content scripts to load. + Content scripts specified by this option are loaded *before* those specified + by the `contentScript` option. See + [Working with Content Scripts](dev-guide/guides/content-scripts/index.html) + for help on setting this property. + @prop [contentScript] {string,array} + A string or an array of strings containing the texts of content scripts to + load. Content scripts specified by this option are loaded *after* those + specified by the `contentScriptFile` option. + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the page is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the page has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + + @prop [onMessage] {function} + Use this to add a listener to the page worker's `message` event. +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/events.html) object that allows you to: + +* send events to the content script using the `port.emit` function +* receive events from the content script using the `port.on` function + +See the guide to +<a href="dev-guide/guides/content-scripts/using-port.html"> +communicating using <code>port</code></a> for details. +</api> + +<api name="contentURL"> +@property {string} +The URL of content to load. This can point to +local content loaded from your add-on's "data" directory or remote content. +Setting it loads the content immediately. +</api> + +<api name="allow"> +@property {object} + A object describing permissions for the content. It contains a single key + named `script` whose value is a boolean that indicates whether or not to + execute script in the content. `script` defaults to true. +</api> + +<api name="contentScriptFile"> +@property {string,array} +A local file URL or an array of local file URLs of content scripts to load. +</api> + +<api name="contentScript"> +@property {string,array} +A string or an array of strings containing the texts of content scripts to +load. +</api> + +<api name="contentScriptWhen"> +@property {string} + When to load the content scripts. This may have one of the following + values: + + * "start": load content scripts immediately after the document + element for the page is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the page has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + +</api> + +<api name="destroy"> +@method +Unloads the page worker. After you destroy a page worker, its memory is freed +and you must create a new instance if you need to load another page. +</api> + +<api name="postMessage"> +@method +Sends a message to the content scripts. +@param message {value} +The message to send. Must be JSON-able. +</api> + +<api name="on"> +@method +Registers an event listener with the page worker. See +[Working with Events](dev-guide/guides/events.html) for help with +events. +@param type {string} +The type of event to listen for. +@param listener {function} +The listener function that handles the event. +</api> + +<api name="removeListener"> +@method +Unregisters an event listener from the page worker. +@param type {string} +The type of event for which `listener` was registered. +@param listener {function} +The listener function that was registered. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this page worker. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the page worker's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/guides/content-scripts/using-port.html#json_serializable">JSON-serializable value</a> +</api> + +<api name="error"> +@event +This event is emitted when an uncaught runtime error occurs in one of the +page worker's content scripts. + +@argument {Error} +Listeners are passed a single argument, the +[Error](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error) +object. +</api> + +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/panel.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/panel.md new file mode 100644 index 0000000..0a85db9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/panel.md @@ -0,0 +1,607 @@ +<!-- 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/. --> + +<!-- contributed by Myk Melez [myk@mozilla.org] --> +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +This module exports a single constructor function `Panel` which constructs a +new panel. + +A panel is a dialog. Its content is specified as HTML and you can +execute scripts in it, so the appearance and behaviour of the panel +is limited only by what you can do using HTML, CSS and JavaScript. + +The screenshot below shows a panel whose content is built from the +list of currently open tabs: + +<img class="image-center" src="static-files/media/screenshots/panel-tabs-osx.png" +alt="Simple panel example"> + +Panels are useful for presenting temporary interfaces to users in a way that is +easier for users to ignore and dismiss than a modal dialog, since panels are +hidden the moment users interact with parts of the application interface outside +them. + +A panel's content is loaded as soon as it is created, before the panel is shown, +and the content remains loaded when a panel is hidden, so it is possible +to keep a panel around in the background, updating its content as appropriate +in preparation for the next time it is shown. + +Your add-on can receive notifications when a panel is shown or hidden by +listening to its `show` and `hide` events. + +## Panel Content ## + +The panel's content is specified as HTML, which is loaded from the URL +supplied in the `contentURL` option to the panel's constructor. + +You can load remote HTML into the panel: + + var panel = require("panel").Panel({ + width: 180, + height: 180, + contentURL: "https://en.wikipedia.org/w/index.php?title=Jetpack&useformat=mobile" + }); + + panel.show(); + +<img class="image-center" src="static-files/media/screenshots/wikipedia-jetpack-panel.png" +alt="Wikipedia Jetpack panel"> + +You can also load HTML that's been packaged with your add-on, and this is +most probably how you will create dialogs. To do this, save +the HTML in your add-on's `data` directory and load it using the `data.url()` +method exported by the +[`self`](packages/addon-kit/self.html) module, like this: + + var panel = require("panel").Panel({ + contentURL: require("self").data.url("myFile.html") + }); + + panel.show(); + +## Updating Panel Content ## + +You can update the panel's content simply by setting the panel's `contentURL` +property. + +Here's an add-on that adds two widgets to the add-on bar, one which +shows Google's mobile site and one which shows Bing's mobile site. The widgets +share a panel object, and switch between the two sites by updating the panel's +`contentURL` property: + + var panel = require("panel").Panel({ + contentURL: "about:blank", + onHide: function () { + panel.contentURL = "about:blank"; + } + }); + + require("widget").Widget({ + id: "bing", + label: "Bing", + contentURL: "http://www.bing.com/favicon.ico", + panel: panel, + onClick: function() { + panel.contentURL = "http://m.bing.com/"; + } + }); + + require("widget").Widget({ + id: "google", + label: "Google", + contentURL: "http://www.google.com/favicon.ico", + panel: panel, + onClick: function() { + panel.contentURL = "http://www.google.com/xhtml"; + } + }); + +## Scripting Panel Content ## + +You can't directly access your panel's content from your main add-on code. +To access the panel's content, you need to load a script into the panel. +In the SDK these scripts are called "content scripts" because they're +explicitly used for interacting with web content. + +While content scripts can access the content they're attached to, they can't +use the SDK's APIs. So implementing a complete solution usually means you +have to send messages between the content script and the main add-on code. + +* You can specify one or more content scripts to load into a panel using the +`contentScript` or `contentScriptFile` options to the +[`Panel()` constructor](packages/addon-kit/panel.html#Panel%28options%29). + +* You can communicate with the script using either the +[`postMessage()`](dev-guide/guides/content-scripts/using-postmessage.html) +API or (preferably, usually) the +[`port`](dev-guide/guides/content-scripts/using-port.html) API. + +For example, here's an add-on whose content script intercepts mouse clicks +on links inside the panel, and sends the target URL to the main add-on +code. The content script sends messages using `self.port.emit()` and the +add-on script receives them using `panel.port.on()`. + + var myScript = "window.addEventListener('click', function(event) {" + + " var t = event.target;" + + " if (t.nodeName == 'A')" + + " self.port.emit('click-link', t.toString());" + + "}, false);" + + var panel = require("panel").Panel({ + contentURL: "http://www.bbc.co.uk/mobile/index.html", + contentScript: myScript + }); + + panel.port.on("click-link", function(url) { + console.log(url); + }); + + panel.show(); + +This example uses `contentScript` to supply the script as a string. It's +usually better practice to use `contentScriptFile`, which is a URL pointing +to a script file saved under your add-on's `data` directory. + +<div class="warning"> +<p>Unless your content script is extremely simple and consists only of a +static string, don't use <code>contentScript</code>: if you do, you may +have problems getting your add-on approved on AMO.</p> +<p>Instead, keep the script in a separate file and load it using +<code>contentScriptFile</code>. This makes your code easier to maintain, +secure, debug and review.</p> +</div> + +<img class="image-right" src="static-files/media/screenshots/text-entry-panel.png" +alt="Text entry panel"> + +### Getting User Input ### + +The following add-on adds a widget which displays a panel when +clicked. The panel just contains a `<textarea>` element: when the user +presses the `return` key, the contents of the `<textarea>` is sent to the +main add-on code. + +The add-on consists of three files: + +* **`main.js`**: the main add-on code, that creates the widget and panel +* **`get-text.js`**: the content script that interacts with the panel content +* **`text-entry.html`**: the panel content itself, specified as HTML + +"main.js" is saved in your add-on's `lib` directory, and the other two files +go in your add-on's `data` directory: + +<pre> +my-addon/ + data/ + get-text.js + text-entry.html + lib/ + main.js +</pre> + +The "main.js" looks like this: + + var data = require("self").data; + + // Create a panel whose content is defined in "text-entry.html". + // Attach a content script called "get-text.js". + var text_entry = require("panel").Panel({ + width: 212, + height: 200, + contentURL: data.url("text-entry.html"), + contentScriptFile: data.url("get-text.js") + }); + + // Send the content script a message called "show" when + // the panel is shown. + text_entry.on("show", function() { + text_entry.port.emit("show"); + }); + + // Listen for messages called "text-entered" coming from + // the content script. The message payload is the text the user + // entered. + // In this implementation we'll just log the text to the console. + text_entry.port.on("text-entered", function (text) { + console.log(text); + text_entry.hide(); + }); + + // Create a widget, and attach the panel to it, so the panel is + // shown when the user clicks the widget. + require("widget").Widget({ + label: "Text entry", + id: "text-entry", + contentURL: "http://www.mozilla.org/favicon.ico", + panel: text_entry + }); + +The content script "get-text.js" looks like this: + + self.port.on("show", function (arg) { + var textArea = document.getElementById('edit-box'); + textArea.focus(); + // When the user hits return, send a message to main.js. + // The message payload is the contents of the edit box. + textArea.onkeyup = function(event) { + if (event.keyCode == 13) { + // Remove the newline. + text = textArea.value.replace(/(\r\n|\n|\r)/gm,""); + self.port.emit("text-entered", text); + textArea.value = ''; + } + }; + }); + +Finally, the "text-entry.html" file defines the `<textarea>` element: + +<pre class="brush: html"> + +<html> + +<head> + <style type="text/css" media="all"> + textarea { + margin: 10px; + } + </style> +</head> + +<body> + <textarea rows="10" cols="20" id="edit-box"></textarea> +</body> + +</html> +</pre> + +To learn much more about content scripts, see the +[Working with Content Scripts](dev-guide/guides/content-scripts/index.html) +guide. + +<div class="experimental"> +<h3>Scripting Trusted Panel Content</h3> + +**Note that the feature described in this section is experimental: we'll +very probably continue to support it, but the name of the `addon` +property might change in a future release.** + +We've already seen that you can package HTML files in your add-on's `data` +directory and use them to define the panel's content. We can call this +"trusted" content, because unlike content loaded from a source outside the +add-on, the add-on author knows exactly what it's doing. To +interact with trusted content you don't need to use content scripts: +you can just include a script from the HTML file in the normal way, using +`script` tags. + +Like a content script, these scripts can communicate with the add-on code +using the +[`postMessage()`](dev-guide/guides/content-scripts/using-postmessage.html) +API or the +[`port`](dev-guide/guides/content-scripts/using-port.html) API. +The crucial difference is that these scripts access the `postMessage` +and `port` objects through the `addon` object, whereas content scripts +access them through the `self` object. + +To show the difference, we can easily convert the `text-entry` add-on above +to use normal page scripts instead of content scripts. + +The main add-on code is exactly the same as the main add-on code in the +previous example, except that we don't attach a content script: + + var data = require("self").data; + + // Create a panel whose content is defined in "text-entry.html". + var text_entry = require("panel").Panel({ + width: 212, + height: 200, + contentURL: data.url("text-entry.html"), + }); + + // Send the page script a message called "show" when + // the panel is shown. + text_entry.on("show", function() { + text_entry.port.emit("show"); + }); + + // Listen for messages called "text-entered" coming from + // the page script. The message payload is the text the user + // entered. + // In this implementation we'll just log the text to the console. + text_entry.port.on("text-entered", function (text) { + console.log(text); + text_entry.hide(); + }); + + // Create a widget, and attach the panel to it, so the panel is + // shown when the user clicks the widget. + require("widget").Widget({ + label: "Text entry", + id: "text-entry", + contentURL: "http://www.mozilla.org/favicon.ico", + panel: text_entry + }); + +The page script is exactly the same as the content script above, except +that instead of `self`, we use `addon` to access the messaging APIs: + + addon.port.on("show", function (arg) { + var textArea = document.getElementById('edit-box'); + textArea.focus(); + // When the user hits return, send a message to main.js. + // The message payload is the contents of the edit box. + textArea.onkeyup = function(event) { + if (event.keyCode == 13) { + // Remove the newline. + text = textArea.value.replace(/(\r\n|\n|\r)/gm,""); + addon.port.emit("text-entered", text); + textArea.value = ''; + } + }; + }); + +Finally, the HTML file now references "get-text.js" inside a `script` tag: + +<pre class="brush: html"> + +<html> + +<head> + <style type="text/css" media="all"> + textarea { + margin: 10px; + } + </style> + <script src="get-text.js"></script> +</head> + +<body> + <textarea rows="10" cols="20" id="edit-box"></textarea> +</body> + +</html> +</pre> +</div> + +## Styling Trusted Panel Content ## + +When the panel's content is specified using an HTML file in your `data` +directory, you can style it using CSS, either embedding the CSS directly +in the file or referencing a CSS file stored under `data`. + +The panel's default style is different for each operating system: + +<img class="image-center" src="static-files/media/screenshots/panel-default-style.png" +alt="OS X panel default style"> + +This helps to ensure that the panel's style is consistent with the dialogs +displayed by Firefox and other applications, but means you need to take care +when applying your own styles. For example, if you set the panel's +`background-color` property to `white` and do not set the `color` property, +then the panel's text will be invisible on OS X although it looks fine on Ubuntu. + +<api name="Panel"> +@class +The Panel object represents a floating modal dialog that can by an add-on to +present user interface content. + +Once a panel object has been created it can be shown and hidden using its +`show()` and `hide()` methods. Once a panel is no longer needed it can be +deactivated using `destroy()`. + +The content of a panel is specified using the `contentURL` option. An add-on +can interact with the content of a panel using content scripts which it +supplies in the `contentScript` and/or `contentScriptFile` options. For example, +a content script could create a menu and send the user's selection to the +add-on. + +<api name="Panel"> +@constructor +Creates a panel. +@param options {object} + Options for the panel, with the following keys: + @prop [width] {number} + The width of the panel in pixels. Optional. + @prop [height] {number} + The height of the panel in pixels. Optional. + @prop [contentURL] {string} + The URL of the content to load in the panel. + @prop [allow] {object} + An optional object describing permissions for the content. It should + contain a single key named `script` whose value is a boolean that indicates + whether or not to execute script in the content. `script` defaults to true. + @prop [contentScriptFile] {string,array} + A local file URL or an array of local file URLs of content scripts to load. + Content scripts specified by this property are loaded *before* those + specified by the `contentScript` property. + @prop [contentScript] {string,array} + A string or an array of strings containing the texts of content scripts to + load. Content scripts specified by this property are loaded *after* those + specified by the `contentScriptFile` property. + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the panel is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the panel has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + + @prop [onMessage] {function} + Include this to listen to the panel's `message` event. + @prop [onShow] {function} + Include this to listen to the panel's `show` event. + @prop [onHide] {function} + Include this to listen to the panel's `hide` event. +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/events.html) object that allows you to: + +* send events to the content script using the `port.emit` function +* receive events from the content script using the `port.on` function + +See the guide to +<a href="dev-guide/guides/content-scripts/using-port.html"> +communicating using <code>port</code></a> for details. +</api> + +<api name="isShowing"> +@property {boolean} +Tells if the panel is currently shown or not. This property is read-only. +</api> + +<api name="height"> +@property {number} +The height of the panel in pixels. +</api> + +<api name="width"> +@property {number} +The width of the panel in pixels. +</api> + +<api name="contentURL"> +@property {string} +The URL of content loaded into the panel. This can point to +local content loaded from your add-on's "data" directory or remote content. +Setting it updates the panel's content immediately. +</api> + +<api name="allow"> +@property {object} +An object describing permissions for the content. It contains a single key +named `script` whose value is a boolean that indicates whether or not to execute +script in the content. +</api> + +<api name="contentScriptFile"> +@property {string,array} +A local file URL or an array of local file URLs of content scripts to load. +Content scripts specified by this property are loaded *before* those +specified by the `contentScript` property. +</api> + +<api name="contentScript"> +@property {string,array} +A string or an array of strings containing the texts of content scripts to +load. Content scripts specified by this property are loaded *after* those +specified by the `contentScriptFile` property. +</api> + +<api name="contentScriptWhen"> +@property {string} +When to load the content scripts. This may have one of the following +values: + +* "start": load content scripts immediately after the document +element for the panel is inserted into the DOM, but before the DOM content +itself has been loaded +* "ready": load content scripts once DOM content has been loaded, +corresponding to the +[DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) +event +* "end": load content scripts once all the content (DOM, JS, CSS, +images) for the panel has been loaded, at the time the +[window.onload event](https://developer.mozilla.org/en/DOM/window.onload) +fires + +</api> + +<api name="destroy"> +@method +Destroys the panel, unloading any content that was loaded in it. Once +destroyed, the panel can no longer be used. If you just want to hide +the panel and might show it later, use `hide` instead. +</api> + +<api name="postMessage"> +@method +Sends a message to the content scripts. +@param message {value} +The message to send. Must be stringifiable to JSON. +</api> + +<api name="show"> +@method +Displays the panel. +</api> + +<api name="hide"> +@method +Stops displaying the panel. +</api> + +<api name="resize"> +@method +Resizes the panel. +@param width {number} +The new width of the panel in pixels. +@param height {number} +The new height of the panel in pixels. +</api> + +<api name="on"> +@method + Registers an event listener with the panel. +@param type {string} + The type of event to listen for. +@param listener {function} + The listener function that handles the event. +</api> + +<api name="removeListener"> +@method + Unregisters an event listener from the panel. +@param type {string} + The type of event for which `listener` was registered. +@param listener {function} + The listener function that was registered. +</api> + +<api name="show"> +@event +This event is emitted when the panel is shown. +</api> + +<api name="hide"> +@event +This event is emitted when the panel is hidden. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this panel. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the panel's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/guides/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="error"> +@event +This event is emitted when an uncaught runtime error occurs in one of the +panel's content scripts. + +@argument {Error} +Listeners are passed a single argument, the +[Error](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error) +object. +</api> + +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/passwords.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/passwords.md new file mode 100644 index 0000000..8ee6109 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/passwords.md @@ -0,0 +1,568 @@ +<!-- 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/. --> + +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `passwords` module allows add-ons to interact with Firefox's +[Password Manager](http://support.mozilla.com/en-US/kb/Remembering%20passwords) +to add, retrieve and remove stored credentials. + +A _credential_ is the set of information a user supplies to authenticate +herself with a service. Typically a credential consists of a username and a +password. + +Using this module you can: + +1. **Search** for credentials which have been stored in the Password Manager. + You can then use the credentials to access their related service (for + example, by logging into a web site). + +2. **Store** credentials in the Password Manager. You can store different sorts + of credentials, as outlined in the "Credentials" section below. + +3. **Remove** stored credentials from the Password Manager. + +## Credentials ## + +In this API, credentials are represented by objects. + +You create credential objects to pass into the API, and the API also returns +credential objects to you. The sections below explain both the properties you +should define on credential objects and the properties you can expect on +credential objects returned by the API. + +All credential objects include `username` and `password` properties. Different +sorts of stored credentials include various additional properties, as +outlined in this section. + +You can use the Passwords API with three sorts of credentials: + +* Add-on credentials +* HTML form credentials +* HTTP Authentication credentials + +### Add-on Credential ### + +These are associated with your add-on rather than a particular web site. +They contain the following properties: + +<table> +<colgroup> +<col width="25%"> +</colgroup> +<tr> + <td> + <code>username</code> + </td> + <td> + The username. + </td> +</tr> + +<tr> + <td> + <code>password</code> + </td> + <td> + The password. + </td> +</tr> + +<tr> + <td> + <code>url</code> + </td> + <td> + <p>For an add-on credential, this property is of the form:<br><code> + addon:<addon-id></code>, where <code><addon-id></code> + is the add-on's + <a href="dev-guide/guides/program-id.html"> + Program ID</a>.</p> + <p>You don't supply this value when storing an add-on credential: it is + automatically generated for you. However, you can use it to work out + which stored credentials belong to your add-on by comparing it with the + <code>uri</code> property of the + <a href="packages/addon-kit/self.html"><code>self</code></a> + module.</p> + </td> +</tr> + +<tr> + <td> + <code>realm</code> + </td> + <td> + <p>You can use this as a name for the credential, to distinguish + it from any other credentials you've stored.</p> + <p>The realm is displayed + in Firefox's Password Manager, under "Site", in brackets after the URL. + For example, if the realm for a credential is "User Registration", then + its "Site" field will look something like:</p> + <code>addon:jid0-01mBBFyu0ZAXCFuB1JYKooSTKIc (User Registration)</code> + </td> +</tr> + +</table> + +### HTML Form Credential ### + +If a web service uses HTML forms to authenticate its users, then the +corresponding credential is an HTML Form credential. + +It contains the following properties: + +<table> +<colgroup> +<col width="25%"> +</colgroup> +<tr> + <td> + <code>username</code> + </td> + <td> + The username. + </td> +</tr> + +<tr> + <td> + <code>password</code> + </td> + <td> + The password. + </td> +</tr> + +<tr> + <td> + <code>url</code> + </td> + <td> + The URL for the web service which requires the credential. + You should omit anything after the hostname and (optional) port. + </td> +</tr> + +<tr> + <td> + <code>formSubmitURL</code> + </td> + <td> + The value of the form's "action" attribute. + You should omit anything after the hostname and (optional) port. + If the form doesn't contain an "action" attribute, this property should + match the <code>url</code> property. + </td> +</tr> + +<tr> + <td> + <code>usernameField</code> + </td> + <td> + The value of the "name" attribute for the form's username field. + </td> +</tr> + +<tr> + <td> + <code>passwordField</code> + </td> + <td> + The value of the "name" attribute for the form's password field. + </td> +</tr> + +</table> + +So: given a form at `http://www.example.com/login` +with the following HTML: + +<script type="syntaxhighlighter" class="brush: html"><![CDATA[ +<form action="http://login.example.com/foo/authenticate.cgi"> + <div>Please log in.</div> + <label>Username:</label> <input type="text" name="uname"> + <label>Password:</label> <input type="password" name="pword"> +</form> +]]> +</script> + +The corresponding values for the credential (excluding username and password) +should be: + +<pre> + url: "http://www.example.com" + formSubmitURL: "http://login.example.com" + usernameField: "uname" + passwordField: "pword" +</pre> + +Note that for both `url` and `formSubmitURL`, the portion of the URL after the +hostname is omitted. + +### HTTP Authentication Credential ### + +These are used to authenticate the user to a web site +which uses HTTP Authentication, as detailed in +[RFC 2617](http://tools.ietf.org/html/rfc2617). +They contain the following properties: + +<table> +<colgroup> +<col width="25%"> +</colgroup> +<tr> + <td> + <code>username</code> + </td> + <td> + The username. + </td> +</tr> + +<tr> + <td> + <code>password</code> + </td> + <td> + The password. + </td> +</tr> + +<tr> + <td> + <code>url</code> + </td> + <td> + The URL for the web service which requires the credential. + You should omit anything after the hostname and (optional) port. + </td> +</tr> + +<tr> + <td> + <code>realm</code> + </td> + <td> + <p>The WWW-Authenticate response header sent by the server may include a + "realm" field as detailed in + <a href="http://tools.ietf.org/html/rfc2617">RFC 2617</a>. If it does, + this property contains the value for the "realm" field. Otherwise, it is + omitted.</p> + <p>The realm is displayed in Firefox's Password Manager, under "Site", + in brackets after the URL.</p> + </td> +</tr> + +</table> + +So: if a web server at `http://www.example.com` requested authentication with +a response code like this: + +<pre> + HTTP/1.0 401 Authorization Required + Server: Apache/1.3.27 + WWW-Authenticate: Basic realm="ExampleCo Login" +</pre> + +The corresponding values for the credential (excluding username and password) +should be: + +<pre> + url: "http://www.example.com" + realm: "ExampleCo Login" +</pre> + +## onComplete and onError ## + +This API is explicitly asynchronous, so all its functions take two callback +functions as additional options: `onComplete` and `onError`. + +`onComplete` is called when the operation has completed successfully and +`onError` is called when the function encounters an error. + +Because the `search` function is expected to return a list of matching +credentials, its `onComplete` option is mandatory. Because the other functions +don't return a value in case of success their `onComplete` options are +optional. + +For all functions, `onError` is optional. + +<api name="search"> +@function + +This function is used to retrieve a credential, or a list of credentials, +stored in the Password Manager. + +You pass it any subset of the possible properties a credential can contain. +Credentials which match all the properties you supplied are returned as an +argument to the `onComplete` callback. + +So if you pass in an empty set of properties, all stored credentials are +returned: + + function show_all_passwords() { + require("passwords").search({ + onComplete: function onComplete(credentials) { + credentials.forEach(function(credential) { + console.log(credential.username); + console.log(credential.password); + }); + } + }); + } + +If you pass it a single property, only credentials matching that property are +returned: + + function show_passwords_for_joe() { + require("passwords").search({ + username: "joe", + onComplete: function onComplete(credentials) { + credentials.forEach(function(credential) { + console.log(credential.username); + console.log(credential.password); + }); + } + }); + } + +If you pass more than one property, returned credentials must match all of +them: + + function show_google_password_for_joe() { + require("passwords").search({ + username: "joe", + url: "https://www.google.com", + onComplete: function onComplete(credentials) { + credentials.forEach(function(credential) { + console.log(credential.username); + console.log(credential.password); + }); + } + }); + } + +To retrieve only credentials associated with your add-on, use the `url` +property, initialized from `self.uri`: + + function show_my_addon_passwords() { + require("passwords").search({ + url: require("self").uri, + onComplete: function onComplete(credentials) { + credentials.forEach(function(credential) { + console.log(credential.username); + console.log(credential.password); + }); + } + }); + } + +@param options {object} +The `options` object may contain any credential properties. It is used to +restrict the set of credentials returned by the `search` function. + +See "Credentials" above for details on what these properties should be. + +Additionally, `options` must contain a function assigned to its `onComplete` +property: this is called when the function completes and is passed the set of +credentials retrieved. + +`options` may contain a function assigned to its `onError` property, which is +called if the function encounters an error. `onError` is passed the error as an +[nsIException](https://developer.mozilla.org/en/nsIException) object. + +@prop [username] {string} +The username for the credential. + +@prop [password] {string} +The password for the credential. + +@prop [url] {string} +The URL associated with the credential. + +@prop [formSubmitURL] {string} +The URL an HTML form credential is submitted to. + +@prop [realm] {string} +For HTTP Authentication credentials, the realm for which the credential was +requested. For add-on credentials, a name for the credential. + +@prop [usernameField] {string} +The value of the `name` attribute for the user name input field in a form. + +@prop [passwordField] {string} +The value of the `name` attribute for the password input field in a form. + +@prop onComplete {function} +The callback function that is called once the function completes successfully. +It is passed all the matching credentials as a list. This is the only +mandatory option. + +@prop [onError] {function} +The callback function that is called if the function failed. The +callback is passed an `error` containing a reason of a failure: this is an +[nsIException](https://developer.mozilla.org/en/nsIException) object. + +</api> + +<api name="store"> +@function + +This function is used to store a credential in the Password Manager. + +It takes an `options` object as an argument: this contains all the properties +for the new credential. + +As different sorts of credentials contain different properties, the +appropriate options differ depending on the sort of credential being stored. + +To store an add-on credential: + + require("passwords").store({ + realm: "User Registration", + username: "joe", + password: "SeCrEt123", + }); + +To store an HTML form credential: + + require("passwords").store({ + url: "http://www.example.com", + formSubmitURL: "http://login.example.com", + username: "joe", + usernameField: "uname", + password: "SeCrEt123", + passwordField: "pword" + }); + +To store an HTTP Authentication credential: + + require("passwords").store({ + url: "http://www.example.com", + realm: "ExampleCo Login", + username: "joe", + password: "SeCrEt123", + }); + +See "Credentials" above for more details on how to set these properties. + +The options parameter may also include `onComplete` and `onError` +callback functions, which are called when the function has completed +successfully and when it encounters an error, respectively. These options +are both optional. + +@param options {object} +An object containing the properties of the credential to be stored, and +optional `onComplete` and `onError` callback functions. + +@prop username {string} +The username for the credential. + +@prop password {string} +The password for the credential. + +@prop [url] {string} +The URL to which the credential applies. Omitted for add-on +credentials. + +@prop [formSubmitURL] {string} +The URL a form-based credential was submitted to. Omitted for add-on +credentials and HTTP Authentication credentials. + +@prop [realm] {string} +For HTTP Authentication credentials, the realm for which the credential was +requested. For add-on credentials, a name for the credential. + +@prop [usernameField] {string} +The value of the `name` attribute for the username input in a form. +Omitted for add-on credentials and HTTP Authentication credentials. + +@prop [passwordField] {string} +The value of the `name` attribute for the password input in a form. +Omitted for add-on credentials and HTTP Authentication credentials. + +@prop [onComplete] {function} +The callback function that is called once the function completes successfully. + +@prop [onError] {function} +The callback function that is called if the function failed. The +callback is passed an `error` argument: this is an +[nsIException](https://developer.mozilla.org/en/nsIException) object. + +</api> + +<api name="remove"> +@function + +Removes a stored credential. You supply it all the properties of the credential +to remove, along with optional `onComplete` and `onError` callbacks. + +Because you must supply all the credential's properties, it may be convenient +to call `search` first, and use its output as the input to `remove`. For +example, to remove all of joe's stored credentials: + + require("passwords").search({ + username: "joe", + onComplete: function onComplete(credentials) { + credentials.forEach(require("passwords").remove); + }) + }); + +To change an existing credential just call `store` after `remove` succeeds: + + require("passwords").remove({ + realm: "User Registration", + username: "joe", + password: "SeCrEt123" + onComplete: function onComplete() { + require("passwords").store({ + realm: "User Registration", + username: "joe", + password: "{{new password}}" + }) + } + }); + +@param options {object} + +An object containing all the properties of the credential to be removed, +and optional `onComplete` and `onError` callback functions. + +@prop username {string} +The username for the credential. + +@prop password {string} +The password for the credential. + +@prop [url] {string} +The URL to which the credential applies. Omitted for add-on +credentials. + +@prop [formSubmitURL] {string} +The URL a form-based credential was submitted to. Omitted for add-on +credentials and HTTP Authentication credentials. + +@prop [realm] {string} +For HTTP Authentication credentials, the realm for which the credential was +requested. For add-on credentials, a name for the credential. + +@prop [usernameField] {string} +The value of the `name` attribute for the username input in a form. +Omitted for add-on credentials and HTTP Authentication credentials. + +@prop [passwordField] {string} +The value of the `name` attribute for the password input in a form. +Omitted for add-on credentials and HTTP Authentication credentials. + +@prop [onComplete] {function} +The callback function that is called once the function has completed +successfully. + +@prop [onError] {function} +The callback function that is called if the function failed. The +callback is passed an `error` argument: this is an +[nsIException](https://developer.mozilla.org/en/nsIException) object. + +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/private-browsing.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/private-browsing.md new file mode 100644 index 0000000..e553357 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/private-browsing.md @@ -0,0 +1,50 @@ +<!-- 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/. --> + +<!-- contributed by Paul O'Shannessy [paul@oshannessy.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `private-browsing` module allows you to access Firefox's private browsing +mode, detecting if it is active and when its state changes. + +This module is available in all applications. However, only Firefox will ever +transition into or out of private browsing mode. For all other applications, +`pb.isActive` will always be `false`, and none of the events will be emitted. + +<api name="isActive"> +@property {boolean} + This read-only boolean is true if private browsing mode is turned on. +</api> + +<api name="activate"> +@function + Turns on private browsing mode. +</api> + +<api name="deactivate"> +@function + Turns off private browsing mode. +</api> + +<api name="start"> +@event +Emitted immediately after the browser enters private browsing mode. + + var pb = require("private-browsing"); + pb.on("start", function() { + // Do something when the browser starts private browsing mode. + }); + +</api> + +<api name="stop"> +@event +Emitted immediately after the browser exits private browsing mode. + + var pb = require("private-browsing"); + pb.on("stop", function() { + // Do something when the browser stops private browsing mode. + }); +</api>
\ No newline at end of file diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/request.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/request.md new file mode 100644 index 0000000..af37ecf --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/request.md @@ -0,0 +1,203 @@ +<!-- 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/. --> + +The `request` module lets you make simple yet powerful network requests. + +<api name="Request"> +@class +The `Request` object is used to make `GET`, `POST` or `PUT` network requests. +It is constructed with a URL to which the request is sent. Optionally the user +may specify a collection of headers and content to send alongside the request +and a callback which will be executed once the request completes. + +Once a `Request` object has been created a `GET` request can be executed by +calling its `get()` method, a `POST` request by calling its `post()` method, +or a `PUT` request by calling its `put()` method. + +When the server completes the request, the `Request` object emits a "complete" +event. Registered event listeners are passed a `Response` object. + +Each `Request` object is designed to be used once. Once `GET`, `POST` or `PUT` +are called, attempting to call either will throw an error. + +Since the request is not being made by any particular website, requests made +here are not subject to the same-domain restriction that requests made in web +pages are subject to. + +With the exception of `response`, all of a `Request` object's properties +correspond with the options in the constructor. Each can be set by simply +performing an assignment. However, keep in mind that the same validation rules +that apply to `options` in the constructor will apply during assignment. Thus, +each can throw if given an invalid value. + +The example below shows how to use Request to get the most recent public tweet. + + var Request = require("request").Request; + var latestTweetRequest = Request({ + url: "http://api.twitter.com/1/statuses/public_timeline.json", + onComplete: function (response) { + var tweet = response.json[0]; + console.log("User: " + tweet.user.screen_name); + console.log("Tweet: " + tweet.text); + } + }); + + // Be a good consumer and check for rate limiting before doing more. + Request({ + url: "http://api.twitter.com/1/account/rate_limit_status.json", + onComplete: function (response) { + if (response.json.remaining_hits) { + latestTweetRequest.get(); + } else { + console.log("You have been rate limited!"); + } + } + }).get(); + +<api name="Request"> +@constructor +This constructor creates a request object that can be used to make network +requests. The constructor takes a single parameter `options` which is used to +set several properties on the resulting `Request`. +@param options {object} + @prop url {string} + This is the url to which the request will be made. + + @prop [onComplete] {function} + This function will be called when the request has received a response (or in + terms of XHR, when `readyState == 4`). The function is passed a `Response` + object. + + @prop [headers] {object} + An unordered collection of name/value pairs representing headers to send + with the request. + + @prop [content] {string,object} + The content to send to the server. If `content` is a string, it should be + URL-encoded (use `encodeURIComponent`). If `content` is an object, it + should be a collection of name/value pairs. Nested objects & arrays should + encode safely. + + For `GET` requests, the query string (`content`) will be appended to the + URL. For `POST` and `PUT` requests, the query string will be sent as the body + of the request. + + @prop [contentType] {string} + The type of content to send to the server. This explicitly sets the + `Content-Type` header. The default value is `application/x-www-form-urlencoded`. + + @prop [overrideMimeType] {string} + Use this string to override the MIME type returned by the server in the + response's Content-Type header. You can use this to treat the content as a + different MIME type, or to force text to be interpreted using a specific + character. + + For example, if you're retrieving text content which was encoded as + ISO-8859-1 (Latin 1), it will be given a content type of "utf-8" and + certain characters will not display correctly. To force the response to + be interpreted as Latin-1, use `overrideMimeType`: + + var Request = require("request").Request; + var quijote = Request({ + url: "http://www.latin1files.org/quijote.txt", + overrideMimeType: "text/plain; charset=latin1", + onComplete: function (response) { + console.log(response.text); + } + }); + + quijote.get(); + +</api> + +<api name="url"> +@property {string} +</api> + +<api name="headers"> +@property {object} +</api> + +<api name="content"> +@property {string,object} +</api> + +<api name="contentType"> +@property {string} +</api> + +<api name="response"> +@property {Response} +</api> + +<api name="get"> +@method +Make a `GET` request. +@returns {Request} +</api> + +<api name="post"> +@method +Make a `POST` request. +@returns {Request} +</api> + +<api name="put"> +@method +Make a `PUT` request. +@returns {Request} +</api> + +<api name="complete"> +@event +The `Request` object emits this event when the request has completed and a +response has been received. + +@argument {Response} +Listener functions are passed the response to the request as a `Response` object. +</api> + +</api> + + +<api name="Response"> +@class +The Response object contains the response to a network request issued using a +`Request` object. It is returned by the `get()`, `post()` or `put()` method of a +`Request` object. + +All members of a `Response` object are read-only. +<api name="text"> +@property {string} +The content of the response as plain text. +</api> + +<api name="json"> +@property {object} +The content of the response as a JavaScript object. The value will be `null` +if the document cannot be processed by `JSON.parse`. +</api> + +<api name="status"> +@property {string} +The HTTP response status code (e.g. *200*). +</api> + +<api name="statusText"> +@property {string} +The HTTP response status line (e.g. *OK*). +</api> + +<api name="headers"> +@property {object} +The HTTP response headers represented as key/value pairs. + +To print all the headers you can do something like this: + + for (var headerName in response.headers) { + console.log(headerName + " : " + response.headers[headerName]); + } + +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/selection.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/selection.md new file mode 100644 index 0000000..f2d4ca5 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/selection.md @@ -0,0 +1,90 @@ +<!-- 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/. --> + +<!-- contributed by Eric H. Jung [eric.jung@yahoo.com] --> +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `selection` module provides a means to get and set text and HTML selections +in the current Firefox page. It can also observe new selections. + +Registering for Selection Notifications +--------------------------------------- + +To be notified when the user makes a selection, register a listener for the +"select" event. Each listener will be called after a selection is made. + + function myListener() { + console.log("A selection has been made."); + } + var selection = require("selection"); + selection.on('select', myListener); + + // You can remove listeners too. + selection.removeListener('select', myListener); + +Iterating Over Discontiguous Selections +--------------------------------------- + +Discontiguous selections can be accessed by iterating over the `selection` +module itself. Each iteration yields a `Selection` object from which `text`, +`html`, and `isContiguous` properties can be accessed. + + +Examples +-------- + +Log the current contiguous selection as text: + + var selection = require("selection"); + if (selection.text) + console.log(selection.text); + +Log the current discontiguous selections as HTML: + + var selection = require("selection"); + if (!selection.isContiguous) { + for (var subselection in selection) { + console.log(subselection.html); + } + } + +Surround HTML selections with delimiters: + + var selection = require("selection"); + selection.on('select', function () { + selection.html = "\\\" + selection.html + "///"; + }); + +<api name="text"> +@property {string} + Gets or sets the current selection as plain text. Setting the selection + removes all current selections, inserts the specified text at the location of + the first selection, and selects the new text. Getting the selection when + there is no current selection returns `null`. Setting the selection when there + is no current selection throws an exception. Getting the selection when + `isContiguous` is `true` returns the text of the first selection. +</api> + +<api name="html"> +@property {string} + Gets or sets the current selection as HTML. Setting the selection removes all + current selections, inserts the specified text at the location of the first + selection, and selects the new text. Getting the selection when there is no + current selection returns `null`. Setting the selection when there is no + current selection throws an exception. Getting the selection when + `isContiguous` is `true` returns the text of the first selection. +</api> + +<api name="isContiguous"> +@property {boolean} + `true` if the current selection is a single, contiguous selection, and `false` + if there are two or more discrete selections, each of which may or may not be + spatially adjacent. (Discontiguous selections can be created by the user with + Ctrl+click-and-drag.) +</api> + +<api name="select"> +@event + This event is emitted whenever the user makes a new selection in a page. +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/self.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/self.md new file mode 100644 index 0000000..8ccdb01 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/self.md @@ -0,0 +1,79 @@ +<!-- 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/. --> + +<!-- edited by Erik Vold [erikvvold@gmail.com] --> + +The `self` module provides access to data that is bundled with the add-on +as a whole. It also provides access to the +[Program ID](dev-guide/guides/program-id.html), a value which is +unique for each add-on. + +Note that the `self` module is completely different from the global `self` +object accessible to content scripts, which is used by a content script to +[communicate with the add-on code](dev-guide/guides/content-scripts/using-port.html). + +<api name="id"> +@property {string} +This property is a printable string that is unique for each add-on. It comes +from the `id` property set in the `package.json` file in the main package +(i.e. the package in which you run `cfx xpi`). While not generally of use to +add-on code directly, it can be used by internal API code to index local +storage and other resources that are associated with a particular add-on. +Eventually, this ID will be unspoofable (see +[JEP 118](https://wiki.mozilla.org/Labs/Jetpack/Reboot/JEP/118) for details). +</api> + +<api name="name"> +@property {string} +This property contains the add-on's short name. It comes from the `name` +property in the main package's `package.json` file. +</api> + +<api name="version"> +@property {string} +This property contains the add-on's version string. It comes from the +`version` property set in the `package.json` file in the main package. +</api> + +<api name="data"> +@property {object} +The `data` object is used to access data that was bundled with the add-on. +This data lives in the main package's `data/` directory, immediately below +the `package.json` file. All files in this directory will be copied into the +XPI and made available through the `data` object. + +The [Package Specification](dev-guide/package-spec.html) +section explains the `package.json` file. + +<api name="data.load"> +@method +The `data.load(NAME)` method returns the contents of an embedded data file, +as a string. It is most useful for data that will be modified or parsed in +some way, such as JSON, XML, plain text, or perhaps an HTML template. For +data that can be displayed directly in a content frame, use `data.url(NAME)`. +@param name {string} The filename to be read, relative to the + package's `data` directory. Each package that uses the `self` module + will see its own `data` directory. +@returns {string} +</api> + +<api name="data.url"> +@method +The `data.url(NAME)` method returns a url that points at an embedded +data file. It is most useful for data that can be displayed directly in a +content frame. The url can be passed to a content frame constructor, such +as the Panel: + + var self = require("self"); + var myPanel = require("panel").Panel({ + contentURL: self.data.url("my-panel-content.html") + }); + myPanel.show(); + +@param name {string} The filename to be read, relative to the + package's `data` directory. Each package that uses the `self` module + will see its own `data` directory. +@returns {String} +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/simple-prefs.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/simple-prefs.md new file mode 100644 index 0000000..b3cd076 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/simple-prefs.md @@ -0,0 +1,75 @@ +<!-- 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/. --> + +<!-- contributed by Erik Vold [erikvvold@gmail.com] --> + +#### *Experimental* + +The `simple-prefs` module lets you easily and persistently store preferences +across application restarts, which can be configured by users in the +Add-ons Manager. + +Introduction +------------ + +With the simple preferences module you can store booleans, integers, and string +values. + + +Inline Options & Default Values +------------------------------- + +In order to have a `options.xul` (for inline options) generated, or a +`defaults/preferences/prefs.js` for default preferences, you will need to +define the preferences in your `package.json`, like so: + + { + "fullName": "Example Add-on", + ... + "preferences": [{ + "name": "somePreference", + "title": "Some preference title", + "description": "Some short description for the preference", + "type": "string", + "value": "this is the default string value" + }] + } + + +<api name="prefs"> +@property {object} + *experimental* A persistent object private to your add-on. Properties with boolean, + number, and string values will be persisted in the Mozilla preferences system. +</api> + + +<api name="on"> +@function + *experimental* Registers an event `listener` that will be called when a preference is changed. + +**Example:** + + function onPrefChange(prefName) { + console.log("The " + prefName + " preference changed."); + } + require("simple-prefs").on("somePreference", onPrefChange); + require("simple-prefs").on("someOtherPreference", onPrefChange); + + +@param prefName {String} + The name of the preference to watch for changes. +@param listener {Function} + The listener function that processes the event. +</api> + +<api name="removeListener"> +@function + *experimental* Unregisters an event `listener` for the specified preference. + +@param prefName {String} + The name of the preference to watch for changes. +@param listener {Function} + The listener function that processes the event. +</api> + diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/simple-storage.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/simple-storage.md new file mode 100644 index 0000000..bbe9cbe --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/simple-storage.md @@ -0,0 +1,220 @@ +<!-- 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/. --> + +The `simple-storage` module lets you easily and persistently store data across +Firefox restarts. If you're familiar with [DOM storage][] on the Web, it's +kind of like that, but for add-ons. + +[DOM storage]: https://developer.mozilla.org/en/DOM/Storage + +The simple storage module exports an object called `storage` that is persistent +and private to your add-on. It's a normal JavaScript object, and you can treat +it as you would any other. + +To store a value, just assign it to a property on `storage`: + + var ss = require("simple-storage"); + ss.storage.myArray = [1, 1, 2, 3, 5, 8, 13]; + ss.storage.myBoolean = true; + ss.storage.myNull = null; + ss.storage.myNumber = 3.1337; + ss.storage.myObject = { a: "foo", b: { c: true }, d: null }; + ss.storage.myString = "O frabjous day!"; + +You can store array, boolean, number, object, null, and string values. +If you'd like to store other types of values, you'll first have to convert +them to strings or another one of these types. + +Be careful to set properties on the `storage` object and not the module itself: + + // This is no good! + var ss = require("simple-storage"); + ss.foo = "I will not be saved! :("; + +Simple Storage and "cfx run" +---------------------------- +The simple storage module stores its data in your profile. +Because `cfx run` by default uses a fresh profile each time it runs, +simple storage won't work with add-ons executed using `cfx run` - that +is, stored data will not persist from one run to the next. + +The easiest solution to this problem is to use the +[`--profiledir` option to `cfx run`](dev-guide/cfx-tool.html#profiledir). + +If you use this method, you must end your debugging session by +quitting Firefox normally, not by cancelling the shell command. +If you don't close Firefox normally, then simple storage will +not be notified that the session is finished, and will not write +your data to the backing store. + +Constructing Arrays +------------------- +Be careful to construct array objects conditionally in your code, or you may +zero them each time the construction code runs. For example, this add-on +tries to store the URLs of pages the user visits: + +<pre><code> +var ss = require("simple-storage"); +ss.storage.pages = []; + +require("tabs").on("ready", function(tab) { + ss.storage.pages.push(tab.url); +}); + +var widget = require("widget").Widget({ + id: "log_history", + label: "Log History", + width: 30, + content: "Log", + onClick: function() { + console.log(ss.storage.pages); + } +}); +</code></pre> + +But this isn't going to work, because it empties the array each time the +add-on runs (for example, each time Firefox is started). Line 2 needs +to be made conditional, so the array is only constructed if it does +not already exist: + +<pre><code> +if (!ss.storage.pages) + ss.storage.pages = []; +</code></pre> + +Deleting Data +------------- +You can delete properties using the `delete` operator. Here's an add-on +that adds three widgets to write, read, and delete a value: + +<pre><code> +var widgets = require("widget"); +var ss = require("simple-storage"); + +var widget = widgets.Widget({ + id: "write", + label: "Write", + width: 50, + content: "Write", + onClick: function() { + ss.storage.value = 1; + console.log("Setting value"); + } +}); + +var widget = widgets.Widget({ + id: "read", + label: "Read", + width: 50, + content: "Read", + onClick: function() { + console.log(ss.storage.value); + } +}); + +var widget = widgets.Widget({ + id: "delete", + label: "Delete", + width: 50, + content: "Delete", + onClick: function() { + delete ss.storage.value; + console.log("Deleting value"); + } +}); +</pre></code> + +If you run it, you'll see that after clicking "Read" after clicking +"Delete" gives you the expected output: + +<pre> +info: undefined +</pre> + +Quotas +------ +The simple storage available to your add-on is limited. Currently this limit is +about five megabytes (5,242,880 bytes). You can choose to be notified when you +go over quota, and you should respond by reducing the amount of data in storage. +If the user quits the application while you are over quota, all data stored +since the last time you were under quota will not be persisted. You should not +let that happen. + +To listen for quota notifications, register a listener for the `"OverQuota"` +event. It will be called when your storage goes over quota. + + function myOnOverQuotaListener() { + console.log("Uh oh."); + } + ss.on("OverQuota", myOnOverQuotaListener); + +Listeners can also be removed: + + ss.removeListener("OverQuota", myOnOverQuotaListener); + +To find out how much of your quota you're using, check the module's `quotaUsage` +property. It indicates the percentage of quota your storage occupies. If +you're within your quota, it's a number from 0 to 1, inclusive, and if you're +over, it's a number greater than 1. + +Therefore, when you're notified that you're over quota, respond by removing +storage until your `quotaUsage` is less than or equal to 1. Which particular +data you remove is up to you. For example: + + ss.storage.myList = [ /* some long array */ ]; + ss.on("OverQuota", function () { + while (ss.quotaUsage > 1) + ss.storage.myList.pop(); + }); + + +Private Browsing +---------------- +If your storage is related to your users' Web history, personal information, or +other sensitive data, your add-on should respect [private browsing mode][SUMO]. +While private browsing mode is active, you should not store any sensitive data. + +Because any kind of data can be placed into simple storage, support for private +browsing is not built into the module. Instead, use the +[`private-browsing`](packages/addon-kit/private-browsing.html) module to +check private browsing status and respond accordingly. + +For example, the URLs your users visit should not be stored during private +browsing. If your add-on records the URL of the selected tab, here's how you +might handle that: + + ss.storage.history = []; + var privateBrowsing = require("private-browsing"); + if (!privateBrowsing.isActive) { + var url = getSelectedTabURL(); + ss.storage.history.push(url); + } + +For more information on supporting private browsing, see its [Mozilla Developer +Network documentation][MDN]. While that page does not apply specifically to +SDK-based add-ons (and its code samples don't apply at all), you should follow +its guidance on best practices and policies. + +[SUMO]: http://support.mozilla.com/en-US/kb/Private+Browsing +[MDN]: https://developer.mozilla.org/En/Supporting_private_browsing_mode + + +<api name="storage"> +@property {object} + A persistent object private to your add-on. Properties with array, boolean, + number, object, null, and string values will be persisted. +</api> + +<api name="quotaUsage"> +@property {number} + A number in the range [0, Infinity) that indicates the percentage of quota + occupied by storage. A value in the range [0, 1] indicates that the storage + is within quota. A value greater than 1 indicates that the storage exceeds + quota. +</api> + +<api name="OverQuota"> +@event +The module emits this event when your add-on's storage goes over its quota. +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/tabs.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/tabs.md new file mode 100644 index 0000000..f1e44df --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/tabs.md @@ -0,0 +1,385 @@ +<!-- 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/. --> + +<!-- contributed by Dietrich Ayala [dietrich@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `tabs` module provides easy access to tabs and tab-related events. + +The module itself can be used like a basic list of all opened +tabs across all windows. In particular, you can enumerate it: + + var tabs = require('tabs'); + for each (var tab in tabs) + console.log(tab.title); + +You can also access individual tabs by index: + + var tabs = require('tabs'); + + tabs.on('ready', function () { + console.log('first: ' + tabs[0].title); + console.log('last: ' + tabs[tabs.length-1].title); + }); + +You can open a new tab, specifying various properties including location: + + var tabs = require("tabs"); + tabs.open("http://www.example.com"); + +You can register event listeners to be notified when tabs open, close, finish +loading DOM content, or are made active or inactive: + + var tabs = require("tabs"); + + // Listen for tab openings. + tabs.on('open', function onOpen(tab) { + myOpenTabs.push(tab); + }); + + // Listen for tab content loads. + tabs.on('ready', function(tab) { + console.log('tab is loaded', tab.title, tab.url) + }); + +You can get and set various properties of tabs (but note that properties + relating to the tab's content, such as the URL, will not contain valid +values until after the tab's `ready` event fires). By setting the `url` +property you can load a new page in the tab: + + var tabs = require("tabs"); + tabs.on('activate', function(tab) { + tab.url = "http://www.example.com"; + }); + +You can attach a [content script](dev-guide/guides/content-scripts/index.html) +to the page hosted in a tab, and use that to access and manipulate the page's +content: + + var tabs = require("tabs"); + + tabs.on('activate', function(tab) { + tab.attach({ + contentScript: 'self.postMessage(document.body.innerHTML);', + onMessage: function (message) { + console.log(message); + } + }); + }); + +<api name="activeTab"> +@property {Tab} + +The currently active tab in the active window. This property is read-only. To +activate a `Tab` object, call its `activate` method. + +**Example** + + // Get the active tab's title. + var tabs = require("tabs"); + console.log("title of active tab is " + tabs.activeTab.title); +</api> + +<api name="length"> +@property {number} +The number of open tabs across all windows. +</api> + +<api name="open"> +@function +Opens a new tab. The new tab will open in the active window or in a new window, +depending on the `inNewWindow` option. + +**Example** + + var tabs = require("tabs"); + + // Open a new tab on active window and make tab active. + tabs.open("http://www.mysite.com"); + + // Open a new tab in a new window and make it active. + tabs.open({ + url: "http://www.mysite.com", + inNewWindow: true + }); + + // Open a new tab on active window in the background. + tabs.open({ + url: "http://www.mysite.com", + inBackground: true + }); + + // Open a new tab as an app tab and do something once it's open. + tabs.open({ + url: "http://www.mysite.com", + isPinned: true, + onOpen: function onOpen(tab) { + // do stuff like listen for content + // loading. + } + }); + +@param options {object} +An object containing configurable options for how and where the tab will be +opened, as well as a listeners for the tab events. + +If the only option being used is `url`, then a bare string URL can be passed to +`open` instead of adding at a property of the `options` object. + +@prop [url] {string} +String URL to be opened in the new tab. +This is a required property. + +@prop [inNewWindow] {boolean} +If present and true, a new browser window will be opened and the URL will be +opened in the first tab in that window. This is an optional property. + +@prop [inBackground] {boolean} +If present and true, the new tab will be opened to the right of the active tab +and will not be active. This is an optional property. + +@prop [isPinned] {boolean} +If present and true, then the new tab will be pinned as an +[app tab](http://support.mozilla.com/en-US/kb/what-are-app-tabs). + +@prop [onOpen] {function} +A callback function that will be registered for 'open' event. +This is an optional property. +@prop [onClose] {function} +A callback function that will be registered for 'close' event. +This is an optional property. +@prop [onReady] {function} +A callback function that will be registered for 'ready' event. +This is an optional property. +@prop [onActivate] {function} +A callback function that will be registered for 'activate' event. +This is an optional property. +@prop [onDeactivate] {function} +A callback function that will be registered for 'deactivate' event. +This is an optional property. +</api> + +<api name="Tab"> +@class +A `Tab` instance represents a single open tab. It contains various tab +properties, several methods for manipulation, as well as per-tab event +registration. + +Tabs emit all the events described in the Events section. Listeners are +passed the `Tab` object that triggered the event. + +<api name="title"> +@property {string} +The title of the page currently loaded in the tab. +This property can be set to change the tab title. +</api> + +<api name="url"> +@property {String} +The URL of the page currently loaded in the tab. +This property can be set to load a different URL in the tab. +</api> + +<api name="favicon"> +@property {string} +The URL of the favicon for the page currently loaded in the tab. +This property is read-only. +</api> + +<api name="index"> +@property {integer} +The index of the tab relative to other tabs in the application window. +This property can be set to change its relative position. +</api> + +<api name="isPinned"> +@property {boolean} +Whether or not tab is pinned as an [app tab][]. +This property is read-only. +[app tab]:http://support.mozilla.com/en-US/kb/what-are-app-tabs +</api> + +<api name="getThumbnail"> +@property {method} +Returns thumbnail data URI of the page currently loaded in this tab. +</api> + +<api name="pin"> +@method +Pins this tab as an [app tab][]. +[app tab]:http://support.mozilla.com/en-US/kb/what-are-app-tabs +</api> + +<api name="unpin"> +@method +Unpins this tab. +</api> + +<api name="close"> +@method +Closes this tab. + +@param [callback] {function} +A function to be called when the tab finishes its closing process. +This is an optional argument. +</api> + +<api name="reload"> +@method +Reloads this tab. +</api> + +<api name="activate"> +@method +Makes this tab active, which will bring this tab to the foreground. +</api> + +<api name="attach"> +@method + Create a page mod and attach it to the document in the tab. + +**Example** + + var tabs = require("tabs"); + + tabs.on('ready', function(tab) { + tab.attach({ + contentScript: + 'document.body.style.border = "5px solid red";' + }); + }); + +@param options {object} + Options for the page mod, with the following keys: + +@prop [contentScriptFile] {string,array} + The local file URLs of content scripts to load. Content scripts specified + by this option are loaded *before* those specified by the `contentScript` + option. Optional. +@prop [contentScript] {string,array} + The texts of content scripts to load. Content scripts specified by this + option are loaded *after* those specified by the `contentScriptFile` option. + Optional. +@prop [onMessage] {function} + A function called when the page mod receives a message from content scripts. + Listeners are passed a single argument, the message posted from the + content script. Optional. + +@returns {Worker} + See [Content Scripts guide](dev-guide/guides/content-scripts/index.html) + to learn how to use the `Worker` object to communicate with the content script. + +</api> + +<api name="close"> +@event + +This event is emitted when the tab is closed. It's also emitted when the +tab's window is closed. + +@argument {Tab} +Listeners are passed the tab object. +</api> + +<api name="ready"> +@event + +This event is emitted when the DOM for the tab's content is ready. It is +equivalent to the `DOMContentLoaded` event for the given content page. + +A single tab will emit this event every time the DOM is loaded: so it will be +emitted again if the tab's location changes or the content is reloaded. + +After this event has been emitted, all properties relating to the tab's +content can be used. + +@argument {Tab} +Listeners are passed the tab object. +</api> + +<api name="activate"> +@event + +This event is emitted when the tab is made active. + +@argument {Tab} +Listeners are passed the tab object. +</api> + +<api name="deactivate"> +@event + +This event is emitted when the tab is made inactive. + +@argument {Tab} +Listeners are passed the tab object. +</api> + +</api> + +<api name="open"> +@event + +This event is emitted when a new tab is opened. This does not mean that +the content has loaded, only that the browser tab itself is fully visible +to the user. + +Properties relating to the tab's content (for example: `title`, `favicon`, +and `url`) will not be correct at this point. If you need to access these +properties, listen for the `ready` event: + + var tabs = require("tabs"); + tabs.on('open', function(tab){ + tab.on('ready', function(tab){ + console.log(tab.url); + }); + }); + +@argument {Tab} +Listeners are passed the tab object that just opened. +</api> + +<api name="close"> +@event + +This event is emitted when a tab is closed. When a window is closed +this event will be emitted for each of the open tabs in that window. + +@argument {Tab} +Listeners are passed the tab object that has closed. +</api> + +<api name="ready"> +@event + +This event is emitted when the DOM for a tab's content is ready. +It is equivalent to the `DOMContentLoaded` event for the given content page. + +A single tab will emit this event every time the DOM is loaded: so it will be +emitted again if the tab's location changes or the content is reloaded. + +After this event has been emitted, all properties relating to the tab's +content can be used. + +@argument {Tab} +Listeners are passed the tab object that has loaded. +</api> + +<api name="activate"> +@event + +This event is emitted when an inactive tab is made active. + +@argument {Tab} +Listeners are passed the tab object that has become active. +</api> + +<api name="deactivate"> +@event + +This event is emitted when the active tab is made inactive. + +@argument {Tab} +Listeners are passed the tab object that has become inactive. +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/timers.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/timers.md new file mode 100644 index 0000000..192663a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/timers.md @@ -0,0 +1,52 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> +<!-- contributed by Irakli Gozalishvil [gozala@mozilla.com] --> + +The `timers` module provides access to web-like timing functionality. + +<api name="setTimeout"> +@function + Schedules `callback` to be called in `ms` milliseconds. Any additional + arguments are passed straight through to the callback. +@returns {integer} + An ID that can later be used to undo this scheduling, if `callback` hasn't yet + been called. +@param callback {function} + Function to be called. +@param ms {integer} + Interval in milliseconds after which the function will be called. +</api> + +<api name="clearTimeout"> +@function + Given an ID returned from `setTimeout()`, prevents the callback with the ID + from being called (if it hasn't yet been called). +@param ID {integer} + An ID returned from `setTimeout()`. +</api> + +<api name="setInterval"> +@function + Schedules `callback` to be called repeatedly every `ms` milliseconds. Any + additional arguments are passed straight through to the callback. +@returns {integer} + An ID that can later be used to unschedule the callback. +@param callback {function} + Function to be called. +@param ms {integer} + Interval in milliseconds at which the function will be called. +</api> + +<api name="clearInterval"> +@function + Given an ID returned from `setInterval()`, prevents the callback with the ID + from being called again. +@param ID {integer} + An ID returned from `setInterval()`. +</api> + diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/widget.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/widget.md new file mode 100644 index 0000000..815a085 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/widget.md @@ -0,0 +1,909 @@ +<!-- 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/. --> + +<!-- contributed by Dietrich Ayala [dietrich@mozilla.com] --> +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `widget` module provides your add-on with a simple user interface that is +consistent with other add-ons and blends in well with Firefox. + +"Widgets" are small pieces of content that live in the Firefox 4 +[add-on bar](https://developer.mozilla.org/en/The_add-on_bar). +They can be simple icons or complex web pages. You can attach +[panels](packages/addon-kit/panel.html) to them that open when they're +clicked, or you can define a custom click handler to perform some other action, +like opening a web page in a tab. + +There are a few advantages to using widgets over an ad hoc user interface. +First, your users will be accustomed to interacting with add-ons via widgets and +the add-on bar. Second, it allows Firefox to treat your interface as a +first-class citizen. For example, in the future Firefox may allow the user to +drag widgets from the add-on bar to other toolbars. By exposing your interface +as a widget, your add-on would automatically inherit such functionality. + +## Creation and Content ## + +Widgets can contain images or arbitrary web content. You can include this +content inline as a string by using the `content` property, or point to content +using a URL with the `contentURL` property. + +Upon creation, the widget is automatically added to the add-on bar. +You can set the width of a widget, but the height is fixed so as to fit in the +add-on bar. If the content is an image, it is automatically scaled to be 16x16 +pixels. + +For example, this widget contains an image, so it looks like a simple icon: + + require("widget").Widget({ + id: "mozilla-icon", + label: "My Mozilla Widget", + contentURL: "http://www.mozilla.org/favicon.ico" + }); + +<img class="image-center" src="static-files/media/screenshots/widget-mozilla-icon.png" +alt="Widget displaying an icon"> + +You can make `contentURL` point to an HTML or icon file which you have +packaged inside your add-on. Just save the file in your add-on's `data` +directory, and reference it using the `data.url()` method of the +[`self`](packages/addon-kit/self.html) module: + + var data = require("self").data; + + require("widget").Widget({ + id: "my-widget", + label: "My Widget", + contentURL: data.url("my-content.html") + }); + +This widget contains an entire web page: + + require("widget").Widget({ + id: "hello-display", + label: "My Hello Widget", + content: "Hello!", + width: 50 + }); + +<img class="image-center" src="static-files/media/screenshots/widget-hello-text.png" +alt="Widget displaying 'hello'"> + +Widgets are quite small by default, so this example used the `width` property to +grow it in order to show all the text. + +## Scripting Widget Content ## + +To interact with the widget's content you need to load a separate script into +the panel. In the SDK these scripts are called "content scripts" because +they're explicitly used for interacting with web content. + +While content scripts can access the content they're attached to, they can't +use the SDK's APIs. So implementing a complete solution usually means you +have to send messages between the content script and the main add-on code. + +* You can specify one or more content scripts to load into the widget using the +`contentScript` or `contentScriptFile` options to the +[`Widget()` constructor](packages/addon-kit/widget.html#Widget(options)). + +* You can communicate with the script using either the +[`postMessage()`](dev-guide/guides/content-scripts/using-postmessage.html) +API or (preferably, usually) the +[`port`](dev-guide/guides/content-scripts/using-port.html) API. + +<div class="warning"> +<p>Unless your content script is extremely simple and consists only of a +static string, don't use <code>contentScript</code>: if you do, you may +have problems getting your add-on approved on AMO.</p> +<p>Instead, keep the script in a separate file and load it using +<code>contentScriptFile</code>. This makes your code easier to maintain, +secure, debug and review.</p> +</div> + +<!-- The icons this widget displays, shown in the screenshot, is taken from the +Glossy Buttons icon set created by IconEden which is made freely available for +commercial and non-commercial use. +See: http://www.iconeden.com/icon/category/free --> + +<img class="image-right" src="static-files/media/screenshots/widget-player-buttons.png" +alt="Media player UI implemented as a widget"> + +For example, suppose we want to implement a media player as an add-on. +We could implement the main user interface as a widget hosting an array +of buttons to control play/pause/stop functions. + +We can then use a content script to listen for clicks on those buttons. +But because content scripts can't use the SDK's APIs, we'll want the +content script to send messages to the main add-on code, which can then +implement the media player functions using the SDK. + +The widget's content is specified using HTML like this: + +<pre class="brush: html"> +<html> + <body> + <img src="play.png" id="play-button"> + <img src="pause.png" id="pause-button"> + <img src="stop.png" id="stop-button"> + </body> +</html> +</pre> + +We just include three icons, and assign an ID to each one. This HTML file, +and the icon files it references, are saved in the add-on's `data` +directory. + +Next, we write a content script that listens for click events on each icon +and sends the corresponding message to the main add-on code: + + var play_button = document.getElementById("play-button"); + play_button.onclick = function() { + self.port.emit("play"); + } + + var pause_button = document.getElementById("pause-button"); + pause_button.onclick = function() { + self.port.emit("pause"); + } + + var stop_button = document.getElementById("stop-button"); + stop_button.onclick = function() { + self.port.emit("stop"); + } + +We save this file in the add-on's `data` directory as "button-script.js". +Finally. in the add-on's "main.js" file, we create the widget, assign it +the HTML file and the content script, and listen for events from the content +script: + + const widgets = require("widget"); + const data = require("self").data; + + var player = widgets.Widget({ + id: "player", + width: 72, + label: "Player", + contentURL: data.url("buttons.html"), + contentScriptFile: data.url("button-script.js") + }); + + player.port.on("play", function() { + console.log("playing"); + }); + + player.port.on("pause", function() { + console.log("pausing"); + }); + + player.port.on("stop", function() { + console.log("stopping"); + }); + +To learn much more about content scripts, see the +[Working with Content Scripts](dev-guide/guides/content-scripts/index.html) +guide. + +<div class="experimental"> +<h3>Scripting Trusted Widget Content</h3> + +**Note that the feature described in this section is experimental: we'll +very probably continue to support it, but the name of the `addon` +property might change in a future release.** + +We've already seen that you can package HTML files in your add-on's `data` +directory and use them to define the widget's content. We can call this +"trusted" content, because unlike content loaded from a source outside the +add-on, the add-on author knows exactly what it's doing. To +interact with trusted content you don't need to use content scripts: +you can just include a script from the HTML file in the normal way, using +`script` tags. + +Like a content script, these scripts can communicate with the add-on code +using the +[`postMessage()`](dev-guide/guides/content-scripts/using-postmessage.html) +API or the +[`port`](dev-guide/guides/content-scripts/using-port.html) API. +The crucial difference is that these scripts access the `postMessage` +and `port` objects through the `addon` object, whereas content scripts +access them through the `self` object. + +To show the difference, convert the `player` add-on above +to use normal page scripts instead of content scripts. + +First, in the content script, change `self` to `addon`, and wrap it in a +function: + + function init() { + var play_button = document.getElementById("play-button"); + play_button.onclick = function() { + addon.port.emit("play"); + } + + var pause_button = document.getElementById("pause-button"); + pause_button.onclick = function() { + addon.port.emit("pause"); + } + + var stop_button = document.getElementById("stop-button"); + stop_button.onclick = function() { + addon.port.emit("stop"); + } + } + +Next, add a `script` tag to reference "button-script.js", and +call its `init()` function on load: + +<pre class="brush: html"> +<html> + <head> + <script src="button-script.js"></script> + </head> + <body onLoad="init()"> + <img src="play.png" id="play-button"> + <img src="pause.png" id="pause-button"> + <img src="stop.png" id="stop-button"> + </body> +</html> +</pre> + +Finally, remove the line attaching the content script from "main.js": + + const widgets = require("widget"); + const data = require("self").data; + + var player = widgets.Widget({ + id: "player", + width: 72, + label: "Player", + contentURL: data.url("buttons.html") + }); + + player.port.emit("init"); + + player.port.on("play", function() { + console.log("playing"); + }); + + player.port.on("pause", function() { + console.log("pausing"); + }); + + player.port.on("stop", function() { + console.log("stopping"); + }); +</div> + +## Attaching Panels to Widgets ## + +You can supply a [panel](packages/addon-kit/panel.html) to the widget's +constructor: if you do this, the panel is automatically displayed when the +user clicks the widget. + +<!-- The icon the widget displays, shown in the screenshot, is taken from the +Circular icon set, http://prothemedesign.com/circular-icons/ which is made +available under the Creative Commons Attribution 2.5 Generic License: +http://creativecommons.org/licenses/by/2.5/ --> + +<img class="image-right" src="static-files/media/screenshots/widget-panel-clock.png" +alt="Panel attached to a widget"> + + data = require("self").data + + var clockPanel = require("panel").Panel({ + width:215, + height:160, + contentURL: data.url("clock.html") + }); + + require("widget").Widget({ + id: "open-clock-btn", + label: "Clock", + contentURL: data.url("History.png"), + panel: clockPanel + }); + +Note that this is, at the moment, the only way you can attach a panel to a widget. + +You must supply the panel in the widget's constructor for it to work. If you +assign the panel to the widget after construction, the panel can still be shown +but will not be anchored to the widget: + + data = require("self").data + + var clockPanel = require("panel").Panel({ + width:215, + height:160, + contentURL: data.url("clock.html") + }); + + widget = require("widget").Widget({ + id: "open-clock-btn", + label: "Clock", + contentURL: data.url("History.png") + }); + + widget.panel = clockPanel; + + // Will not be anchored + widget.panel.show(); + +Also, if you try to call `panel.show()` inside your widget's `click` event +listener, the panel will not be anchored: + + data = require("self").data + + var clockPanel = require("panel").Panel({ + width:215, + height:160, + contentURL: data.url("clock.html") + }); + + require("widget").Widget({ + id: "open-clock-btn", + label: "Clock", + contentURL: data.url("History.png"), + panel: clockPanel, + onClick: function() { + // Will not be anchored + this.panel.show(); + } + }); + +See [bug 638142](https://bugzilla.mozilla.org/show_bug.cgi?id=638142). + +## Examples ## + +For conciseness, these examples create their content scripts as strings and use +the `contentScript` property. In your own add-ons, you will probably want to +create your content scripts in separate files and pass their URLs using the +`contentScriptFile` property. See +[Working with Content Scripts](dev-guide/guides/content-scripts/index.html) for more +information. + + var widgets = require("widget"); + + // A basic click-able image widget. + widgets.Widget({ + id: "google-link", + label: "Widget with an image and a click handler", + contentURL: "http://www.google.com/favicon.ico", + onClick: function() { + require("tabs").activeTab.url = "http://www.google.com/"; + } + }); +<br> + + // A widget that changes display on mouseover. + widgets.Widget({ + id: "mouseover-effect", + label: "Widget with changing image on mouseover", + contentURL: "http://www.yahoo.com/favicon.ico", + onMouseover: function() { + this.contentURL = "http://www.bing.com/favicon.ico"; + }, + onMouseout: function() { + this.contentURL = "http://www.yahoo.com/favicon.ico"; + } + }); +<br> + + // A widget that updates content on a timer. + widgets.Widget({ + id: "auto-update-widget", + label: "Widget that updates content on a timer", + content: "0", + contentScript: 'setTimeout(function() {' + + ' document.body.innerHTML++;' + + '}, 2000)', + contentScriptWhen: "ready" + }); +<br> + + // A widget that loads a random Flickr photo every 5 minutes. + widgets.Widget({ + id: "random-flickr", + label: "Random Flickr Photo Widget", + contentURL: "http://www.flickr.com/explore/", + contentScriptWhen: "ready", + contentScript: 'postMessage(document.querySelector(".pc_img").src);' + + 'setTimeout(function() {' + + ' document.location = "http://www.flickr.com/explore/";' + + '}, 5 * 60 * 1000);', + onMessage: function(imgSrc) { + this.contentURL = imgSrc; + }, + onClick: function() { + require("tabs").activeTab.url = this.contentURL; + } + }); +<br> + + // A widget created with a specified width, that grows. + let myWidget = widgets.Widget({ + id: "widget-effect", + label: "Wide widget that grows wider on a timer", + content: "I'm getting longer.", + width: 50, + }); + require("timers").setInterval(function() { + myWidget.width += 10; + }, 1000); +<br> + + // A widget communicating bi-directionally with a content script. + let widget = widgets.Widget({ + id: "message-test", + label: "Bi-directional communication!", + content: "<foo>bar</foo>", + contentScriptWhen: "ready", + contentScript: 'on("message", function(message) {' + + ' alert("Got message: " + message);' + + '});' + + 'postMessage("ready");', + onMessage: function(message) { + if (message == "ready") + widget.postMessage("me too"); + } + }); + +<api-name="Widget"> +@class +Represents a widget object. + +<api name="Widget"> +@constructor {options} + Creates a new widget. The widget is immediately added to the add-on bar. + +@param options {object} + An object with the following keys: + + @prop label {string} + A required string description of the widget used for accessibility, + title bars, and error reporting. + + @prop id {string} + Mandatory string used to identify your widget in order to save its + location when the user moves it in the browser. + This string has to be unique and must not be changed over time. + + @prop [content] {string} + An optional string value containing the displayed content of the widget. + It may contain HTML. Widgets must have either the `content` property or the + `contentURL` property set. + + If the content is an image, it is automatically scaled to be 16x16 pixels. + + @prop [contentURL] {string} + An optional string URL to content to load into the widget. This can be + [local content](dev-guide/guides/content-scripts/index.html) or remote + content, an image or web content. Widgets must have either the `content` + property or the `contentURL` property set. + + If the content is an image, it is automatically scaled to be 16x16 pixels. + + @prop [panel] {Panel} + An optional [panel](packages/addon-kit/panel.html) to open when the + user clicks on the widget. Note: If you also register a "click" listener, + it will be called instead of the panel being opened. However, you can show + the panel from the listener by calling `this.panel.show()`. + + @prop [width] {integer} + Optional width in pixels of the widget. If not given, a default width is + used. + + @prop [onClick] {function} + Include this to listen to the widget's `click` event. + + @prop [onMessage] {function} + Include this to listen to the widget's `message` event. + + @prop [onMouseover] {function} + Include this to listen to the widget's `mouseover` event. + + @prop [onMouseout] {function} + Include this to listen to the widget's `mouseout` event. + + @prop [onAttach] {function} + Include this to listen to the widget's `attach` event. + + @prop [tooltip] {string} + Optional text to show when the user's mouse hovers over the widget. If not + given, the `label` is used. + + @prop [allow] {object} + An optional object describing permissions for the content. It should + contain a single key named `script` whose value is a boolean that indicates + whether or not to execute script in the content. `script` defaults to true. + + @prop [contentScriptFile] {string,array} + A local file URL or an array of local file URLs of content scripts to load. + Content scripts specified by this property are loaded *before* those + specified by the `contentScript` property. + + @prop [contentScript] {string,array} + A string or an array of strings containing the texts of content scripts to + load. Content scripts specified by this property are loaded *after* those + specified by the `contentScriptFile` property. + + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the widget is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the widget has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + +</api> + +<api name="destroy"> +@method + Removes the widget from the add-on bar. +</api> + +<api name="postMessage"> +@method + Sends a message to the widget's content scripts. +@param data {value} + The message to send. + The message can be any +<a href = "dev-guide/guides/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="on"> +@method + Registers an event listener with the widget. +@param type {string} + The type of event to listen for. +@param listener {function} + The listener function that handles the event. +</api> + +<api name="removeListener"> +@method + Unregisters an event listener from the widget. +@param type {string} + The type of event for which `listener` was registered. +@param listener {function} + The listener function that was registered. +</api> + +<api name="getView"> +@method + Retrieve a `WidgetView` instance of this widget relative to a browser window. +@param window {BrowserWindow} + The [BrowserWindow](packages/addon-kit/windows.html) instance to match. +@returns {WidgetView} + A `WidgetView` instance associated with the browser window. Any changes + subsequently applied to this object will only be applied to the widget + attached to that window. +</api> + +<api name="label"> +@property {string} + The widget's label. Read-only. +</api> + +<api name="content"> +@property {string} + A string containing the widget's content. It can contain HTML. Setting it + updates the widget's appearance immediately. However, if the widget was + created using `contentURL`, then this property is meaningless, and setting it + has no effect. +</api> + +<api name="contentURL"> +@property {string} + The URL of content to load into the widget. This can point to + local content loaded from your add-on's "data" directory or remote + content, an image or web content. Setting it updates the widget's appearance + immediately. However, if the widget was created using `content`, then this + property is meaningless, and setting it has no effect. +</api> + +<api name="panel"> +@property {Panel} + A [panel](packages/addon-kit/panel.html) to open when the user clicks on + the widget. +</api> + +<api name="width"> +@property {number} + The widget's width in pixels. Setting it updates the widget's appearance + immediately. +</api> + +<api name="tooltip"> +@property {string} + The text of the tooltip that appears when the user hovers over the widget. +</api> + +<api name="allow"> +@property {object} + A object describing permissions for the content. It contains a single key + named `script` whose value is a boolean that indicates whether or not to + execute script in the content. +</api> + +<api name="contentScriptFile"> +@property {string,array} + A local file URL or an array of local file URLs of content scripts to load. +</api> + +<api name="contentScript"> +@property {string,array} + A string or an array of strings containing the texts of content scripts to + load. +</api> + +<api name="contentScriptWhen"> +@property {string} + When to load the content scripts. This may have one of the following + values: + + * "start": load content scripts immediately after the document + element for the widget is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the widget has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/events.html) object that allows you to: + +* send events to the content script using the `port.emit` function +* receive events from the content script using the `port.on` function + +See the guide to +<a href="dev-guide/guides/content-scripts/using-port.html"> +communicating using <code>port</code></a> for details. +</api> + +<api name="attach"> +@event +This event is emitted when a new `WidgetView` object is created using the +`getView()` function. +</api> + +<api name="click"> +@event +This event is emitted when the widget is clicked. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this widget. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the widget's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/guides/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="mouseover"> +@event +This event is emitted when the user moves the mouse over the widget. +</api> + +<api name="mouseout"> +@event +This event is emitted when the user moves the mouse away from the widget. +</api> + +</api> + + +<api-name="WidgetView"> +@class +Represents a widget instance specific to one browser window. + +Anything you do to an instance of this object will only be applied to the +instance attached to its browser window: widget instances attached to other +browser windows will be unaffected. + +By contrast, any changes you make to an instance of the normal `Widget` class +will be applied across all browser windows. + +This class has all the same methods, attributes and events as the `Widget` +class except for the `getView` method and the `attach` event. + +In this example `WidgetView` is used to display different content for +`http` and `https` schemes: + + // A widget that update its content specifically to each window. + let tabs = require("tabs"); + let windows = require("windows").browserWindows; + let widget = widgets.Widget({ + id: "window-specific-test", + label: "Widget with content specific to each window", + content: " ", + width: 50 + }); + // Observe tab switch or document changes in each existing tab: + function updateWidgetState(tab) { + let view = widget.getView(tab.window); + if (!view) return; + // Update widget displayed text: + view.content = tab.url.match(/^https/) ? "Secured" : "Unsafe"; + } + tabs.on('ready', updateWidgetState); + tabs.on('activate', updateWidgetState); + +<api name="destroy"> +@method + Removes the widget view from the add-on bar. +</api> + +<api name="postMessage"> +@method + Sends a message to the widget view's content scripts. +@param data {value} + The message to send. The message can be any +<a href = "dev-guide/guides/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="on"> +@method + Registers an event listener with the widget view. +@param type {string} + The type of event to listen for. +@param listener {function} + The listener function that handles the event. +</api> + +<api name="removeListener"> +@method + Unregisters an event listener from the widget view. +@param type {string} + The type of event for which `listener` was registered. +@param listener {function} + The listener function that was registered. +</api> + +<api name="label"> +@property {string} + The widget view's label. Read-only. +</api> + +<api name="content"> +@property {string} + A string containing the widget view's content. It can contain HTML. + Setting it updates the widget view's appearance immediately. However, + if the widget view was created using `contentURL`, then this property + is meaningless, and setting it has no effect. +</api> + +<api name="contentURL"> +@property {string} + The URL of content to load into the widget. This can point to + local content loaded from your add-on's "data" directory or remote + content, an image or web content. Setting it updates the widget's appearance + immediately. However, if the widget was created using `content`, then this + property is meaningless, and setting it has no effect. +</api> + +<api name="panel"> +@property {Panel} + A [panel](packages/addon-kit/panel.html) to open when the user clicks on + the widget view. +</api> + +<api name="width"> +@property {number} + The widget view's width in pixels. Setting it updates the widget view's + appearance immediately. +</api> + +<api name="tooltip"> +@property {string} + The text of the tooltip that appears when the user hovers over the widget + view. +</api> + +<api name="allow"> +@property {object} + A object describing permissions for the content. It contains a single key + named `script` whose value is a boolean that indicates whether or not to + execute script in the content. +</api> + +<api name="contentScriptFile"> +@property {string,array} + A local file URL or an array of local file URLs of content scripts to load. +</api> + +<api name="contentScript"> +@property {string,array} + A string or an array of strings containing the texts of content scripts to + load. +</api> + +<api name="contentScriptWhen"> +@property {string} + When to load the content scripts. This may have one of the following + values: + + * "start": load content scripts immediately after the document + element for the widget view is inserted into the DOM, but before the DOM + content itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the widget view has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/events.html) object that allows you to: + +* send events to the content script using the `port.emit` function +* receive events from the content script using the `port.on` + +See the guide to +<a href="dev-guide/guides/content-scripts/using-port.html"> +communicating using <code>port</code></a> for details. +</api> + +<api name="detach"> +@event +The `detach` event is fired when the widget view is removed from its related +window. +This can occur if the window is closed, Firefox exits, or the add-on is +disabled. +</api> + +<api name="click"> +@event +This event is emitted when the widget view is clicked. +</api> + +<api name="message"> +@event +If you listen to this event you can receive message events from content +scripts associated with this widget view. When a content script posts a +message using `self.postMessage()`, the message is delivered to the add-on +code in the widget view's `message` event. + +@argument {value} +Listeners are passed a single argument which is the message posted +from the content script. The message can be any +<a href = "dev-guide/guides/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="mouseover"> +@event +This event is emitted when the user moves the mouse over the widget view. +</api> + +<api name="mouseout"> +@event +This event is emitted when the user moves the mouse away from the widget view. +</api> + +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/docs/windows.md b/tools/addon-sdk-1.7/packages/addon-kit/docs/windows.md new file mode 100644 index 0000000..f44817e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/docs/windows.md @@ -0,0 +1,191 @@ +<!-- 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/. --> + +<!-- contributed by Felipe Gomes [felipc@gmail.com] --> + + +The `windows` module provides easy access to browser windows, their +tabs, and open/close related functions and events. + +This module currently only supports browser windows and does not provide +access to non-browser windows such as the Bookmarks Library, preferences +or other non-browser windows created via add-ons. + +<api name="browserWindows"> +@property {List} +An object that contains various properties and methods to access +functionality from browser windows, such as opening new windows, accessing +their tabs or switching the current active window. + +`browserWindows` provides access to all the currently open browser windows: + + var windows = require("windows"); + for each (var window in windows.browserWindows) { + console.log(window.title); + } + + console.log(windows.browserWindows.length); + +Object emits all the events listed under "Events" section. + +####Examples#### + + var windows = require("windows").browserWindows; + + // add a listener to the 'open' event + windows.on('open', function(window) { + myOpenWindows.push(window); + }); + + // add a listener to the 'close' event + windows.on('close', function(window) { + console.log("A window was closed."); + }); + +<api name="activeWindow"> +@property {BrowserWindow} + +The currently active window. This property is read-only. + +**Example** + + // get + var windows = require("windows"); + console.log("title of active window is " + + windows.browserWindows.activeWindow.title); + + anotherWindow.activate(); + // set + windows.activeWindow == anotherWindow // true +</api> + +</api> + +<api name="open"> +@function +Open a new window. + + var windows = require("windows").browserWindows; + + // Open a new window. + windows.open("http://www.example.com"); + + // Open a new window and set a listener for "open" event. + windows.open({ + url: "http://www.example.com", + onOpen: function(window) { + // do stuff like listen for content + // loading. + } + }); + +Returns the window that was opened: + + var widgets = require("widget"); + var windows = require("windows").browserWindows; + + var example = windows.open("http://www.example.com"); + + var widget = widgets.Widget({ + id: "close-window", + label: "Close window", + contentURL: "http://www.mozilla.org/favicon.ico", + onClick: function() { + example.close(); + } + }); + +@param options {object} +An object containing configurable options for how this window will be opened, +as well as a callback for being notified when the window has fully opened. + +If the only option being used is `url`, then a bare string URL can be passed to +`open` instead of specifying it as a property of the `options` object. + +@prop url {string} +String URL to be opened in the new window. +This is a required property. + +@prop [onOpen] {function} +A callback function that is called when the window has opened. This does not +mean that the URL content has loaded, only that the window itself is fully +functional and its properties can be accessed. This is an optional property. + +@prop [onClose] {function} +A callback function that is called when the window will be called. +This is an optional property. + +@returns {BrowserWindow} +</api> + +<api name="BrowserWindow"> +@class +A `BrowserWindow` instance represents a single open window. They can be +retrieved from the `browserWindows` property exported by this module. + + var windows = require("windows").browserWindows; + + //Print how many tabs the current window has + console.log("The active window has " + + windows.activeWindow.tabs.length + + " tabs."); + + // Print the title of all browser windows + for each (var window in windows) { + console.log(window.title); + } + + // close the active window + windows.activeWindow.close(); + + windows.activeWindow.close(function() { + console.log("The active window was closed"); + }); + +<api name="title"> +@property {string} +The current title of the window. Usually the title of the active tab, +plus an app identifier. +This property is read-only. +</api> + +<api name="tabs"> +@property {TabList} +A live list of tabs in this window. This object has the same interface as the +[`tabs` API](packages/addon-kit/tabs.html), except it contains only the +tabs in this window, not all tabs in all windows. This property is read-only. +</api> + +<api name="activate"> +@method +Makes window active, which will focus that window and bring it to the +foreground. +</api> + +<api name="close"> +@method +Close the window. + +@param [callback] {function} +A function to be called when the window finishes its closing process. +This is an optional argument. +</api> + +</api> + +<api name="open"> +@event +Event emitted when a new window is open. +This does not mean that the content has loaded, only that the browser window +itself is fully visible to the user. +@argument {Window} +Listeners are passed the `window` object that triggered the event. +</api> + +<api name="close"> +@event +Event emitted when a window is closed. +@argument {Window} +Listeners are passed the `window` object that triggered the event. +</api> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/addon-page.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/addon-page.js new file mode 100644 index 0000000..eeb5461 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/addon-page.js @@ -0,0 +1,33 @@ +/* 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 { uriPrefix, name } = require('@packaging'); +const { WindowTracker, isBrowser } = require('api-utils/window-utils'); +const { add, remove } = require('api-utils/array'); +const { getTabs, closeTab } = require('api-utils/tabs/utils'); + +// Note: This is an URL that will be returned by calling +// `require('self').data.url('index.html')` from the add-on modules. +// We could not use this expression as in this module it would have +// returned "addon-kit/data/index.html" instead. +const addonURL = uriPrefix + name + '/data/index.html'; + +WindowTracker({ + onTrack: function onTrack(window) { + if (isBrowser(window)) + add(window.XULBrowserWindow.inContentWhitelist, addonURL); + }, + onUntrack: function onUntrack(window) { + getTabs(window). + filter(function(tab) tab.linkedBrowser.currentURI.spec === addonURL). + forEach(function(tab) { + // Note: `onUntrack` will be called for all windows on add-on unloads, + // so we want to clean them up from these URLs. + remove(window.XULBrowserWindow.inContentWhitelist, addonURL); + closeTab(tab); + }); + } +}); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/clipboard.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/clipboard.js new file mode 100644 index 0000000..2feab22 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/clipboard.js @@ -0,0 +1,230 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 errors = require("api-utils/errors"); +const apiUtils = require("api-utils/api-utils"); + +/* +While these data flavors resemble Internet media types, they do +no directly map to them. +*/ +const kAllowableFlavors = [ + "text/unicode", + "text/html" + /* CURRENTLY UNSUPPORTED FLAVORS + "text/plain", + "image/png", + "image/jpg", + "image/gif" + "text/x-moz-text-internal", + "AOLMAIL", + "application/x-moz-file", + "text/x-moz-url", + "text/x-moz-url-data", + "text/x-moz-url-desc", + "text/x-moz-url-priv", + "application/x-moz-nativeimage", + "application/x-moz-nativehtml", + "application/x-moz-file-promise-url", + "application/x-moz-file-promise-dest-filename", + "application/x-moz-file-promise", + "application/x-moz-file-promise-dir" + */ +]; + +/* +Aliases for common flavors. Not all flavors will +get an alias. New aliases must be approved by a +Jetpack API druid. +*/ +const kFlavorMap = [ + { short: "text", long: "text/unicode" }, + { short: "html", long: "text/html" } + // Images are currently unsupported. + //{ short: "image", long: "image/png" }, +]; + +let clipboardService = Cc["@mozilla.org/widget/clipboard;1"]. + getService(Ci.nsIClipboard); + +let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + + +exports.set = function(aData, aDataType) { + let options = { + data: aData, + datatype: aDataType || "text" + }; + options = apiUtils.validateOptions(options, { + data: { + is: ["string"] + }, + datatype: { + is: ["string"] + } + }); + + var flavor = fromJetpackFlavor(options.datatype); + + if (!flavor) + throw new Error("Invalid flavor"); + + // Additional checks for using the simple case + if (flavor == "text/unicode") { + clipboardHelper.copyString(options.data); + return true; + } + + // Below are the more complex cases where we actually have to work with a + // nsITransferable object + var xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + if (!xferable) + throw new Error("Couldn't set the clipboard due to an internal error " + + "(couldn't create a Transferable object)."); + + switch (flavor) { + case "text/html": + // add text/html flavor + let (str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString)) + { + str.data = options.data; + xferable.addDataFlavor(flavor); + xferable.setTransferData(flavor, str, str.data.length * 2); + } + + // add a text/unicode flavor (html converted to plain text) + let (str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString), + converter = Cc["@mozilla.org/feed-textconstruct;1"]. + createInstance(Ci.nsIFeedTextConstruct)) + { + converter.type = "html"; + converter.text = options.data; + str.data = converter.plainText(); + xferable.addDataFlavor("text/unicode"); + xferable.setTransferData("text/unicode", str, str.data.length * 2); + } + break; + // TODO: images! + default: + throw new Error("Unable to handle the flavor " + flavor + "."); + } + + // TODO: Not sure if this will ever actually throw. -zpao + try { + clipboardService.setData( + xferable, + null, + clipboardService.kGlobalClipboard + ); + } catch (e) { + throw new Error("Couldn't set clipboard data due to an internal error: " + e); + } + return true; +}; + + +exports.get = function(aDataType) { + let options = { + datatype: aDataType || "text" + }; + options = apiUtils.validateOptions(options, { + datatype: { + is: ["string"] + } + }); + + var xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + if (!xferable) + throw new Error("Couldn't set the clipboard due to an internal error " + + "(couldn't create a Transferable object)."); + + var flavor = fromJetpackFlavor(options.datatype); + + // Ensure that the user hasn't requested a flavor that we don't support. + if (!flavor) + throw new Error("Getting the clipboard with the flavor '" + flavor + + "' is > not supported."); + + // TODO: Check for matching flavor first? Probably not worth it. + + xferable.addDataFlavor(flavor); + + // Get the data into our transferable. + clipboardService.getData( + xferable, + clipboardService.kGlobalClipboard + ); + + var data = {}; + var dataLen = {}; + try { + xferable.getTransferData(flavor, data, dataLen); + } catch (e) { + // Clipboard doesn't contain data in flavor, return null. + return null; + } + + // There's no data available, return. + if (data.value === null) + return null; + + // TODO: Add flavors here as we support more in kAllowableFlavors. + switch (flavor) { + case "text/unicode": + case "text/html": + data = data.value.QueryInterface(Ci.nsISupportsString).data; + break; + default: + data = null; + } + + return data; +}; + +exports.__defineGetter__("currentFlavors", function() { + // Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each. + // This doesn't seem like the most efficient way, but we can't get + // confirmation for specific flavors any other way. This is supposed to be + // an inexpensive call, so performance shouldn't be impacted (much). + var currentFlavors = []; + for each (var flavor in kAllowableFlavors) { + var matches = clipboardService.hasDataMatchingFlavors( + [flavor], + 1, + clipboardService.kGlobalClipboard + ); + if (matches) + currentFlavors.push(toJetpackFlavor(flavor)); + } + return currentFlavors; +}); + +// SUPPORT FUNCTIONS //////////////////////////////////////////////////////// + +function toJetpackFlavor(aFlavor) { + for each (let flavorMap in kFlavorMap) + if (flavorMap.long == aFlavor) + return flavorMap.short; + // Return null in the case where we don't match + return null; +} + +function fromJetpackFlavor(aJetpackFlavor) { + // TODO: Handle proper flavors better + for each (let flavorMap in kFlavorMap) + if (flavorMap.short == aJetpackFlavor || flavorMap.long == aJetpackFlavor) + return flavorMap.long; + // Return null in the case where we don't match. + return null; +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/context-menu.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/context-menu.js new file mode 100644 index 0000000..75ccbea --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/context-menu.js @@ -0,0 +1,1492 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 {Ci} = require("chrome"); + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The context-menu module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +const apiUtils = require("api-utils/api-utils"); +const collection = require("api-utils/collection"); +const { Worker } = require("api-utils/content"); +const { URL } = require("api-utils/url"); +const { MatchPattern } = require("api-utils/match-pattern"); +const { EventEmitterTrait: EventEmitter } = require("api-utils/events"); +const observerServ = require("api-utils/observer-service"); +const jpSelf = require("self"); +const winUtils = require("api-utils/window-utils"); +const { getInnerId } = require("api-utils/window/utils"); +const { Trait } = require("api-utils/light-traits"); +const { Cortex } = require("api-utils/cortex"); +const timer = require("timer"); + +// All user items we add have this class name. +const ITEM_CLASS = "jetpack-context-menu-item"; + +// Items in the top-level context menu also have this class. +const TOPLEVEL_ITEM_CLASS = "jetpack-context-menu-item-toplevel"; + +// Items in the overflow submenu also have this class. +const OVERFLOW_ITEM_CLASS = "jetpack-context-menu-item-overflow"; + +// The ID of the menu separator that separates standard context menu items from +// our user items. +const SEPARATOR_ID = "jetpack-context-menu-separator"; + +// If more than this number of items are added to the context menu, all items +// overflow into a "Jetpack" submenu. +const OVERFLOW_THRESH_DEFAULT = 10; +const OVERFLOW_THRESH_PREF = + "extensions.addon-sdk.context-menu.overflowThreshold"; + +// The label of the overflow sub-xul:menu. +// +// TODO: Localize this. +const OVERFLOW_MENU_LABEL = "Add-ons"; + +// The ID of the overflow sub-xul:menu. +const OVERFLOW_MENU_ID = "jetpack-content-menu-overflow-menu"; + +// The ID of the overflow submenu's xul:menupopup. +const OVERFLOW_POPUP_ID = "jetpack-content-menu-overflow-popup"; + +// These are used by PageContext.isCurrent below. If the popupNode or any of +// its ancestors is one of these, Firefox uses a tailored context menu, and so +// the page context doesn't apply. +const NON_PAGE_CONTEXT_ELTS = [ + Ci.nsIDOMHTMLAnchorElement, + Ci.nsIDOMHTMLAppletElement, + Ci.nsIDOMHTMLAreaElement, + Ci.nsIDOMHTMLButtonElement, + Ci.nsIDOMHTMLCanvasElement, + Ci.nsIDOMHTMLEmbedElement, + Ci.nsIDOMHTMLImageElement, + Ci.nsIDOMHTMLInputElement, + Ci.nsIDOMHTMLMapElement, + Ci.nsIDOMHTMLMediaElement, + Ci.nsIDOMHTMLMenuElement, + Ci.nsIDOMHTMLObjectElement, + Ci.nsIDOMHTMLOptionElement, + Ci.nsIDOMHTMLSelectElement, + Ci.nsIDOMHTMLTextAreaElement, +]; + +// This is used to access private properties of Item and Menu instances. +const PRIVATE_PROPS_KEY = { + valueOf: function valueOf() "private properties key" +}; + +// Used as an internal ID for items and as part of a public ID for item DOM +// elements. Careful: This number is not necessarily unique to any one instance +// of the module. For each module instance, when the first item is created this +// number will be 0, when the second is created it will be 1, and so on. +let nextItemID = 0; + +// The number of items that haven't finished initializing yet. See +// AIT__finishActiveItemInit(). +let numItemsWithUnfinishedInit = 0; + +exports.Item = Item; +exports.Menu = Menu; +exports.Separator = Separator; + + +// A word about traits and privates. `this` inside of traits methods is an +// object private to the implementation. It should never be publicly leaked. +// We use Cortex in the exported menu item constructors to create public +// reflections of the private objects that hide private properties -- those +// prefixed with an underscore. Public reflections are attached to the private +// objects via the `_public` property. +// +// All item objects passed into the implementation by the client will be public +// reflections, not private objects. Likewise, all item objects passed out of +// the implementation to the client must be public, not private. Mixing up +// public and private is bad and easy to do, so not only are private objects +// restricted to the implementation, but as much as possible we try to restrict +// them to the Item, Menu, and Separator traits and constructors. Everybody +// else in the implementation should expect to be passed public reflections, and +// they must specifically request private objects via privateItem(). + +// Item, Menu, and Separator are composed of this trait. +const ItemBaseTrait = Trait({ + + _initBase: function IBT__initBase(opts, optRules, optsToNotSet) { + this._optRules = optRules; + for (let optName in optRules) + if (optsToNotSet.indexOf(optName) < 0) + this[optName] = opts[optName]; + optsToNotSet.forEach(function (opt) validateOpt(opts[opt], optRules[opt])); + this._isInited = true; + + this._id = nextItemID++; + this._parentMenu = null; + + // This makes the private properties accessible to anyone with access to + // PRIVATE_PROPS_KEY. Barring loader tricks, only this file has has access + // to it, so only this file has access to the private properties. + const self = this; + this.valueOf = function IBT_valueOf(key) { + return key === PRIVATE_PROPS_KEY ? self : self._public; + }; + }, + + destroy: function IBT_destroy() { + if (this._wasDestroyed) + return; + if (this.parentMenu) + this.parentMenu.removeItem(this._public); + else if (!(this instanceof Separator) && this._hasFinishedInit) + browserManager.removeTopLevelItem(this._public); + browserManager.unregisterItem(this._public); + this._wasDestroyed = true; + }, + + get parentMenu() { + return this._parentMenu; + }, + + set parentMenu(val) { + throw new Error("The 'parentMenu' property is not intended to be set. " + + "Use menu.addItem(item) instead."); + }, + + set _isTopLevel(val) { + if (val) + this._workerReg = new WorkerRegistry(this._public); + else { + this._workerReg.destroy(); + delete this._workerReg; + } + }, + + get _topLevelItem() { + let topLevelItem = this._public; + let parentMenu = this.parentMenu; + while (parentMenu) { + topLevelItem = parentMenu; + parentMenu = parentMenu.parentMenu; + } + return topLevelItem; + } +}); + +// Item and Menu are composed of this trait. +const ActiveItemTrait = Trait.compose(ItemBaseTrait, EventEmitter, Trait({ + + _initActiveItem: function AIT__initActiveItem(opts, optRules, optsToNotSet) { + this._initBase(opts, optRules, + optsToNotSet.concat(["onMessage", "context"])); + + if ("onMessage" in opts) + this.on("message", opts.onMessage); + + // When a URL context is removed (by calling context.remove(urlContext)), we + // may need to create workers for windows containing pages that the item now + // matches. Likewise, when a URL context is added, we need to destroy + // workers for windows containing pages that the item now does not match. + // + // collection doesn't provide a way to listen for removals. utils/registry + // does, but it doesn't allow its elements to be enumerated. So as a hack, + // use a collection for item.context and replace its add and remove methods. + collection.addCollectionProperty(this, "context"); + if (opts.context) + this.context.add(opts.context); + + const self = this; + + let add = this.context.add; + this.context.add = function itemContextAdd() { + let args = Array.slice(arguments); + add.apply(self.context, args); + if (self._workerReg && args.some(function (a) a instanceof URLContext)) + self._workerReg.destroyUnneededWorkers(); + }; + + let remove = this.context.remove; + this.context.remove = function itemContextRemove() { + let args = Array.slice(arguments); + remove.apply(self.context, args); + if (self._workerReg && args.some(function (a) a instanceof URLContext)) + self._workerReg.createNeededWorkers(); + }; + }, + + // Workers are only created for top-level menu items. When a top-level item + // is later added to a Menu, its workers are destroyed. Well, all items start + // out as top-level because there is, unfortunately, no contextMenu.add(). So + // when an item is created and immediately added to a Menu, workers for it are + // needlessly created and destroyed. The point of this timeout is to avoid + // that. Items that are created and added to Menus in the same turn of the + // event loop won't have workers created for them. + _finishActiveItemInit: function AIT__finishActiveItemInit() { + numItemsWithUnfinishedInit++; + const self = this; + timer.setTimeout(function AIT__finishActiveItemInitTimeout() { + if (!self.parentMenu && !self._wasDestroyed) + browserManager.addTopLevelItem(self._public); + self._hasFinishedInit = true; + numItemsWithUnfinishedInit--; + }, 0); + }, + + get label() { + return this._label; + }, + + set label(val) { + this._label = validateOpt(val, this._optRules.label); + if (this._isInited) + browserManager.setItemLabel(this, this._label); + return this._label; + }, + + get image() { + return this._image; + }, + + set image(val) { + this._image = validateOpt(val, this._optRules.image); + if (this._isInited) + browserManager.setItemImage(this, this._image); + return this._image; + }, + + get contentScript() { + return this._contentScript; + }, + + set contentScript(val) { + this._contentScript = validateOpt(val, this._optRules.contentScript); + return this._contentScript; + }, + + get contentScriptFile() { + return this._contentScriptFile; + }, + + set contentScriptFile(val) { + this._contentScriptFile = + validateOpt(val, this._optRules.contentScriptFile); + return this._contentScriptFile; + } +})); + +// Item is composed of this trait. +const ItemTrait = Trait.compose(ActiveItemTrait, Trait({ + + _initItem: function IT__initItem(opts, optRules) { + this._initActiveItem(opts, optRules, []); + }, + + get data() { + return this._data; + }, + + set data(val) { + this._data = validateOpt(val, this._optRules.data); + if (this._isInited) + browserManager.setItemData(this, this._data); + return this._data; + }, + + toString: function IT_toString() { + return '[object Item "' + this.label + '"]'; + } +})); + +// The exported Item constructor. +function Item(options) { + let optRules = optionsRules(); + optRules.data = { + map: function (v) v.toString(), + is: ["string", "undefined"] + }; + + let item = ItemTrait.create(Item.prototype); + item._initItem(options, optRules); + + item._public = Cortex(item); + browserManager.registerItem(item._public); + item._finishActiveItemInit(); + + return item._public; +} + +// Menu is composed of this trait. +const MenuTrait = Trait.compose( + ActiveItemTrait.resolve({ destroy: "_destroyThisItem" }), + Trait({ + + _initMenu: function MT__initMenu(opts, optRules, optsToNotSet) { + this._items = []; + this._initActiveItem(opts, optRules, optsToNotSet); + }, + + destroy: function MT_destroy() { + while (this.items.length) + this.items[0].destroy(); + this._destroyThisItem(); + }, + + get items() { + return this._items; + }, + + set items(val) { + let newItems = validateOpt(val, this._optRules.items); + while (this.items.length) + this.items[0].destroy(); + newItems.forEach(function (i) this.addItem(i), this); + return newItems; + }, + + addItem: function MT_addItem(item) { + // First, remove the item from its current parent. + let privates = privateItem(item); + if (item.parentMenu) + item.parentMenu.removeItem(item); + else if (!(item instanceof Separator) && privates._hasFinishedInit) + browserManager.removeTopLevelItem(item); + + // Now add the item to this menu. + this._items.push(item); + privates._parentMenu = this._public; + browserManager.addItemToMenu(item, this._public); + }, + + removeItem: function MT_removeItem(item) { + let idx = this._items.indexOf(item); + if (idx < 0) + return; + this._items.splice(idx, 1); + privateItem(item)._parentMenu = null; + browserManager.removeItemFromMenu(item, this._public); + }, + + toString: function MT_toString() { + return '[object Menu "' + this.label + '"]'; + } +})); + +// The exported Menu constructor. +function Menu(options) { + let optRules = optionsRules(); + optRules.items = { + is: ["array"], + ok: function (v) { + return v.every(function (item) { + return (item instanceof Item) || + (item instanceof Menu) || + (item instanceof Separator); + }); + }, + msg: "items must be an array, and each element in the array must be an " + + "Item, Menu, or Separator." + }; + + let menu = MenuTrait.create(Menu.prototype); + + // We can't rely on _initBase to set the `items` property, because the menu + // needs to be registered with and added to the browserManager before any + // child items are added to it. + menu._initMenu(options, optRules, ["items"]); + + menu._public = Cortex(menu); + browserManager.registerItem(menu._public); + menu.items = options.items; + menu._finishActiveItemInit(); + + return menu._public; +} + +// The exported Separator constructor. +function Separator() { + let sep = ItemBaseTrait.create(Separator.prototype); + sep._initBase({}, {}, []); + + sep._public = Cortex(sep); + browserManager.registerItem(sep._public); + sep._hasFinishedInit = true; + return sep._public; +} + + +function Context() {} + +function PageContext() { + this.isCurrent = function PageContext_isCurrent(popupNode) { + let win = popupNode.ownerDocument.defaultView; + if (win && !win.getSelection().isCollapsed) + return false; + + let cursor = popupNode; + while (cursor && !(cursor instanceof Ci.nsIDOMHTMLHtmlElement)) { + if (NON_PAGE_CONTEXT_ELTS.some(function (iface) cursor instanceof iface)) + return false; + cursor = cursor.parentNode; + } + return true; + }; +} + +PageContext.prototype = new Context(); + +function SelectorContext(selector) { + let opts = apiUtils.validateOptions({ selector: selector }, { + selector: { + is: ["string"], + msg: "selector must be a string." + } + }); + + this.adjustPopupNode = function SelectorContext_adjustPopupNode(node) { + return closestMatchingAncestor(node); + }; + + this.isCurrent = function SelectorContext_isCurrent(popupNode) { + return !!closestMatchingAncestor(popupNode); + }; + + // Returns node if it matches selector, or the closest ancestor of node that + // matches, or null if node and none of its ancestors matches. + function closestMatchingAncestor(node) { + let cursor = node; + while (cursor) { + if (cursor.mozMatchesSelector(selector)) + return cursor; + if (cursor instanceof Ci.nsIDOMHTMLHtmlElement) + break; + cursor = cursor.parentNode; + } + return null; + } +} + +SelectorContext.prototype = new Context(); + +function SelectionContext() { + this.isCurrent = function SelectionContext_isCurrent(popupNode) { + let win = popupNode.ownerDocument.defaultView; + if (!win) + return false; + + let hasSelection = !win.getSelection().isCollapsed; + if (!hasSelection) { + // window.getSelection doesn't return a selection for text selected in a + // form field (see bug 85686), so before returning false we want to check + // if the popupNode is a text field. + let { selectionStart, selectionEnd } = popupNode; + hasSelection = !isNaN(selectionStart) && + !isNaN(selectionEnd) && + selectionStart !== selectionEnd; + } + return hasSelection; + }; +} + +SelectionContext.prototype = new Context(); + +function URLContext(patterns) { + let opts = apiUtils.validateOptions({ patterns: patterns }, { + patterns: { + map: function (v) apiUtils.getTypeOf(v) === "array" ? v : [v], + ok: function (v) v.every(function (p) typeof(p) === "string"), + msg: "patterns must be a string or an array of strings." + } + }); + try { + patterns = opts.patterns.map(function (p) new MatchPattern(p)); + } + catch (err) { + console.error("Error creating URLContext match pattern:"); + throw err; + } + + const self = this; + + this.isCurrent = function URLContext_isCurrent(popupNode) { + return self.isCurrentForURL(popupNode.ownerDocument.URL); + }; + + this.isCurrentForURL = function URLContext_isCurrentForURL(url) { + return patterns.some(function (p) p.test(url)); + }; +} + +URLContext.prototype = new Context(); + +exports.PageContext = apiUtils.publicConstructor(PageContext); +exports.SelectorContext = apiUtils.publicConstructor(SelectorContext); +exports.SelectionContext = apiUtils.publicConstructor(SelectionContext); +exports.URLContext = apiUtils.publicConstructor(URLContext); + + +// Returns a version of opt validated against the given rule. +function validateOpt(opt, rule) { + return apiUtils.validateOptions({ opt: opt }, { opt: rule }).opt; +} + +// Returns rules for apiUtils.validateOptions() common to Item and Menu. +function optionsRules() { + return { + context: { + is: ["undefined", "object", "array"], + ok: function (v) { + if (!v) + return true; + let arr = apiUtils.getTypeOf(v) === "array" ? v : [v]; + return arr.every(function (o) o instanceof Context); + }, + msg: "The 'context' option must be a Context object or an array of " + + "Context objects." + }, + label: { + map: function (v) v.toString(), + is: ["string"], + ok: function (v) !!v, + msg: "The item must have a non-empty string label." + }, + image: { + map: function (v) v.toString(), + is: ["string", "undefined", "null"] + }, + contentScript: { + is: ["string", "array", "undefined"], + ok: function (v) { + return apiUtils.getTypeOf(v) !== "array" || + v.every(function (s) typeof(s) === "string"); + } + }, + contentScriptFile: { + is: ["string", "array", "undefined"], + ok: function (v) { + if (!v) + return true; + let arr = apiUtils.getTypeOf(v) === "array" ? v : [v]; + try { + return arr.every(function (s) { + return apiUtils.getTypeOf(s) === "string" && + URL(s).scheme === 'resource'; + }); + } + catch (err) {} + return false; + }, + msg: "The 'contentScriptFile' option must be a local file URL or " + + "an array of local file URLs." + }, + onMessage: { + is: ["function", "undefined"] + } + }; +} + +// Does a binary search on elts, a NodeList, and returns the DOM element +// before which an item with targetLabel should be inserted. null is returned +// if the new item should be inserted at the end. +function insertionPoint(targetLabel, elts) { + let from = 0; + let to = elts.length - 1; + + while (from <= to) { + let i = Math.floor((from + to) / 2); + let comp = targetLabel.localeCompare(elts[i].getAttribute("label")); + if (comp < 0) + to = i - 1; + else if (comp > 0) + from = i + 1; + else + return elts[i]; + } + return elts[from] || null; +} + +// Builds an ID suitable for a DOM element from the given item ID. +// isInOverflowSubtree should be true if the returned element will be inserted +// into the DOM subtree rooted at the overflow menu. +function domEltIDFromItemID(itemID, isInOverflowSubtree) { + let suffix = isInOverflowSubtree ? "-overflow" : ""; + return jpSelf.id + "-context-menu-item-" + itemID + suffix; +} + +// Parses the item ID out of the given DOM element ID and returns it. If the +// element's ID is malformed or it indicates that the element was not created by +// the instance of the module calling this function, returns -1. +function itemIDFromDOMEltID(domEltID) { + let match = /^(.+?)-context-menu-item-([0-9]+)[-a-z]*$/.exec(domEltID); + return !match || match[1] !== jpSelf.id ? -1 : match[2]; +} + +// Returns the private version of the given public reflection. +function privateItem(publicItem) { + return publicItem.valueOf(PRIVATE_PROPS_KEY); +} + + +// A type of Worker tailored to our uses. +const ContextMenuWorker = Worker.compose({ + destroy: Worker.required, + + // Returns true if any context listeners are defined in the worker's port. + anyContextListeners: function CMW_anyContextListeners() { + return this._contentWorker.hasListenerFor("context"); + }, + + // Returns the first string or truthy value returned by a context listener in + // the worker's port. If none return a string or truthy value or if there are + // no context listeners, returns false. popupNode is the node that was + // context-clicked. + isAnyContextCurrent: function CMW_isAnyContextCurrent(popupNode) { + let results = this._contentWorker.emitSync("context", popupNode); + for (let i = 0; i < results.length; i++) { + let val = results[i]; + if (typeof val === "string" || val) + return val; + } + return false; + }, + + // Emits a click event in the worker's port. popupNode is the node that was + // context-clicked, and clickedItemData is the data of the item that was + // clicked. + fireClick: function CMW_fireClick(popupNode, clickedItemData) { + this._contentWorker.emitSync("click", popupNode, clickedItemData); + } +}); + + +// This class creates and stores content workers for pairs of menu items and +// content windows. Use one instance for each item. Not all pairs need a +// worker: if an item has a URL context that does not match a window's page, +// then no worker is created for the pair. +function WorkerRegistry(item) { + this.item = item; + + // inner window ID => { win, worker } + this.winWorkers = {}; + + // inner window ID => content window + this.winsWithoutWorkers = {}; +} + +WorkerRegistry.prototype = { + + // Registers a content window, creating a worker for it if it needs one. + registerContentWin: function WR_registerContentWin(win) { + let innerWinID = getInnerId(win); + if ((innerWinID in this.winWorkers) || + (innerWinID in this.winsWithoutWorkers)) + return; + if (this._doesURLNeedWorker(win.document.URL)) + this.winWorkers[innerWinID] = { win: win, worker: this._makeWorker(win) }; + else + this.winsWithoutWorkers[innerWinID] = win; + }, + + // Unregisters a content window, destroying its related worker if it has one. + unregisterContentWin: function WR_unregisterContentWin(innerWinID) { + if (innerWinID in this.winWorkers) { + let winWorker = this.winWorkers[innerWinID]; + winWorker.worker.destroy(); + delete winWorker.worker; + delete winWorker.win; + delete this.winWorkers[innerWinID]; + } + else + delete this.winsWithoutWorkers[innerWinID]; + }, + + // Creates a worker for each window that needs a worker but doesn't have one. + createNeededWorkers: function WR_createNeededWorkers() { + for (let [innerWinID, win] in Iterator(this.winsWithoutWorkers)) { + delete this.winsWithoutWorkers[innerWinID]; + this.registerContentWin(win); + } + }, + + // Destroys the worker for each window that has a worker but doesn't need it. + destroyUnneededWorkers: function WR_destroyUnneededWorkers() { + for (let [innerWinID, winWorker] in Iterator(this.winWorkers)) { + if (!this._doesURLNeedWorker(winWorker.win.document.URL)) { + this.unregisterContentWin(innerWinID); + this.winsWithoutWorkers[innerWinID] = winWorker.win; + } + } + }, + + // Returns the worker for the item-window pair or null if none exists. + find: function WR_find(contentWin) { + let innerWinID = getInnerId(contentWin); + return (innerWinID in this.winWorkers) ? + this.winWorkers[innerWinID].worker : + null; + }, + + // Unregisters all content windows from the registry, which destroys all + // workers. + destroy: function WR_destroy() { + for (let innerWinID in this.winWorkers) + this.unregisterContentWin(innerWinID); + for (let innerWinID in this.winsWithoutWorkers) + this.unregisterContentWin(innerWinID); + }, + + // Returns false if the item has a URL context that does not match the given + // URL. + _doesURLNeedWorker: function WR__doesURLNeedWorker(url) { + for (let ctxt in this.item.context) + if ((ctxt instanceof URLContext) && !ctxt.isCurrentForURL(url)) + return false; + return true; + }, + + _makeWorker: function WR__makeWorker(win) { + let worker = ContextMenuWorker({ + window: win, + contentScript: this.item.contentScript, + contentScriptFile: this.item.contentScriptFile, + onError: function (err) console.exception(err) + }); + let item = this.item; + worker.on("message", function workerOnMessage(msg) { + try { + privateItem(item)._emitOnObject(item, "message", msg); + } + catch (err) { + console.exception(err); + } + }); + return worker; + } +}; + + +// Mirrors state across all browser windows. Also responsible for detecting +// all content window loads and unloads. +let browserManager = { + topLevelItems: [], + browserWins: [], + + // inner window ID => content window + contentWins: {}, + + // Call this when a new item is created, top-level or not. + registerItem: function BM_registerItem(item) { + this.browserWins.forEach(function (w) w.registerItem(item)); + }, + + // Call this when an item is destroyed and won't be used again, top-level or + // not. + unregisterItem: function BM_unregisterItem(item) { + this.browserWins.forEach(function (w) w.unregisterItem(item)); + }, + + addTopLevelItem: function BM_addTopLevelItem(item) { + this.topLevelItems.push(item); + this.browserWins.forEach(function (w) w.addTopLevelItem(item)); + + // Create the item's worker registry and register all currently loaded + // content windows with it. + let privates = privateItem(item); + privates._isTopLevel = true; + for each (let win in this.contentWins) + privates._workerReg.registerContentWin(win); + }, + + removeTopLevelItem: function BM_removeTopLevelItem(item) { + let idx = this.topLevelItems.indexOf(item); + if (idx < 0) + throw new Error("Internal error: item not in top-level menu: " + item); + this.topLevelItems.splice(idx, 1); + this.browserWins.forEach(function (w) w.removeTopLevelItem(item)); + privateItem(item)._isTopLevel = false; + }, + + addItemToMenu: function BM_addItemToMenu(item, parentMenu) { + this.browserWins.forEach(function (w) w.addItemToMenu(item, parentMenu)); + }, + + removeItemFromMenu: function BM_removeItemFromMenu(item, parentMenu) { + this.browserWins.forEach(function (w) w.removeItemFromMenu(item, + parentMenu)); + }, + + setItemLabel: function BM_setItemLabel(item, label) { + this.browserWins.forEach(function (w) w.setItemLabel(item, label)); + }, + + setItemImage: function BM_setItemImage(item, imageURL) { + this.browserWins.forEach(function (w) w.setItemImage(item, imageURL)); + }, + + setItemData: function BM_setItemData(item, data) { + this.browserWins.forEach(function (w) w.setItemData(item, data)); + }, + + // Note that calling this method will cause onTrack to be called immediately + // for each currently open browser window. + init: function BM_init() { + require("api-utils/unload").ensure(this); + let windowTracker = winUtils.WindowTracker(this); + + // Register content windows on content-document-global-created and + // unregister them on inner-window-destroyed. For rationale, see bug 667957 + // for the former and bug 642004 for the latter. + observerServ.add("content-document-global-created", + this._onDocGlobalCreated, this); + observerServ.add("inner-window-destroyed", + this._onInnerWinDestroyed, this); + }, + + _onDocGlobalCreated: function BM__onDocGlobalCreated(contentWin) { + let doc = contentWin.document; + if (doc.readyState == "loading") { + const self = this; + doc.addEventListener("readystatechange", function onReadyStateChange(e) { + if (e.target != doc || doc.readyState != "complete") + return; + doc.removeEventListener("readystatechange", onReadyStateChange, false); + self._registerContentWin(contentWin); + }, false); + } + else if (doc.readyState == "complete") + this._registerContentWin(contentWin); + }, + + _onInnerWinDestroyed: function BM__onInnerWinDestroyed(subj) { + this._unregisterContentWin( + subj.QueryInterface(Ci.nsISupportsPRUint64).data); + }, + + // Stores the given content window with the manager and registers it with each + // top-level item's worker registry. + _registerContentWin: function BM__registerContentWin(win) { + let innerID = getInnerId(win); + + // It's an error to call this method for the same window more than once, but + // we allow it in one case: when onTrack races _onDocGlobalCreated. (See + // the comment in onTrack.) Make sure the window is registered only once. + if (innerID in this.contentWins) + return; + + this.contentWins[innerID] = win; + this.topLevelItems.forEach(function (item) { + privateItem(item)._workerReg.registerContentWin(win); + }); + }, + + // Removes the given content window from the manager and unregisters it from + // each top-level item's worker registry. + _unregisterContentWin: function BM__unregisterContentWin(innerID) { + delete this.contentWins[innerID]; + this.topLevelItems.forEach(function (item) { + privateItem(item)._workerReg.unregisterContentWin(innerID); + }); + }, + + unload: function BM_unload() { + // The window tracker is unloaded at the same time this method is called, + // which causes onUntrack to be called for each open browser window, so + // there's no need to clean up browser windows here. + + while (this.topLevelItems.length) { + let item = this.topLevelItems[0]; + this.removeTopLevelItem(item); + this.unregisterItem(item); + } + delete this.contentWins; + }, + + // Registers a browser window with the manager. This is a WindowTracker + // callback. Note that this is called in two cases: for each newly opened + // chrome window, and for each chrome window that is open when the loader + // loads this module. + onTrack: function BM_onTrack(window) { + if (!this._isBrowserWindow(window)) + return; + + let browserWin = new BrowserWindow(window); + this.browserWins.push(browserWin); + + // Register all loaded content windows in the browser window. Be sure to + // include frames and iframes. If onTrack is called as a result of a new + // browser window being opened, as opposed to the module being loaded, then + // this will race the content-document-global-created notification. That's + // OK, since _registerContentWin will not register the same content window + // more than once. + window.gBrowser.browsers.forEach(function (browser) { + let topContentWin = browser.contentWindow; + let allContentWins = Array.slice(topContentWin.frames); + allContentWins.push(topContentWin); + allContentWins.forEach(function (contentWin) { + if (contentWin.document.readyState == "complete") + this._registerContentWin(contentWin); + }, this); + }, this); + + // Add all top-level items and, recursively, their child items to the new + // browser window. + function addItemTree(item, parentMenu) { + browserWin.registerItem(item); + if (parentMenu) + browserWin.addItemToMenu(item, parentMenu); + else + browserWin.addTopLevelItem(item); + if (item instanceof Menu) + item.items.forEach(function (subitem) addItemTree(subitem, item)); + } + this.topLevelItems.forEach(function (item) addItemTree(item, null)); + }, + + // Unregisters a browser window from the manager. This is a WindowTracker + // callback. Note that this is called in two cases: for each newly closed + // chrome window, and for each chrome window that is open when this module is + // unloaded. + onUntrack: function BM_onUntrack(window) { + if (!this._isBrowserWindow(window)) + return; + + // Remove the window from the window list. + let idx = 0; + for (; idx < this.browserWins.length; idx++) + if (this.browserWins[idx].window == window) + break; + if (idx == this.browserWins.length) + throw new Error("Internal error: browser window not found"); + let browserWin = this.browserWins.splice(idx, 1)[0]; + + // Remove all top-level items from the window. + this.topLevelItems.forEach(function (i) browserWin.removeTopLevelItem(i)); + browserWin.destroy(); + }, + + _isBrowserWindow: function BM__isBrowserWindow(win) { + let winType = win.document.documentElement.getAttribute("windowtype"); + return winType === "navigator:browser"; + } +}; + + +// Responsible for creating and managing context menu item DOM elements for a +// browser window. Also responsible for providing a description of the window's +// current context and determining whether an item matches the current context. +// +// TODO: If other apps besides Firefox want to support the context menu in +// whatever way is appropriate for them, plugging in a substitute for or an +// adapter to this class should be the way to do it. Make it easy for them. +// See bug 560716. +function BrowserWindow(window) { + this.window = window; + this.doc = window.document; + + let popupDOMElt = this.doc.getElementById("contentAreaContextMenu"); + if (!popupDOMElt) + throw new Error("Internal error: Context menu popup not found."); + this.contextMenuPopup = new ContextMenuPopup(popupDOMElt, this); + + // item ID => { item, domElt, overflowDOMElt, popup, overflowPopup } + // item may or may not be top-level. domElt is the item's DOM element + // contained in the subtree rooted in the top-level context menu. + // overflowDOMElt is the item's DOM element contained in the subtree rooted in + // the overflow submenu. popup and overflowPopup are only defined if the item + // is a Menu; they're the Popup instances containing the Menu's child items, + // with the aforementioned distinction between top-level and overflow + // subtrees. + this.items = {}; +} + +BrowserWindow.prototype = { + + // Creates and stores DOM elements for the given item, top-level or not. + registerItem: function BW_registerItem(item) { + // this.items[id] is referenced by _makeMenu, so it needs to be defined + // before _makeDOMElt is called. + let props = { item: item }; + this.items[privateItem(item)._id] = props; + props.domElt = this._makeDOMElt(item, false); + props.overflowDOMElt = this._makeDOMElt(item, true); + }, + + // Removes the given item's DOM elements from the store. + unregisterItem: function BW_unregisterItem(item) { + delete this.items[privateItem(item)._id]; + }, + + addTopLevelItem: function BW_addTopLevelItem(item) { + this.contextMenuPopup.addItem(item); + }, + + removeTopLevelItem: function BW_removeTopLevelItem(item) { + this.contextMenuPopup.removeItem(item); + }, + + addItemToMenu: function BW_addItemToMenu(item, parentMenu) { + let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; + popup.addItem(item); + overflowPopup.addItem(item); + }, + + removeItemFromMenu: function BW_removeItemFromMenu(item, parentMenu) { + let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; + popup.removeItem(item); + overflowPopup.removeItem(item); + }, + + setItemLabel: function BW_setItemLabel(item, label) { + let privates = privateItem(item); + let { domElt, overflowDOMElt } = this.items[privates._id]; + this._setDOMEltLabel(domElt, label); + this._setDOMEltLabel(overflowDOMElt, label); + if (!item.parentMenu && privates._hasFinishedInit) + this.contextMenuPopup.itemLabelDidChange(item); + }, + + _setDOMEltLabel: function BW__setDOMEltLabel(domElt, label) { + domElt.setAttribute("label", label); + }, + + setItemImage: function BW_setItemImage(item, imageURL) { + let { domElt, overflowDOMElt } = this.items[privateItem(item)._id]; + let isMenu = item instanceof Menu; + this._setDOMEltImage(domElt, imageURL, isMenu); + this._setDOMEltImage(overflowDOMElt, imageURL, isMenu); + }, + + _setDOMEltImage: function BW__setDOMEltImage(domElt, imageURL, isMenu) { + if (!imageURL) { + domElt.removeAttribute("image"); + domElt.classList.remove("menu-iconic"); + domElt.classList.remove("menuitem-iconic"); + } + else { + domElt.setAttribute("image", imageURL); + domElt.classList.add(isMenu ? "menu-iconic" : "menuitem-iconic"); + } + }, + + setItemData: function BW_setItemData(item, data) { + let { domElt, overflowDOMElt } = this.items[privateItem(item)._id]; + this._setDOMEltData(domElt, data); + this._setDOMEltData(overflowDOMElt, data); + }, + + _setDOMEltData: function BW__setDOMEltData(domElt, data) { + domElt.setAttribute("value", data); + }, + + // The context specified for a top-level item may not match exactly the real + // context that triggers it. For example, if the user context-clicks a span + // inside an anchor, we want items that specify an anchor context to be + // triggered, but the real context will indicate that the span was clicked, + // not the anchor. Where the real context and an item's context conflict, + // clients should be given the item's context, and this method can be used to + // make such adjustments. Returns an adjusted popupNode. + adjustPopupNode: function BW_adjustPopupNode(popupNode, topLevelItem) { + for (let ctxt in topLevelItem.context) { + if (typeof(ctxt.adjustPopupNode) === "function") { + let ctxtNode = ctxt.adjustPopupNode(popupNode); + if (ctxtNode) { + popupNode = ctxtNode; + break; + } + } + } + return popupNode; + }, + + // Returns true if all of item's contexts are current in the window. + areAllContextsCurrent: function BW_areAllContextsCurrent(item, popupNode) { + let win = popupNode.ownerDocument.defaultView; + let worker = privateItem(item)._workerReg.find(win); + + // If the worker for the item-window pair doesn't exist (e.g., because the + // page hasn't loaded yet), we can't really make a good decision since the + // content script may have a context listener. So just don't show the item + // at all. + if (!worker) + return false; + + // If there are no contexts given at all, the page context applies. + let hasContentContext = worker.anyContextListeners(); + if (!hasContentContext && !item.context.length) + return new PageContext().isCurrent(popupNode); + + // Otherwise, determine if all given contexts are current. Evaluate the + // declarative contexts first and the worker's context listeners last. That + // way the listener might be able to avoid some work. + let curr = true; + for (let ctxt in item.context) { + curr = curr && ctxt.isCurrent(popupNode); + if (!curr) + return false; + } + return !hasContentContext || worker.isAnyContextCurrent(popupNode); + }, + + // Returns the node the user context-clicked to invoke the context menu. + // For Gecko 2.0 and later, triggerNode is this node; + // if it's falsey, document.popupNode is used. + capturePopupNode: function BW_capturePopupNode(triggerNode) { + var popupNode = triggerNode || this.doc.popupNode; + if (!popupNode) + console.warn("popupNode is null."); + return popupNode; + }, + + destroy: function BW_destroy() { + this.contextMenuPopup.destroy(); + delete this.window; + delete this.doc; + delete this.items; + }, + + // Emits a click event in the port of the content worker related to given top- + // level item and popupNode's content window. Listeners will be passed + // popupNode and clickedItemData. + fireClick: function BW_fireClick(topLevelItem, popupNode, clickedItemData) { + let win = popupNode.ownerDocument.defaultView; + let worker = privateItem(topLevelItem)._workerReg.find(win); + if (worker) + worker.fireClick(popupNode, clickedItemData); + }, + + _makeDOMElt: function BW__makeDOMElt(item, isInOverflowSubtree) { + let elt = item instanceof Item ? this._makeMenuitem(item) : + item instanceof Menu ? this._makeMenu(item, isInOverflowSubtree) : + item instanceof Separator ? this._makeSeparator() : + null; + if (!elt) + throw new Error("Internal error: can't make element, unknown item type"); + + elt.id = domEltIDFromItemID(privateItem(item)._id, isInOverflowSubtree); + elt.classList.add(ITEM_CLASS); + return elt; + }, + + // Returns a new xul:menu representing the menu. + _makeMenu: function BW__makeMenu(menu, isInOverflowSubtree) { + let menuElt = this.doc.createElement("menu"); + this._setDOMEltLabel(menuElt, menu.label); + if (menu.image) + this._setDOMEltImage(menuElt, menu.image, true); + let popupElt = this.doc.createElement("menupopup"); + menuElt.appendChild(popupElt); + + let popup = new Popup(popupElt, this, isInOverflowSubtree); + let props = this.items[privateItem(menu)._id]; + if (isInOverflowSubtree) + props.overflowPopup = popup; + else + props.popup = popup; + + return menuElt; + }, + + // Returns a new xul:menuitem representing the item. + _makeMenuitem: function BW__makeMenuitem(item) { + let elt = this.doc.createElement("menuitem"); + this._setDOMEltLabel(elt, item.label); + if (item.image) + this._setDOMEltImage(elt, item.image, false); + if (item.data) + this._setDOMEltData(elt, item.data); + return elt; + }, + + // Returns a new xul:menuseparator. + _makeSeparator: function BW__makeSeparator() { + return this.doc.createElement("menuseparator"); + } +}; + + +// Responsible for adding DOM elements to and removing them from poupDOMElt. +function Popup(popupDOMElt, browserWin, isInOverflowSubtree) { + this.popupDOMElt = popupDOMElt; + this.browserWin = browserWin; + this.isInOverflowSubtree = isInOverflowSubtree; +} + +Popup.prototype = { + + addItem: function Popup_addItem(item) { + let props = this.browserWin.items[privateItem(item)._id]; + let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; + this.popupDOMElt.appendChild(elt); + }, + + removeItem: function Popup_removeItem(item) { + let props = this.browserWin.items[privateItem(item)._id]; + let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; + this.popupDOMElt.removeChild(elt); + } +}; + + +// Represents a browser window's context menu popup. Responsible for hiding and +// showing items according to the browser window's current context and for +// handling item clicks. +function ContextMenuPopup(popupDOMElt, browserWin) { + this.popupDOMElt = popupDOMElt; + this.browserWin = browserWin; + this.doc = popupDOMElt.ownerDocument; + + // item ID => item + // Calling this variable "topLevelItems" is redundant, since Popup and + // ContextMenuPopup are only responsible for their child items, not all their + // descendant items. But calling it "items" might encourage one to believe + // otherwise, so topLevelItems it is. + this.topLevelItems = {}; + + popupDOMElt.addEventListener("popupshowing", this, false); + popupDOMElt.addEventListener("command", this, false); +} + +ContextMenuPopup.prototype = { + + addItem: function CMP_addItem(item) { + this._ensureStaticEltsExist(); + let itemID = privateItem(item)._id; + this.topLevelItems[itemID] = item; + let props = this.browserWin.items[itemID]; + props.domElt.classList.add(TOPLEVEL_ITEM_CLASS); + props.overflowDOMElt.classList.add(OVERFLOW_ITEM_CLASS); + this._insertItemInSortedOrder(item); + }, + + removeItem: function CMP_removeItem(item) { + let itemID = privateItem(item)._id; + delete this.topLevelItems[itemID]; + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + domElt.classList.remove(TOPLEVEL_ITEM_CLASS); + overflowDOMElt.classList.remove(OVERFLOW_ITEM_CLASS); + this.popupDOMElt.removeChild(domElt); + this._overflowPopup.removeChild(overflowDOMElt); + }, + + // Call this after the item's label changes. This re-inserts the item into + // the popup so that it remains in sorted order. + itemLabelDidChange: function CMP_itemLabelDidChange(item) { + let itemID = privateItem(item)._id; + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + this.popupDOMElt.removeChild(domElt); + this._overflowPopup.removeChild(overflowDOMElt); + this._insertItemInSortedOrder(item); + }, + + destroy: function CMP_destroy() { + // If there are no more items from any instance of the module, remove the + // separator and overflow submenu, if they exist. + let elts = this._topLevelElts; + if (!elts.length) { + let submenu = this._overflowMenu; + if (submenu) + this.popupDOMElt.removeChild(submenu); + + let sep = this._separator; + if (sep) + this.popupDOMElt.removeChild(sep); + } + + this.popupDOMElt.removeEventListener("popupshowing", this, false); + this.popupDOMElt.removeEventListener("command", this, false); + }, + + handleEvent: function CMP_handleEvent(event) { + try { + if (event.type === "command") + this._handleClick(event.target); + else if (event.type === "popupshowing" && + event.target === this.popupDOMElt) + this._handlePopupShowing(); + } + catch (err) { + console.exception(err); + } + }, + + // command events bubble to the context menu's top-level xul:menupopup and are + // caught here. + _handleClick: function CMP__handleClick(clickedDOMElt) { + if (!clickedDOMElt.classList.contains(ITEM_CLASS)) + return; + let itemID = itemIDFromDOMEltID(clickedDOMElt.id); + if (itemID < 0) + return; + let { item, domElt, overflowDOMElt } = this.browserWin.items[itemID]; + + // Bail if the DOM element was not created by this module instance. In + // real-world add-ons, the itemID < 0 check above is sufficient, but for the + // unit test the JID never changes, making this necessary. + if (clickedDOMElt != domElt && clickedDOMElt != overflowDOMElt) + return; + + let topLevelItem = privateItem(item)._topLevelItem; + let popupNode = this.browserWin.adjustPopupNode(this._getPopupNode(), + topLevelItem); + this.browserWin.fireClick(topLevelItem, popupNode, item.data); + }, + + _getPopupNode: function CMP__getPopupNode() { + // popupDOMElt.triggerNode was added in Gecko 2.0 by bug 383930. The || is + // to avoid a Spidermonkey strict warning on earlier versions. + let triggerNode = this.popupDOMElt.triggerNode || undefined; + return this.browserWin.capturePopupNode(triggerNode); + }, + + // popupshowing is used to show top-level items that match the browser + // window's current context and hide items that don't. Each module instance + // is responsible for showing and hiding the items it owns. + _handlePopupShowing: function CMP__handlePopupShowing() { + + // If there are items queued up to finish initializing, let them go first. + // Otherwise the overflow submenu and menu separator may end up in an + // inappropriate state when those items are later added to the menu. + if (numItemsWithUnfinishedInit) { + const self = this; + timer.setTimeout(function popupShowingTryAgain() { + self._handlePopupShowing(); + }, 0); + return; + } + + let popupNode = this._getPopupNode(); + // Show and hide items. Set a "jetpackContextCurrent" property on the + // DOM elements to signal which of our items match the current context. + for (let [itemID, item] in Iterator(this.topLevelItems)) { + let areContextsCurr = + this.browserWin.areAllContextsCurrent(item, popupNode); + + // Change the item's label if the return value was a string. + if (typeof(areContextsCurr) === "string") { + item.label = areContextsCurr; + areContextsCurr = true; + } + + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + domElt.jetpackContextCurrent = areContextsCurr; + domElt.hidden = !areContextsCurr; + overflowDOMElt.jetpackContextCurrent = areContextsCurr; + overflowDOMElt.hidden = !areContextsCurr; + } + + // Get the total number of items that match the current context. It's a + // little tricky: There may be other instances of this module loaded, + // each hiding and showing their items. So we can't base this number on + // only our items, or on the hidden state of items. That's why we set + // the jetpackContextCurrent property above. The last instance to run + // will leave the menupopup in the correct state. + let elts = this._topLevelElts; + let numShown = Array.reduce(elts, function (total, elt) { + return total + (elt.jetpackContextCurrent ? 1 : 0); + }, 0); + + // If too many items are shown, show the submenu and hide the top-level + // items. Otherwise, hide the submenu and show the top-level items. + let overflow = numShown > this._overflowThreshold; + if (overflow) + Array.forEach(elts, function (e) e.hidden = true); + + let submenu = this._overflowMenu; + if (submenu) + submenu.hidden = !overflow; + + // If no items are shown, hide the menu separator. + let sep = this._separator; + if (sep) + sep.hidden = numShown === 0; + }, + + // Adds the menu separator and overflow submenu if they don't exist. + _ensureStaticEltsExist: function CMP__ensureStaticEltsExist() { + let sep = this._separator; + if (!sep) { + sep = this._makeSeparator(); + this.popupDOMElt.appendChild(sep); + } + + let submenu = this._overflowMenu; + if (!submenu) { + submenu = this._makeOverflowMenu(); + submenu.hidden = true; + this.popupDOMElt.insertBefore(submenu, sep.nextSibling); + } + }, + + // Inserts the given item's DOM element into the popup in sorted order. + _insertItemInSortedOrder: function CMP__insertItemInSortedOrder(item) { + let props = this.browserWin.items[privateItem(item)._id]; + this.popupDOMElt.insertBefore( + props.domElt, insertionPoint(item.label, this._topLevelElts)); + this._overflowPopup.insertBefore( + props.overflowDOMElt, insertionPoint(item.label, this._overflowElts)); + }, + + // Creates and returns the xul:menu that's shown when too many items are added + // to the popup. + _makeOverflowMenu: function CMP__makeOverflowMenu() { + let submenu = this.doc.createElement("menu"); + submenu.id = OVERFLOW_MENU_ID; + submenu.setAttribute("label", OVERFLOW_MENU_LABEL); + let popup = this.doc.createElement("menupopup"); + popup.id = OVERFLOW_POPUP_ID; + submenu.appendChild(popup); + return submenu; + }, + + // Creates and returns the xul:menuseparator that separates the standard + // context menu items from our items. + _makeSeparator: function CMP__makeSeparator() { + let elt = this.doc.createElement("menuseparator"); + elt.id = SEPARATOR_ID; + return elt; + }, + + // Returns the item elements contained in the overflow menu, a NodeList. + get _overflowElts() { + return this._overflowPopup.getElementsByClassName(OVERFLOW_ITEM_CLASS); + }, + + // Returns the overflow xul:menu. + get _overflowMenu() { + return this.doc.getElementById(OVERFLOW_MENU_ID); + }, + + // Returns the overflow xul:menupopup. + get _overflowPopup() { + return this.doc.getElementById(OVERFLOW_POPUP_ID); + }, + + // Returns the OVERFLOW_THRESH_PREF pref value if it exists or + // OVERFLOW_THRESH_DEFAULT if it doesn't. + get _overflowThreshold() { + let prefs = require("api-utils/preferences-service"); + return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); + }, + + // Returns the xul:menuseparator. + get _separator() { + return this.doc.getElementById(SEPARATOR_ID); + }, + + // Returns the item elements contained in the top-level menu, a NodeList. + get _topLevelElts() { + return this.popupDOMElt.getElementsByClassName(TOPLEVEL_ITEM_CLASS); + } +}; + + +// Init the browserManager only after setting prototypes and such above, because +// it will cause browserManager.onTrack to be called immediately if there are +// open windows. +browserManager.init(); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/hotkeys.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/hotkeys.js new file mode 100644 index 0000000..f864f1f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/hotkeys.js @@ -0,0 +1,37 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 INVALID_HOTKEY = "Hotkey must have at least one modifier."; + +const { toJSON: jsonify, toString: stringify, + isFunctionKey } = require("api-utils/keyboard/utils"); +const { register, unregister } = require("api-utils/keyboard/hotkeys"); + +const Hotkey = exports.Hotkey = function Hotkey(options) { + if (!(this instanceof Hotkey)) + return new Hotkey(options); + + // Parsing key combination string. + let hotkey = jsonify(options.combo); + if (!isFunctionKey(hotkey.key) && !hotkey.modifiers.length) { + throw new TypeError(INVALID_HOTKEY); + } + + this.onPress = options.onPress; + this.toString = stringify.bind(null, hotkey); + // Registering listener on keyboard combination enclosed by this hotkey. + // Please note that `this.toString()` is a normalized version of + // `options.combination` where order of modifiers is sorted and `accel` is + // replaced with platform specific key. + register(this.toString(), this.onPress); + // We freeze instance before returning it in order to make it's properties + // read-only. + return Object.freeze(this); +}; +Hotkey.prototype.destroy = function destroy() { + unregister(this.toString(), this.onPress); +}; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/l10n.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/l10n.js new file mode 100644 index 0000000..198f711 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/l10n.js @@ -0,0 +1,149 @@ +/* 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 { getPreferedLocales, findClosestLocale } = require("api-utils/l10n/locale"); +const { getRulesForLocale } = require("api-utils/l10n/plural-rules"); + +// Get URI for the addon root folder: +const { rootURI } = require("@packaging"); + +let globalHash = {}; +let pluralMappingFunction = getRulesForLocale("en"); + +exports.get = function get(k) { + + // For now, we only accept a "string" as first argument + // TODO: handle plural forms in gettext pattern + if (typeof k !== "string") + throw new Error("First argument of localization method should be a string"); + + // Get translation from big hashmap or default to hard coded string: + let localized = globalHash[k] || k; + + // # Simplest usecase: + // // String hard coded in source code: + // _("Hello world") + // // Identifier of a key stored in properties file + // _("helloString") + if (arguments.length <= 1) + return localized; + + let args = arguments; + + if (typeof localized == "object" && "other" in localized) { + // # Plural form: + // // Strings hard coded in source code: + // _(["One download", "%d downloads"], 10); + // // Identifier of a key stored in properties file + // _("downloadNumber", 0); + let n = arguments[1]; + + // First handle simple universal forms that may not be mandatory + // for each language, (i.e. not different than 'other' form, + // but still usefull for better phrasing) + // For example 0 in english is the same form than 'other' + // but we accept 'zero' form if specified in localization file + if (n === 0 && "zero" in localized) + localized = localized["zero"]; + else if (n === 1 && "one" in localized) + localized = localized["one"]; + else if (n === 2 && "two" in localized) + localized = localized["two"]; + else { + let pluralForm = pluralMappingFunction(n); + if (pluralForm in localized) + localized = localized[pluralForm]; + else // Fallback in case of error: missing plural form + localized = localized["other"]; + } + + // Simulate a string with one placeholder: + args = [null, n]; + } + + // # String with placeholders: + // // Strings hard coded in source code: + // _("Hello %s", username) + // // Identifier of a key stored in properties file + // _("helloString", username) + // * We supports `%1s`, `%2s`, ... pattern in order to change arguments order + // in translation. + // * In case of plural form, we has `%d` instead of `%s`. + let offset = 1; + localized = localized.replace(/%(\d*)(s|d)/g, function (v, n) { + let rv = args[n != "" ? n : offset]; + offset++; + return rv; + }); + + return localized; +} + +function readURI(uri) { + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + request.open('GET', uri, false); + request.overrideMimeType('text/plain'); + request.send(); + return request.responseText; +} + +function readJsonUri(uri) { + try { + return JSON.parse(readURI(uri)); + } + catch(e) { + console.error("Error while reading locale file:\n" + uri + "\n" + e); + } + return {}; +} + +// Returns the array stored in `locales.json` manifest that list available +// locales files +function getAvailableLocales() { + let uri = rootURI + "locales.json"; + let manifest = readJsonUri(uri); + + return "locales" in manifest && Array.isArray(manifest.locales) ? + manifest.locales : []; +} + +// Returns URI of the best locales file to use from the XPI +function getBestLocaleFile() { + + // Read localization manifest file that contains list of available languages + let availableLocales = getAvailableLocales(); + + // Retrieve list of prefered locales to use + let preferedLocales = getPreferedLocales(); + + // Compute the most preferable locale to use by using these two lists + let bestMatchingLocale = findClosestLocale(availableLocales, preferedLocales); + + // It may be null if the addon doesn't have any locale file + if (!bestMatchingLocale) + return null; + + // Retrieve the related plural mapping function + let shortLocaleCode = bestMatchingLocale.split("-")[0].toLowerCase(); + pluralMappingFunction = getRulesForLocale(shortLocaleCode); + + return rootURI + "locale/" + bestMatchingLocale + ".json"; +} + +function init() { + // First, search for a locale file: + let localeURI = getBestLocaleFile(); + if (!localeURI) + return; + + // Locale files only contains one big JSON object that is used as + // an hashtable of: "key to translate" => "translated key" + // TODO: We are likely to change this in order to be able to overload + // a specific key translation. For a specific package, module or line? + globalHash = readJsonUri(localeURI); +} +init(); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/notifications.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/notifications.js new file mode 100644 index 0000000..8e66355 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/notifications.js @@ -0,0 +1,79 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * 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, Cr } = require("chrome"); +const apiUtils = require("api-utils/api-utils"); +const errors = require("api-utils/errors"); + +try { + let alertServ = Cc["@mozilla.org/alerts-service;1"]. + getService(Ci.nsIAlertsService); + + // The unit test sets this to a mock notification function. + var notify = alertServ.showAlertNotification.bind(alertServ); +} +catch (err) { + // An exception will be thrown if the platform doesn't provide an alert + // service, e.g., if Growl is not installed on OS X. In that case, use a + // mock notification function that just logs to the console. + notify = notifyUsingConsole; +} + +exports.notify = function notifications_notify(options) { + let valOpts = validateOptions(options); + let clickObserver = !valOpts.onClick ? null : { + observe: function notificationClickObserved(subject, topic, data) { + if (topic === "alertclickcallback") + errors.catchAndLog(valOpts.onClick).call(exports, valOpts.data); + } + }; + function notifyWithOpts(notifyFn) { + notifyFn(valOpts.iconURL, valOpts.title, valOpts.text, !!clickObserver, + valOpts.data, clickObserver); + } + try { + notifyWithOpts(notify); + } + catch (err if err instanceof Ci.nsIException && + err.result == Cr.NS_ERROR_FILE_NOT_FOUND) { + console.warn("The notification icon named by " + valOpts.iconURL + + " does not exist. A default icon will be used instead."); + delete valOpts.iconURL; + notifyWithOpts(notify); + } + catch (err) { + notifyWithOpts(notifyUsingConsole); + } +}; + +function notifyUsingConsole(iconURL, title, text) { + title = title ? "[" + title + "]" : ""; + text = text || ""; + let str = [title, text].filter(function (s) s).join(" "); + console.log(str); +} + +function validateOptions(options) { + return apiUtils.validateOptions(options, { + data: { + is: ["string", "undefined"] + }, + iconURL: { + is: ["string", "undefined"] + }, + onClick: { + is: ["function", "undefined"] + }, + text: { + is: ["string", "undefined"] + }, + title: { + is: ["string", "undefined"] + } + }); +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/page-mod.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/page-mod.js new file mode 100644 index 0000000..2cc75f9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/page-mod.js @@ -0,0 +1,319 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 observers = require("api-utils/observer-service"); +const { Worker, Loader } = require('api-utils/content'); +const { EventEmitter } = require('api-utils/events'); +const { List } = require('api-utils/list'); +const { Registry } = require('api-utils/utils/registry'); +const xulApp = require("api-utils/xul-app"); +const { MatchPattern } = require('api-utils/match-pattern'); +const { validateOptions : validate } = require('api-utils/api-utils'); +const { validationAttributes } = require('api-utils/content/loader'); +const { Cc, Ci } = require('chrome'); +const { merge } = require('api-utils/utils/object'); + +// Whether or not the host application dispatches a document-element-inserted +// notification when the document element is inserted into the DOM of a page. +// The notification was added in Gecko 2.0b6, it's a better time to attach +// scripts with contentScriptWhen "start" than content-document-global-created, +// since libraries like jQuery assume the presence of the document element. +const HAS_DOCUMENT_ELEMENT_INSERTED = + xulApp.versionInRange(xulApp.platformVersion, "2.0b6", "*"); +const ON_CONTENT = HAS_DOCUMENT_ELEMENT_INSERTED ? 'document-element-inserted' : + 'content-document-global-created'; + +// Workaround bug 642145: document-element-inserted is fired multiple times. +// This bug is fixed in Firefox 4.0.1, but we want to keep FF 4.0 compatibility +// Tracking bug 641457. To be removed when 4.0 has disappeared from earth. +const HAS_BUG_642145_FIXED = + xulApp.versionInRange(xulApp.platformVersion, "2.0.1", "*"); + +const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]. + getService(Ci.nsIStyleSheetService); + +const USER_SHEET = styleSheetService.USER_SHEET; + +const io = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); + +// contentStyle* / contentScript* are sharing the same validation constraints, +// so they can be mostly reused, except for the messages. +const validStyleOptions = { + contentStyle: merge(Object.create(validationAttributes.contentScript), { + msg: 'The `contentStyle` option must be a string or an array of strings.' + }), + contentStyleFile: merge(Object.create(validationAttributes.contentScriptFile), { + msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' + }) +}; + +// rules registry +const RULES = {}; + +const Rules = EventEmitter.resolve({ toString: null }).compose(List, { + add: function() Array.slice(arguments).forEach(function onAdd(rule) { + if (this._has(rule)) + return; + // registering rule to the rules registry + if (!(rule in RULES)) + RULES[rule] = new MatchPattern(rule); + this._add(rule); + this._emit('add', rule); + }.bind(this)), + remove: function() Array.slice(arguments).forEach(function onRemove(rule) { + if (!this._has(rule)) + return; + this._remove(rule); + this._emit('remove', rule); + }.bind(this)), +}); + +/** + * Returns the content of the uri given + */ +function readURI(uri) { + let channel = io.newChannel(uri, null, null); + + let stream = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + + stream.init(channel.open()); + + let data = stream.read(stream.available()); + + stream.close(); + + return data; +} + +/** + * PageMod constructor (exported below). + * @constructor + */ +const PageMod = Loader.compose(EventEmitter, { + on: EventEmitter.required, + _listeners: EventEmitter.required, + contentScript: Loader.required, + contentScriptFile: Loader.required, + contentScriptWhen: Loader.required, + include: null, + constructor: function PageMod(options) { + this._onContent = this._onContent.bind(this); + options = options || {}; + + let { contentStyle, contentStyleFile } = validate(options, validStyleOptions); + + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('onAttach' in options) + this.on('attach', options.onAttach); + if ('onError' in options) + this.on('error', options.onError); + + let include = options.include; + let rules = this.include = Rules(); + rules.on('add', this._onRuleAdd = this._onRuleAdd.bind(this)); + rules.on('remove', this._onRuleRemove = this._onRuleRemove.bind(this)); + + if (Array.isArray(include)) + rules.add.apply(null, include); + else + rules.add(include); + + let styleRules = ""; + + if (contentStyleFile) + styleRules = [].concat(contentStyleFile).map(readURI).join(""); + + if (contentStyle) + styleRules += [].concat(contentStyle).join(""); + + if (styleRules) { + this._onRuleUpdate = this._onRuleUpdate.bind(this); + + this._styleRules = styleRules; + + this._registerStyleSheet(); + rules.on('add', this._onRuleUpdate); + rules.on('remove', this._onRuleUpdate); + } + + this.on('error', this._onUncaughtError = this._onUncaughtError.bind(this)); + pageModManager.add(this._public); + + this._loadingWindows = []; + }, + + destroy: function destroy() { + + this._unregisterStyleSheet(); + + this.include.removeListener('add', this._onRuleUpdate); + this.include.removeListener('remove', this._onRuleUpdate); + + for each (let rule in this.include) + this.include.remove(rule); + pageModManager.remove(this._public); + this._loadingWindows = []; + + }, + + _loadingWindows: [], + + _onContent: function _onContent(window) { + // not registered yet + if (!pageModManager.has(this)) + return; + + if (!HAS_BUG_642145_FIXED) { + if (this._loadingWindows.indexOf(window) != -1) + return; + this._loadingWindows.push(window); + } + + if ('start' == this.contentScriptWhen) { + this._createWorker(window); + return; + } + + let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; + let self = this; + window.addEventListener(eventName, function onReady(event) { + if (event.target.defaultView != window) + return; + window.removeEventListener(eventName, onReady, true); + + self._createWorker(window); + }, true); + }, + _createWorker: function _createWorker(window) { + let worker = Worker({ + window: window, + contentScript: this.contentScript, + contentScriptFile: this.contentScriptFile, + onError: this._onUncaughtError + }); + this._emit('attach', worker); + let self = this; + worker.once('detach', function detach() { + worker.destroy(); + + if (!HAS_BUG_642145_FIXED) { + let idx = self._loadingWindows.indexOf(window); + if (idx != -1) + self._loadingWindows.splice(idx, 1); + } + }); + }, + _onRuleAdd: function _onRuleAdd(url) { + pageModManager.on(url, this._onContent); + }, + _onRuleRemove: function _onRuleRemove(url) { + pageModManager.off(url, this._onContent); + }, + _onUncaughtError: function _onUncaughtError(e) { + if (this._listeners('error').length == 1) + console.exception(e); + }, + _onRuleUpdate: function _onRuleUpdate(){ + this._registerStyleSheet(); + }, + + _registerStyleSheet : function _registerStyleSheet() { + let rules = this.include; + let styleRules = this._styleRules; + + let documentRules = []; + + this._unregisterStyleSheet(); + + for each (let rule in rules) { + let pattern = RULES[rule]; + + if (!pattern) + continue; + + if (pattern.regexp) + documentRules.push("regexp(\"" + pattern.regexp.source + "\")") + else if (pattern.exactURL) + documentRules.push("url(" + pattern.exactURL + ")") + else if (pattern.domain) + documentRules.push("domain(" + pattern.domain + ")") + else if (pattern.urlPrefix) + documentRules.push("url-prefix(" + pattern.urlPrefix + ")") + else if (pattern.anyWebPage) { + documentRules.length = 0; + break; + } + } + + let uri = "data:text/css,"; + if (documentRules.length > 0) + uri += encodeURIComponent("@-moz-document " + + documentRules.join(",") + " {" + styleRules + "}"); + else + uri += encodeURIComponent(styleRules); + + this._registeredStyleURI = io.newURI(uri, null, null); + + styleSheetService.loadAndRegisterSheet( + this._registeredStyleURI, + USER_SHEET + ); + }, + + _unregisterStyleSheet : function () { + let uri = this._registeredStyleURI; + + if (uri && styleSheetService.sheetRegistered(uri, USER_SHEET)) + styleSheetService.unregisterSheet(uri, USER_SHEET); + + this._registeredStyleURI = null; + } +}); +exports.PageMod = function(options) PageMod(options) +exports.PageMod.prototype = PageMod.prototype; + +const PageModManager = Registry.resolve({ + constructor: '_init', + _destructor: '_registryDestructor' +}).compose({ + constructor: function PageModRegistry(constructor) { + this._init(PageMod); + observers.add( + ON_CONTENT, this._onContentWindow = this._onContentWindow.bind(this) + ); + }, + _destructor: function _destructor() { + observers.remove(ON_CONTENT, this._onContentWindow); + this._removeAllListeners(); + for (let rule in RULES) { + delete RULES[rule]; + } + this._registryDestructor(); + }, + _onContentWindow: function _onContentWindow(domObj) { + let window = HAS_DOCUMENT_ELEMENT_INSERTED ? domObj.defaultView : domObj; + // XML documents don't have windows, and we don't yet support them. + if (!window) + return; + for (let rule in RULES) + if (RULES[rule].test(window.document.URL)) + this._emit(rule, window); + }, + off: function off(topic, listener) { + this.removeListener(topic, listener); + if (!this._listeners(topic).length) + delete RULES[topic]; + } +}); +const pageModManager = PageModManager();
\ No newline at end of file diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/page-worker.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/page-worker.js new file mode 100644 index 0000000..7e7b73e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/page-worker.js @@ -0,0 +1,65 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { Symbiont } = require("api-utils/content"); +const { Trait } = require("api-utils/traits"); + +if (!require("api-utils/xul-app").isOneOf(["Firefox", "Thunderbird"])) { + throw new Error([ + "The page-worker module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")); +} + +const Page = Trait.compose( + Symbiont.resolve({ + constructor: '_initSymbiont' + }), + { + _frame: Trait.required, + _initFrame: Trait.required, + postMessage: Symbiont.required, + on: Symbiont.required, + destroy: Symbiont.required, + + constructor: function Page(options) { + options = options || {}; + + this.contentURL = 'contentURL' in options ? options.contentURL + : 'about:blank'; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('allow' in options) + this.allow = options.allow; + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + + this.on('propertyChange', this._onChange.bind(this)); + + this._initSymbiont(); + }, + + _onChange: function _onChange(e) { + if ('contentURL' in e && this._frame) { + // Cleanup the worker before injecting the content script in the new + // document + this._workerCleanup(); + this._initFrame(this._frame); + } + } + } +); +exports.Page = function(options) Page(options); +exports.Page.prototype = Page.prototype; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/panel.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/panel.js new file mode 100644 index 0000000..5593276 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/panel.js @@ -0,0 +1,381 @@ +/* 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"; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The panel module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps ", + "for more information." + ].join("")); +} + +const { Cc, Ci } = require("chrome"); + +const { validateOptions: valid } = require("api-utils/api-utils"); +const { Symbiont } = require("api-utils/content"); +const { EventEmitter } = require('api-utils/events'); +const timer = require("api-utils/timer"); +const runtime = require("api-utils/runtime"); + +const windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + ON_SHOW = 'popupshown', + ON_HIDE = 'popuphidden', + validNumber = { is: ['number', 'undefined', 'null'] }; + +/** + * Emits show and hide events. + */ +const Panel = Symbiont.resolve({ + constructor: '_init', + _onInit: '_onSymbiontInit', + destroy: '_symbiontDestructor', + _documentUnload: '_workerDocumentUnload' +}).compose({ + _frame: Symbiont.required, + _init: Symbiont.required, + _onSymbiontInit: Symbiont.required, + _symbiontDestructor: Symbiont.required, + _emit: Symbiont.required, + on: Symbiont.required, + removeListener: Symbiont.required, + + _inited: false, + + /** + * If set to `true` frame loaders between xul panel frame and + * hidden frame are swapped. If set to `false` frame loaders are + * set back to normal. Setting the value that was already set will + * have no effect. + */ + set _frameLoadersSwapped(value) { + if (this.__frameLoadersSwapped == value) return; + this._frame.QueryInterface(Ci.nsIFrameLoaderOwner) + .swapFrameLoaders(this._viewFrame); + this.__frameLoadersSwapped = value; + }, + __frameLoadersSwapped: false, + + constructor: function Panel(options) { + this._onShow = this._onShow.bind(this); + this._onHide = this._onHide.bind(this); + this.on('inited', this._onSymbiontInit.bind(this)); + this.on('propertyChange', this._onChange.bind(this)); + + options = options || {}; + if ('onShow' in options) + this.on('show', options.onShow); + if ('onHide' in options) + this.on('hide', options.onHide); + if ('width' in options) + this.width = options.width; + if ('height' in options) + this.height = options.height; + if ('contentURL' in options) + this.contentURL = options.contentURL; + + this._init(options); + }, + _destructor: function _destructor() { + this.hide(); + this._removeAllListeners('show'); + this._removeAllListeners('hide'); + this._removeAllListeners('propertyChange'); + this._removeAllListeners('inited'); + // defer cleanup to be performed after panel gets hidden + this._xulPanel = null; + this._symbiontDestructor(this); + this._removeAllListeners(); + }, + destroy: function destroy() { + this._destructor(); + }, + /* Public API: Panel.width */ + get width() this._width, + set width(value) + this._width = valid({ $: value }, { $: validNumber }).$ || this._width, + _width: 320, + /* Public API: Panel.height */ + get height() this._height, + set height(value) + this._height = valid({ $: value }, { $: validNumber }).$ || this._height, + _height: 240, + + /* Public API: Panel.isShowing */ + get isShowing() !!this._xulPanel && this._xulPanel.state == "open", + + /* Public API: Panel.show */ + show: function show(anchor) { + anchor = anchor || null; + let document = getWindow(anchor).document; + let xulPanel = this._xulPanel; + if (!xulPanel) { + xulPanel = this._xulPanel = document.createElementNS(XUL_NS, 'panel'); + xulPanel.setAttribute("type", "arrow"); + + // One anonymous node has a big padding that doesn't work well with + // Jetpack, as we would like to display an iframe that completely fills + // the panel. + // -> Use a XBL wrapper with inner stylesheet to remove this padding. + let css = ".panel-inner-arrowcontent, .panel-arrowcontent {padding: 0;}"; + let originalXBL = "chrome://global/content/bindings/popup.xml#arrowpanel"; + let binding = + '<bindings xmlns="http://www.mozilla.org/xbl">' + + '<binding id="id" extends="' + originalXBL + '">' + + '<resources>' + + '<stylesheet src="data:text/css,' + + document.defaultView.encodeURIComponent(css) + '"/>' + + '</resources>' + + '</binding>' + + '</bindings>'; + xulPanel.style.MozBinding = 'url("data:text/xml,' + + document.defaultView.encodeURIComponent(binding) + '")'; + + let frame = document.createElementNS(XUL_NS, 'iframe'); + frame.setAttribute('type', 'content'); + frame.setAttribute('flex', '1'); + frame.setAttribute('transparent', 'transparent'); + if (runtime.OS === "Darwin") { + frame.style.borderRadius = "6px"; + frame.style.padding = "1px"; + } + + // Load an empty document in order to have an immediatly loaded iframe, + // so swapFrameLoaders is going to work without having to wait for load. + frame.setAttribute("src","data:,"); + + xulPanel.appendChild(frame); + document.getElementById("mainPopupSet").appendChild(xulPanel); + } + let { width, height } = this, x, y, position; + + if (!anchor) { + // Open the popup in the middle of the window. + x = document.documentElement.clientWidth / 2 - width / 2; + y = document.documentElement.clientHeight / 2 - height / 2; + position = null; + } + else { + // Open the popup by the anchor. + let rect = anchor.getBoundingClientRect(); + + let window = anchor.ownerDocument.defaultView; + + let zoom = window.mozScreenPixelsPerCSSPixel; + let screenX = rect.left + window.mozInnerScreenX * zoom; + let screenY = rect.top + window.mozInnerScreenY * zoom; + + // Set up the vertical position of the popup relative to the anchor + // (always display the arrow on anchor center) + let horizontal, vertical; + if (screenY > window.screen.availHeight / 2 + height) + vertical = "top"; + else + vertical = "bottom"; + + if (screenY > window.screen.availWidth / 2 + width) + horizontal = "left"; + else + horizontal = "right"; + + let verticalInverse = vertical == "top" ? "bottom" : "top"; + position = vertical + "center " + verticalInverse + horizontal; + + // Allow panel to flip itself if the panel can't be displayed at the + // specified position (useful if we compute a bad position or if the + // user moves the window and panel remains visible) + xulPanel.setAttribute("flip","both"); + } + + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + xulPanel.firstChild.style.width = width + "px"; + xulPanel.firstChild.style.height = height + "px"; + + // Wait for the XBL binding to be constructed + function waitForBinding() { + if (!xulPanel.openPopup) { + timer.setTimeout(waitForBinding, 50); + return; + } + xulPanel.openPopup(anchor, position, x, y); + } + waitForBinding(); + + return this._public; + }, + /* Public API: Panel.hide */ + hide: function hide() { + // The popuphiding handler takes care of swapping back the frame loaders + // and removing the XUL panel from the application window, we just have to + // trigger it by hiding the popup. + // XXX Sometimes I get "TypeError: xulPanel.hidePopup is not a function" + // when quitting the host application while a panel is visible. To suppress + // them, this now checks for "hidePopup" in xulPanel before calling it. + // It's not clear if there's an actual issue or the error is just normal. + let xulPanel = this._xulPanel; + if (xulPanel && "hidePopup" in xulPanel) + xulPanel.hidePopup(); + return this._public; + }, + + /* Public API: Panel.resize */ + resize: function resize(width, height) { + this.width = width; + this.height = height; + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + let xulPanel = this._xulPanel; + if (xulPanel) { + xulPanel.firstChild.style.width = width + "px"; + xulPanel.firstChild.style.height = height + "px"; + } + }, + + // While the panel is visible, this is the XUL <panel> we use to display it. + // Otherwise, it's null. + get _xulPanel() this.__xulPanel, + set _xulPanel(value) { + let xulPanel = this.__xulPanel; + if (value === xulPanel) return; + if (xulPanel) { + xulPanel.removeEventListener(ON_HIDE, this._onHide, false); + xulPanel.removeEventListener(ON_SHOW, this._onShow, false); + xulPanel.parentNode.removeChild(xulPanel); + } + if (value) { + value.addEventListener(ON_HIDE, this._onHide, false); + value.addEventListener(ON_SHOW, this._onShow, false); + } + this.__xulPanel = value; + }, + __xulPanel: null, + get _viewFrame() this.__xulPanel.children[0], + /** + * When the XUL panel becomes hidden, we swap frame loaders back to move + * the content of the panel to the hidden frame & remove panel element. + */ + _onHide: function _onHide() { + try { + this._frameLoadersSwapped = false; + this._xulPanel = null; + this._emit('hide'); + } catch(e) { + this._emit('error', e); + } + }, + /** + * When the XUL panel becomes shown, we swap frame loaders between panel + * frame and hidden frame to preserve state of the content dom. + */ + _onShow: function _onShow() { + try { + if (!this._inited) { // defer if not initialized yet + this.on('inited', this._onShow.bind(this)); + } else { + this._frameLoadersSwapped = true; + + // Retrieve computed text color style in order to apply to the iframe + // document. As MacOS background is dark gray, we need to use skin's + // text color. + let win = this._xulPanel.ownerDocument.defaultView; + let node = win.document.getAnonymousElementByAttribute(this._xulPanel, + "class", "panel-inner-arrowcontent"); + let textColor = win.getComputedStyle(node).getPropertyValue("color"); + let doc = this._xulPanel.firstChild.contentDocument; + let style = doc.createElement("style"); + style.textContent = "body { color: " + textColor + "; }"; + let container = doc.head ? doc.head : doc.documentElement; + + if (container.firstChild) + container.insertBefore(style, container.firstChild); + else + container.appendChild(style); + + this._emit('show'); + } + } catch(e) { + this._emit('error', e); + } + }, + /** + * Notification that panel was fully initialized. + */ + _onInit: function _onInit() { + this._inited = true; + + // Avoid panel document from resizing the browser window + // New platform capability added through bug 635673 + if ("allowWindowControl" in this._frame.docShell) + this._frame.docShell.allowWindowControl = false; + + // perform all deferred tasks like initSymbiont, show, hide ... + // TODO: We're publicly exposing a private event here; this + // 'inited' event should really be made private, somehow. + this._emit('inited'); + }, + + // Catch document unload event in order to rebind load event listener with + // Symbiont._initFrame if Worker._documentUnload destroyed the worker + _documentUnload: function(subject, topic, data) { + if (this._workerDocumentUnload(subject, topic, data)) { + this._initFrame(this._frame); + return true; + } + return false; + }, + + _onChange: function _onChange(e) { + if ('contentURL' in e && this._frame) { + // Cleanup the worker before injecting the content script in the new + // document + this._workerCleanup(); + this._initFrame(this._frame); + } + } +}); +exports.Panel = function(options) Panel(options) +exports.Panel.prototype = Panel.prototype; + +function getWindow(anchor) { + let window; + + if (anchor) { + let anchorWindow = anchor.ownerDocument.defaultView.top; + let anchorDocument = anchorWindow.document; + + let enumerator = windowMediator.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + let enumWindow = enumerator.getNext(); + + // Check if the anchor is in this browser window. + if (enumWindow == anchorWindow) { + window = anchorWindow; + break; + } + + // Check if the anchor is in a browser tab in this browser window. + let browser = enumWindow.gBrowser.getBrowserForDocument(anchorDocument); + if (browser) { + window = enumWindow; + break; + } + + // Look in other subdocuments (sidebar, etc.)? + } + } + + // If we didn't find the anchor's window (or we have no anchor), + // return the most recent browser window. + if (!window) + window = windowMediator.getMostRecentWindow("navigator:browser"); + + return window; +} + diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/passwords.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/passwords.js new file mode 100644 index 0000000..3da63ad --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/passwords.js @@ -0,0 +1,59 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { search, remove, store } = require("api-utils/passwords/utils"); +const { defer, delay } = require("api-utils/functional"); + +/** + * Utility function that returns `onComplete` and `onError` callbacks form the + * given `options` objects. Also properties are removed from the passed + * `options` objects. + * @param {Object} options + * Object that is passed to the exported functions of this module. + * @returns {Function[]} + * Array with two elements `onComplete` and `onError` functions. + */ +function getCallbacks(options) { + let value = [ + 'onComplete' in options ? options.onComplete : null, + 'onError' in options ? defer(options.onError) : console.exception + ]; + + delete options.onComplete; + delete options.onError; + + return value; +}; + +/** + * Creates a wrapper function that tries to call `onComplete` with a return + * value of the wrapped function or falls back to `onError` if wrapped function + * throws an exception. + */ +function createWrapperMethod(wrapped) { + return function (options) { + let [ onComplete, onError ] = getCallbacks(options); + try { + let value = wrapped(options); + if (onComplete) { + delay(function() { + try { + onComplete(value); + } catch (exception) { + onError(exception); + } + }); + } + } catch (exception) { + onError(exception); + } + }; +} + +exports.search = createWrapperMethod(search); +exports.store = createWrapperMethod(store); +exports.remove = createWrapperMethod(remove); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/private-browsing.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/private-browsing.js new file mode 100644 index 0000000..ff40c98 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/private-browsing.js @@ -0,0 +1,61 @@ +/* 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 { emit, on, once, off } = require("api-utils/event/core"); +const { defer } = require("api-utils/functional"); +const { when: unload } = require("api-utils/unload"); +const observers = require("api-utils/observer-service"); + +// Model holding a state. +const model = { active: false }; + +let deferredEmit = defer(emit); + +let pbService; +// Currently, only Firefox implements the private browsing service. +if (require("api-utils/xul-app").is("Firefox")) { + pbService = Cc["@mozilla.org/privatebrowsing;1"]. + getService(Ci.nsIPrivateBrowsingService); + + // Update model state. + model.active = pbService.privateBrowsingEnabled; + + // set up an observer for private browsing switches. + observers.add('private-browsing-transition-complete', function onChange() { + // Update model state. + model.active = pbService.privateBrowsingEnabled; + // Emit event with in next turn of event loop. + deferredEmit(exports, model.active ? 'start' : 'stop'); + }); +} + +let setMode = defer(function setMode(value) { + // We toggle private browsing mode asynchronously in order to work around + // bug 659629. Since private browsing transitions are asynchronous + // anyway, this doesn't significantly change the behavior of the API. + pbService.privateBrowsingEnabled = !!value +}); + + +// Make sure listeners are cleaned up. +unload(function() off(exports)); + +Object.defineProperty(exports, "isActive", { get: function() model.active }); +exports.activate = function activate() pbService && setMode(true) +exports.deactivate = function deactivate() pbService && setMode(false) +exports.on = on.bind(null, exports); +exports.once = once.bind(null, exports); +exports.removeListener = function removeListener(type, listener) { + // Note: We can't just bind `off` as we do it for other methods cause skipping + // a listener argument will remove all listeners for the given event type + // causing misbehavior. This way we make sure all arguments are passed. + off(exports, type, listener); +}; + +// This is workaround making sure that exports is wrapped before it's +// frozen, which needs to happen in order to workaround Bug 673468. +off(exports, 'workaround-bug-673468'); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/request.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/request.js new file mode 100644 index 0000000..b213af8 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/request.js @@ -0,0 +1,208 @@ +/* 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 { Base, Class } = require("api-utils/base"); +const { ns } = require("api-utils/namespace"); +const { emit } = require("api-utils/event/core"); +const { merge } = require("api-utils/utils/object"); +const { stringify } = require("api-utils/querystring"); +const { EventTarget } = require("api-utils/event/target"); +const { XMLHttpRequest } = require("api-utils/xhr"); +const apiUtils = require("api-utils/api-utils"); + +const response = ns(); +const request = ns(); + +// Instead of creating a new validator for each request, just make one and +// reuse it. +const { validateOptions, validateSingleOption } = new OptionsValidator({ + url: { + //XXXzpao should probably verify that url is a valid url as well + is: ["string"] + }, + headers: { + map: function (v) v || {}, + is: ["object"], + }, + content: { + map: function (v) v || null, + is: ["string", "object", "null"], + }, + contentType: { + map: function (v) v || "application/x-www-form-urlencoded", + is: ["string"], + }, + overrideMimeType: { + map: function(v) v || null, + is: ["string", "null"], + } +}); + +const REUSE_ERROR = "This request object has been used already. You must " + + "create a new one to make a new request." + +// Utility function to prep the request since it's the same between GET and +// POST +function runRequest(mode, target) { + let source = request(target) + let { xhr, url, content, contentType, headers, overrideMimeType } = source; + + // If this request has already been used, then we can't reuse it. + // Throw an error. + if (xhr) + throw new Error(REUSE_ERROR); + + xhr = source.xhr = new XMLHttpRequest(); + + // Build the data to be set. For GET requests, we want to append that to + // the URL before opening the request. + let data = stringify(content); + // If the URL already has ? in it, then we want to just use & + if (mode == "GET" && data) + url = url + (/\?/.test(url) ? "&" : "?") + data; + + // open the request + xhr.open(mode, url); + + // request header must be set after open, but before send + xhr.setRequestHeader("Content-Type", contentType); + + // set other headers + Object.keys(headers).forEach(function(name) { + xhr.setRequestHeader(name, headers[name]); + }); + + // set overrideMimeType + if (overrideMimeType) + xhr.overrideMimeType(overrideMimeType); + + // handle the readystate, create the response, and call the callback + xhr.onreadystatechange = function onreadystatechange() { + if (xhr.readyState === 4) { + let response = Response.new(xhr); + source.response = response; + emit(target, 'complete', response); + } + }; + + // actually send the request. + // We don't want to send data on GET requests. + xhr.send(mode !== "GET" ? data : null); +} + +const Request = EventTarget.extend({ + initialize: function initialize(options) { + // `EventTarget.initialize` will set event listeners that are named + // like `onEvent` in this case `onComplete` listener will be set to + // `complete` event. + EventTarget.initialize.call(this, options); + + // Copy normalized options. + merge(request(this), validateOptions(options)); + }, + get url() { return request(this).url; }, + set url(value) { request(this).url = validateSingleOption('url', value); }, + get headers() { return request(this).headers; }, + set headers(value) { + return request(this).headers = validateSingleOption('headers', value); + }, + get content() { return request(this).content; }, + set content(value) { + request(this).content = validateSingleOption('content', value); + }, + get contentType() { return request(this).contentType; }, + set contentType(value) { + request(this).contentType = validateSingleOption('contentType', value); + }, + get response() { return request(this).response; }, + get: function() { + runRequest('GET', this); + return this; + }, + post: function() { + runRequest('POST', this); + return this; + }, + put: function() { + runRequest('PUT', this); + return this; + } +}); +exports.Request = Class(Request); + +const Response = Base.extend({ + initialize: function initialize(request) { + response(this).request = request; + }, + get text() response(this).request.responseText, + get xml() { + throw new Error("Sorry, the 'xml' property is no longer available. " + + "see bug 611042 for more information."); + }, + get status() response(this).request.status, + get statusText() response(this).request.statusText, + get json() { + try { + return JSON.parse(this.text); + } catch(error) { + return null; + } + }, + get headers() { + let headers = {}, lastKey; + // Since getAllResponseHeaders() will return null if there are no headers, + // defend against it by defaulting to "" + let rawHeaders = response(this).request.getAllResponseHeaders() || ""; + rawHeaders.split("\n").forEach(function (h) { + // According to the HTTP spec, the header string is terminated by an empty + // line, so we can just skip it. + if (!h.length) { + return; + } + + let index = h.indexOf(":"); + // The spec allows for leading spaces, so instead of assuming a single + // leading space, just trim the values. + let key = h.substring(0, index).trim(), + val = h.substring(index + 1).trim(); + + // For empty keys, that means that the header value spanned multiple lines. + // In that case we should append the value to the value of lastKey with a + // new line. We'll assume lastKey will be set because there should never + // be an empty key on the first pass. + if (key) { + headers[key] = val; + lastKey = key; + } + else { + headers[lastKey] += "\n" + val; + } + }); + return headers; + } +}); + +// apiUtils.validateOptions doesn't give the ability to easily validate single +// options, so this is a wrapper that provides that ability. +function OptionsValidator(rules) { + return { + validateOptions: function (options) { + return apiUtils.validateOptions(options, rules); + }, + validateSingleOption: function (field, value) { + // We need to create a single rule object from our listed rules. To avoid + // JavaScript String warnings, check for the field & default to an empty object. + let singleRule = {}; + if (field in rules) { + singleRule[field] = rules[field]; + } + let singleOption = {}; + singleOption[field] = value; + // This should throw if it's invalid, which will bubble up & out. + return apiUtils.validateOptions(singleOption, singleRule)[field]; + } + }; +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/selection.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/selection.js new file mode 100644 index 0000000..f10d890 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/selection.js @@ -0,0 +1,421 @@ +/* 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"; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The selection module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +let { Ci, Cc } = require("chrome"), + { setTimeout } = require("api-utils/timer"), + { emit, off } = require("api-utils/event/core"), + { Unknown } = require("api-utils/xpcom"), + { Base } = require("api-utils/base"), + { EventTarget } = require("api-utils/event/target"); + + +const windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + + +// The selection type HTML +const HTML = 0x01; + +// The selection type TEXT +const TEXT = 0x02; + +// The selection type DOM (internal use only) +const DOM = 0x03; + +// A more developer-friendly message than the caught exception when is not +// possible change a selection. +const ERR_CANNOT_CHANGE_SELECTION = + "It isn't possible to change the selection, as there isn't currently a selection"; + +const Selection = Base.extend({ + /** + * Creates an object from which a selection can be set, get, etc. Each + * object has an associated with a range number. Range numbers are the + * 0-indexed counter of selection ranges as explained at + * https://developer.mozilla.org/en/DOM/Selection. + * + * @param rangeNumber + * The zero-based range index into the selection + */ + initialize: function initialize(rangeNumber) { + // In order to hide the private `rangeNumber` argument from API consumers + // while still enabling Selection getters/setters to access it, we define + // it as non enumerable, non configurable property. While consumers still + // may discover it they won't be able to do any harm which is good enough + // in this case. + Object.defineProperties(this, { + rangeNumber: { + enumerable: false, + configurable: false, + value: rangeNumber + } + }); + }, + get text() { return getSelection(TEXT, this.rangeNumber); }, + set text(value) { setSelection(value, this.rangeNumber); }, + get html() { return getSelection(HTML, this.rangeNumber); }, + set html(value) { setSelection(value, this.rangeNumber); }, + get isContiguous() { + let selection = getSelection(DOM); + + // If there are multiple ranges, the selection is definitely discontiguous. + // It returns `false` also if there are no selection; and `true` if there is + // a single non empty range, or a selection in a text field - contiguous or + // not (text field selection APIs doesn't support multiple selections). + + if (selection.rangeCount > 1) + return false; + + return !!(safeGetRange(selection, 0) || getElementWithSelection()); + } +}); + +/** + * Returns the most recent content window + */ +function context() { + // Overlay names should probably go into the xul-app module instead of here + return windowMediator.getMostRecentWindow("navigator:browser").document. + commandDispatcher.focusedWindow; +} + +/** + * Returns the current selection from most recent content window. Depending on + * the specified |type|, the value returned can be a string of text, stringified + * HTML, or a DOM selection object as described at + * https://developer.mozilla.org/en/DOM/Selection. + * + * @param type + * Specifies the return type of the selection. Valid values are the one + * of the constants HTML, TEXT, or DOM. + * + * @param rangeNumber + * Specifies the zero-based range index of the returned selection. + */ +function getSelection(type, rangeNumber) { + let window, selection; + try { + window = context(); + selection = window.getSelection(); + } + catch (e) { + return null; + } + + // Get the selected content as the specified type + if (type == DOM) + return selection; + else if (type == TEXT) { + let range = safeGetRange(selection, rangeNumber); + + if (range) + return range.toString(); + + let node = getElementWithSelection(window); + + if (!node) + return null; + + return node.value.substring(node.selectionStart, node.selectionEnd); + } + else if (type == HTML) { + let range = safeGetRange(selection, rangeNumber); + // Another way, but this includes the xmlns attribute for all elements in + // Gecko 1.9.2+ : + // return Cc["@mozilla.org/xmlextras/xmlserializer;1"]. + // createInstance(Ci.nsIDOMSerializer).serializeToSTring(range. + // cloneContents()); + if (!range) + return null; + let node = window.document.createElement("span"); + node.appendChild(range.cloneContents()); + return node.innerHTML; + } + throw new Error("Type " + type + " is unrecognized."); +} + +/** + * Returns the specified range in a selection without throwing an exception. + * + * @param selection + * A selection object as described at + * https://developer.mozilla.org/en/DOM/Selection + * + * @param rangeNumber + * Specifies the zero-based range index of the returned selection. + */ +function safeGetRange(selection, rangeNumber) { + try { + let range = selection.getRangeAt(rangeNumber); + if (!range || range.toString() == "") + return null; + return range; + } + catch (e) { + return null; + } +} + +/** + * Returns a reference of the DOM's active element for the window given, if it + * supports the text field selection API and has a text selected. + * + * Note: + * we need this method because window.getSelection doesn't return a selection + * for text selected in a form field (see bug 85686) + * + * @param {nsIWindow} [window] + * A reference to a window + */ +function getElementWithSelection(window) { + let element; + + try { + element = (window || context()).document.activeElement; + } + catch (e) { + element = null; + } + + if (!element) + return null; + + let { value, selectionStart, selectionEnd } = element; + + let hasSelection = typeof value === "string" && + !isNaN(selectionStart) && + !isNaN(selectionEnd) && + selectionStart !== selectionEnd; + + return hasSelection ? element : null; +} +/** + * Sets the current selection of the most recent content document by changing + * the existing selected text/HTML range to the specified value. + * + * @param val + * The value for the new selection + * + * @param rangeNumber + * The zero-based range index of the selection to be set + * + */ +function setSelection(val, rangeNumber) { + // Make sure we have a window context & that there is a current selection. + // Selection cannot be set unless there is an existing selection. + let window, selection; + + try { + window = context(); + selection = window.getSelection(); + } + catch (e) { + throw new Error(ERR_CANNOT_CHANGE_SELECTION); + } + + let range = safeGetRange(selection, rangeNumber); + + if (range) { + // Get rid of the current selection and insert our own + range.deleteContents(); + let node = window.document.createElement("span"); + range.surroundContents(node); + + // Some relevant JEP-111 requirements: + + // Setting the text property replaces the selection with the value to + // which the property is set and sets the html property to the same value + // to which the text property is being set. + + // Setting the html property replaces the selection with the value to + // which the property is set and sets the text property to the text version + // of the HTML value. + + // This sets both the HTML and text properties. + node.innerHTML = val; + } else { + let node = getElementWithSelection(window); + + if (!node) + throw new Error(ERR_CANNOT_CHANGE_SELECTION); + + let { value, selectionStart, selectionEnd } = node; + + let newSelectionEnd = selectionStart + val.length; + + node.value = value.substring(0, selectionStart) + + val + + value.substring(selectionEnd, value.length); + + node.setSelectionRange(selectionStart, newSelectionEnd); + } +} + +function onLoad(event) { + SelectionListenerManager.onLoad(event); +} + +function onUnload(event) { + SelectionListenerManager.onUnload(event); +} + +function onSelect() { + SelectionListenerManager.onSelect(); +} + +let SelectionListenerManager = Unknown.extend({ + interfaces: [ 'nsISelectionListener' ], + /** + * This is the nsISelectionListener implementation. This function is called + * by Gecko when a selection is changed interactively. + * + * We only pay attention to the SELECTALL, KEYPRESS, and MOUSEUP selection + * reasons. All reasons are listed here: + * + * http://mxr.mozilla.org/mozilla1.9.2/source/content/base/public/ + * nsISelectionListener.idl + * + * The other reasons (NO_REASON, DRAG_REASON, MOUSEDOWN_REASON) aren't + * applicable to us. + */ + notifySelectionChanged: function notifySelectionChanged(document, selection, + reason) { + if (!["SELECTALL", "KEYPRESS", "MOUSEUP"].some(function(type) reason & + Ci.nsISelectionListener[type + "_REASON"]) || selection.toString() == "") + return; + + this.onSelect(); + }, + + onSelect : function onSelect() { + setTimeout(emit, 0, module.exports, "select"); + }, + + /** + * Part of the Tracker implementation. This function is called by the + * tabs module when a browser is being tracked. Often, that means a new tab + * has been opened, but it can also mean an addon has been installed while + * tabs are already opened. In that case, this function is called for those + * already-opened tabs. + * + * @param browser + * The browser being tracked + */ + onTrack: function onTrack(browser) { + browser.addEventListener("load", onLoad, true); + browser.addEventListener("unload", onUnload, true); + }, + + onLoad: function onLoad(event) { + // Nothing to do without a useful window + let window = event.target.defaultView; + if (!window) + return; + + // Wrap the add selection call with some number of setTimeout 0 because some + // reason it's possible to add a selection listener "too early". 2 sometimes + // works for gmail, and more consistently with 3, so make it 5 to be safe. + let count = 0; + let self = this; + function wrap(count, func) { + if (count-- > 0) + setTimeout(wrap, 0); + else + self.addSelectionListener(window); + } + wrap(); + }, + + addSelectionListener: function addSelectionListener(window) { + if (window.jetpack_core_selection_listener) + return; + let selection = window.getSelection(); + if (selection instanceof Ci.nsISelectionPrivate) + selection.addSelectionListener(this); + + // nsISelectionListener implementation seems not fire a notification if + // a selection is in a text field, therefore we need to add a listener to + // window.onselect, that is fired only for text fields. + // https://developer.mozilla.org/en/DOM/window.onselect + window.addEventListener("select", onSelect, true); + + window.jetpack_core_selection_listener = true; + }, + + onUnload: function onUnload(event) { + // Nothing to do without a useful window + let window = event.target.defaultView; + if (!window) + return; + this.removeSelectionListener(window); + off(exports); + }, + + removeSelectionListener: function removeSelectionListener(window) { + if (!window.jetpack_core_selection_listener) + return; + let selection = window.getSelection(); + if (selection instanceof Ci.nsISelectionPrivate) + selection.removeSelectionListener(this); + + window.removeEventListener("select", onSelect); + + window.jetpack_core_selection_listener = false; + }, + + /** + * Part of the TabTracker implementation. This function is called by the + * tabs module when a browser is being untracked. Usually, that means a tab + * has been closed. + * + * @param browser + * The browser being untracked + */ + onUntrack: function onUntrack(browser) { + browser.removeEventListener("load", onLoad, true); + browser.removeEventListener("unload", onUnload, true); + } +}); + +/** + * Install |SelectionListenerManager| as tab tracker in order to watch + * tab opening/closing + */ +require("api-utils/tab-browser").Tracker(SelectionListenerManager); + +// Note: We use `Object.create` form just in order to define `__iterator__` +// as non-enumerable, to ensure that it won't be returned by an `Object.keys`. +var SelectionIterator = Object.create(Object.prototype, { + /** + * Exports an iterator so that discontiguous selections can be iterated. + * + * If discontiguous selections are in a text field, only the first one + * is returned because the text field selection APIs doesn't support + * multiple selections. + */ + __iterator__: { enumerable: false, value: function() { + let selection = getSelection(DOM); + let count = selection.rangeCount || (getElementWithSelection() ? 1 : 0); + + for (let i = 0; i < count; i++) + yield Selection.new(i); + }} +}); + +var selection = EventTarget.extend(Selection, SelectionIterator).new(0); + +// This is workaround making sure that exports is wrapped before it's +// frozen, which needs to happen in order to workaround Bug 673468. +off(selection, 'workaround-bug-673468'); +module.exports = selection; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/simple-prefs.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/simple-prefs.js new file mode 100644 index 0000000..5f73cc5 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/simple-prefs.js @@ -0,0 +1,68 @@ +/* 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/. */ + +const { Cc, Ci } = require("chrome"); +const { emit, off } = require("api-utils/event/core"); +const { EventTarget } = require("api-utils/event/target"); +const { when: unload } = require("api-utils/unload"); +const { jetpackID } = require("@packaging"); +const prefService = require("api-utils/preferences-service"); +const observers = require("api-utils/observer-service"); + +const ADDON_BRANCH = "extensions." + jetpackID + "."; +const BUTTON_PRESSED = jetpackID + "-cmdPressed"; + +// XXX Currently, only Firefox implements the inline preferences. +if (!require("xul-app").is("Firefox")) + throw Error("This API is only supported in Firefox"); + +const branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(ADDON_BRANCH). + QueryInterface(Ci.nsIPrefBranch2); + +// Listen to changes in the preferences +function preferenceChange(subject, topic, name) { + if (topic === 'nsPref:changed') + emit(target, name, name); +} +branch.addObserver('', preferenceChange, false); + +// Listen to clicks on buttons +function buttonClick(subject, data) { + emit(target, data); +} +observers.add(BUTTON_PRESSED, buttonClick); + +// Make sure we cleanup listeners on unload. +unload(function() { + off(exports); + branch.removeObserver('', preferenceChange, false); + observers.remove(BUTTON_PRESSED, buttonClick); +}); + +const prefs = Proxy.create({ + get: function(receiver, pref) { + return prefService.get(ADDON_BRANCH + pref); + }, + set: function(receiver, pref, val) { + prefService.set(ADDON_BRANCH + pref, val); + }, + delete: function(pref) { + prefService.reset(ADDON_BRANCH + pref); + return true; + }, + has: function(pref) { + return prefService.has(ADDON_BRANCH + pref); + } +}); + +// Event target we will expose as module exports in order to be able to +// emit events on it. +const target = EventTarget.extend({ prefs: prefs }).new(); +module.exports = target; + +// This is workaround making sure that exports is wrapped before it's +// frozen, which needs to happen in order to workaround Bug 673468. +off(target, 'workaround-bug-673468'); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/simple-storage.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/simple-storage.js new file mode 100644 index 0000000..85395f8 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/simple-storage.js @@ -0,0 +1,237 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * 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 file = require("api-utils/file"); +const prefs = require("api-utils/preferences-service"); +const jpSelf = require("self"); +const timer = require("api-utils/timer"); +const unload = require("api-utils/unload"); +const { emit, on, off } = require("api-utils/event/core"); + +const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod"; +const WRITE_PERIOD_DEFAULT = 300000; // 5 minutes + +const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota"; +const QUOTA_DEFAULT = 5242880; // 5 MiB + +const JETPACK_DIR_BASENAME = "jetpack"; + +Object.defineProperties(exports, { + storage: { + enumerable: true, + get: function() { return manager.root; }, + set: function(value) { manager.root = value; } + }, + quotaUsage: { + get: function() { return manager.quotaUsage; } + } +}); + +// A generic JSON store backed by a file on disk. This should be isolated +// enough to move to its own module if need be... +function JsonStore(options) { + this.filename = options.filename; + this.quota = options.quota; + this.writePeriod = options.writePeriod; + this.onOverQuota = options.onOverQuota; + this.onWrite = options.onWrite; + + unload.ensure(this); + + this.writeTimer = timer.setInterval(this.write.bind(this), + this.writePeriod); +} + +JsonStore.prototype = { + // The store's root. + get root() { + return this.isRootInited ? this._root : {}; + }, + + // Performs some type checking. + set root(val) { + let types = ["array", "boolean", "null", "number", "object", "string"]; + if (types.indexOf(typeof(val)) < 0) { + throw new Error("storage must be one of the following types: " + + types.join(", ")); + } + this._root = val; + return val; + }, + + // True if the root has ever been set (either via the root setter or by the + // backing file's having been read). + get isRootInited() { + return this._root !== undefined; + }, + + // Percentage of quota used, as a number [0, Inf). > 1 implies over quota. + // Undefined if there is no quota. + get quotaUsage() { + return this.quota > 0 ? + JSON.stringify(this.root).length / this.quota : + undefined; + }, + + // Removes the backing file and all empty subdirectories. + purge: function JsonStore_purge() { + try { + // This'll throw if the file doesn't exist. + file.remove(this.filename); + let parentPath = this.filename; + do { + parentPath = file.dirname(parentPath); + // This'll throw if the dir isn't empty. + file.rmdir(parentPath); + } while (file.basename(parentPath) !== JETPACK_DIR_BASENAME); + } + catch (err) {} + }, + + // Initializes the root by reading the backing file. + read: function JsonStore_read() { + try { + let str = file.read(this.filename); + + // Ideally we'd log the parse error with console.error(), but logged + // errors cause tests to fail. Supporting "known" errors in the test + // harness appears to be non-trivial. Maybe later. + this.root = JSON.parse(str); + } + catch (err) { + this.root = {}; + } + }, + + // If the store is under quota, writes the root to the backing file. + // Otherwise quota observers are notified and nothing is written. + write: function JsonStore_write() { + if (this.quotaUsage > 1) + this.onOverQuota(this); + else + this._write(); + }, + + // Cleans up on unload. If unloading because of uninstall, the store is + // purged; otherwise it's written. + unload: function JsonStore_unload(reason) { + timer.clearInterval(this.writeTimer); + this.writeTimer = null; + + if (reason === "uninstall") + this.purge(); + else + this._write(); + }, + + // True if the root is an empty object. + get _isEmpty() { + if (this.root && typeof(this.root) === "object") { + let empty = true; + for (let key in this.root) { + empty = false; + break; + } + return empty; + } + return false; + }, + + // Writes the root to the backing file, notifying write observers when + // complete. If the store is over quota or if it's empty and the store has + // never been written, nothing is written and write observers aren't notified. + _write: function JsonStore__write() { + // Don't write if the root is uninitialized or if the store is empty and the + // backing file doesn't yet exist. + if (!this.isRootInited || (this._isEmpty && !file.exists(this.filename))) + return; + + // If the store is over quota, don't write. The current under-quota state + // should persist. + if (this.quotaUsage > 1) + return; + + // Finally, write. + let stream = file.open(this.filename, "w"); + try { + stream.writeAsync(JSON.stringify(this.root), function writeAsync(err) { + if (err) + console.error("Error writing simple storage file: " + this.filename); + else if (this.onWrite) + this.onWrite(this); + }.bind(this)); + } + catch (err) { + // writeAsync closes the stream after it's done, so only close on error. + stream.close(); + } + } +}; + + +// This manages a JsonStore singleton and tailors its use to simple storage. +// The root of the JsonStore is lazy-loaded: The backing file is only read the +// first time the root's gotten. +let manager = ({ + jsonStore: null, + + // The filename of the store, based on the profile dir and extension ID. + get filename() { + let storeFile = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + storeFile.append(JETPACK_DIR_BASENAME); + storeFile.append(jpSelf.id); + storeFile.append("simple-storage"); + file.mkpath(storeFile.path); + storeFile.append("store.json"); + return storeFile.path; + }, + + get quotaUsage() { + return this.jsonStore.quotaUsage; + }, + + get root() { + if (!this.jsonStore.isRootInited) + this.jsonStore.read(); + return this.jsonStore.root; + }, + + set root(val) { + return this.jsonStore.root = val; + }, + + unload: function manager_unload() { + off(this); + }, + + new: function manager_constructor() { + let manager = Object.create(this); + unload.ensure(manager); + + manager.jsonStore = new JsonStore({ + filename: manager.filename, + writePeriod: prefs.get(WRITE_PERIOD_PREF, WRITE_PERIOD_DEFAULT), + quota: prefs.get(QUOTA_PREF, QUOTA_DEFAULT), + onOverQuota: emit.bind(null, exports, "OverQuota") + }); + + return manager; + } +}).new(); + +// This is workaround making sure that exports is wrapped before it's +// frozen, which needs to happen in order to workaround Bug 673468. +off(exports, 'workaround-bug-673468'); + +exports.on = on.bind(null, exports); +exports.removeListener = function(type, listener) { + off(exports, type, listener); +}; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/tabs.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/tabs.js new file mode 100644 index 0000000..ab915f5 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/tabs.js @@ -0,0 +1,28 @@ +/* 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"; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The tabs module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +const { browserWindows } = require("./windows"); +const { tabs } = require("api-utils/windows/tabs"); + +Object.defineProperties(tabs, { + open: { value: function open(options) { + if (options.inNewWindow) + // `tabs` option is under review and may be removed. + return browserWindows.open({ tabs: [ options ] }); + // Open in active window if new window was not required. + return browserWindows.activeWindow.tabs.open(options); + }} +}); +// It's a hack but we will be able to remove it once will implement CommonJS +// feature that would allow us to override exports. +exports.__proto__ = tabs; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/timers.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/timers.js new file mode 100644 index 0000000..45fcf8d --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/timers.js @@ -0,0 +1,8 @@ +/* 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"; + +// This module just proxies to the low level equivalent "timer" in "api-utils". +module.exports = require("api-utils/timer"); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/widget.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/widget.js new file mode 100644 index 0000000..e0bbfb6 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/widget.js @@ -0,0 +1,923 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +// Widget content types +const CONTENT_TYPE_URI = 1; +const CONTENT_TYPE_HTML = 2; +const CONTENT_TYPE_IMAGE = 3; + +const ERR_CONTENT = "No content or contentURL property found. Widgets must " + + "have one or the other.", + ERR_LABEL = "The widget must have a non-empty label property.", + ERR_ID = "You have to specify a unique value for the id property of " + + "your widget in order for the application to remember its " + + "position.", + ERR_DESTROYED = "The widget has been destroyed and can no longer be used."; + +// Supported events, mapping from DOM event names to our event names +const EVENTS = { + "click": "click", + "mouseover": "mouseover", + "mouseout": "mouseout", +}; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The widget module currently supports only Firefox. In the future ", + "it will support other applications. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +const { validateOptions } = require("api-utils/api-utils"); +const panels = require("./panel"); +const { EventEmitter, EventEmitterTrait } = require("api-utils/events"); +const { Trait } = require("api-utils/traits"); +const LightTrait = require('api-utils/light-traits').Trait; +const { Loader, Symbiont } = require("api-utils/content"); +const { Cortex } = require('api-utils/cortex'); +const windowsAPI = require("./windows"); +const { setTimeout } = require("api-utils/timer"); +const unload = require("api-utils/unload"); +const { uuid } = require("api-utils/uuid"); + +// Data types definition +const valid = { + number: { is: ["null", "undefined", "number"] }, + string: { is: ["null", "undefined", "string"] }, + id: { + is: ["string"], + ok: function (v) v.length > 0, + msg: ERR_ID, + readonly: true + }, + label: { + is: ["string"], + ok: function (v) v.length > 0, + msg: ERR_LABEL + }, + panel: { + is: ["null", "undefined", "object"], + ok: function(v) !v || v instanceof panels.Panel + }, + width: { + is: ["null", "undefined", "number"], + map: function (v) { + if (null === v || undefined === v) v = 16; + return v; + }, + defaultValue: 16 + }, + allow: { + is: ["null", "undefined", "object"], + map: function (v) { + if (!v) v = { script: true }; + return v; + }, + get defaultValue() ({ script: true }) + }, +}; + +// Widgets attributes definition +let widgetAttributes = { + label: valid.label, + id: valid.id, + tooltip: valid.string, + width: valid.width, + content: valid.string, + panel: valid.panel, + allow: valid.allow +}; + +// Import data definitions from loader, but don't compose with it as Model +// functions allow us to recreate easily all Loader code. +let loaderAttributes = require("api-utils/content/loader").validationAttributes; +for (let i in loaderAttributes) + widgetAttributes[i] = loaderAttributes[i]; + +widgetAttributes.contentURL.optional = true; + +// Widgets public events list, that are automatically binded in options object +const WIDGET_EVENTS = [ + "click", + "mouseover", + "mouseout", + "error", + "message", + "attach" +]; + +// `Model` utility functions that help creating these various Widgets objects +let model = { + + // Validate one attribute using api-utils.js:validateOptions function + _validate: function _validate(name, suspect, validation) { + let $1 = {}; + $1[name] = suspect; + let $2 = {}; + $2[name] = validation; + return validateOptions($1, $2)[name]; + }, + + /** + * This method has two purposes: + * 1/ Validate and define, on a given object, a set of attribute + * 2/ Emit a "change" event on this object when an attribute is changed + * + * @params {Object} object + * Object on which we can bind attributes on and watch for their changes. + * This object must have an EventEmitter interface, or, at least `_emit` + * method + * @params {Object} attrs + * Dictionary of attributes definition following api-utils:validateOptions + * scheme + * @params {Object} values + * Dictionary of attributes default values + */ + setAttributes: function setAttributes(object, attrs, values) { + let properties = {}; + for (let name in attrs) { + let value = values[name]; + let req = attrs[name]; + + // Retrieve default value from typedef if the value is not defined + if ((typeof value == "undefined" || value == null) && req.defaultValue) + value = req.defaultValue; + + // Check for valid value if value is defined or mandatory + if (!req.optional || typeof value != "undefined") + value = model._validate(name, value, req); + + // In any case, define this property on `object` + let property = null; + if (req.readonly) { + property = { + value: value, + writable: false, + enumerable: true, + configurable: false + }; + } + else { + property = model._createWritableProperty(name, value); + } + + properties[name] = property; + } + Object.defineProperties(object, properties); + }, + + // Generate ES5 property definition for a given attribute + _createWritableProperty: function _createWritableProperty(name, value) { + return { + get: function () { + return value; + }, + set: function (newValue) { + value = newValue; + // The main goal of all this Model stuff is here: + // We want to forward all changes to some listeners + this._emit("change", name, value); + }, + enumerable: true, + configurable: false + }; + }, + + /** + * Automagically register listeners in options dictionary + * by detecting listener attributes with name starting with `on` + * + * @params {Object} object + * Target object that need to follow EventEmitter interface, or, at least, + * having `on` method. + * @params {Array} events + * List of events name to automatically bind. + * @params {Object} listeners + * Dictionary of event listener functions to register. + */ + setEvents: function setEvents(object, events, listeners) { + for (let i = 0, l = events.length; i < l; i++) { + let name = events[i]; + let onName = "on" + name[0].toUpperCase() + name.substr(1); + if (!listeners[onName]) + continue; + object.on(name, listeners[onName].bind(object)); + } + } + +}; + + +/** + * Main Widget class: entry point of the widget API + * + * Allow to control all widget across all existing windows with a single object. + * Widget.getView allow to retrieve a WidgetView instance to control a widget + * specific to one window. + */ +const WidgetTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ + + _initWidget: function _initWidget(options) { + model.setAttributes(this, widgetAttributes, options); + + browserManager.validate(this); + + // We must have at least content or contentURL defined + if (!(this.content || this.contentURL)) + throw new Error(ERR_CONTENT); + + this._views = []; + + // Set tooltip to label value if we don't have tooltip defined + if (!this.tooltip) + this.tooltip = this.label; + + model.setEvents(this, WIDGET_EVENTS, options); + + this.on('change', this._onChange.bind(this)); + + let self = this; + this._port = EventEmitterTrait.create({ + emit: function () { + let args = arguments; + self._views.forEach(function(v) v.port.emit.apply(v.port, args)); + } + }); + // expose wrapped port, that exposes only public properties. + this._port._public = Cortex(this._port); + + // Register this widget to browser manager in order to create new widget on + // all new windows + browserManager.addItem(this); + }, + + _onChange: function _onChange(name, value) { + // Set tooltip to label value if we don't have tooltip defined + if (name == 'tooltip' && !value) { + // we need to change tooltip again in order to change the value of the + // attribute itself + this.tooltip = this.label; + return; + } + + // Forward attributes changes to WidgetViews + if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { + this._views.forEach(function(v) v[name] = value); + } + }, + + _onEvent: function _onEvent(type, eventData) { + this._emit(type, eventData); + }, + + _createView: function _createView() { + // Create a new WidgetView instance + let view = WidgetView(this); + + // Keep a reference to it + this._views.push(view); + + // Emit an `attach` event with a WidgetView instance without private attrs + this._emit("attach", view._public); + + return view; + }, + + // a WidgetView instance is destroyed + _onViewDestroyed: function _onViewDestroyed(view) { + let idx = this._views.indexOf(view); + this._views.splice(idx, 1); + }, + + /** + * Called on browser window closed, to destroy related WidgetViews + * @params {ChromeWindow} window + * Window that has been closed + */ + _onWindowClosed: function _onWindowClosed(window) { + for each (let view in this._views) { + if (view._isInChromeWindow(window)) { + view.destroy(); + break; + } + } + }, + + /** + * Get the WidgetView instance related to a BrowserWindow instance + * @params {BrowserWindow} window + * BrowserWindow reference from "windows" module + */ + getView: function getView(window) { + for each (let view in this._views) { + if (view._isInWindow(window)) { + return view._public; + } + } + return null; + }, + + get port() this._port._public, + set port(v) {}, // Work around Cortex failure with getter without setter + // See bug 653464 + _port: null, + + postMessage: function postMessage(message) { + this._views.forEach(function(v) v.postMessage(message)); + }, + + destroy: function destroy() { + if (this.panel) + this.panel.destroy(); + + // Dispatch destroy calls to views + // we need to go backward as we remove items from this array in + // _onViewDestroyed + for (let i = this._views.length - 1; i >= 0; i--) + this._views[i].destroy(); + + // Unregister widget to stop creating it over new windows + // and allow creation of new widget with same id + browserManager.removeItem(this); + } + +})); + +// Widget constructor +const Widget = function Widget(options) { + let w = WidgetTrait.create(Widget.prototype); + w._initWidget(options); + + // Return a Cortex of widget in order to hide private attributes like _onEvent + let _public = Cortex(w); + unload.ensure(_public, "destroy"); + return _public; +} +exports.Widget = Widget; + + + +/** + * WidgetView is an instance of a widget for a specific window. + * + * This is an external API that can be retrieved by calling Widget.getView or + * by watching `attach` event on Widget. + */ +const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ + + // Reference to the matching WidgetChrome + // set right after constructor call + _chrome: null, + + // Public interface of the WidgetView, passed in `attach` event or in + // Widget.getView + _public: null, + + _initWidgetView: function WidgetView__initWidgetView(baseWidget) { + this._baseWidget = baseWidget; + + model.setAttributes(this, widgetAttributes, baseWidget); + + this.on('change', this._onChange.bind(this)); + + let self = this; + this._port = EventEmitterTrait.create({ + emit: function () { + if (!self._chrome) + throw new Error(ERR_DESTROYED); + self._chrome.update(self._baseWidget, "emit", arguments); + } + }); + // expose wrapped port, that exposes only public properties. + this._port._public = Cortex(this._port); + + this._public = Cortex(this); + }, + + _onChange: function WidgetView__onChange(name, value) { + if (name == 'tooltip' && !value) { + this.tooltip = this.label; + return; + } + + // Forward attributes changes to WidgetChrome instance + if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { + this._chrome.update(this._baseWidget, name, value); + } + }, + + _onEvent: function WidgetView__onEvent(type, eventData, domNode) { + // Dispatch event in view + this._emit(type, eventData); + + // And forward it to the main Widget object + if ("click" == type || type.indexOf("mouse") == 0) + this._baseWidget._onEvent(type, this._public); + else + this._baseWidget._onEvent(type, eventData); + + // Special case for click events: if the widget doesn't have a click + // handler, but it does have a panel, display the panel. + if ("click" == type && !this._listeners("click").length && this.panel) + this.panel.show(domNode); + }, + + _isInWindow: function WidgetView__isInWindow(window) { + return windowsAPI.BrowserWindow({ + window: this._chrome.window + }) == window; + }, + + _isInChromeWindow: function WidgetView__isInChromeWindow(window) { + return this._chrome.window == window; + }, + + _onPortEvent: function WidgetView__onPortEvent(args) { + let port = this._port; + port._emit.apply(port, args); + let basePort = this._baseWidget._port; + basePort._emit.apply(basePort, args); + }, + + get port() this._port._public, + set port(v) {}, // Work around Cortex failure with getter without setter + // See bug 653464 + _port: null, + + postMessage: function WidgetView_postMessage(message) { + if (!this._chrome) + throw new Error(ERR_DESTROYED); + this._chrome.update(this._baseWidget, "postMessage", message); + }, + + destroy: function WidgetView_destroy() { + this._chrome.destroy(); + delete this._chrome; + this._baseWidget._onViewDestroyed(this); + this._emit("detach"); + } + +})); + +const WidgetView = function WidgetView(baseWidget) { + let w = WidgetViewTrait.create(WidgetView.prototype); + w._initWidgetView(baseWidget); + return w; +} + + + +/** + * Keeps track of all browser windows. + * Exposes methods for adding/removing widgets + * across all open windows (and future ones). + * Create a new instance of BrowserWindow per window. + */ +let browserManager = { + items: [], + windows: [], + + // Registers the manager to listen for window openings and closings. Note + // that calling this method can cause onTrack to be called immediately if + // there are open windows. + init: function () { + let windowTracker = new (require("api-utils/window-utils").WindowTracker)(this); + unload.ensure(windowTracker); + }, + + // Registers a window with the manager. This is a WindowTracker callback. + onTrack: function browserManager_onTrack(window) { + if (this._isBrowserWindow(window)) { + let win = new BrowserWindow(window); + win.addItems(this.items); + this.windows.push(win); + } + }, + + // Unregisters a window from the manager. It's told to undo all + // modifications. This is a WindowTracker callback. Note that when + // WindowTracker is unloaded, it calls onUntrack for every currently opened + // window. The browserManager therefore doesn't need to specially handle + // unload itself, since unloading the browserManager means untracking all + // currently opened windows. + onUntrack: function browserManager_onUntrack(window) { + if (this._isBrowserWindow(window)) { + this.items.forEach(function(i) i._onWindowClosed(window)); + for (let i = 0; i < this.windows.length; i++) { + if (this.windows[i].window == window) { + this.windows.splice(i, 1)[0]; + return; + } + } + + } + }, + + // Used to validate widget by browserManager before adding it, + // in order to check input very early in widget constructor + validate : function (item) { + let idx = this.items.indexOf(item); + if (idx > -1) + throw new Error("The widget " + item + " has already been added."); + if (item.id) { + let sameId = this.items.filter(function(i) i.id == item.id); + if (sameId.length > 0) + throw new Error("This widget ID is already used: " + item.id); + } else { + item.id = this.items.length; + } + }, + + // Registers an item with the manager. It's added to all currently registered + // windows, and when new windows are registered it will be added to them, too. + addItem: function browserManager_addItem(item) { + this.items.push(item); + this.windows.forEach(function (w) w.addItems([item])); + }, + + // Unregisters an item from the manager. It's removed from all windows that + // are currently registered. + removeItem: function browserManager_removeItem(item) { + let idx = this.items.indexOf(item); + if (idx > -1) + this.items.splice(idx, 1); + }, + + _isBrowserWindow: function browserManager__isBrowserWindow(win) { + let winType = win.document.documentElement.getAttribute("windowtype"); + return winType === "navigator:browser"; + } +}; + + + +/** + * Keeps track of a single browser window. + * + * This is where the core of how a widget's content is added to a window lives. + */ +function BrowserWindow(window) { + this.window = window; + this.doc = window.document; +} + +BrowserWindow.prototype = { + + // Adds an array of items to the window. + addItems: function BW_addItems(items) { + items.forEach(this._addItemToWindow, this); + }, + + _addItemToWindow: function BW__addItemToWindow(baseWidget) { + // Create a WidgetView instance + let widget = baseWidget._createView(); + + // Create a WidgetChrome instance + let item = new WidgetChrome({ + widget: widget, + doc: this.doc, + window: this.window + }); + + widget._chrome = item; + + this._insertNodeInToolbar(item.node); + + // We need to insert Widget DOM Node before finishing widget view creation + // (because fill creates an iframe and tries to access its docShell) + item.fill(); + }, + + _insertNodeInToolbar: function BW__insertNodeInToolbar(node) { + // Add to the customization palette + let toolbox = this.doc.getElementById("navigator-toolbox"); + let palette = toolbox.palette; + palette.appendChild(node); + + // Search for widget toolbar by reading toolbar's currentset attribute + let container = null; + let toolbars = this.doc.getElementsByTagName("toolbar"); + let id = node.getAttribute("id"); + for (let i = 0, l = toolbars.length; i < l; i++) { + let toolbar = toolbars[i]; + if (toolbar.getAttribute("currentset").indexOf(id) == -1) + continue; + container = toolbar; + } + + // if widget isn't in any toolbar, add it to the addon-bar + // TODO: we may want some "first-launch" module to do this only on very + // first execution + if (!container) { + container = this.doc.getElementById("addon-bar"); + // TODO: find a way to make the following code work when we use "cfx run": + // http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#8586 + // until then, force display of addon bar directly from sdk code + // https://bugzilla.mozilla.org/show_bug.cgi?id=627484 + if (container.collapsed) + this.window.toggleAddonBar(); + } + + // Now retrieve a reference to the next toolbar item + // by reading currentset attribute on the toolbar + let nextNode = null; + let currentSet = container.getAttribute("currentset"); + let ids = (currentSet == "__empty") ? [] : currentSet.split(","); + let idx = ids.indexOf(id); + if (idx != -1) { + for (let i = idx; i < ids.length; i++) { + nextNode = this.doc.getElementById(ids[i]); + if (nextNode) + break; + } + } + + // Finally insert our widget in the right toolbar and in the right position + container.insertItem(id, nextNode, null, false); + + // Update DOM in order to save position: which toolbar, and which position + // in this toolbar. But only do this the first time we add it to the toolbar + // Otherwise, this code will collide with other instance of Widget module + // during Firefox startup. See bug 685929. + if (ids.indexOf(id) == -1) { + container.setAttribute("currentset", container.currentSet); + // Save DOM attribute in order to save position on new window opened + this.window.document.persist(container.id, "currentset"); + } + } +} + + +/** + * Final Widget class that handles chrome DOM Node: + * - create initial DOM nodes + * - receive instruction from WidgetView through update method and update DOM + * - watch for DOM events and forward them to WidgetView + */ +function WidgetChrome(options) { + this.window = options.window; + this._doc = options.doc; + this._widget = options.widget; + this._symbiont = null; // set later + this.node = null; // set later + + this._createNode(); +} + +// Update a property of a widget. +WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) { + switch(property) { + case "contentURL": + case "content": + this.setContent(); + break; + case "width": + this.node.style.minWidth = value + "px"; + this.node.querySelector("iframe").style.width = value + "px"; + break; + case "tooltip": + this.node.setAttribute("tooltiptext", value); + break; + case "postMessage": + this._symbiont.postMessage(value); + break; + case "emit": + let port = this._symbiont.port; + port.emit.apply(port, value); + break; + } +} + +// Add a widget to this window. +WidgetChrome.prototype._createNode = function WC__createNode() { + // XUL element container for widget + let node = this._doc.createElement("toolbaritem"); + let guid = String(uuid()); + + // Temporary work around require("self") failing on unit-test execution ... + let jetpackID = "testID"; + try { + jetpackID = require("self").id; + } catch(e) {} + + // Compute an unique and stable widget id with jetpack id and widget.id + let id = "widget:" + jetpackID + "-" + this._widget.id; + node.setAttribute("id", id); + node.setAttribute("label", this._widget.label); + node.setAttribute("tooltiptext", this._widget.tooltip); + node.setAttribute("align", "center"); + // Bug 626326: Prevent customize toolbar context menu to appear + node.setAttribute("context", ""); + + // TODO move into a stylesheet, configurable by consumers. + // Either widget.style, exposing the style object, or a URL + // (eg, can load local stylesheet file). + node.setAttribute("style", [ + "overflow: hidden; margin: 1px 2px 1px 2px; padding: 0px;", + "min-height: 16px;", + ].join("")); + + node.style.minWidth = this._widget.width + "px"; + + this.node = node; +} + +// Initial population of a widget's content. +WidgetChrome.prototype.fill = function WC_fill() { + // Create element + var iframe = this._doc.createElement("iframe"); + iframe.setAttribute("type", "content"); + iframe.setAttribute("transparent", "transparent"); + iframe.style.overflow = "hidden"; + iframe.style.height = "16px"; + iframe.style.maxHeight = "16px"; + iframe.style.width = this._widget.width + "px"; + iframe.setAttribute("flex", "1"); + iframe.style.border = "none"; + iframe.style.padding = "0px"; + + // Do this early, because things like contentWindow are null + // until the node is attached to a document. + this.node.appendChild(iframe); + + // add event handlers + this.addEventHandlers(); + + // set content + this.setContent(); +} + +// Get widget content type. +WidgetChrome.prototype.getContentType = function WC_getContentType() { + if (this._widget.content) + return CONTENT_TYPE_HTML; + return (this._widget.contentURL && /\.(jpg|gif|png|ico)$/.test(this._widget.contentURL)) + ? CONTENT_TYPE_IMAGE : CONTENT_TYPE_URI; +} + +// Set widget content. +WidgetChrome.prototype.setContent = function WC_setContent() { + let type = this.getContentType(); + let contentURL = null; + + switch (type) { + case CONTENT_TYPE_HTML: + contentURL = "data:text/html," + encodeURIComponent(this._widget.content); + break; + case CONTENT_TYPE_URI: + contentURL = this._widget.contentURL; + break; + case CONTENT_TYPE_IMAGE: + let imageURL = this._widget.contentURL; + contentURL = "data:text/html,<html><body><img src='" + + encodeURI(imageURL) + "'></body></html>"; + break; + default: + throw new Error("The widget's type cannot be determined."); + } + + let iframe = this.node.firstElementChild; + + let self = this; + // Cleanup previously created symbiont (in case we are update content) + if (this._symbiont) + this._symbiont.destroy(); + + this._symbiont = Trait.compose(Symbiont.resolve({ + _onContentScriptEvent: "_onContentScriptEvent-not-used" + }), { + _onContentScriptEvent: function () { + // Redirect events to WidgetView + self._widget._onPortEvent(arguments); + } + })({ + frame: iframe, + contentURL: contentURL, + contentScriptFile: this._widget.contentScriptFile, + contentScript: this._widget.contentScript, + contentScriptWhen: this._widget.contentScriptWhen, + allow: this._widget.allow, + onMessage: function(message) { + setTimeout(function() { + self._widget._onEvent("message", message); + }, 0); + } + }); +} + +// Detect if document consists of a single image. +WidgetChrome._isImageDoc = function WC__isImageDoc(doc) { + return doc.body.childNodes.length == 1 && + doc.body.firstElementChild && + doc.body.firstElementChild.tagName == "IMG"; +} + +// Set up all supported events for a widget. +WidgetChrome.prototype.addEventHandlers = function WC_addEventHandlers() { + let contentType = this.getContentType(); + + let self = this; + let listener = function(e) { + // Ignore event firings that target the iframe. + if (e.target == self.node.firstElementChild) + return; + + // The widget only supports left-click for now, + // so ignore right-clicks. + if (e.type == "click" && e.button == 2) + return; + + // Proxy event to the widget + setTimeout(function() { + self._widget._onEvent(EVENTS[e.type], null, self.node); + }, 0); + }; + + this.eventListeners = {}; + let iframe = this.node.firstElementChild; + for (let [type, method] in Iterator(EVENTS)) { + iframe.addEventListener(type, listener, true, true); + + // Store listeners for later removal + this.eventListeners[type] = listener; + } + + // On document load, make modifications required for nice default + // presentation. + function loadListener(e) { + let containerStyle = self.window.getComputedStyle(self.node.parentNode); + // Ignore event firings that target the iframe + if (e.target == iframe) + return; + // Ignore about:blank loads + if (e.type == "load" && e.target.location == "about:blank") + return; + + // We may have had an unload event before that cleaned up the symbiont + if (!self._symbiont) + self.setContent(); + + let doc = e.target; + if (contentType == CONTENT_TYPE_IMAGE || WidgetChrome._isImageDoc(doc)) { + // Force image content to size. + // Add-on authors must size their images correctly. + doc.body.firstElementChild.style.width = self._widget.width + "px"; + doc.body.firstElementChild.style.height = "16px"; + } + + // Extend the add-on bar's default text styles to the widget. + doc.body.style.color = containerStyle.color; + doc.body.style.fontFamily = containerStyle.fontFamily; + doc.body.style.fontSize = containerStyle.fontSize; + doc.body.style.fontWeight = containerStyle.fontWeight; + doc.body.style.textShadow = containerStyle.textShadow; + // Allow all content to fill the box by default. + doc.body.style.margin = "0"; + } + iframe.addEventListener("load", loadListener, true); + this.eventListeners["load"] = loadListener; + + // Register a listener to unload symbiont if the toolbaritem is moved + // on user toolbars customization + function unloadListener(e) { + if (e.target.location == "about:blank") + return; + self._symbiont.destroy(); + self._symbiont = null; + // This may fail but not always, it depends on how the node is + // moved or removed + try { + self.setContent(); + } catch(e) {} + + } + + iframe.addEventListener("unload", unloadListener, true); + this.eventListeners["unload"] = unloadListener; +} + +// Remove and unregister the widget from everything +WidgetChrome.prototype.destroy = function WC_destroy(removedItems) { + // remove event listeners + for (let [type, listener] in Iterator(this.eventListeners)) + this.node.firstElementChild.removeEventListener(type, listener, true); + // remove dom node + this.node.parentNode.removeChild(this.node); + // cleanup symbiont + this._symbiont.destroy(); + // cleanup itself + this.eventListeners = null; + this._widget = null; + this._symbiont = null; +} + +// Init the browserManager only after setting prototypes and such above, because +// it will cause browserManager.onTrack to be called immediately if there are +// open windows. +browserManager.init(); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/lib/windows.js b/tools/addon-sdk-1.7/packages/addon-kit/lib/windows.js new file mode 100644 index 0000000..8f13e6c --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/lib/windows.js @@ -0,0 +1,210 @@ +/* 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"; + +if (!require("api-utils/xul-app").is("Firefox")) { + throw new Error([ + "The windows module currently supports only Firefox. In the future", + " we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=571449 for more information." + ].join("")); +} + +const { Cc, Ci } = require('chrome'), + { Trait } = require('api-utils/traits'), + { List } = require('api-utils/list'), + { EventEmitter } = require('api-utils/events'), + { WindowTabs, WindowTabTracker } = require('api-utils/windows/tabs'), + { WindowDom } = require('api-utils/windows/dom'), + { WindowLoader } = require('api-utils/windows/loader'), + { WindowTrackerTrait } = require('api-utils/window-utils'), + { Options } = require('api-utils/tabs/tab'), + apiUtils = require('api-utils/api-utils'), + unload = require('api-utils/unload'), + + WM = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator), + + BROWSER = 'navigator:browser'; + +/** + * Window trait composes safe wrappers for browser window that are E10S + * compatible. + */ +const BrowserWindowTrait = Trait.compose( + EventEmitter, + WindowDom.resolve({ close: '_close' }), + WindowTabs, + WindowTabTracker, + WindowLoader, + /* WindowSidebars, */ + Trait.compose({ + _emit: Trait.required, + _close: Trait.required, + _load: Trait.required, + /** + * Constructor returns wrapper of the specified chrome window. + * @param {nsIWindow} window + */ + constructor: function BrowserWindow(options) { + // Register this window ASAP, in order to avoid loop that would try + // to create this window instance over and over (see bug 648244) + windows.push(this); + + // make sure we don't have unhandled errors + this.on('error', console.exception.bind(console)); + + if ('onOpen' in options) + this.on('open', options.onOpen); + if ('onClose' in options) + this.on('close', options.onClose); + if ('window' in options) + this._window = options.window; + if ('tabs' in options) { + this._tabOptions = Array.isArray(options.tabs) ? + options.tabs.map(Options) : + [ Options(options.tabs) ]; + } + else if ('url' in options) { + this._tabOptions = [ Options(options.url) ]; + } + this._load(); + return this; + }, + destroy: function () this._onUnload(), + _tabOptions: [], + _onLoad: function() { + try { + this._initWindowTabTracker(); + } catch(e) { + this._emit('error', e) + } + this._emitOnObject(browserWindows, 'open', this._public); + }, + _onUnload: function() { + if (!this._window) + return; + this._destroyWindowTabTracker(); + this._emitOnObject(browserWindows, 'close', this._public); + this._window = null; + // Removing reference from the windows array. + windows.splice(windows.indexOf(this), 1); + this._removeAllListeners(); + }, + close: function close(callback) { + // maybe we should deprecate this with message ? + if (callback) this.on('close', callback); + return this._close(); + } + }) +); +/** + * Wrapper for `BrowserWindowTrait`. Creates new instance if wrapper for + * window doesn't exists yet. If wrapper already exists then returns it + * instead. + * @params {Object} options + * Options that are passed to the the `BrowserWindowTrait` + * @returns {BrowserWindow} + * @see BrowserWindowTrait + */ +function BrowserWindow(options) { + let chromeWindow = options.window; + for each (let window in windows) { + if (chromeWindow == window._window) + return window._public + } + let window = BrowserWindowTrait(options); + return window._public; +} +// to have proper `instanceof` behavior will go away when #596248 is fixed. +BrowserWindow.prototype = BrowserWindowTrait.prototype; +exports.BrowserWindow = BrowserWindow +const windows = []; + +/** + * `BrowserWindows` trait is composed out of `List` trait and it represents + * "live" list of currently open browser windows. Instance mutates itself + * whenever new browser window gets opened / closed. + */ +// Very stupid to resolve all `toStrings` but this will be fixed by #596248 +const browserWindows = Trait.resolve({ toString: null }).compose( + List.resolve({ constructor: '_initList' }), + EventEmitter.resolve({ toString: null }), + WindowTrackerTrait.resolve({ constructor: '_initTracker', toString: null }), + Trait.compose({ + _emit: Trait.required, + _add: Trait.required, + _remove: Trait.required, + + // public API + + /** + * Constructor creates instance of `Windows` that represents live list of open + * windows. + */ + constructor: function BrowserWindows() { + this._trackedWindows = []; + this._initList(); + this._initTracker(); + unload.ensure(this, "_destructor"); + }, + _destructor: function _destructor() { + this._removeAllListeners('open'); + this._removeAllListeners('close'); + this._clear(); + }, + /** + * This property represents currently active window. + * Property is non-enumerable, in order to preserve array like enumeration. + * @type {Window|null} + */ + get activeWindow() { + let window = WM.getMostRecentWindow(BROWSER); + return this._isBrowser(window) ? BrowserWindow({ window: window }) : null; + }, + open: function open(options) { + if (typeof options === "string") + // `tabs` option is under review and may be removed. + options = { tabs: [Options(options)] }; + return BrowserWindow(options); + }, + /** + * Returns true if specified window is a browser window. + * @param {nsIWindow} window + * @returns {Boolean} + */ + _isBrowser: function _isBrowser(window) + BROWSER === window.document.documentElement.getAttribute("windowtype") + , + /** + * Internal listener which is called whenever new window gets open. + * Creates wrapper and adds to this list. + * @param {nsIWindow} chromeWindow + */ + _onTrack: function _onTrack(chromeWindow) { + if (!this._isBrowser(chromeWindow)) return; + let window = BrowserWindow({ window: chromeWindow }); + this._add(window); + this._emit('open', window); + }, + /** + * Internal listener which is called whenever window gets closed. + * Cleans up references and removes wrapper from this list. + * @param {nsIWindow} window + */ + _onUntrack: function _onUntrack(chromeWindow) { + if (!this._isBrowser(chromeWindow)) return; + let window = BrowserWindow({ window: chromeWindow }); + this._remove(window); + this._emit('close', window); + + // Bug 724404: do not leak this module and linked windows: + // We have to do it on untrack and not only when `_onUnload` is called + // when windows are closed, otherwise, we will leak on addon disabling. + window.destroy(); + } + }).resolve({ toString: null }) +)(); +exports.browserWindows = browserWindows; + diff --git a/tools/addon-sdk-1.7/packages/addon-kit/locale/en-GB.properties b/tools/addon-sdk-1.7/packages/addon-kit/locale/en-GB.properties new file mode 100644 index 0000000..08db753 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/locale/en-GB.properties @@ -0,0 +1,11 @@ +# 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/. + +Translated= Yes + +downloadsCount=%d downloads +downloadsCount[one]=one download + +pluralTest=fallback to other +pluralTest[zero]=optional zero form diff --git a/tools/addon-sdk-1.7/packages/addon-kit/locale/eo.properties b/tools/addon-sdk-1.7/packages/addon-kit/locale/eo.properties new file mode 100644 index 0000000..a979fca --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/locale/eo.properties @@ -0,0 +1,5 @@ +# 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/. + +Translated= jes diff --git a/tools/addon-sdk-1.7/packages/addon-kit/locale/fr-FR.properties b/tools/addon-sdk-1.7/packages/addon-kit/locale/fr-FR.properties new file mode 100644 index 0000000..2c5ffbb --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/locale/fr-FR.properties @@ -0,0 +1,14 @@ +# 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/. + +Translated= Oui + +placeholderString= Placeholder %s + +# Plural forms +%d downloads=%d téléchargements +%d downloads[one]=%d téléchargement + +downloadsCount=%d téléchargements +downloadsCount[one]=%d téléchargement diff --git a/tools/addon-sdk-1.7/packages/addon-kit/package.json b/tools/addon-sdk-1.7/packages/addon-kit/package.json new file mode 100644 index 0000000..8cd643f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/package.json @@ -0,0 +1,12 @@ +{ + "name": "addon-kit", + "description": "Add-on development made easy.", + "keywords": ["javascript", "engine", "platform", "xulrunner", "jetpack-high-level"], + "author": "Atul Varma (http://toolness.com/) <atul@mozilla.com>", + "contributors": [ + "Myk Melez (http://melez.com/) <myk@mozilla.org>", + "Daniel Aquino <mr.danielaquino@gmail.com>" + ], + "license": "MPL 2.0", + "dependencies": ["api-utils"] +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/helpers.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/helpers.js new file mode 100644 index 0000000..399046f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/helpers.js @@ -0,0 +1,23 @@ +/* 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 { Loader } = require("@loader"); + +exports.Loader = function(module, globals) { + var options = JSON.parse(JSON.stringify(require("@packaging"))); + options.globals = globals; + let loader = Loader.new(options); + return Object.create(loader, { + require: { value: Loader.require.bind(loader, module.path) }, + sandbox: { value: function sandbox(id) { + let path = options.manifest[module.path].requirements[id].path; + return loader.sandboxes[path].sandbox; + }}, + unload: { value: function unload(reason, callback) { + loader.unload(reason, callback); + }} + }) +}; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/pagemod-test-helpers.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/pagemod-test-helpers.js new file mode 100644 index 0000000..7c741eb --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/pagemod-test-helpers.js @@ -0,0 +1,67 @@ +/* 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 timer = require("timer"); +const xulApp = require("xul-app"); +const { Loader } = require('./helpers'); + +/** + * A helper function that creates a PageMod, then opens the specified URL + * and checks the effect of the page mod on 'onload' event via testCallback. + */ +exports.testPageMod = function testPageMod(test, testURL, pageModOptions, + testCallback, timeout) { + if (!xulApp.versionInRange(xulApp.platformVersion, "1.9.3a3", "*") && + !xulApp.versionInRange(xulApp.platformVersion, "1.9.2.7", "1.9.2.*")) { + test.pass("Note: not testing PageMod, as it doesn't work on this platform version"); + return null; + } + + var wm = Cc['@mozilla.org/appshell/window-mediator;1'] + .getService(Ci.nsIWindowMediator); + var browserWindow = wm.getMostRecentWindow("navigator:browser"); + if (!browserWindow) { + test.pass("page-mod tests: could not find the browser window, so " + + "will not run. Use -a firefox to run the pagemod tests.") + return null; + } + + if (timeout !== undefined) + test.waitUntilDone(timeout); + else + test.waitUntilDone(); + + let loader = Loader(module); + let pageMod = loader.require("page-mod"); + + var pageMods = [new pageMod.PageMod(opts) for each(opts in pageModOptions)]; + + var tabBrowser = browserWindow.gBrowser; + var newTab = tabBrowser.addTab(testURL); + tabBrowser.selectedTab = newTab; + var b = tabBrowser.getBrowserForTab(newTab); + + function onPageLoad() { + b.removeEventListener("load", onPageLoad, true); + // Delay callback execute as page-mod content scripts may be executed on + // load event. So page-mod actions may not be already done. + // If we delay even more contentScriptWhen:'end', we may want to modify + // this code again. + timer.setTimeout(testCallback, 0, + b.contentWindow.wrappedJSObject, + function done() { + pageMods.forEach(function(mod) mod.destroy()); + // XXX leaks reported if we don't close the tab? + tabBrowser.removeTab(newTab); + loader.unload(); + test.done(); + }); + } + b.addEventListener("load", onPageLoad, true); + + return pageMods; +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-addon-page.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-addon-page.js new file mode 100644 index 0000000..c0ee35d --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-addon-page.js @@ -0,0 +1,51 @@ +/* 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 { isTabOpen, activateTab, openTab, closeTab } = require('api-utils/tabs/utils'); +const windows = require('api-utils/window-utils'); +const { Loader } = require('./helpers'); +const { setTimeout } = require('api-utils/timer'); + +let uri = require('self').data.url('index.html'); + +function isChromeVisible(window) + window.document.documentElement.getAttribute('disablechrome') !== 'true' + +exports['test that add-on page has no chrome'] = function(assert, done) { + let loader = Loader(module); + loader.require('addon-kit/addon-page'); + + let window = windows.activeBrowserWindow; + let tab = openTab(window, uri); + + assert.ok(isChromeVisible(window), 'chrome is visible for non addon page'); + + // need to do this in another turn to make sure event listener + // that sets property has time to do that. + setTimeout(function() { + activateTab(tab); + + assert.ok(!isChromeVisible(window), 'chrome is not visible for addon page'); + + closeTab(tab); + assert.ok(isChromeVisible(window), 'chrome is visible again'); + loader.unload(); + done(); + }); +}; + +exports['test that add-on pages is closed on unload'] = function(assert) { + let loader = Loader(module); + loader.require('addon-kit/addon-page'); + + let tab = openTab(windows.activeBrowserWindow, uri); + loader.unload(); + + assert.ok(isTabOpen(tab), 'add-on page tabs are closed on unload'); +}; + + +require('test').run(exports); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-clipboard.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-clipboard.js new file mode 100644 index 0000000..1819d68 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-clipboard.js @@ -0,0 +1,64 @@ +/* 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/. */ + + +// Test the typical use case, setting & getting with no flavors specified +exports.testWithNoFlavor = function(test) { + var contents = "hello there"; + var flavor = "text"; + var fullFlavor = "text/unicode"; + var clip = require("clipboard"); + // Confirm we set the clipboard + test.assert(clip.set(contents)); + // Confirm flavor is set + test.assertEqual(clip.currentFlavors[0], flavor); + // Confirm we set the clipboard + test.assertEqual(clip.get(), contents); + // Confirm we can get the clipboard using the flavor + test.assertEqual(clip.get(flavor), contents); + // Confirm we can still get the clipboard using the full flavor + test.assertEqual(clip.get(fullFlavor), contents); +}; + +// Test the slightly less common case where we specify the flavor +exports.testWithFlavor = function(test) { + var contents = "<b>hello there</b>"; + var contentsText = "hello there"; + var flavor = "html"; + var fullFlavor = "text/html"; + var unicodeFlavor = "text"; + var unicodeFullFlavor = "text/unicode"; + var clip = require("clipboard"); + test.assert(clip.set(contents, flavor)); + test.assertEqual(clip.currentFlavors[0], unicodeFlavor); + test.assertEqual(clip.currentFlavors[1], flavor); + test.assertEqual(clip.get(), contentsText); + test.assertEqual(clip.get(flavor), contents); + test.assertEqual(clip.get(fullFlavor), contents); + test.assertEqual(clip.get(unicodeFlavor), contentsText); + test.assertEqual(clip.get(unicodeFullFlavor), contentsText); +}; + +// Test that the typical case still works when we specify the flavor to set +exports.testWithRedundantFlavor = function(test) { + var contents = "<b>hello there</b>"; + var flavor = "text"; + var fullFlavor = "text/unicode"; + var clip = require("clipboard"); + test.assert(clip.set(contents, flavor)); + test.assertEqual(clip.currentFlavors[0], flavor); + test.assertEqual(clip.get(), contents); + test.assertEqual(clip.get(flavor), contents); + test.assertEqual(clip.get(fullFlavor), contents); +}; + +exports.testNotInFlavor = function(test) { + var contents = "hello there"; + var flavor = "html"; + var clip = require("clipboard"); + test.assert(clip.set(contents)); + // If there's nothing on the clipboard with this flavor, should return null + test.assertEqual(clip.get(flavor), null); +}; +// TODO: Test error cases. diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-context-menu.html b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-context-menu.html new file mode 100644 index 0000000..4196d5f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-context-menu.html @@ -0,0 +1,45 @@ +<!-- 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/. --> + +<html> + <head> + <title>Context menu test</title> + </head> + <body> + <p> + <img id="image" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="> + </p> + + <p> + <a id="link" href=""> + A simple link. + </a> + </p> + + <p> + <a href=""> + <span id="span-link"> + A span inside a link. + </span> + </a> + </p> + + <p id="text"> + Some text. + </p> + + <p> + <textarea id="textfield"> + A text field, + with some text. + </textarea> + </p> + + <p> + <iframe id="iframe" src="data:text/html,An iframe." + width="200" height="100"> + </iframe> + </p> + </body> +</html> diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-context-menu.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-context-menu.js new file mode 100644 index 0000000..c398933 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-context-menu.js @@ -0,0 +1,2067 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +let {Cc,Ci} = require("chrome"); +const { Loader } = require('./helpers'); + +// These should match the same constants in the module. +const ITEM_CLASS = "jetpack-context-menu-item"; +const SEPARATOR_ID = "jetpack-context-menu-separator"; +const OVERFLOW_THRESH_DEFAULT = 10; +const OVERFLOW_THRESH_PREF = + "extensions.addon-sdk.context-menu.overflowThreshold"; +const OVERFLOW_MENU_ID = "jetpack-content-menu-overflow-menu"; +const OVERFLOW_POPUP_ID = "jetpack-content-menu-overflow-popup"; + +const TEST_DOC_URL = module.uri.replace(/\.js$/, ".html"); + +// Destroying items that were previously created should cause them to be absent +// from the menu. +exports.testConstructDestroy = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + // Create an item. + let item = new loader.cm.Item({ label: "item" }); + test.assertEqual(item.parentMenu, null, "item's parent menu should be null"); + + test.showMenu(null, function (popup) { + + // It should be present when the menu is shown. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Destroy the item. Multiple destroys should be harmless. + item.destroy(); + item.destroy(); + test.showMenu(null, function (popup) { + + // It should be removed from the menu. + test.checkMenu([], [], [item]); + test.done(); + }); + }); +}; + + +// Destroying an item twice should not cause an error. +exports.testDestroyTwice = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + item.destroy(); + item.destroy(); + + test.pass("Destroying an item twice should not cause an error."); + test.done(); +}; + + +// CSS selector contexts should cause their items to be present in the menu +// when the menu is invoked on nodes that match the selectors. +exports.testSelectorContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("img") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// CSS selector contexts should cause their items to be present in the menu +// when the menu is invoked on nodes that have ancestors that match the +// selectors. +exports.testSelectorAncestorContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("a[href]") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("span-link"), function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// CSS selector contexts should cause their items to be absent from the menu +// when the menu is not invoked on nodes that match or have ancestors that +// match the selectors. +exports.testSelectorContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); +}; + + +// Page contexts should cause their items to be present in the menu when the +// menu is not invoked on an active element. +exports.testPageContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1", + context: undefined + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.PageContext() + }), + new loader.cm.Item({ + label: "item 3", + context: [loader.cm.PageContext()] + }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Page contexts should cause their items to be absent from the menu when the +// menu is invoked on an active element. +exports.testPageContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1", + context: undefined + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.PageContext() + }), + new loader.cm.Item({ + label: "item 3", + context: [loader.cm.PageContext()] + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu([], items, []); + test.done(); + }); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists. +exports.testSelectionContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + window.getSelection().selectAllChildren(doc.body); + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists in +// a text field. +exports.testSelectionContextMatchInTextField = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + let textfield = doc.getElementById("textfield"); + textfield.setSelectionRange(0, textfield.value.length); + test.showMenu(textfield, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Selection contexts should not cause items to appear when a selection does +// not exist in a text field. +exports.testSelectionContextNoMatchInTextField = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + let textfield = doc.getElementById("textfield"); + textfield.setSelectionRange(0, 0); + test.showMenu(textfield, function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); + }); +}; + + +// Selection contexts should not cause items to appear when a selection does +// not exist. +exports.testSelectionContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.showMenu(null, function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); +}; + + +// URL contexts should cause items to appear on pages that match. +exports.testURLContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + loader.cm.Item({ + label: "item 0", + context: loader.cm.URLContext(TEST_DOC_URL) + }), + loader.cm.Item({ + label: "item 1", + context: loader.cm.URLContext([TEST_DOC_URL, "*.bogus.com"]) + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// URL contexts should not cause items to appear on pages that do not match. +exports.testURLContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + loader.cm.Item({ + label: "item 0", + context: loader.cm.URLContext("*.bogus.com") + }), + loader.cm.Item({ + label: "item 1", + context: loader.cm.URLContext(["*.bogus.com", "*.gnarly.com"]) + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu([], items, []); + test.done(); + }); + }); +}; + + +// Removing a non-matching URL context after its item is created and the page is +// loaded should cause the item's content script to be evaluated. +exports.testURLContextRemove = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let shouldBeEvaled = false; + let context = loader.cm.URLContext("*.bogus.com"); + let item = loader.cm.Item({ + label: "item", + context: context, + contentScript: 'self.postMessage("ok");', + onMessage: function (msg) { + test.assert(shouldBeEvaled, + "content script should be evaluated when expected"); + shouldBeEvaled = false; + test.done(); + } + }); + + test.withTestDoc(function (window, doc) { + shouldBeEvaled = true; + item.context.remove(context); + }); +}; + + +// Adding a non-matching URL context after its item is created and the page is +// loaded should cause the item's worker to be destroyed. +exports.testURLContextAdd = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ label: "item" }); + + test.withTestDoc(function (window, doc) { + let privatePropsKey = loader.globalScope.PRIVATE_PROPS_KEY; + let workerReg = item.valueOf(privatePropsKey)._workerReg; + + let found = false; + for each (let winWorker in workerReg.winWorkers) { + if (winWorker.win === window) { + found = true; + break; + } + } + this.test.assert(found, "window should be present in worker registry"); + + item.context.add(loader.cm.URLContext("*.bogus.com")); + + for each (let winWorker in workerReg.winWorkers) + this.test.assertNotEqual(winWorker.win, window, + "window should not be present in worker registry"); + + test.done(); + }); +}; + + +// Content contexts that return true should cause their items to be present +// in the menu. +exports.testContentContextMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () true);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// Content contexts that return false should cause their items to be absent +// from the menu. +exports.testContentContextNoMatch = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () false);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); +}; + + +// Content contexts that return a string should cause their items to be present +// in the menu and the items' labels to be updated. +exports.testContentContextMatchString = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "first label", + contentScript: 'self.on("context", function () "second label");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.assertEqual(item.label, "second label", + "item's label should be updated"); + test.done(); + }); +}; + + +// Ensure that contentScripFile is working correctly +exports.testContentScriptFile = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + // Reject remote files + test.assertRaises(function() { + new loader.cm.Item({ + label: "item", + contentScriptFile: "http://mozilla.com/context-menu.js" + }); + }, + "The 'contentScriptFile' option must be a local file URL " + + "or an array of local file URLs.", + "Item throws when contentScriptFile is a remote URL"); + + // But accept files from data folder + let item = new loader.cm.Item({ + label: "item", + contentScriptFile: require("self").data.url("test-context-menu.js") + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// The args passed to context listeners should be correct. +exports.testContentContextArgs = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + let callbacks = 0; + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function (node) {' + + ' let Ci = Components.interfaces;' + + ' self.postMessage(node instanceof Ci.nsIDOMHTMLElement);' + + ' return false;' + + '});', + onMessage: function (isElt) { + test.assert(isElt, "node should be an HTML element"); + if (++callbacks == 2) test.done(); + } + }); + + test.showMenu(null, function () { + if (++callbacks == 2) test.done(); + }); +}; + +// Multiple contexts imply intersection, not union, and content context +// listeners should not be called if all declarative contexts are not current. +exports.testMultipleContexts = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + context: [loader.cm.SelectorContext("a[href]"), loader.cm.PageContext()], + contentScript: 'self.on("context", function () self.postMessage());', + onMessage: function () { + test.fail("Context listener should not be called"); + } + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("span-link"), function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); + }); +}; + +// Once a context is removed, it should no longer cause its item to appear. +exports.testRemoveContext = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item", + context: ctxt + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + + // The item should be present at first. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the img context and check again. + item.context.remove(ctxt); + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu([], [item], []); + test.done(); + }); + }); + }); +}; + + +// Lots of items should overflow into the overflow submenu. +exports.testOverflow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT + 1; i++) { + let item = new loader.cm.Item({ label: "item " + i }); + items.push(item); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Module unload should cause all items to be removed. +exports.testUnload = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + + // The menu should contain the item. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Unload the module. + loader.unload(); + test.showMenu(null, function (popup) { + + // The item should be removed from the menu. + test.checkMenu([], [], [item]); + test.done(); + }); + }); +}; + + +// Using multiple module instances to add items without causing overflow should +// work OK. Assumes OVERFLOW_THRESH_DEFAULT <= 2. +exports.testMultipleModulesAdd = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + // Use each module to add an item, then unload each module in turn. + let item0 = new loader0.cm.Item({ label: "item 0" }); + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain both items. + test.checkMenu([item0, item1], [], []); + popup.hidePopup(); + + // Unload the first module. + loader0.unload(); + test.showMenu(null, function (popup) { + + // The first item should be removed from the menu. + test.checkMenu([item1], [], [item0]); + popup.hidePopup(); + + // Unload the second module. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to add items causing overflow should work OK. +exports.testMultipleModulesAddOverflow = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + // Use module 0 to add OVERFLOW_THRESH_DEFAULT items. + let items0 = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i++) { + let item = new loader0.cm.Item({ label: "item 0 " + i }); + items0.push(item); + } + + // Use module 1 to add one item. + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let allItems = items0.concat(item1); + + test.showMenu(null, function (popup) { + + // The menu should contain all items in overflow. + test.checkMenu(allItems, [], []); + popup.hidePopup(); + + // Unload the first module. + loader0.unload(); + test.showMenu(null, function (popup) { + + // The first items should be removed from the menu, which should not + // overflow. + test.checkMenu([item1], [], items0); + popup.hidePopup(); + + // Unload the second module. + loader1.unload(); + test.showMenu(null, function (popup) { + + // All items should be removed from the menu. + test.checkMenu([], [], allItems); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader0 create item -> loader1 create item -> loader0.unload -> +// loader1.unload +exports.testMultipleModulesDiffContexts1 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item1], [item0], []); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // item0 should be removed from the menu. + test.checkMenu([item1], [], [item0]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader1 create item -> loader0 create item -> loader0.unload -> +// loader1.unload +exports.testMultipleModulesDiffContexts2 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item1], [item0], []); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // item0 should be removed from the menu. + test.checkMenu([item1], [], [item0]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader0 create item -> loader1 create item -> loader1.unload -> +// loader0.unload +exports.testMultipleModulesDiffContexts3 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item1], [item0], []); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // item1 should be removed from the menu. + test.checkMenu([], [item0], [item1]); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader1 create item -> loader0 create item -> loader1.unload -> +// loader0.unload +exports.testMultipleModulesDiffContexts4 = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item1], [item0], []); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // item1 should be removed from the menu. + test.checkMenu([], [item0], [item1]); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Test interactions between a loaded module, unloading another module, and the +// menu separator and overflow submenu. +exports.testMultipleModulesAddRemove = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item = new loader0.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + + // The menu should contain the item. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the item. + item.destroy(); + test.showMenu(null, function (popup) { + + // The item should be removed from the menu. + test.checkMenu([], [], [item]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // There shouldn't be any errors involving the menu separator or + // overflow submenu. + test.checkMenu([], [], [item]); + test.done(); + }); + }); + }); +}; + + +// An item's click listener should work. +exports.testItemClick = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item data", + contentScript: 'self.on("click", function (node, data) {' + + ' let Ci = Components.interfaces;' + + ' self.postMessage({' + + ' isElt: node instanceof Ci.nsIDOMHTMLElement,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + test.assertEqual(this, item, "`this` inside onMessage should be item"); + test.assert(data.isElt, "node should be an HTML element"); + test.assertEqual(data.data, item.data, "data should be item data"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + let elt = test.getItemElt(popup, item); + elt.click(); + }); +}; + + +// A menu's click listener should work and receive bubbling clicks from +// sub-items appropriately. This also tests menus and ensures that when a CSS +// selector context matches the clicked node's ancestor, the matching ancestor +// is passed to listeners as the clicked node. +exports.testMenuClick = function (test) { + // Create a top-level menu, submenu, and item, like this: + // topMenu -> submenu -> item + // Click the item and make sure the click bubbles. + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "submenu item", + data: "submenu item data" + }); + + let submenu = new loader.cm.Menu({ + label: "submenu", + items: [item] + }); + + let topMenu = new loader.cm.Menu({ + label: "top menu", + contentScript: 'self.on("click", function (node, data) {' + + ' let Ci = Components.interfaces;' + + ' self.postMessage({' + + ' isAnchor: node instanceof Ci.nsIDOMHTMLAnchorElement,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + test.assertEqual(this, topMenu, "`this` inside top menu should be menu"); + test.assert(data.isAnchor, "Clicked node should be anchor"); + test.assertEqual(data.data, item.data, + "Clicked item data should be correct"); + test.done(); + }, + items: [submenu], + context: loader.cm.SelectorContext("a") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("span-link"), function (popup) { + test.checkMenu([topMenu], [], []); + let topMenuElt = test.getItemElt(popup, topMenu); + let topMenuPopup = topMenuElt.firstChild; + let submenuElt = test.getItemElt(topMenuPopup, submenu); + let submenuPopup = submenuElt.firstChild; + let itemElt = test.getItemElt(submenuPopup, item); + itemElt.click(); + }); + }); +}; + +// Click listeners should work when multiple modules are loaded. +exports.testItemClickMultipleModules = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = loader0.cm.Item({ + label: "loader 0 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.fail("loader 0 item should not emit click event"); + } + }); + let item1 = loader1.cm.Item({ + label: "loader 1 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.pass("loader 1 item clicked as expected"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item0, item1], [], []); + let item1Elt = test.getItemElt(popup, item1); + item1Elt.click(); + }); +}; + + +// Adding a separator to a submenu should work OK. +exports.testSeparator = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = new loader.cm.Menu({ + label: "submenu", + items: [new loader.cm.Separator()] + }); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Existing context menu modifications should apply to new windows. +exports.testNewWindow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.withNewWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// When a new window is opened, items added by an unloaded module should not +// be present in the menu. +exports.testNewWindowMultipleModules = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + loader.unload(); + test.withNewWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([], [], []); + test.done(); + }); + }); + }); +}; + + +// Items in the context menu should be sorted according to locale. +exports.testSorting = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + // Make an unsorted items list. It'll look like this: + // item 1, item 0, item 3, item 2, item 5, item 4, ... + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i += 2) { + items.push(new loader.cm.Item({ label: "item " + (i + 1) })); + items.push(new loader.cm.Item({ label: "item " + i })); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Items in the overflow menu should be sorted according to locale. +exports.testSortingOverflow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + // Make an unsorted items list. It'll look like this: + // item 1, item 0, item 3, item 2, item 5, item 4, ... + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT * 2; i += 2) { + items.push(new loader.cm.Item({ label: "item " + (i + 1) })); + items.push(new loader.cm.Item({ label: "item " + i })); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Multiple modules shouldn't interfere with sorting. +exports.testSortingMultipleModules = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let items0 = []; + let items1 = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i++) { + if (i % 2) { + let item = new loader0.cm.Item({ label: "item " + i }); + items0.push(item); + } + else { + let item = new loader1.cm.Item({ label: "item " + i }); + items1.push(item); + } + } + let allItems = items0.concat(items1); + + test.showMenu(null, function (popup) { + + // All items should be present and sorted. + test.checkMenu(allItems, [], []); + popup.hidePopup(); + loader0.unload(); + loader1.unload(); + test.showMenu(null, function (popup) { + + // All items should be removed. + test.checkMenu([], [], allItems); + test.done(); + }); + }); +}; + + +// The binary search of insertionPoint should work OK. +exports.testInsertionPoint = function (test) { + function mockElts(labels) { + return labels.map(function (label) { + return { label: label, getAttribute: function (l) label }; + }); + } + + test = new TestHelper(test); + let loader = test.newLoader(); + let insertionPoint = loader.globalScope.insertionPoint; + + let ip = insertionPoint("a", []); + test.assertStrictEqual(ip, null, "Insertion point should be null"); + + ip = insertionPoint("a", mockElts(["b"])); + test.assertEqual(ip.label, "b", "Insertion point should be 'b'"); + + ip = insertionPoint("c", mockElts(["b"])); + test.assertStrictEqual(ip, null, "Insertion point should be null"); + + ip = insertionPoint("b", mockElts(["a", "c"])); + test.assertEqual(ip.label, "c", "Insertion point should be 'c'"); + + ip = insertionPoint("c", mockElts(["a", "b", "d"])); + test.assertEqual(ip.label, "d", "Insertion point should be 'd'"); + + ip = insertionPoint("a", mockElts(["b", "c", "d"])); + test.assertEqual(ip.label, "b", "Insertion point should be 'b'"); + + ip = insertionPoint("d", mockElts(["a", "b", "c"])); + test.assertStrictEqual(ip, null, "Insertion point should be null"); + + test.done(); +}; + + +// Content click handlers and context handlers should be able to communicate, +// i.e., they're eval'ed in the same worker and sandbox. +exports.testContentCommunication = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'var potato;' + + 'self.on("context", function () {' + + ' potato = "potato";' + + ' return true;' + + '});' + + 'self.on("click", function () {' + + ' self.postMessage(potato);' + + '});', + }); + + item.on("message", function (data) { + test.assertEqual(data, "potato", "That's a lot of potatoes!"); + test.done(); + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + let elt = test.getItemElt(popup, item); + elt.click(); + }); +}; + + +// When the context menu is invoked on a tab that was already open when the +// module was loaded, it should contain the expected items and content workers +// should function as expected. +exports.testLoadWithOpenTab = function (test) { + test = new TestHelper(test); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item", + contentScript: + 'self.on("click", function () self.postMessage("click"));', + onMessage: function (msg) { + if (msg === "click") + test.done(); + } + }); + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.getItemElt(popup, item).click(); + }); + }); +}; + + +// Setting an item's label before the menu is ever shown should correctly change +// its label and, if necessary, its order within the menu. +exports.testSetLabelBeforeShow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ] + items[0].label = "z"; + test.assertEqual(items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu([items[1], items[0]], [], []); + test.done(); + }); +}; + + +// Setting an item's label after the menu is shown should correctly change its +// label and, if necessary, its order within the menu. +exports.testSetLabelAfterShow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + popup.hidePopup(); + + items[0].label = "z"; + test.assertEqual(items[0].label, "z"); + test.showMenu(null, function (popup) { + test.checkMenu([items[1], items[0]], [], []); + test.done(); + }); + }); +}; + + +// Setting an item's label before the menu is ever shown should correctly change +// its label and, if necessary, its order within the menu if the item is in the +// overflow submenu. +exports.testSetLabelBeforeShowOverflow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let prefs = loader.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ] + items[0].label = "z"; + test.assertEqual(items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu([items[1], items[0]], [], []); + prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); + test.done(); + }); +}; + + +// Setting an item's label after the menu is shown should correctly change its +// label and, if necessary, its order within the menu if the item is in the +// overflow submenu. +exports.testSetLabelAfterShowOverflow = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let prefs = loader.loader.require("preferences-service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + popup.hidePopup(); + + items[0].label = "z"; + test.assertEqual(items[0].label, "z"); + test.showMenu(null, function (popup) { + test.checkMenu([items[1], items[0]], [], []); + prefs.set(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); + test.done(); + }); + }); +}; + + +// Setting the label of an item in a Menu should work. +exports.testSetLabelMenuItem = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [loader.cm.Item({ label: "a" })] + }); + menu.items[0].label = "z"; + + test.assertEqual(menu.items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.addItem() should work. +exports.testMenuAddItem = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }) + ] + }); + menu.addItem(loader.cm.Item({ label: "item 1" })); + menu.addItem(loader.cm.Item({ label: "item 2" })); + + test.assertEqual(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + test.assertEqual(menu.items[i].label, "item " + i, + "item label should be correct"); + test.assertEqual(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding the same item twice to a menu should work as expected. +exports.testMenuAddItemTwice = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [] + }); + let subitem = loader.cm.Item({ label: "item 1" }) + menu.addItem(subitem); + menu.addItem(loader.cm.Item({ label: "item 0" })); + menu.addItem(subitem); + + test.assertEqual(menu.items.length, 2, + "menu should have correct number of items"); + for (let i = 0; i < 2; i++) { + test.assertEqual(menu.items[i].label, "item " + i, + "item label should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.removeItem() should work. +exports.testMenuRemoveItem = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item 1" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }), + subitem, + loader.cm.Item({ label: "item 2" }) + ] + }); + + // Removing twice should be harmless. + menu.removeItem(subitem); + menu.removeItem(subitem); + + test.assertEqual(subitem.parentMenu, null, + "item's parent menu should be correct"); + + test.assertEqual(menu.items.length, 2, + "menu should have correct number of items"); + test.assertEqual(menu.items[0].label, "item 0", + "item label should be correct"); + test.assertEqual(menu.items[1].label, "item 2", + "item label should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding an item currently contained in one menu to another menu should work. +exports.testMenuItemSwap = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu0 = loader.cm.Menu({ + label: "menu 0", + items: [subitem] + }); + let menu1 = loader.cm.Menu({ + label: "menu 1", + items: [] + }); + menu1.addItem(subitem); + + test.assertEqual(menu0.items.length, 0, + "menu should have correct number of items"); + + test.assertEqual(menu1.items.length, 1, + "menu should have correct number of items"); + test.assertEqual(menu1.items[0].label, "item", + "item label should be correct"); + + test.assertEqual(subitem.parentMenu, menu1, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu0, menu1], [], []); + test.done(); + }); +}; + + +// Destroying an item should remove it from its parent menu. +exports.testMenuItemDestroy = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [subitem] + }); + subitem.destroy(); + + test.assertEqual(menu.items.length, 0, + "menu should have correct number of items"); + test.assertEqual(subitem.parentMenu, null, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Setting Menu.items should work. +exports.testMenuItemsSetter = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "old item 0" }), + loader.cm.Item({ label: "old item 1" }) + ] + }); + menu.items = [ + loader.cm.Item({ label: "new item 0" }), + loader.cm.Item({ label: "new item 1" }), + loader.cm.Item({ label: "new item 2" }) + ]; + + test.assertEqual(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + test.assertEqual(menu.items[i].label, "new item " + i, + "item label should be correct"); + test.assertEqual(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Setting Item.data should work. +exports.testItemDataSetter = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ label: "old item 0", data: "old" }); + item.data = "new"; + + test.assertEqual(item.data, "new", "item should have correct data"); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// Open the test doc, load the module, make sure items appear when context- +// clicking the iframe. +exports.testAlreadyOpenIframe = function (test) { + test = new TestHelper(test); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item" + }); + test.showMenu(doc.getElementById("iframe"), function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Test image support. +exports.testItemImage = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let imageURL = require("self").data.url("moz_favicon.ico"); + let item = new loader.cm.Item({ label: "item", image: imageURL }); + let menu = new loader.cm.Menu({ label: "menu", image: imageURL, items: [] }); + + test.showMenu(null, function (popup) { + test.checkMenu([item, menu], [], []); + + let imageURL2 = require("self").data.url("dummy.ico"); + item.image = imageURL2; + menu.image = imageURL2; + test.checkMenu([item, menu], [], []); + + item.image = null; + menu.image = null; + test.checkMenu([item, menu], [], []); + + test.done(); + }); +}; + + +// Menu.destroy should destroy the item tree rooted at that menu. +exports.testMenuDestroy = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }), + loader.cm.Menu({ + label: "item 1", + items: [ + loader.cm.Item({ label: "subitem 0" }), + loader.cm.Item({ label: "subitem 1" }), + loader.cm.Item({ label: "subitem 2" }) + ] + }), + loader.cm.Item({ label: "item 2" }) + ] + }); + menu.destroy(); + + let numRegistryEntries = 0; + loader.globalScope.browserManager.browserWins.forEach(function (bwin) { + for (let itemID in bwin.items) + numRegistryEntries++; + }); + test.assertEqual(numRegistryEntries, 0, "All items should be unregistered."); + + test.showMenu(null, function (popup) { + test.checkMenu([], [], [menu]); + test.done(); + }); +}; + + +// NO TESTS BELOW THIS LINE! /////////////////////////////////////////////////// + +// Run only a dummy test if context-menu doesn't support the host app. +if (!require("xul-app").is("Firefox")) { + for (let [prop, val] in Iterator(exports)) + if (/^test/.test(prop) && typeof(val) === "function") + delete exports[prop]; + exports.testAppNotSupported = function (test) { + test.pass("context-menu does not support this application."); + }; +} + + +// This makes it easier to run tests by handling things like opening the menu, +// opening new windows, making assertions, etc. Methods on |test| can be called +// on instances of this class. Don't forget to call done() to end the test! +// WARNING: This looks up items in popups by comparing labels, so don't give two +// items the same label. +function TestHelper(test) { + // default waitUntilDone timeout is 10s, which is too short on the win7 + // buildslave + test.waitUntilDone(30*1000); + this.test = test; + this.loaders = []; + this.browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); +} + +TestHelper.prototype = { + get contextMenuPopup() { + return this.browserWindow.document.getElementById("contentAreaContextMenu"); + }, + + get contextMenuSeparator() { + return this.browserWindow.document.getElementById(SEPARATOR_ID); + }, + + get overflowPopup() { + return this.browserWindow.document.getElementById(OVERFLOW_POPUP_ID); + }, + + get overflowSubmenu() { + return this.browserWindow.document.getElementById(OVERFLOW_MENU_ID); + }, + + get tabBrowser() { + return this.browserWindow.gBrowser; + }, + + // Methods on the wrapped test can be called on this object. + __noSuchMethod__: function (methodName, args) { + this.test[methodName].apply(this.test, args); + }, + + // Asserts that absentItems -- an array of items that should not match the + // current context -- aren't present in the menu. + checkAbsentItems: function (presentItems, absentItems) { + for (let i = 0; i < absentItems.length; i++) { + let item = absentItems[i]; + let elt = this.getItemElt(this.contextMenuPopup, item); + + // The implementation actually hides items rather than removing or not + // adding them in the first place, but that's an implementation detail. + this.test.assert(!elt || elt.hidden, + "Item should not be present in top-level menu"); + + if (this.shouldOverflow(presentItems)) { + elt = getItemElt(this.overflowPopup, item); + this.test.assert(!elt || elt.hidden, + "Item should not be present in overflow submenu"); + } + } + }, + + // Asserts that elt, a DOM element representing item, looks OK. + checkItemElt: function (elt, item) { + let itemType = this.getItemType(item); + + switch (itemType) { + case "Item": + this.test.assertEqual(elt.localName, "menuitem", + "Item DOM element should be a xul:menuitem"); + if (typeof(item.data) === "string") { + this.test.assertEqual(elt.getAttribute("value"), item.data, + "Item should have correct data"); + } + break + case "Menu": + this.test.assertEqual(elt.localName, "menu", + "Menu DOM element should be a xul:menu"); + let subPopup = elt.firstChild; + this.test.assert(subPopup, "xul:menu should have a child"); + this.test.assertEqual(subPopup.localName, "menupopup", + "xul:menu's first child should be a menupopup"); + break; + case "Separator": + this.test.assertEqual(elt.localName, "menuseparator", + "Separator DOM element should be a xul:menuseparator"); + break; + } + + if (itemType === "Item" || itemType === "Menu") { + this.test.assertEqual(elt.getAttribute("label"), item.label, + "Item should have correct title"); + if (typeof(item.image) === "string") + this.test.assertEqual(elt.getAttribute("image"), item.image, + "Item should have correct image"); + else + this.test.assert(!elt.hasAttribute("image"), + "Item should not have image"); + } + }, + + // Asserts that the context menu looks OK given the arguments. presentItems + // are items that should match the current context. absentItems are items + // that shouldn't. removedItems are items that have been removed from the + // menu. + checkMenu: function (presentItems, absentItems, removedItems) { + this.checkSeparator(presentItems); + this.checkOverflow(presentItems); + this.checkPresentItems(presentItems); + this.checkAbsentItems(presentItems, absentItems); + this.checkRemovedItems(removedItems); + this.checkSort(presentItems); + }, + + // Asserts that the overflow submenu is present or absent as appropriate for + // presentItems. + checkOverflow: function (presentItems) { + let submenu = this.overflowSubmenu; + if (this.shouldOverflow(presentItems)) { + this.test.assert(submenu && !submenu.hidden, + "Overflow submenu should be present"); + this.test.assert(submenu.localName, "menu", + "Overflow submenu should be a <menu>"); + let overflowPopup = this.overflowPopup; + this.test.assert(overflowPopup, + "Overflow submenu popup should be present"); + this.test.assert(overflowPopup.localName, "menupopup", + "Overflow submenu popup should be a <menupopup>"); + } + else { + this.test.assert(!submenu || submenu.hidden, + "Overflow submenu should be absent"); + } + }, + + // Asserts that the items that are present in the menu because they match the + // current context look OK. + checkPresentItems: function (presentItems) { + function recurse(popup, items, isTopLevel) { + items.forEach(function (item) { + let elt = this.getItemElt(popup, item); + + if (isTopLevel) { + if (this.shouldOverflow(items)) { + this.test.assert(!elt || elt.hidden, + "Item should not be present in top-level menu"); + + let overflowPopup = this.overflowPopup; + this.test.assert(overflowPopup, + "Overflow submenu should be present"); + + elt = this.getItemElt(overflowPopup, item); + this.test.assert(elt && !elt.hidden, + "Item should be present in overflow submenu"); + } + else { + this.test.assert(elt && !elt.hidden, + "Item should be present in top-level menu"); + } + } + else { + this.test.assert(elt && !elt.hidden, + "Item should be present in menu"); + } + + this.checkItemElt(elt, item); + if (this.getItemType(item) === "Menu") + recurse.call(this, elt.firstChild, item.items, false); + }, this); + } + + recurse.call(this, this.contextMenuPopup, presentItems, true); + }, + + // Asserts that items that have been removed from the menu are really removed. + checkRemovedItems: function (removedItems) { + for (let i = 0; i < removedItems.length; i++) { + let item = removedItems[i]; + + let elt = this.getItemElt(this.contextMenuPopup, item); + this.test.assert(!elt, "Item should be removed from top-level menu"); + + let overflowPopup = this.overflowPopup; + if (overflowPopup) { + elt = this.getItemElt(overflowPopup, item); + this.test.assert(!elt, "Item should be removed from overflow submenu"); + } + } + }, + + // Asserts that the menu separator separating standard items from our items + // looks OK. + checkSeparator: function (presentItems) { + let sep = this.contextMenuSeparator; + if (presentItems.length) { + this.test.assert(sep && !sep.hidden, "Menu separator should be present"); + this.test.assertEqual(sep.localName, "menuseparator", + "Menu separator should be a <menuseparator>"); + } + else { + this.test.assert(!sep || sep.hidden, "Menu separator should be absent"); + } + }, + + // Asserts that our items are sorted. + checkSort: function (presentItems) { + // Get the first item in sorted order, get its elt, walk the nextSibling + // chain, making sure each is greater than the previous. + if (presentItems.length) { + let sorted = presentItems.slice(0). + sort(function (a, b) a.label.localeCompare(b.label)); + let elt = this.shouldOverflow(presentItems) ? + this.getItemElt(this.overflowPopup, sorted[0]) : + this.getItemElt(this.contextMenuPopup, sorted[0]); + let numElts = 1; + while (elt.nextSibling && + elt.nextSibling.className.split(/\s+/).indexOf(ITEM_CLASS) >= 0) { + let eltLabel = elt.getAttribute("label"); + let nextLabel = elt.nextSibling.getAttribute("label"); + this.test.assert(eltLabel.localeCompare(nextLabel) < 0, + "Item label should be < next item's label"); + elt = elt.nextSibling; + numElts++; + } + this.test.assertEqual(numElts, presentItems.length, + "The first item in sorted order should have the " + + "first element in sorted order"); + } + }, + + // Attaches an event listener to node. The listener is automatically removed + // when it's fired (so it's assumed it will fire), and callback is called + // after a short delay. Since the module we're testing relies on the same + // event listeners to do its work, this is to give them a little breathing + // room before callback runs. Inside callback |this| is this object. + delayedEventListener: function (node, event, callback, useCapture) { + const self = this; + node.addEventListener(event, function handler(evt) { + node.removeEventListener(event, handler, useCapture); + require("timer").setTimeout(function () { + try { + callback.call(self, evt); + } + catch (err) { + self.test.exception(err); + self.test.done(); + } + }, 20); + }, useCapture); + }, + + // Call to finish the test. + done: function () { + function commonDone() { + if (this.tab) { + this.tabBrowser.removeTab(this.tab); + this.tabBrowser.selectedTab = this.oldSelectedTab; + } + while (this.loaders.length) { + let browserManager = this.loaders[0].globalScope.browserManager; + let topLevelItems = browserManager.topLevelItems.slice(); + let privatePropsKey = this.loaders[0].globalScope.PRIVATE_PROPS_KEY; + let workerRegs = topLevelItems.map(function (item) { + return item.valueOf(privatePropsKey)._workerReg; + }); + + this.loaders[0].unload(); + + // Make sure the browser manager is cleaned up. + this.test.assertEqual(browserManager.browserWins.length, 0, + "browserManager should have no windows left"); + this.test.assertEqual(browserManager.topLevelItems.length, 0, + "browserManager should have no items left"); + this.test.assert(!("contentWins" in browserManager), + "browserManager should have no content windows left"); + + // Make sure the items' worker registries are cleaned up. + topLevelItems.forEach(function (item) { + this.test.assert(!("_workerReg" in item.valueOf(privatePropsKey)), + "item's worker registry should be removed"); + }, this); + workerRegs.forEach(function (workerReg) { + this.test.assertEqual(Object.keys(workerReg.winWorkers).length, 0, + "worker registry should be empty"); + this.test.assertEqual( + Object.keys(workerReg.winsWithoutWorkers).length, 0, + "worker registry list of windows without workers should be empty"); + }, this); + } + this.test.done(); + } + + function closeBrowserWindow() { + if (this.oldBrowserWindow) { + this.delayedEventListener(this.browserWindow, "unload", commonDone, + false); + this.browserWindow.close(); + this.browserWindow = this.oldBrowserWindow; + delete this.oldBrowserWindow; + } + else { + commonDone.call(this); + } + }; + + if (this.contextMenuPopup.state == "closed") { + closeBrowserWindow.call(this); + } + else { + this.delayedEventListener(this.contextMenuPopup, "popuphidden", + function () closeBrowserWindow.call(this), + false); + this.contextMenuPopup.hidePopup(); + } + }, + + // Returns the DOM element in popup corresponding to item. + // WARNING: The element is found by comparing labels, so don't give two items + // the same label. + getItemElt: function (popup, item) { + let nodes = popup.childNodes; + for (let i = nodes.length - 1; i >= 0; i--) { + if (this.getItemType(item) === "Separator") { + if (nodes[i].localName === "menuseparator") + return nodes[i]; + } + else if (nodes[i].getAttribute("label") === item.label) + return nodes[i]; + } + return null; + }, + + // Returns "Item", "Menu", or "Separator". + getItemType: function (item) { + // Could use instanceof here, but that would require accessing the loader + // that created the item, and I don't want to A) somehow search through the + // this.loaders list to find it, and B) assume there are any live loaders at + // all. + return /^\[object (Item|Menu|Separator)/.exec(item.toString())[1]; + }, + + // Returns a wrapper around a new loader: { loader, cm, unload, globalScope }. + // loader is a Cuddlefish sandboxed loader, cm is the context menu module, + // globalScope is the context menu module's global scope, and unload is a + // function that unloads the loader and associated resources. + newLoader: function () { + const self = this; + let loader = Loader(module); + let wrapper = { + loader: loader, + cm: loader.require("context-menu"), + globalScope: loader.sandbox("context-menu"), + unload: function () { + loader.unload(); + let idx = self.loaders.indexOf(wrapper); + if (idx < 0) + throw new Error("Test error: tried to unload nonexistent loader"); + self.loaders.splice(idx, 1); + } + }; + this.loaders.push(wrapper); + return wrapper; + }, + + // Returns true if the number of presentItems crosses the overflow threshold. + shouldOverflow: function (presentItems) { + return presentItems.length > + (this.loaders.length ? + this.loaders[0].loader.require("preferences-service"). + get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT) : + OVERFLOW_THRESH_DEFAULT); + }, + + // Opens the context menu on the current page. If targetNode is null, the + // menu is opened in the top-left corner. onShowncallback is passed the + // popup. + showMenu: function(targetNode, onshownCallback) { + function sendEvent() { + this.delayedEventListener(this.browserWindow, "popupshowing", + function (e) { + let popup = e.target; + onshownCallback.call(this, popup); + }, false); + + let rect = targetNode ? + targetNode.getBoundingClientRect() : + { left: 0, top: 0, width: 0, height: 0 }; + let contentWin = this.browserWindow.content; + contentWin. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils). + sendMouseEvent("contextmenu", + rect.left + (rect.width / 2), + rect.top + (rect.height / 2), + 2, 1, 0); + } + + // If a new tab or window has not yet been opened, open a new tab now. For + // some reason using the tab already opened when the test starts causes + // leaks. See bug 566351 for details. + if (!targetNode && !this.oldSelectedTab && !this.oldBrowserWindow) { + this.oldSelectedTab = this.tabBrowser.selectedTab; + this.tab = this.tabBrowser.addTab("about:blank"); + let browser = this.tabBrowser.getBrowserForTab(this.tab); + + this.delayedEventListener(browser, "load", function () { + this.tabBrowser.selectedTab = this.tab; + sendEvent.call(this); + }, true); + } + else + sendEvent.call(this); + }, + + // Opens a new browser window. The window will be closed automatically when + // done() is called. + withNewWindow: function (onloadCallback) { + let win = this.browserWindow.OpenBrowserWindow(); + this.delayedEventListener(win, "load", onloadCallback, true); + this.oldBrowserWindow = this.browserWindow; + this.browserWindow = win; + }, + + // Opens a new tab with our test page in the current window. The tab will + // be closed automatically when done() is called. + withTestDoc: function (onloadCallback) { + this.oldSelectedTab = this.tabBrowser.selectedTab; + this.tab = this.tabBrowser.addTab(TEST_DOC_URL); + let browser = this.tabBrowser.getBrowserForTab(this.tab); + + this.delayedEventListener(browser, "load", function () { + this.tabBrowser.selectedTab = this.tab; + onloadCallback.call(this, browser.contentWindow, browser.contentDocument); + }, true); + } +}; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-hotkeys.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-hotkeys.js new file mode 100644 index 0000000..0e0ecd6 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-hotkeys.js @@ -0,0 +1,160 @@ +/* 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 { Hotkey } = require("hotkeys"); +const { keyDown } = require("dom/events/keys"); +const { Loader } = require('./helpers'); + +exports["test hotkey: function key"] = function(assert, done) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var showHotKey = Hotkey({ + combo: "f1", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "f2"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "f2", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "f1"); +}; + +exports["test hotkey: accel alt shift"] = function(assert, done) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var showHotKey = Hotkey({ + combo: "accel-shift-6", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "accel-alt-shift-6"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "accel-alt-shift-6", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "accel-shift-6"); +}; + +exports["test hotkey meta & control"] = function(assert, done) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var showHotKey = Hotkey({ + combo: "meta-3", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "alt-control-shift-b"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "Ctrl-Alt-Shift-B", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "meta-3"); +}; + +exports["test hotkey: control-1 / meta--"] = function(assert, done) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var showHotKey = Hotkey({ + combo: "control-1", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "meta--"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "meta--", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "control-1"); +}; + +exports["test invalid combos"] = function(assert) { + assert.throws(function() { + Hotkey({ + combo: "d", + onPress: function() {} + }); + }, "throws if no modifier is present"); + assert.throws(function() { + Hotkey({ + combo: "alt", + onPress: function() {} + }); + }, "throws if no key is present"); + assert.throws(function() { + Hotkey({ + combo: "alt p b", + onPress: function() {} + }); + }, "throws if more then one key is present"); +}; + +exports["test no exception on unmodified keypress"] = function(assert) { + var element = require("window-utils").activeBrowserWindow.document.documentElement; + var someHotkey = Hotkey({ + combo: "control-alt-1", + onPress: function() { + } + }); + keyDown(element, "a"); + assert.pass("No exception throw, unmodified keypress passed"); +}; + +exports["test hotkey: automatic destroy"] = function(assert, done) { + // Hacky way to be able to create unloadable modules via makeSandboxedLoader. + let loader = Loader(module); + + var called = false; + var element = loader.require("window-utils").activeBrowserWindow.document.documentElement; + var hotkey = loader.require("hotkeys").Hotkey({ + combo: "accel-shift-x", + onPress: function() { + called = true; + } + }); + + // Unload the module so that previous hotkey is automatically destroyed + loader.unload(); + + // Ensure that the hotkey is really destroyed + keyDown(element, "accel-shift-x"); + + require("timer").setTimeout(function () { + assert.ok(!called, "Hotkey is destroyed and not called."); + done(); + }, 0); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-l10n.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-l10n.js new file mode 100644 index 0000000..259b160 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-l10n.js @@ -0,0 +1,97 @@ +/* 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/. */ + +const prefs = require("preferences-service"); +const { Loader } = require('./helpers'); + +const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; +const PREF_SELECTED_LOCALE = "general.useragent.locale"; + +function setLocale(locale) { + prefs.set(PREF_MATCH_OS_LOCALE, false); + prefs.set(PREF_SELECTED_LOCALE, locale); +} + +function resetLocale() { + prefs.reset(PREF_MATCH_OS_LOCALE); + prefs.reset(PREF_SELECTED_LOCALE); +} + +exports.testExactMatching = function(test) { + let loader = Loader(module); + setLocale("fr-FR"); + + let _ = loader.require("l10n").get; + test.assertEqual(_("Not translated"), "Not translated", + "Key not translated"); + test.assertEqual(_("Translated"), "Oui", + "Simple key translated"); + + // Placeholders + test.assertEqual(_("placeholderString", "works"), "Placeholder works", + "Value with placeholder"); + test.assertEqual(_("Placeholder %s", "works"), "Placeholder works", + "Key without value but with placeholder"); + test.assertEqual(_("Placeholders %2s %1s %s.", "working", "are", "correctly"), + "Placeholders are working correctly.", + "Multiple placeholders"); + + // Plurals + test.assertEqual(_("downloadsCount", 0), + "0 téléchargement", + "PluralForm form 'one' for 0 in french"); + test.assertEqual(_("downloadsCount", 1), + "1 téléchargement", + "PluralForm form 'one' for 1 in french"); + test.assertEqual(_("downloadsCount", 2), + "2 téléchargements", + "PluralForm form 'other' for n > 1 in french"); + + loader.unload(); + resetLocale(); +} + +exports.testEnUsLocaleName = function(test) { + let loader = Loader(module); + setLocale("en-US"); + + let _ = loader.require("l10n").get; + test.assertEqual(_("Not translated"), "Not translated"); + test.assertEqual(_("Translated"), "Yes"); + + // Check plural forms regular matching + test.assertEqual(_("downloadsCount", 0), + "0 downloads", + "PluralForm form 'other' for 0 in english"); + test.assertEqual(_("downloadsCount", 1), + "one download", + "PluralForm form 'one' for 1 in english"); + test.assertEqual(_("downloadsCount", 2), + "2 downloads", + "PluralForm form 'other' for n != 1 in english"); + + // Check optional plural forms + test.assertEqual(_("pluralTest", 0), + "optional zero form", + "PluralForm form 'zero' can be optionaly specified. (Isn't mandatory in english)"); + test.assertEqual(_("pluralTest", 1), + "fallback to other", + "If the specific plural form is missing, we fallback to 'other'"); + + loader.unload(); + resetLocale(); +} + +exports.testShortLocaleName = function(test) { + let loader = Loader(module); + setLocale("eo"); + + let _ = loader.require("l10n").get; + test.assertEqual(_("Not translated"), "Not translated"); + test.assertEqual(_("Translated"), "jes"); + + loader.unload(); + resetLocale(); +} + diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-module.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-module.js new file mode 100644 index 0000000..957d075 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-module.js @@ -0,0 +1,37 @@ +/* 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"; + +/** Disabled because of Bug 672199 +exports["test module exports are frozen"] = function(assert) { + assert.ok(Object.isFrozen(require("addon-kit/hotkeys")), + "module exports are frozen"); +}; + +exports["test redefine exported property"] = function(assert) { + let hotkeys = require("addon-kit/hotkeys"); + let { Hotkey } = hotkeys; + try { Object.defineProperty(hotkeys, 'Hotkey', { value: {} }); } catch(e) {} + assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be redefined"); +}; +*/ + +exports["test can't delete exported property"] = function(assert) { + let hotkeys = require("addon-kit/hotkeys"); + let { Hotkey } = hotkeys; + + try { delete hotkeys.Hotkey; } catch(e) {} + assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be deleted"); +}; + +exports["test can't override exported property"] = function(assert) { + let hotkeys = require("addon-kit/hotkeys"); + let { Hotkey } = hotkeys; + + try { hotkeys.Hotkey = Object } catch(e) {} + assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be overriden"); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-notifications.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-notifications.js new file mode 100644 index 0000000..b0e1f37 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-notifications.js @@ -0,0 +1,46 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * 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/. */ + +const { Loader } = require('./helpers'); + +exports.testOnClick = function (test) { + let [loader, mockAlertServ] = makeLoader(module); + let notifs = loader.require("notifications"); + let data = "test data"; + let opts = { + onClick: function (clickedData) { + test.assertEqual(this, notifs, "|this| should be notifications module"); + test.assertEqual(clickedData, data, + "data passed to onClick should be correct"); + }, + data: data, + title: "test title", + text: "test text", + iconURL: "test icon URL" + }; + notifs.notify(opts); + mockAlertServ.click(); + loader.unload(); +}; + +// Returns [loader, mockAlertService]. +function makeLoader(test) { + let loader = Loader(module); + let mockAlertServ = { + showAlertNotification: function (imageUrl, title, text, textClickable, + cookie, alertListener, name) { + this._cookie = cookie; + this._alertListener = alertListener; + }, + click: function () { + this._alertListener.observe(null, "alertclickcallback", this._cookie); + } + }; + loader.require("notifications"); + let scope = loader.sandbox("notifications"); + scope.notify = mockAlertServ.showAlertNotification.bind(mockAlertServ); + return [loader, mockAlertServ]; +}; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-page-mod.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-page-mod.js new file mode 100644 index 0000000..14a3ef9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-page-mod.js @@ -0,0 +1,526 @@ +/* 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"; + +var pageMod = require("page-mod"); +var testPageMod = require("pagemod-test-helpers").testPageMod; +const { Loader } = require('./helpers'); +const tabs = require("tabs"); + +/* XXX This can be used to delay closing the test Firefox instance for interactive + * testing or visual inspection. This test is registered first so that it runs + * the last. */ +exports.delay = function(test) { + if (false) { + test.waitUntilDone(60000); + require("timer").setTimeout(function() {test.done();}, 4000); + } else + test.pass(); +} + +/* Tests for the PageMod APIs */ + +exports.testPageMod1 = function(test) { + let mods = testPageMod(test, "about:", [{ + include: /about:/, + contentScriptWhen: 'end', + contentScript: 'new ' + function WorkerScope() { + window.document.body.setAttribute("JEP-107", "worked"); + }, + onAttach: function() { + test.assertEqual(this, mods[0], "The 'this' object is the page mod."); + } + }], + function(win, done) { + test.assertEqual( + win.document.body.getAttribute("JEP-107"), + "worked", + "PageMod.onReady test" + ); + done(); + } + ); +}; + +exports.testPageMod2 = function(test) { + testPageMod(test, "about:", [{ + include: "about:*", + contentScript: [ + 'new ' + function contentScript() { + window.AUQLUE = function() { return 42; } + try { + window.AUQLUE() + } + catch(e) { + throw new Error("PageMod scripts executed in order"); + } + document.documentElement.setAttribute("first", "true"); + }, + 'new ' + function contentScript() { + document.documentElement.setAttribute("second", "true"); + } + ] + }], function(win, done) { + test.assertEqual(win.document.documentElement.getAttribute("first"), + "true", + "PageMod test #2: first script has run"); + test.assertEqual(win.document.documentElement.getAttribute("second"), + "true", + "PageMod test #2: second script has run"); + test.assertEqual("AUQLUE" in win, false, + "PageMod test #2: scripts get a wrapped window"); + done(); + }); +}; + +exports.testPageModIncludes = function(test) { + var asserts = []; + function createPageModTest(include, expectedMatch) { + // Create an 'onload' test function... + asserts.push(function(test, win) { + var matches = include in win.localStorage; + test.assert(expectedMatch ? matches : !matches, + "'" + include + "' match test, expected: " + expectedMatch); + }); + // ...and corresponding PageMod options + return { + include: include, + contentScript: 'new ' + function() { + self.on("message", function(msg) { + window.localStorage[msg] = true; + }); + }, + // The testPageMod callback with test assertions is called on 'end', + // and we want this page mod to be attached before it gets called, + // so we attach it on 'start'. + contentScriptWhen: 'start', + onAttach: function(worker) { + worker.postMessage(this.include[0]); + } + }; + } + + testPageMod(test, "about:buildconfig", [ + createPageModTest("*", false), + createPageModTest("*.google.com", false), + createPageModTest("about:*", true), + createPageModTest("about:", false), + createPageModTest("about:buildconfig", true) + ], + function (win, done) { + test.waitUntil(function () win.localStorage["about:buildconfig"], + "about:buildconfig page-mod to be executed") + .then(function () { + asserts.forEach(function(fn) { + fn(test, win); + }); + done(); + }); + } + ); +}; + +exports.testPageModErrorHandling = function(test) { + test.assertRaises(function() { + new pageMod.PageMod(); + }, + 'pattern is undefined', + "PageMod() throws when 'include' option is not specified."); +}; + +/* Tests for internal functions. */ +exports.testCommunication1 = function(test) { + let workerDone = false, + callbackDone = null; + + testPageMod(test, "about:", [{ + include: "about:*", + contentScriptWhen: 'end', + contentScript: 'new ' + function WorkerScope() { + self.on('message', function(msg) { + document.body.setAttribute('JEP-107', 'worked'); + self.postMessage(document.body.getAttribute('JEP-107')); + }) + }, + onAttach: function(worker) { + worker.on('error', function(e) { + test.fail('Errors where reported'); + }); + worker.on('message', function(value) { + test.assertEqual( + "worked", + value, + "test comunication" + ); + workerDone = true; + if (callbackDone) + callbackDone(); + }); + worker.postMessage('do it!') + } + }], + function(win, done) { + (callbackDone = function() { + if (workerDone) { + test.assertEqual( + 'worked', + win.document.body.getAttribute('JEP-107'), + 'attribute should be modified' + ); + done(); + } + })(); + } + ); +}; + +exports.testCommunication2 = function(test) { + let callbackDone = null, + window; + + testPageMod(test, "about:credits", [{ + include: "about:*", + contentScriptWhen: 'start', + contentScript: 'new ' + function WorkerScope() { + document.documentElement.setAttribute('AUQLUE', 42); + window.addEventListener('load', function listener() { + self.postMessage('onload'); + }, false); + self.on("message", function() { + self.postMessage(document.documentElement.getAttribute("test")) + }); + }, + onAttach: function(worker) { + worker.on('error', function(e) { + test.fail('Errors where reported'); + }); + worker.on('message', function(msg) { + if ('onload' == msg) { + test.assertEqual( + '42', + window.document.documentElement.getAttribute('AUQLUE'), + 'PageMod scripts executed in order' + ); + window.document.documentElement.setAttribute('test', 'changes in window'); + worker.postMessage('get window.test') + } else { + test.assertEqual( + 'changes in window', + msg, + 'PageMod test #2: second script has run' + ) + callbackDone(); + } + }); + } + }], + function(win, done) { + window = win; + callbackDone = done; + } + ); +}; + +exports.testEventEmitter = function(test) { + let workerDone = false, + callbackDone = null; + + testPageMod(test, "about:", [{ + include: "about:*", + contentScript: 'new ' + function WorkerScope() { + self.port.on('addon-to-content', function(data) { + self.port.emit('content-to-addon', data); + }); + }, + onAttach: function(worker) { + worker.on('error', function(e) { + test.fail('Errors were reported : '+e); + }); + worker.port.on('content-to-addon', function(value) { + test.assertEqual( + "worked", + value, + "EventEmitter API works!" + ); + if (callbackDone) + callbackDone(); + else + workerDone = true; + }); + worker.port.emit('addon-to-content', 'worked'); + } + }], + function(win, done) { + if (workerDone) + done(); + else + callbackDone = done; + } + ); +}; + +// Execute two concurrent page mods on same document to ensure that their +// JS contexts are different +exports.testMixedContext = function(test) { + let doneCallback = null; + let messages = 0; + let modObject = { + include: "data:text/html,", + contentScript: 'new ' + function WorkerScope() { + // Both scripts will execute this, + // context is shared if one script see the other one modification. + let isContextShared = "sharedAttribute" in document; + self.postMessage(isContextShared); + document.sharedAttribute = true; + }, + onAttach: function(w) { + w.on("message", function (isContextShared) { + if (isContextShared) { + test.fail("Page mod contexts are mixed."); + doneCallback(); + } + else if (++messages == 2) { + test.pass("Page mod contexts are different."); + doneCallback(); + } + }); + } + }; + testPageMod(test, "data:text/html,", [modObject, modObject], + function(win, done) { + doneCallback = done; + } + ); +}; + +exports.testHistory = function(test) { + // We need a valid url in order to have a working History API. + // (i.e do not work on data: or about: pages) + // Test bug 679054. + let url = require("self").data.url("test-page-mod.html"); + let callbackDone = null; + testPageMod(test, url, [{ + include: url, + contentScriptWhen: 'end', + contentScript: 'new ' + function WorkerScope() { + history.pushState({}, "", "#"); + history.replaceState({foo: "bar"}, "", "#"); + self.postMessage(history.state); + }, + onAttach: function(worker) { + worker.on('message', function (data) { + test.assertEqual(JSON.stringify(data), JSON.stringify({foo: "bar"}), + "History API works!"); + callbackDone(); + }); + } + }], + function(win, done) { + callbackDone = done; + } + ); +}; + +exports.testRelatedTab = function(test) { + test.waitUntilDone(); + + let tab; + let { PageMod } = require("page-mod"); + let pageMod = new PageMod({ + include: "about:*", + onAttach: function(worker) { + test.assertEqual(tab, worker.tab, "Worker.tab is valid"); + pageMod.destroy(); + tab.close(); + test.done(); + } + }); + + tabs.open({ + url: "about:", + onOpen: function onOpen(t) { + tab = t; + } + }); + +}; + +exports['test tab worker on message'] = function(test) { + test.waitUntilDone(); + + let { browserWindows } = require("windows"); + let tabs = require("tabs"); + let { PageMod } = require("page-mod"); + + let url1 = "data:text/html,<title>tab1</title><h1>worker1.tab</h1>"; + let url2 = "data:text/html,<title>tab2</title><h1>worker2.tab</h1>"; + let worker1 = null; + + let mod = PageMod({ + include: "data:text/html,*", + contentScriptWhen: "ready", + contentScript: "self.postMessage('#1');", + onAttach: function onAttach(worker) { + worker.on("message", function onMessage() { + this.tab.attach({ + contentScriptWhen: "ready", + contentScript: "self.postMessage({ url: window.location.href, title: document.title });", + onMessage: function onMessage(data) { + test.assertEqual(this.tab.url, data.url, "location is correct"); + test.assertEqual(this.tab.title, data.title, "title is correct"); + if (this.tab.url === url1) { + worker1 = this; + tabs.open({ url: url2, inBackground: true }); + } + else if (this.tab.url === url2) { + mod.destroy(); + worker1.tab.close(); + worker1.destroy(); + worker.tab.close(); + worker.destroy(); + test.done(); + } + } + }); + }); + } + }); + + tabs.open(url1); +}; + +exports.testAutomaticDestroy = function(test) { + test.waitUntilDone(); + let loader = Loader(module); + + let pageMod = loader.require("page-mod").PageMod({ + include: "about:*", + contentScriptWhen: "start", + onAttach: function(w) { + test.fail("Page-mod should have been detroyed during module unload"); + } + }); + + // Unload the page-mod module so that our page mod is destroyed + loader.unload(); + + // Then create a second tab to ensure that it is correctly destroyed + let tabs = require("tabs"); + tabs.open({ + url: "about:", + onReady: function onReady(tab) { + test.pass("check automatic destroy"); + tab.close(); + test.done(); + } + }); + +} + +exports.testPageModCss = function(test) { + let [pageMod] = testPageMod(test, + 'data:text/html,<div style="background: silver">css test</div>', [{ + include: "data:*", + contentStyle: "div { height: 100px; }", + contentStyleFile: + require("self").data.url("pagemod-css-include-file.css") + }], + function(win, done) { + let div = win.document.querySelector("div"); + test.assertEqual( + div.clientHeight, + 100, + "PageMod contentStyle worked" + ); + test.assertEqual( + div.offsetHeight, + 120, + "PageMod contentStyleFile worked" + ); + done(); + } + ); +}; + +exports.testPageModCssList = function(test) { + let [pageMod] = testPageMod(test, + 'data:text/html,<div style="width:320px; max-width: 480px!important">css test</div>', [{ + include: "data:*", + contentStyleFile: [ + // Highlight evaluation order in this list + "data:text/css,div { border: 1px solid black; }", + "data:text/css,div { border: 10px solid black; }", + // Highlight evaluation order between contentStylesheet & contentStylesheetFile + "data:text/css,div { height: 1000px; }", + // Highlight precedence between the author and user style sheet + "data:text/css,div { width: 200px; max-width: 640px!important}", + ], + contentStyle: [ + "div { height: 10px; }", + "div { height: 100px; }" + ] + }], + function(win, done) { + let div = win.document.querySelector("div"), + style = win.getComputedStyle(div); + + test.assertEqual( + div.clientHeight, + 100, + "PageMod contentStyle list works and is evaluated after contentStyleFile" + ); + + test.assertEqual( + div.offsetHeight, + 120, + "PageMod contentStyleFile list works" + ); + + test.assertEqual( + style.width, + "320px", + "PageMod author/user style sheet precedence works" + ); + + test.assertEqual( + style.maxWidth, + "640px", + "PageMod author/user style sheet precedence with !important works" + ); + + done(); + } + ); +}; + +exports.testPageModCssDestroy = function(test) { + let [pageMod] = testPageMod(test, + 'data:text/html,<div style="width:200px">css test</div>', [{ + include: "data:*", + contentStyle: "div { width: 100px!important; }" + }], + + function(win, done) { + let div = win.document.querySelector("div"), + style = win.getComputedStyle(div); + + test.assertEqual( + style.width, + "100px", + "PageMod contentStyle worked" + ); + + pageMod.destroy(); + test.assertEqual( + style.width, + "200px", + "PageMod contentStyle is removed after destroy" + ); + + done(); + + } + ); +}; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-page-worker.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-page-worker.js new file mode 100644 index 0000000..5fc3bec --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-page-worker.js @@ -0,0 +1,366 @@ +/* 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/. */ + +let tests = {}, Pages, Page; +const { Loader } = require('./helpers'); + +const ERR_DESTROYED = + "The page has been destroyed and can no longer be used."; + +tests.testSimplePageCreation = function(test) { + test.waitUntilDone(); + + let page = new Page({ + contentScript: "self.postMessage(window.location.href)", + contentScriptWhen: "end", + onMessage: function (message) { + test.assertEqual(message, "about:blank", + "Page Worker should start with a blank page by default"); + test.assertEqual(this, page, "The 'this' object is the page itself."); + test.done(); + } + }); +} + +/* + * Tests that we can't be tricked by document overloads as we have access + * to wrapped nodes + */ +tests.testWrappedDOM = function(test) { + test.waitUntilDone(); + + let page = Page({ + allow: { script: true }, + contentURL: "data:text/html,<script>document.getElementById=3;window.scrollTo=3;</script>", + contentScript: "window.addEventListener('load', function () " + + "self.postMessage([typeof(document.getElementById), " + + "typeof(window.scrollTo)]), true)", + onMessage: function (message) { + test.assertEqual(message[0], + "function", + "getElementById from content script is the native one"); + + test.assertEqual(message[1], + "function", + "scrollTo from content script is the native one"); + + test.done(); + } + }); +} + +/* +// We do not offer unwrapped access to DOM since bug 601295 landed +// See 660780 to track progress of unwrap feature +tests.testUnwrappedDOM = function(test) { + test.waitUntilDone(); + + let page = Page({ + allow: { script: true }, + contentURL: "data:text/html,<script>document.getElementById=3;window.scrollTo=3;</script>", + contentScript: "window.addEventListener('load', function () " + + "self.postMessage([typeof(unsafeWindow.document.getElementById), " + + "typeof(unsafeWindow.scrollTo)]), true)", + onMessage: function (message) { + test.assertEqual(message[0], + "number", + "document inside page is free to be changed"); + + test.assertEqual(message[1], + "number", + "window inside page is free to be changed"); + + test.done(); + } + }); +} +*/ + +tests.testPageProperties = function(test) { + let page = new Page(); + + for each (let prop in ['contentURL', 'allow', 'contentScriptFile', + 'contentScript', 'contentScriptWhen', 'on', + 'postMessage', 'removeListener']) { + test.assert(prop in page, prop + " property is defined on page."); + } + + test.assert(function () page.postMessage("foo") || true, + "postMessage doesn't throw exception on page."); +} + +tests.testConstructorAndDestructor = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let Pages = loader.require("page-worker"); + let global = loader.sandbox("page-worker"); + + let pagesReady = 0; + + let page1 = Pages.Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: pageReady + }); + let page2 = Pages.Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: pageReady + }); + + test.assertNotEqual(page1, page2, + "Page 1 and page 2 should be different objects."); + + function pageReady() { + if (++pagesReady == 2) { + page1.destroy(); + page2.destroy(); + + test.assert(isDestroyed(page1), "page1 correctly unloaded."); + test.assert(isDestroyed(page2), "page2 correctly unloaded."); + + loader.unload(); + test.done(); + } + } +} + +tests.testAutoDestructor = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let Pages = loader.require("page-worker"); + + let page = Pages.Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: function() { + loader.unload(); + test.assert(isDestroyed(page), "Page correctly unloaded."); + test.done(); + } + }); +} + +tests.testValidateOptions = function(test) { + test.assertRaises( + function () Page({ contentURL: 'home' }), + "The `contentURL` option must be a valid URL.", + "Validation correctly denied a non-URL contentURL" + ); + + test.assertRaises( + function () Page({ onMessage: "This is not a function."}), + "The event listener must be a function.", + "Validation correctly denied a non-function onMessage." + ); + + test.pass("Options validation is working."); +} + +tests.testContentAndAllowGettersAndSetters = function(test) { + test.waitUntilDone(); + let content = "data:text/html,<script>window.localStorage.allowScript=3;</script>"; + let page = Page({ + contentURL: content, + contentScript: "self.postMessage(window.localStorage.allowScript)", + contentScriptWhen: "end", + onMessage: step0 + }); + + function step0(message) { + test.assertEqual(message, "3", + "Correct value expected for allowScript - 3"); + test.assertEqual(page.contentURL, content, + "Correct content expected"); + page.removeListener('message', step0); + page.on('message', step1); + page.allow = { script: false }; + page.contentURL = content = + "data:text/html,<script>window.localStorage.allowScript='f'</script>"; + } + + function step1(message) { + test.assertEqual(message, "3", + "Correct value expected for allowScript - 3"); + test.assertEqual(page.contentURL, content, "Correct content expected"); + page.removeListener('message', step1); + page.on('message', step2); + page.allow = { script: true }; + page.contentURL = content = + "data:text/html,<script>window.localStorage.allowScript='g'</script>"; + } + + function step2(message) { + test.assertEqual(message, "g", + "Correct value expected for allowScript - g"); + test.assertEqual(page.contentURL, content, "Correct content expected"); + page.removeListener('message', step2); + page.on('message', step3); + page.allow.script = false; + page.contentURL = content = + "data:text/html,<script>window.localStorage.allowScript=3</script>"; + } + + function step3(message) { + test.assertEqual(message, "g", + "Correct value expected for allowScript - g"); + test.assertEqual(page.contentURL, content, "Correct content expected"); + page.removeListener('message', step3); + page.on('message', step4); + page.allow.script = true; + page.contentURL = content = + "data:text/html,<script>window.localStorage.allowScript=4</script>"; + } + + function step4(message) { + test.assertEqual(message, "4", + "Correct value expected for allowScript - 4"); + test.assertEqual(page.contentURL, content, "Correct content expected"); + test.done(); + } + +} + +tests.testOnMessageCallback = function(test) { + test.waitUntilDone(); + + Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: function() { + test.pass("onMessage callback called"); + test.done(); + } + }); +} + +tests.testMultipleOnMessageCallbacks = function(test) { + test.waitUntilDone(); + + let count = 0; + let page = Page({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: function() count += 1 + }); + page.on('message', function() count += 2); + page.on('message', function() count *= 3); + page.on('message', function() + test.assertEqual(count, 9, "All callbacks were called, in order.")); + page.on('message', function() test.done()); + +} + +tests.testLoadContentPage = function(test) { + + test.waitUntilDone(); + + let page = Page({ + onMessage: function(message) { + // The message is an array whose first item is the test method to call + // and the rest of whose items are arguments to pass it. + test[message.shift()].apply(test, message); + }, + contentURL: require("self").data.url("test-page-worker.html"), + contentScriptFile: require("self").data.url("test-page-worker.js"), + contentScriptWhen: "ready" + }); + +} + +tests.testAllowScriptDefault = function(test) { + + test.waitUntilDone(); + + let page = Page({ + onMessage: function(message) { + test.assert(message, "Script is allowed to run by default."); + test.done(); + }, + contentURL: "data:text/html,<script>document.documentElement.setAttribute('foo', 3);</script>", + contentScript: "self.postMessage(document.documentElement.getAttribute('foo'))", + contentScriptWhen: "ready" + }); +} + +tests.testAllowScript = function(test) { + + test.waitUntilDone(); + + let page = Page({ + onMessage: function(message) { + test.assert(message, "Script runs when allowed to do so."); + test.done(); + }, + allow: { script: true }, + contentURL: "data:text/html,<script>document.documentElement.setAttribute('foo', 3);</script>", + contentScript: "self.postMessage(document.documentElement.hasAttribute('foo') && " + + " document.documentElement.getAttribute('foo') == 3)", + contentScriptWhen: "ready" + }); +} + +tests.testPingPong = function(test) { + test.waitUntilDone(); + let page = Page({ + contentURL: 'data:text/html,ping-pong', + contentScript: 'self.on("message", function(message) self.postMessage("pong"));' + + 'self.postMessage("ready");', + onMessage: function(message) { + if ('ready' == message) { + page.postMessage('ping'); + } + else { + test.assert(message, 'pong', 'Callback from contentScript'); + test.done(); + } + } + }); +}; + +tests.testMultipleDestroys = function(test) { + let page = Page(); + page.destroy(); + page.destroy(); + test.pass("Multiple destroys should not cause an error"); +}; + + +function isDestroyed(page) { + try { + page.postMessage("foo"); + } + catch (err if err.message == ERR_DESTROYED) { + return true; + } + return false; +} + + +let pageWorkerSupported = true; + +try { + Pages = require("page-worker"); + Page = Pages.Page; +} +catch (ex if ex.message == [ + "The page-worker module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")) { + pageWorkerSupported = false; +} + +if (pageWorkerSupported) { + for (let test in tests) { + exports[test] = tests[test]; + } +} else { + exports.testPageWorkerNotSupported = function(test) { + test.pass("The page-worker module is not supported on this app."); + } +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-panel.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-panel.js new file mode 100644 index 0000000..e05400e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-panel.js @@ -0,0 +1,466 @@ +/* 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/. */ + +let { Cc, Ci } = require("chrome"); +let panels = require('panel'); +let tests = {}, panels, Panel; +const { Loader } = require('./helpers'); + +tests.testPanel = function(test) { + test.waitUntilDone(); + let panel = Panel({ + contentURL: "about:buildconfig", + contentScript: "self.postMessage(1); self.on('message', function() self.postMessage(2));", + onMessage: function (message) { + test.assertEqual(this, panel, "The 'this' object is the panel."); + switch(message) { + case 1: + test.pass("The panel was loaded."); + panel.postMessage(''); + break; + case 2: + test.pass("The panel posted a message and received a response."); + panel.destroy(); + test.done(); + break; + } + } + }); +}; + +tests.testPanelEmit = function(test) { + test.waitUntilDone(); + let panel = Panel({ + contentURL: "about:buildconfig", + contentScript: "self.port.emit('loaded');" + + "self.port.on('addon-to-content', " + + " function() self.port.emit('received'));", + }); + panel.port.on("loaded", function () { + test.pass("The panel was loaded and sent a first event."); + panel.port.emit("addon-to-content"); + }); + panel.port.on("received", function () { + test.pass("The panel posted a message and received a response."); + panel.destroy(); + test.done(); + }); +}; + +tests.testPanelEmitEarly = function(test) { + test.waitUntilDone(); + let panel = Panel({ + contentURL: "about:buildconfig", + contentScript: "self.port.on('addon-to-content', " + + " function() self.port.emit('received'));", + }); + panel.port.on("received", function () { + test.pass("The panel posted a message early and received a response."); + panel.destroy(); + test.done(); + }); + panel.port.emit("addon-to-content"); +}; + +tests.testShowHidePanel = function(test) { + test.waitUntilDone(); + let panel = Panel({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + onMessage: function (message) { + panel.show(); + }, + onShow: function () { + test.pass("The panel was shown."); + test.assertEqual(this, panel, "The 'this' object is the panel."); + test.assertEqual(this.isShowing, true, "panel.isShowing == true."); + panel.hide(); + }, + onHide: function () { + test.pass("The panel was hidden."); + test.assertEqual(this, panel, "The 'this' object is the panel."); + test.assertEqual(this.isShowing, false, "panel.isShowing == false."); + panel.destroy(); + test.done(); + } + }); +}; + +tests.testDocumentReload = function(test) { + test.waitUntilDone(); + let content = + "<script>" + + "setTimeout(function () {" + + " window.location = 'about:blank';" + + "}, 250);" + + "</script>"; + let messageCount = 0; + let panel = Panel({ + contentURL: "data:text/html," + encodeURIComponent(content), + contentScript: "self.postMessage(window.location.href)", + onMessage: function (message) { + messageCount++; + if (messageCount == 1) { + test.assertMatches(message, /data:text\/html,/, "First document had a content script"); + } + else if (messageCount == 2) { + test.assertEqual(message, "about:blank", "Second document too"); + panel.destroy(); + test.done(); + } + } + }); +}; + +tests.testParentResizeHack = function(test) { + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); + let docShell = browserWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + if (!("allowWindowControl" in docShell)) { + // bug 635673 is not fixed in this firefox build + test.pass("allowWindowControl attribute that allow to fix browser window " + + "resize is not available on this build."); + return; + } + + test.waitUntilDone(30000); + + let previousWidth = browserWindow.outerWidth, previousHeight = browserWindow.outerHeight; + + let content = "<script>" + + "function contentResize() {" + + " resizeTo(200,200);" + + " resizeBy(200,200);" + + "}" + + "</script>" + + "Try to resize browser window"; + let panel = Panel({ + contentURL: "data:text/html," + encodeURIComponent(content), + contentScript: "self.on('message', function(message){" + + " if (message=='resize') " + + " unsafeWindow.contentResize();" + + "});", + contentScriptWhen: "ready", + onMessage: function (message) { + + }, + onShow: function () { + panel.postMessage('resize'); + require("timer").setTimeout(function () { + test.assertEqual(previousWidth,browserWindow.outerWidth,"Size doesn't change by calling resizeTo/By/..."); + test.assertEqual(previousHeight,browserWindow.outerHeight,"Size doesn't change by calling resizeTo/By/..."); + panel.destroy(); + test.done(); + },0); + } + }); + panel.show(); +} + +tests.testResizePanel = function(test) { + test.waitUntilDone(); + + // These tests fail on Linux if the browser window in which the panel + // is displayed is not active. And depending on what other tests have run + // before this one, it might not be (the untitled window in which the test + // runner executes is often active). So we make sure the browser window + // is focused by focusing it before running the tests. Then, to be the best + // possible test citizen, we refocus whatever window was focused before we + // started running these tests. + + let activeWindow = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher). + activeWindow; + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); + + + function onFocus() { + browserWindow.removeEventListener("focus", onFocus, true); + + let panel = Panel({ + contentScript: "self.postMessage('')", + contentScriptWhen: "end", + height: 10, + width: 10, + onMessage: function (message) { + panel.show(); + }, + onShow: function () { + panel.resize(100,100); + panel.hide(); + }, + onHide: function () { + test.assert((panel.width == 100) && (panel.height == 100), + "The panel was resized."); + if (activeWindow) + activeWindow.focus(); + test.done(); + } + }); + } + + if (browserWindow === activeWindow) { + onFocus(); + } + else { + browserWindow.addEventListener("focus", onFocus, true); + browserWindow.focus(); + } +}; + +tests.testHideBeforeShow = function(test) { + test.waitUntilDone(); + let showCalled = false; + let panel = Panel({ + onShow: function () { + showCalled = true; + }, + onHide: function () { + test.assert(!showCalled, 'must not emit show if was hidden before'); + test.done(); + } + }); + panel.show(); + panel.hide(); +}; + +tests.testSeveralShowHides = function(test) { + test.waitUntilDone(); + let hideCalled = 0; + let panel = panels.Panel({ + contentURL: "about:buildconfig", + onShow: function () { + panel.hide(); + }, + onHide: function () { + hideCalled++; + if (hideCalled < 3) + panel.show(); + else { + test.pass("onHide called three times as expected"); + test.done(); + } + } + }); + panel.on('error', function(e) { + test.fail('error was emitted:' + e.message + '\n' + e.stack); + }); + panel.show(); +}; + +tests.testAnchorAndArrow = function(test) { + test.waitUntilDone(20000); + let count = 0; + function newPanel(tab, anchor) { + let panel = panels.Panel({ + contentURL: "data:text/html,<html><body style='padding: 0; margin: 0; " + + "background: gray; text-align: center;'>Anchor: " + + anchor.id + "</body></html>", + width: 200, + height: 100, + onShow: function () { + count++; + panel.destroy(); + if (count==5) { + test.pass("All anchored panel test displayed"); + tab.close(function () { + test.done(); + }); + } + } + }); + panel.show(anchor); + } + + let tabs= require("tabs"); + let url = 'data:text/html,' + + '<html><head><title>foo</title></head><body>' + + '<style>div {background: gray; position: absolute; width: 300px; ' + + 'border: 2px solid black;}</style>' + + '<div id="tl" style="top: 0px; left: 0px;">Top Left</div>' + + '<div id="tr" style="top: 0px; right: 0px;">Top Right</div>' + + '<div id="bl" style="bottom: 0px; left: 0px;">Bottom Left</div>' + + '<div id="br" style="bottom: 0px; right: 0px;">Bottom right</div>' + + '</body></html>'; + + tabs.open({ + url: url, + onReady: function(tab) { + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); + let window = browserWindow.content; + newPanel(tab, window.document.getElementById('tl')); + newPanel(tab, window.document.getElementById('tr')); + newPanel(tab, window.document.getElementById('bl')); + newPanel(tab, window.document.getElementById('br')); + let anchor = browserWindow.document.getElementById("identity-box"); + newPanel(tab, anchor); + } + }); + + + +}; + +tests.testPanelTextColor = function(test) { + test.waitUntilDone(); + let html = "<html><head><style>body {color: yellow}</style></head>" + + "<body><p>Foo</p></body></html>"; + let panel = Panel({ + contentURL: "data:text/html," + encodeURI(html), + contentScript: "self.port.emit('color', " + + "window.getComputedStyle(document.body.firstChild, null). " + + " getPropertyValue('color'));" + }); + panel.port.on("color", function (color) { + test.assertEqual(color, "rgb(255, 255, 0)", + "The panel text color style is preserved when a style exists."); + panel.destroy(); + test.done(); + }); +}; + +// Bug 696552: Ensure panel.contentURL modification support +tests.testChangeContentURL = function(test) { + test.waitUntilDone(); + + let panel = Panel({ + contentURL: "about:blank", + contentScript: "self.port.emit('ready', document.location.href);" + }); + let count = 0; + panel.port.on("ready", function (location) { + count++; + if (count == 1) { + test.assertEqual(location, "about:blank"); + test.assertEqual(panel.contentURL, "about:blank"); + panel.contentURL = "about:buildconfig"; + } + else { + test.assertEqual(location, "about:buildconfig"); + test.assertEqual(panel.contentURL, "about:buildconfig"); + panel.destroy(); + test.done(); + } + }); +}; + +function makeEventOrderTest(options) { + let expectedEvents = []; + + return function(test) { + let panel = panels.Panel({ contentURL: "about:buildconfig" }); + + function expect(event, cb) { + expectedEvents.push(event); + panel.on(event, function() { + test.assertEqual(event, expectedEvents.shift()); + if (cb) + require("timer").setTimeout(cb, 1); + }); + return {then: expect}; + } + + test.waitUntilDone(); + options.test(test, expect, panel); + } +} + +tests.testAutomaticDestroy = function(test) { + let loader = Loader(module); + let panel = loader.require("panel").Panel({ + contentURL: "about:buildconfig", + contentScript: + "self.port.on('event', function() self.port.emit('event-back'));" + }); + + loader.unload(); + + panel.port.on("event-back", function () { + test.fail("Panel should have been destroyed on module unload"); + }); + panel.port.emit("event"); + test.pass("check automatic destroy"); +}; + +tests.testWaitForInitThenShowThenDestroy = makeEventOrderTest({ + test: function(test, expect, panel) { + expect('inited', function() { panel.show(); }). + then('show', function() { panel.destroy(); }). + then('hide', function() { test.done(); }); + } +}); + +tests.testShowThenWaitForInitThenDestroy = makeEventOrderTest({ + test: function(test, expect, panel) { + panel.show(); + expect('inited'). + then('show', function() { panel.destroy(); }). + then('hide', function() { test.done(); }); + } +}); + +tests.testShowThenHideThenDestroy = makeEventOrderTest({ + test: function(test, expect, panel) { + panel.show(); + expect('show', function() { panel.hide(); }). + then('hide', function() { panel.destroy(); test.done(); }); + } +}); + +tests.testContentURLOption = function(test) { + const URL_STRING = "about:buildconfig"; + const HTML_CONTENT = "<html><title>Test</title><p>This is a test.</p></html>"; + + let (panel = Panel({ contentURL: URL_STRING })) { + test.pass("contentURL accepts a string URL."); + test.assertEqual(panel.contentURL, URL_STRING, + "contentURL is the string to which it was set."); + } + + let dataURL = "data:text/html," + encodeURIComponent(HTML_CONTENT); + let (panel = Panel({ contentURL: dataURL })) { + test.pass("contentURL accepts a data: URL."); + } + + let (panel = Panel({})) { + test.assert(panel.contentURL == null, + "contentURL is undefined."); + } + + test.assertRaises(function () Panel({ contentURL: "foo" }), + "The `contentURL` option must be a valid URL.", + "Panel throws an exception if contentURL is not a URL."); +}; + +let panelSupported = true; + +try { + panels = require("panel"); + Panel = panels.Panel; +} +catch(ex if ex.message == [ + "The panel module currently supports only Firefox. In the future ", + "we would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps ", + "for more information." + ].join("")) { + panelSupported = false; +} + +if (panelSupported) { + for (let test in tests) + exports[test] = tests[test]; +} +else { + exports.testPanelNotSupported = function(test) { + test.pass("The panel module is not supported on this app."); + } +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-passwords.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-passwords.js new file mode 100644 index 0000000..bfb137a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-passwords.js @@ -0,0 +1,281 @@ +/* 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 { store, search, remove } = require("passwords"); + +exports["test store requires `password` field"] = function(assert, done) { + store({ + username: "foo", + realm: "bar", + onComplete: function onComplete() { + assert.fail("onComplete should not be called"); + }, + onError: function onError() { + assert.pass("'`password` is required"); + done(); + } + }); +}; + +exports["test store requires `username` field"] = function(assert, done) { + store({ + password: "foo", + realm: "bar", + onComplete: function onComplete() { + assert.fail("onComplete should not be called"); + }, + onError: function onError() { + assert.pass("'`username` is required"); + done(); + } + }); +}; + +exports["test onComplete is optional"] = function(assert, done) { + store({ + realm: "bla", + username: "bla", + password: "bla", + onError: function onError() { + assert.fail("onError was called"); + } + }); + assert.pass("exception is not thrown if `onComplete is missing") + done(); +}; + +exports["test exceptions in onComplete are reported"] = function(assert, done) { + store({ + realm: "throws", + username: "error", + password: "boom!", + onComplete: function onComplete(error) { + throw new Error("Boom!") + }, + onError: function onError(error) { + assert.equal(error.message, "Boom!", "Error thrown is reported"); + done(); + } + }); +}; + +exports["test store requires `realm` field"] = function(assert, done) { + store({ + username: "foo", + password: "bar", + onComplete: function onComplete() { + assert.fail("onComplete should not be called"); + }, + onError: function onError() { + assert.pass("'`realm` is required"); + done(); + } + }); +}; + +exports["test can't store same login twice"] = function(assert, done) { + store({ + username: "user", + password: "pass", + realm: "realm", + onComplete: function onComplete() { + assert.pass("credential saved"); + + store({ + username: "user", + password: "pass", + realm: "realm", + onComplete: function onComplete() { + assert.fail("onComplete should not be called"); + }, + onError: function onError() { + assert.pass("re-saving credential failed"); + + remove({ + username: "user", + password: "pass", + realm: "realm", + onComplete: function onComplete() { + assert.pass("credential was removed"); + done(); + }, + onError: function onError() { + assert.fail("remove should not fail"); + } + }); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); +}; + +exports["test remove fails if no login found"] = function(assert, done) { + remove({ + username: "foo", + password: "bar", + realm: "baz", + onComplete: function onComplete() { + assert.fail("should not be able to remove unstored credentials"); + }, + onError: function onError() { + assert.pass("can't remove unstored credentials"); + done(); + } + }); +}; + +exports["test addon associated credentials"] = function(assert, done) { + store({ + username: "foo", + password: "bar", + realm: "baz", + onComplete: function onComplete() { + search({ + username: "foo", + password: "bar", + realm: "baz", + onComplete: function onComplete([credential]) { + assert.equal(credential.url.indexOf("addon:"), 0, + "`addon:` uri is used for add-on credentials"); + assert.equal(credential.username, "foo", + "username matches"); + assert.equal(credential.password, "bar", + "password matches"); + assert.equal(credential.realm, "baz", "realm matches"); + assert.equal(credential.formSubmitURL, null, + "`formSubmitURL` is `null` for add-on credentials"); + assert.equal(credential.usernameField, "", "usernameField is empty"); + assert.equal(credential.passwordField, "", "passwordField is empty"); + + remove({ + username: credential.username, + password: credential.password, + realm: credential.realm, + onComplete: function onComplete() { + assert.pass("credential is removed"); + done(); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); +}; + +exports["test web page associated credentials"] = function(assert, done) { + store({ + url: "http://bar.foo.com/authentication/?login", + formSubmitURL: "http://login.foo.com/authenticate.cgi", + username: "user", + password: "pass", + usernameField: "user-f", + passwordField: "pass-f", + onComplete: function onComplete() { + search({ + username: "user", + password: "pass", + url: "http://bar.foo.com", + formSubmitURL: "http://login.foo.com", + onComplete: function onComplete([credential]) { + assert.equal(credential.url, "http://bar.foo.com", "url matches"); + assert.equal(credential.username, "user", "username matches"); + assert.equal(credential.password, "pass", "password matches"); + assert.equal(credential.realm, null, "realm is null"); + assert.equal(credential.formSubmitURL, "http://login.foo.com", + "formSubmitURL matches"); + assert.equal(credential.usernameField, "user-f", + "usernameField is matches"); + assert.equal(credential.passwordField, "pass-f", + "passwordField matches"); + + remove({ + url: credential.url, + formSubmitURL: credential.formSubmitURL, + username: credential.username, + password: credential.password, + usernameField: credential.usernameField, + passwordField: credential.passwordField, + + onComplete: function onComplete() { + assert.pass("credential is removed"); + done(); + }, + onError: function onError(e) { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); +}; + +exports["test site authentication credentials"] = function(assert, done) { + store({ + url: "http://authentication.com", + username: "U", + password: "P", + realm: "R", + onComplete: function onComplete() { + search({ + url: "http://authentication.com", + username: "U", + password: "P", + realm: "R", + onComplete: function onComplete([credential]) { + assert.equal(credential.url,"http://authentication.com", + "url matches"); + assert.equal(credential.username, "U", "username matches"); + assert.equal(credential.password, "P", "password matches"); + assert.equal(credential.realm, "R", "realm matches"); + assert.equal(credential.formSubmitURL, null, "formSubmitURL is null"); + assert.equal(credential.usernameField, "", "usernameField is empty"); + assert.equal(credential.passwordField, "", "passwordField is empty"); + + remove({ + url: credential.url, + username: credential.username, + password: credential.password, + realm: credential.realm, + onComplete: function onComplete() { + assert.pass("credential is removed"); + done(); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); + }, + onError: function onError() { + assert.fail("onError should not be called"); + } + }); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-private-browsing.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-private-browsing.js new file mode 100644 index 0000000..1d60f6b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-private-browsing.js @@ -0,0 +1,204 @@ +/* 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/. */ + +let pb = require("private-browsing"); +let {Cc,Ci} = require("chrome"); +const { Loader } = require('./helpers'); + +let pbService; +// Currently, only Firefox implements the private browsing service. +if (require("xul-app").is("Firefox")) { + pbService = Cc["@mozilla.org/privatebrowsing;1"]. + getService(Ci.nsIPrivateBrowsingService); +} + +if (pbService) { + + // tests that isActive has the same value as the private browsing service + // expects + exports.testGetIsActive = function (test) { + test.assertEqual(pb.isActive, false, + "private-browsing.isActive is correct without modifying PB service"); + + pbService.privateBrowsingEnabled = true; + test.assert(pb.isActive, + "private-browsing.isActive is correct after modifying PB service"); + + // Switch back to normal mode. + pbService.privateBrowsingEnabled = false; + }; + + // tests that activating does put the browser into private browsing mode + exports.testActivateDeactivate = function (test) { + test.waitUntilDone(); + pb.once("start", function onStart() { + test.assertEqual(pbService.privateBrowsingEnabled, true, + "private browsing mode was activated"); + pb.deactivate(); + }); + pb.once("stop", function onStop() { + test.assertEqual(pbService.privateBrowsingEnabled, false, + "private browsing mode was deactivate"); + test.done(); + }); + pb.activate(); + }; + + exports.testStart = function(test) { + test.waitUntilDone(); + pb.on("start", function onStart() { + test.assertEqual(this, pb, "`this` should be private-browsing module"); + test.assert(pbService.privateBrowsingEnabled, + 'private mode is active when "start" event is emitted'); + test.assert(pb.isActive, + '`isActive` is `true` when "start" event is emitted'); + pb.removeListener("start", onStart); + test.done(); + }); + pb.activate(); + }; + + exports.testStop = function(test) { + test.waitUntilDone(); + pb.on("stop", function onStop() { + test.assertEqual(this, pb, "`this` should be private-browsing module"); + test.assertEqual(pbService.privateBrowsingEnabled, false, + "private mode is disabled when stop event is emitted"); + test.assertEqual(pb.isActive, false, + "`isActive` is `false` when stop event is emitted"); + pb.removeListener("stop", onStop); + test.done(); + }); + pb.activate(); + pb.deactivate(); + }; + + exports.testAutomaticUnload = function(test) { + test.waitUntilDone(); + // Create another private browsing instance and unload it + let loader = Loader(module); + let pb2 = loader.require("private-browsing"); + let called = false; + pb2.on("start", function onStart() { + called = true; + test.fail("should not be called:x"); + }); + loader.unload(); + + // Then switch to private mode in order to check that the previous instance + // is correctly destroyed + pb.activate(); + pb.once("start", function onStart() { + require("timer").setTimeout(function () { + test.assert(!called, + "First private browsing instance is destroyed and inactive"); + + // Must reset to normal mode, so that next test starts with it. + pb.deactivate(); + test.done(); + }, 0); + }); + }; + + exports.testBothListeners = function(test) { + test.waitUntilDone(); + let stop = false; + let start = false; + + function onStop() { + test.assertEqual(stop, false, + "stop callback must be called only once"); + test.assertEqual(pbService.privateBrowsingEnabled, false, + "private mode is disabled when stop event is emitted"); + test.assertEqual(pb.isActive, false, + "`isActive` is `false` when stop event is emitted"); + + pb.on("start", finish); + pb.removeListener("start", onStart); + pb.removeListener("start", onStart2); + pb.activate(); + stop = true; + } + + function onStart() { + test.assertEqual(false, start, + "stop callback must be called only once"); + test.assert(pbService.privateBrowsingEnabled, + "private mode is active when start event is emitted"); + test.assert(pb.isActive, + "`isActive` is `true` when start event is emitted"); + + pb.on("stop", onStop); + pb.deactivate(); + start = true; + } + + function onStart2() { + test.assert(start, "start listener must be called already"); + test.assertEqual(false, stop, "stop callback must not be called yet"); + } + + function finish() { + test.assert(pbService.privateBrowsingEnabled, true, + "private mode is active when start event is emitted"); + test.assert(pb.isActive, + "`isActive` is `true` when start event is emitted"); + + pb.removeListener("start", finish); + pb.removeListener("stop", onStop); + + pb.deactivate(); + pb.once("stop", function () { + test.assertEqual(pbService.privateBrowsingEnabled, false); + test.assertEqual(pb.isActive, false); + + test.done(); + }); + } + + pb.on("start", onStart); + pb.on("start", onStart2); + pbService.privateBrowsingEnabled = true; + }; + + exports["test activate private mode via handler"] = function(test) { + const tabs = require("tabs"); + + test.waitUntilDone(); + function onReady(tab) { + if (tab.url == "about:robots") + tab.close(function() pb.activate()); + } + function cleanup(tab) { + if (tab.url == "about:") { + tabs.removeListener("ready", cleanup); + tab.close(function onClose() { + test.done(); + }); + } + } + + tabs.on("ready", onReady); + pb.once("start", function onStart() { + test.pass("private mode was activated"); + pb.deactivate(); + }); + pb.once("stop", function onStop() { + test.pass("private mode was deactivated"); + tabs.removeListener("ready", onReady); + tabs.on("ready", cleanup); + }); + tabs.once("open", function onOpen() { + tabs.open("about:robots"); + }); + tabs.open("about:"); + }; +} +else { + // tests for the case where private browsing doesn't exist + exports.testNoImpl = function (test) { + test.assertEqual(pb.isActive, false, + "pb.isActive returns false when private browsing isn't supported"); + }; +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-request.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-request.js new file mode 100644 index 0000000..42425d7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-request.js @@ -0,0 +1,340 @@ +/* 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/. */ + +const { Request } = require("addon-kit/request"); +const { pathFor } = require("api-utils/system"); +const { startServerAsync } = require("api-utils/httpd"); +const file = require("api-utils/file"); + +const basePath = pathFor("TmpD") +const port = 8099; + + +exports.testOptionsValidator = function(test) { + // First, a simple test to make sure we didn't break normal functionality. + test.assertRaises(function () { + Request({ + url: null + }); + }, 'The option "url" must be one of the following types: string'); + + // Next we'll have a Request that doesn't throw from c'tor, but from a setter. + let req = Request({ + url: "http://playground.zpao.com/jetpack/request/text.php", + onComplete: function () {} + }); + test.assertRaises(function () { + req.url = null; + }, 'The option "url" must be one of the following types: string'); + // The url shouldn't have changed, so check that + test.assertEqual(req.url, "http://playground.zpao.com/jetpack/request/text.php"); +} + +exports.testContentValidator = function(test) { + test.waitUntilDone(); + Request({ + url: "data:text/html,response", + content: { 'key1' : null, 'key2' : 'some value' }, + onComplete: function(response) { + test.assertEqual(response.text, "response?key1=null&key2=some+value"); + test.done(); + } + }).get(); +}; + +// All tests below here require a network connection. They will be commented out +// when checked in. If you'd like to run them, simply uncomment them. +// +// When we have the means, these tests will be converted so that they don't +// require an external server nor a network connection. + +// This is a request to a file that exists. +exports.testStatus200 = function (test) { + let srv = startServerAsync(port, basePath); + let content = "Look ma, no hands!\n"; + let basename = "test-request.txt" + prepareFile(basename, content); + + test.waitUntilDone(); + var req = Request({ + url: "http://localhost:" + port + "/" + basename, + onComplete: function (response) { + test.assertEqual(this, req, "`this` should be request"); + test.assertEqual(response.status, 200); + test.assertEqual(response.statusText, "OK"); + test.assertEqual(response.headers["Content-Type"], "text/plain"); + test.assertEqual(response.text, content); + srv.stop(function() test.done()); + } + }).get(); +} + +// This tries to get a file that doesn't exist +exports.testStatus404 = function (test) { + var srv = startServerAsync(port, basePath); + + test.waitUntilDone(); + Request({ + // the following URL doesn't exist + url: "http://localhost:" + port + "/test-request-404.txt", + onComplete: function (response) { + test.assertEqual(response.status, 404); + test.assertEqual(response.statusText, "Not Found"); + srv.stop(function() test.done()); + } + }).get(); +} + +/* +// a simple file with a known header +exports.testKnownHeader = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/headers.php", + onComplete: function (response) { + test.assertEqual(response.headers["x-zpao-header"], "Jamba Juice"); + test.done(); + } + }).get(); +} + +// complex headers +exports.testKnownHeader = function (test) { + let headers = { + "x-zpao-header": "Jamba Juice is: delicious", + "x-zpao-header-2": "foo, bar", + "Set-Cookie": "foo=bar\nbaz=foo" + } + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/complex_headers.php", + onComplete: function (response) { + for (k in headers) { + test.assertEqual(response.headers[k], headers[k]); + } + test.done(); + } + }).get(); +} +*/ + +exports.testSimpleJSON = function (test) { + let srv = startServerAsync(port, basePath); + let json = { foo: "bar" }; + let basename = "test-request.json"; + prepareFile(basename, JSON.stringify(json)); + + test.waitUntilDone(); + Request({ + url: "http://localhost:" + port + "/" + basename, + onComplete: function (response) { + assertDeepEqual(test, response.json, json); + srv.stop(function() test.done()); + } + }).get(); +} + +exports.testInvalidJSON = function (test) { + let srv = startServerAsync(port, basePath); + let basename = "test-request-invalid.json"; + prepareFile(basename, '"this": "isn\'t JSON"'); + + test.waitUntilDone(); + Request({ + url: "http://localhost:" + port + "/" + basename, + onComplete: function (response) { + test.assertEqual(response.json, null); + srv.stop(function() test.done()); + } + }).get(); +} + +/* +exports.testGetWithParamsNotContent = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php?foo=bar", + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithContent = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { foo: "bar" }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithParamsAndContent = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php?foo=bar", + content: { baz: "foo" }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar", baz: "foo" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testSimplePost = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { foo: "bar" }, + onComplete: function (response) { + let expected = { + "POST": { foo: "bar" }, + "GET" : [] + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).post(); +} + +exports.testEncodedContent = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: "foo=bar&baz=foo", + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar", baz: "foo" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testEncodedContentWithSpaces = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: "foo=bar+hop!&baz=foo", + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: "bar hop!", baz: "foo" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithArray = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { foo: [1, 2], baz: "foo" }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : { foo: [1, 2], baz: "foo" } + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithNestedArray = function (test) { + test.waitUntilDone(); + Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { foo: [1, 2, [3, 4]], bar: "baz" }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : this.content + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} + +exports.testGetWithNestedArray = function (test) { + test.waitUntilDone(); + let request = Request({ + url: "http://playground.zpao.com/jetpack/request/getpost.php", + content: { + foo: [1, 2, { + omg: "bbq", + "all your base!": "are belong to us" + }], + bar: "baz" + }, + onComplete: function (response) { + let expected = { + "POST": [], + "GET" : request.content + }; + assertDeepEqual(test, response.json, expected); + test.done(); + } + }).get(); +} +*/ + +// This is not a proper testing for deep equal, but it's good enough for my uses +// here. It will do type coercion to check equality, but that's good here. Data +// coming from the server will be stringified and so "0" should be equal to 0. +function assertDeepEqual(test, obj1, obj2, msg) { + function equal(o1, o2) { + // cover our non-object cases well enough + if (o1 == o2) + return true; + if (typeof(o1) != typeof(o2)) + return false; + if (typeof(o1) != "object") + return o1 == o2; + + let e = true; + for (let [key, val] in Iterator(o1)) { + e = e && key in o2 && equal(o2[key], val); + if (!e) + break; + } + for (let [key, val] in Iterator(o2)) { + e = e && key in o1 && equal(o1[key], val); + if (!e) + break; + } + return e; + } + msg = msg || "objects not equal - " + JSON.stringify(obj1) + " != " + + JSON.stringify(obj2); + test.assert(equal(obj1, obj2), msg); +} + +function prepareFile(basename, content) { + let filePath = file.join(basePath, basename); + let fileStream = file.open(filePath, 'w'); + fileStream.write(content); + fileStream.close(); +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-selection.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-selection.js new file mode 100644 index 0000000..06feb7e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-selection.js @@ -0,0 +1,458 @@ +/* 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/. */ + +let timer = require("timer"); +let {Cc,Ci} = require("chrome"); + +// Arbitrary delay needed to avoid weird behavior. +// TODO: We need to find all uses of this and replace them +// with more deterministic solutions. +const ARB_DELAY = 100; + +// Select all divs elements in an HTML document +function selectAllDivs(window) { + let divs = window.document.getElementsByTagName("div"); + let s = window.getSelection(); + if (s.rangeCount > 0) + s.removeAllRanges(); + for (let i = 0; i < divs.length; i++) { + let range = window.document.createRange(); + range.selectNode(divs[i]); + s.addRange(range); + } +} + +function selectTextarea(window, from, to) { + let textarea = window.document.getElementsByTagName("textarea")[0]; + + from = from || 0; + to = to || textarea.value.length; + + textarea.setSelectionRange(from, to); + textarea.focus(); +} + +function primeTestCase(html, test, callback) { + let tabBrowser = require("tab-browser"); + let dataURL = "data:text/html," + encodeURI(html); + let tracker = tabBrowser.whenContentLoaded( + function(window) { + if (window.document.location.href != dataURL) + return; + callback(window, test); + timer.setTimeout(function() { + tracker.unload(); + test.done(); + window.close(); + }, + ARB_DELAY); + } + ); + tabBrowser.addTab(dataURL); +} + +const DIV1 = '<div id="foo">bar</div>'; +const DIV2 = '<div>noodles</div>'; +const HTML_MULTIPLE = '<html><body>' + DIV1 + DIV2 + '</body></html>'; +const HTML_SINGLE = '<html><body>' + DIV1 + '</body></html>'; + +// Tests of contiguous + +exports.testContiguousMultiple = function testContiguousMultiple(test) { + let selection = require("selection"); + primeTestCase(HTML_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.isContiguous, false, + "selection.isContiguous multiple works."); + }); + + test.waitUntilDone(5000); +}; + +exports.testContiguousSingle = function testContiguousSingle(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.isContiguous, true, + "selection.isContiguous single works."); + }); + + test.waitUntilDone(5000); +}; + +exports.testContiguousWithoutSelection = + function testContiguousWithoutSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + test.assertEqual(selection.isContiguous, false, + "selection.isContiguous without selection works."); + }); + + test.waitUntilDone(5000); +}; + +/** + * Test that setting the contiguous property has no effect. + */ +/*exports.testSetContiguous = function testSetContiguous(test) { + let selection = require("selection"); + primeTestCase(HTML_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + try { + selection.isContiguous = true; + test.assertEqual(selection.isContiguous, false, + "setting selection.isContiguous doesn't work (as expected)."); + } + catch (e) { + test.pass("setting selection.isContiguous doesn't work (as expected)."); + } + }); + + test.waitUntilDone(5000); +};*/ + + +// HTML tests + +exports.testGetHTMLSingleSelection = function testGetHTMLSingleSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.html, DIV1, "get html selection works"); + }); + + test.waitUntilDone(5000); +}; + +/* Myk's comments: This is fine. However, it reminds me to figure out and + specify whether iteration is ordered. If so, we'll want to change this + test in the future to test that the discontiguous selections are returned in + the appropriate order. In the meantime, add a comment to that effect here */ +exports.testGetHTMLMultipleSelection = + function testGetHTMLMultipleSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + let assertions = false; + for each (let i in selection) { + test.assertEqual(true, [DIV1, DIV2].some(function(t) t == i.html), + "get multiple selection html works"); + assertions = true; + } + // Ensure we ran at least one assertEqual() + test.assert(assertions, "No assertions were called"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetHTMLNull = function testGetHTMLNull(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + test.assertEqual(selection.html, null, "get html null works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetHTMLWeird = function testGetHTMLWeird(test) { + let selection = require("selection"); + // If the getter is used when there are contiguous selections, the first + // selection should be returned + primeTestCase(HTML_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.html, DIV1, "get html weird works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetHTMLNullInTextareaSelection = + function testGetHTMLNullInTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + test.assertEqual(selection.html, null, "get html null in textarea works") + }); + + test.waitUntilDone(5000); +}; + +const REPLACEMENT_HTML = "<b>Lorem ipsum dolor sit amet</b>"; + +exports.testSetHTMLSelection = function testSetHTMLSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + selectAllDivs(window); + selection.html = REPLACEMENT_HTML; + test.assertEqual(selection.html, "<span>" + REPLACEMENT_HTML + + "</span>", "selection html works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testSetHTMLException = function testSetHTMLException(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + try { + selection.html = REPLACEMENT_HTML; + test.fail("set HTML throws when there's no selection."); + } + catch (e) { + test.pass("set HTML throws when there's no selection."); + } + }); + + test.waitUntilDone(5000); +}; + +const TEXT1 = "foo"; +const TEXT2 = "noodles"; +const TEXT_MULTIPLE = "<html><body><div>" + TEXT1 + "</div><div>" + TEXT2 + + "</div></body></html>"; +const TEXT_SINGLE = "<html><body><div>" + TEXT1 + "</div></body></html>"; +const TEXT_FIELD = "<html><body><textarea>" + TEXT1 + "</textarea></body></html>"; + +// Text tests + +exports.testSetHTMLinTextareaSelection = + function testSetHTMLinTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + // HTML string is set as plain text in textareas, that's because + // `selection.html` and `selection.text` are basically aliases when a + // value is set. See bug 677269 + selection.html = REPLACEMENT_HTML; + + test.assertEqual(selection.text, REPLACEMENT_HTML, + "set selection html in textarea works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextSingleSelection = + function testGetTextSingleSelection(test) { + let selection = require("selection"); + primeTestCase(TEXT_SINGLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.text, TEXT1, "get text selection works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextMultipleSelection = + function testGetTextMultipleSelection(test) { + let selection = require("selection"); + primeTestCase(TEXT_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + let assertions = false; + for each (let i in selection) { + test.assertEqual(true, [TEXT1, TEXT2].some(function(t) t == i.text), + "get multiple selection text works"); + assertions = true; + } + // Ensure we ran at least one assertEqual() + test.assert(assertions, "No assertions were called"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextNull = function testGetTextNull(test) { + let selection = require("selection"); + primeTestCase(TEXT_SINGLE, test, function(window, test) { + test.assertEqual(selection.text, null, "get text null works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextWeird = function testGetTextWeird(test) { + let selection = require("selection"); + // If the getter is used when there are contiguous selections, the first + // selection should be returned + primeTestCase(TEXT_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + test.assertEqual(selection.text, TEXT1, "get text weird works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextNullInTextareaSelection = + function testGetTextInTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + test.assertEqual(selection.text, null, "get text null in textarea works") + }); + + test.waitUntilDone(5000); +}; + +exports.testGetTextInTextareaSelection = + function testGetTextInTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + test.assertEqual(selection.text, TEXT1, "get text null in textarea works") + }); + + test.waitUntilDone(5000); +}; + +const REPLACEMENT_TEXT = "Lorem ipsum dolor sit amet"; + +exports.testSetTextSelection = function testSetTextSelection(test) { + let selection = require("selection"); + primeTestCase(TEXT_SINGLE, test, function(window, test) { + selectAllDivs(window); + selection.text = REPLACEMENT_TEXT; + test.assertEqual(selection.text, REPLACEMENT_TEXT, "selection text works"); + }); + + test.waitUntilDone(5000); +}; + +exports.testSetHTMLException = function testSetHTMLException(test) { + let selection = require("selection"); + primeTestCase(TEXT_SINGLE, test, function(window, test) { + try { + selection.text = REPLACEMENT_TEXT; + test.fail("set HTML throws when there's no selection."); + } + catch (e) { + test.pass("set HTML throws when there's no selection."); + } + }); + + test.waitUntilDone(5000); +}; + +exports.testSetTextInTextareaSelection = + function testSetTextInTextareaSelection(test) { + let selection = require("selection"); + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + selection.text = REPLACEMENT_TEXT; + + test.assertEqual(selection.text, REPLACEMENT_TEXT, + "set selection text in textarea works"); + }); + + test.waitUntilDone(5000); +}; + +// Iterator tests + +exports.testIterator = function testIterator(test) { + let selection = require("selection"); + let selectionCount = 0; + primeTestCase(TEXT_MULTIPLE, test, function(window, test) { + selectAllDivs(window); + for each (let i in selection) + selectionCount++; + test.assertEqual(2, selectionCount, "iterator works."); + }); + + test.waitUntilDone(5000); +}; + +exports.testIteratorWithTextareaSelection = + function testIteratorWithTextareaSelection(test) { + let selection = require("selection"); + let selectionCount = 0; + + primeTestCase(TEXT_FIELD, test, function(window, test) { + selectTextarea(window); + + for each (let i in selection) + selectionCount++; + + test.assertEqual(1, selectionCount, "iterator works in textarea."); + }); + + test.waitUntilDone(5000); +}; + +/* onSelect tests */ + +/* +function sendSelectionSetEvent(window) { + const Ci = Components.interfaces; + let utils = window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + if (!utils.sendSelectionSetEvent(0, 1, false)) + dump("**** sendSelectionSetEvent did not select anything\n"); + else + dump("**** sendSelectionSetEvent succeeded\n"); +} + +// testOnSelect() requires nsIDOMWindowUtils, which is only available in +// Firefox 3.7+. +exports.testOnSelect = function testOnSelect(test) { + let selection = require("selection"); + let callbackCount = 0; + primeTestCase(TEXT_SINGLE, test, function(window, test) { + selection.onSelect = function() {callbackCount++}; + // Now simulate the user selecting stuff + sendSelectionSetEvent(window); + selection.text = REPLACEMENT_TEXT; + test.assertEqual(1, callbackCount, "onSelect text listener works."); + //test.pass(); + //test.done(); + }); + + test.waitUntilDone(5000); +}; + +// testOnSelectExceptionNoBubble() requires nsIDOMWindowUtils, which is only +// available in Firefox 3.7+. +exports.testOnSelectExceptionNoBubble = + function testOnSelectTextSelection(test) { + let selection = require("selection"); + primeTestCase(HTML_SINGLE, test, function(window, test) { + selection.onSelect = function() { + throw new Error("Exception thrown in testOnSelectExceptionNoBubble"); + }; + // Now simulate the user selecting stuff + sendSelectionSetEvent(window); + test.pass("onSelect catches exceptions."); + }); + + test.waitUntilDone(5000); +}; +*/ + +// 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("selection"); +} +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("The selection module does not support this application."); + }; +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-simple-prefs.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-simple-prefs.js new file mode 100644 index 0000000..7452a8a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-simple-prefs.js @@ -0,0 +1,175 @@ +/* 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/. */ + + +const { Loader } = require("./helpers"); +const { setTimeout } = require("timers"); +const { notify } = require("observer-service"); +const { jetpackID } = require("@packaging"); + +exports.testSetGetBool = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + test.assertEqual(sp.test, undefined, "Value should not exist"); + sp.test = true; + test.assert(sp.test, "Value read should be the value previously set"); + + loader.unload(); + test.done(); +}; + +exports.testSetGetInt = function(test) { + test.waitUntilDone(); + + // Load the module once, set a value. + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + test.assertEqual(sp["test-int"], undefined, "Value should not exist"); + sp["test-int"] = 1; + test.assertEqual(sp["test-int"], 1, "Value read should be the value previously set"); + + loader.unload(); + test.done(); +}; + +exports.testSetComplex = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + try { + sp["test-complex"] = {test: true}; + test.fail("Complex values are not allowed"); + } + catch (e) { + test.pass("Complex values are not allowed"); + } + + loader.unload(); + test.done(); +}; + +exports.testSetGetString = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + test.assertEqual(sp["test-string"], undefined, "Value should not exist"); + sp["test-string"] = "test"; + test.assertEqual(sp["test-string"], "test", "Value read should be the value previously set"); + + loader.unload(); + test.done(); +}; + +exports.testHasAndRemove = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs").prefs; + + sp.test = true; + test.assert(("test" in sp), "Value exists"); + delete sp.test; + test.assertEqual(sp.test, undefined, "Value should be undefined"); + + loader.unload(); + test.done(); + +}; + +exports.testPrefListener = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs"); + + let listener = function(prefName) { + test.assertEqual(prefName, "test-listen", "The prefs listener heard the right event"); + test.done(); + }; + + sp.on("test-listen", listener); + + sp.prefs["test-listen"] = true; + loader.unload(); +}; + +exports.testBtnListener = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs"); + + sp.on("test-btn-listen", function() { + test.pass("Button press event was heard"); + test.done(); + }); + notify((jetpackID + "-cmdPressed"), "", "test-btn-listen"); + + loader.unload(); +}; + +exports.testPrefRemoveListener = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs"); + let counter = 0; + + let listener = function() { + test.pass("The prefs listener was not removed yet"); + + if (++counter > 1) + test.fail("The prefs listener was not removed"); + + sp.removeListener("test-listen2", listener); + + sp.prefs["test-listen2"] = false; + + setTimeout(function() { + test.pass("The prefs listener was removed"); + loader.unload(); + test.done(); + }, 250); + }; + + sp.on("test-listen2", listener); + + // emit change + sp.prefs["test-listen2"] = true; +}; + +// Bug 710117: Test that simple-pref listeners are removed on unload +exports.testPrefUnloadListener = function(test) { + test.waitUntilDone(); + + let loader = Loader(module); + let sp = loader.require("simple-prefs"); + let counter = 0; + + let listener = function() { + test.assertEqual(++counter, 1, "This listener should only be called once"); + + loader.unload(); + + // this may not execute after unload, but definitely shouldn't fire listener + sp.prefs["test-listen3"] = false; + // this should execute, but also definitely shouldn't fire listener + require("simple-prefs").prefs["test-listen3"] = false; // + + test.done(); + }; + + sp.on("test-listen3", listener); + + // emit change + sp.prefs["test-listen3"] = true; +}; diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-simple-storage.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-simple-storage.js new file mode 100644 index 0000000..25d0770 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-simple-storage.js @@ -0,0 +1,311 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * 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/. */ + +const file = require("file"); +const prefs = require("preferences-service"); + +const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota"; + +let {Cc,Ci} = require("chrome"); + +const { Loader } = require("./helpers"); +const options = require("@packaging"); + +let storeFile = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); +storeFile.append("jetpack"); +storeFile.append(options.jetpackID); +storeFile.append("simple-storage"); +storeFile.append("store.json"); +let storeFilename = storeFile.path; + +function manager(loader) loader.sandbox("simple-storage").manager; + +exports.testSetGet = function (test) { + test.waitUntilDone(); + + // Load the module once, set a value. + let loader = Loader(module); + let ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + test.assert(file.exists(storeFilename), "Store file should exist"); + + // Load the module again and make sure the value stuck. + loader = Loader(module); + ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + file.remove(storeFilename); + test.done(); + }; + test.assertEqual(ss.storage.foo, val, "Value should persist"); + loader.unload(); + }; + let val = "foo"; + ss.storage.foo = val; + test.assertEqual(ss.storage.foo, val, "Value read should be value set"); + loader.unload(); +}; + +exports.testSetGetRootArray = function (test) { + setGetRoot(test, [1, 2, 3], function (arr1, arr2) { + if (arr1.length !== arr2.length) + return false; + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) + return false; + } + return true; + }); +}; + +exports.testSetGetRootBool = function (test) { + setGetRoot(test, true); +}; + +exports.testSetGetRootFunction = function (test) { + setGetRootError(test, function () {}, + "Setting storage to a function should fail"); +}; + +exports.testSetGetRootNull = function (test) { + setGetRoot(test, null); +}; + +exports.testSetGetRootNumber = function (test) { + setGetRoot(test, 3.14); +}; + +exports.testSetGetRootObject = function (test) { + setGetRoot(test, { foo: 1, bar: 2 }, function (obj1, obj2) { + for (let [prop, val] in Iterator(obj1)) { + if (!(prop in obj2) || obj2[prop] !== val) + return false; + } + for (let [prop, val] in Iterator(obj2)) { + if (!(prop in obj1) || obj1[prop] !== val) + return false; + } + return true; + }); +}; + +exports.testSetGetRootString = function (test) { + setGetRoot(test, "sho' 'nuff"); +}; + +exports.testSetGetRootUndefined = function (test) { + setGetRootError(test, undefined, "Setting storage to undefined should fail"); +}; + +exports.testEmpty = function (test) { + let loader = Loader(module); + let ss = loader.require("simple-storage"); + loader.unload(); + test.assert(!file.exists(storeFilename), "Store file should not exist"); +}; + +exports.testMalformed = function (test) { + let stream = file.open(storeFilename, "w"); + stream.write("i'm not json"); + stream.close(); + let loader = Loader(module); + let ss = loader.require("simple-storage"); + let empty = true; + for (let key in ss.storage) { + empty = false; + break; + } + test.assert(empty, "Malformed storage should cause root to be empty"); + loader.unload(); +}; + +// Go over quota and handle it by listener. +exports.testQuotaExceededHandle = function (test) { + test.waitUntilDone(); + prefs.set(QUOTA_PREF, 18); + + let loader = Loader(module); + let ss = loader.require("simple-storage"); + ss.on("OverQuota", function () { + test.pass("OverQuota was emitted as expected"); + test.assertEqual(this, ss, "`this` should be simple storage"); + ss.storage = { x: 4, y: 5 }; + + manager(loader).jsonStore.onWrite = function () { + loader = Loader(module); + ss = loader.require("simple-storage"); + let numProps = 0; + for (let prop in ss.storage) + numProps++; + test.assert(numProps, 2, + "Store should contain 2 values: " + ss.storage.toSource()); + test.assertEqual(ss.storage.x, 4, "x value should be correct"); + test.assertEqual(ss.storage.y, 5, "y value should be correct"); + manager(loader).jsonStore.onWrite = function (storage) { + prefs.reset(QUOTA_PREF); + test.done(); + }; + loader.unload(); + }; + loader.unload(); + }); + // This will be JSON.stringify()ed to: {"a":1,"b":2,"c":3} (19 bytes) + ss.storage = { a: 1, b: 2, c: 3 }; + manager(loader).jsonStore.write(); +}; + +// Go over quota but don't handle it. The last good state should still persist. +exports.testQuotaExceededNoHandle = function (test) { + test.waitUntilDone(); + prefs.set(QUOTA_PREF, 5); + + let loader = Loader(module); + let ss = loader.require("simple-storage"); + + manager(loader).jsonStore.onWrite = function (storage) { + loader = Loader(module); + ss = loader.require("simple-storage"); + test.assertEqual(ss.storage, val, + "Value should have persisted: " + ss.storage); + ss.storage = "some very long string that is very long"; + ss.on("OverQuota", function () { + test.pass("OverQuota emitted as expected"); + manager(loader).jsonStore.onWrite = function () { + test.fail("Over-quota value should not have been written"); + }; + loader.unload(); + + loader = Loader(module); + ss = loader.require("simple-storage"); + test.assertEqual(ss.storage, val, + "Over-quota value should not have been written, " + + "old value should have persisted: " + ss.storage); + loader.unload(); + prefs.reset(QUOTA_PREF); + test.done(); + }); + manager(loader).jsonStore.write(); + }; + + let val = "foo"; + ss.storage = val; + loader.unload(); +}; + +exports.testQuotaUsage = function (test) { + test.waitUntilDone(); + + let quota = 21; + prefs.set(QUOTA_PREF, quota); + + let loader = Loader(module); + let ss = loader.require("simple-storage"); + + // {"a":1} (7 bytes) + ss.storage = { a: 1 }; + test.assertEqual(ss.quotaUsage, 7 / quota, "quotaUsage should be correct"); + + // {"a":1,"bb":2} (14 bytes) + ss.storage = { a: 1, bb: 2 }; + test.assertEqual(ss.quotaUsage, 14 / quota, "quotaUsage should be correct"); + + // {"a":1,"bb":2,"cc":3} (21 bytes) + ss.storage = { a: 1, bb: 2, cc: 3 }; + test.assertEqual(ss.quotaUsage, 21 / quota, "quotaUsage should be correct"); + + manager(loader).jsonStore.onWrite = function () { + prefs.reset(QUOTA_PREF); + test.done(); + }; + loader.unload(); +}; + +exports.testUninstall = function (test) { + test.waitUntilDone(); + let loader = Loader(module); + let ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function () { + test.assert(file.exists(storeFilename), "Store file should exist"); + + loader = Loader(module); + ss = loader.require("simple-storage"); + loader.unload("uninstall"); + test.assert(!file.exists(storeFilename), "Store file should be removed"); + test.done(); + }; + ss.storage.foo = "foo"; + loader.unload(); +}; + +exports.testSetNoSetRead = function (test) { + test.waitUntilDone(); + + // Load the module, set a value. + let loader = Loader(module); + let ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + test.assert(file.exists(storeFilename), "Store file should exist"); + + // Load the module again but don't access ss.storage. + loader = Loader(module); + ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + test.fail("Nothing should be written since `storage` was not accessed."); + }; + loader.unload(); + + // Load the module a third time and make sure the value stuck. + loader = Loader(module); + ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function (storage) { + file.remove(storeFilename); + test.done(); + }; + test.assertEqual(ss.storage.foo, val, "Value should persist"); + loader.unload(); + }; + let val = "foo"; + ss.storage.foo = val; + test.assertEqual(ss.storage.foo, val, "Value read should be value set"); + loader.unload(); +}; + + +function setGetRoot(test, val, compare) { + test.waitUntilDone(); + + compare = compare || function (a, b) a === b; + + // Load the module once, set a value. + let loader = Loader(module); + let ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function () { + test.assert(file.exists(storeFilename), "Store file should exist"); + + // Load the module again and make sure the value stuck. + loader = Loader(module); + ss = loader.require("simple-storage"); + manager(loader).jsonStore.onWrite = function () { + file.remove(storeFilename); + test.done(); + }; + test.assert(compare(ss.storage, val), "Value should persist"); + loader.unload(); + }; + ss.storage = val; + test.assert(compare(ss.storage, val), "Value read should be value set"); + loader.unload(); +} + +function setGetRootError(test, val, msg) { + let pred = "storage must be one of the following types: " + + "array, boolean, null, number, object, string"; + let loader = Loader(module); + let ss = loader.require("simple-storage"); + test.assertRaises(function () ss.storage = val, pred, msg); + loader.unload(); +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-tabs.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-tabs.js new file mode 100644 index 0000000..5719ab4 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-tabs.js @@ -0,0 +1,900 @@ +/* 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"; + +var {Cc,Ci} = require("chrome"); +const { Loader } = require("./helpers"); + +// test tab.activeTab getter +exports.testActiveTab_getter = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + + let url = "data:text/html,<html><head><title>foo</title></head></html>"; + require("tab-browser").addTab( + url, + { + onLoad: function(e) { + test.assert(tabs.activeTab); + test.assertEqual(tabs.activeTab.url, url); + test.assertEqual(tabs.activeTab.title, "foo"); + closeBrowserWindow(window, function() test.done()); + } + } + ); + }); +}; + +// test 'BrowserWindow' instance creation on tab 'activate' event +// See bug 648244: there was a infinite loop. +exports.testBrowserWindowCreationOnActivate = function(test) { + test.waitUntilDone(); + + let windows = require("windows").browserWindows; + let tabs = require("tabs"); + + let gotActivate = false; + + tabs.once('activate', function onActivate(eventTab) { + test.assert(windows.activeWindow, "Is able to fetch activeWindow"); + gotActivate = true; + }); + + openBrowserWindow(function(window, browser) { + test.assert(gotActivate, "Received activate event before openBrowserWindow's callback is called"); + closeBrowserWindow(window, function () test.done()); + }); +} + +// test tab.activeTab setter +exports.testActiveTab_setter = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,<html><head><title>foo</title></head></html>"; + + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + test.assertEqual(tabs.activeTab.url, "about:blank", "activeTab url has not changed"); + test.assertEqual(tab.url, url, "url of new background tab matches"); + tabs.on('activate', function onActivate(eventTab) { + tabs.removeListener('activate', onActivate); + test.assertEqual(tabs.activeTab.url, url, "url after activeTab setter matches"); + test.assertEqual(eventTab, tab, "event argument is the activated tab"); + test.assertEqual(eventTab, tabs.activeTab, "the tab is the active one"); + closeBrowserWindow(window, function() test.done()); + }); + tab.activate(); + }) + + tabs.open({ + url: url, + inBackground: true + }); + }); +}; + +exports.testAutomaticDestroy = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + + // Create a second tab instance that we will destroy + let called = false; + + let loader = Loader(module); + let tabs2 = loader.require("tabs"); + tabs2.on('open', function onOpen(tab) { + called = true; + }); + + loader.unload(); + + // Fire a tab event an ensure that this destroyed tab is inactive + tabs.once('open', function () { + require("timer").setTimeout(function () { + test.assert(!called, "Unloaded tab module is destroyed and inactive"); + closeBrowserWindow(window, function() test.done()); + }, 0); + }); + + tabs.open("data:text/html,foo"); + + }); +}; + +// test tab properties +exports.testTabProperties = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs= require("tabs"); + let url = "data:text/html,<html><head><title>foo</title></head><body>foo</body></html>"; + tabs.open({ + url: url, + onReady: function(tab) { + test.assertEqual(tab.title, "foo", "title of the new tab matches"); + test.assertEqual(tab.url, url, "URL of the new tab matches"); + test.assert(tab.favicon, "favicon of the new tab is not empty"); + test.assertEqual(tab.style, null, "style of the new tab matches"); + test.assertEqual(tab.index, 1, "index of the new tab matches"); + test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// test tabs iterator and length property +exports.testTabsIteratorAndLength = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let startCount = 0; + for each (let t in tabs) startCount++; + test.assertEqual(startCount, tabs.length, "length property is correct"); + let url = "data:text/html,default"; + tabs.open(url); + tabs.open(url); + tabs.open({ + url: url, + onOpen: function(tab) { + let count = 0; + for each (let t in tabs) count++; + test.assertEqual(count, startCount + 3, "iterated tab count matches"); + test.assertEqual(startCount + 3, tabs.length, "iterated tab count matches length property"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// test tab.url setter +exports.testTabLocation = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url1 = "data:text/html,foo"; + let url2 = "data:text/html,bar"; + + tabs.on('ready', function onReady(tab) { + if (tab.url != url2) + return; + tabs.removeListener('ready', onReady); + test.pass("tab.load() loaded the correct url"); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.open({ + url: url1, + onOpen: function(tab) { + tab.url = url2 + } + }); + }); +}; + +// test tab.close() +exports.testTabClose = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,foo"; + + test.assertNotEqual(tabs.activeTab.url, url, "tab is now the active tab"); + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + test.assertEqual(tabs.activeTab.url, tab.url, "tab is now the active tab"); + tab.close(function() { + closeBrowserWindow(window, function() test.done()); + }); + test.assertNotEqual(tabs.activeTab.url, url, "tab is no longer the active tab"); + }); + + tabs.open(url); + }); +}; + +// test tab.reload() +exports.testTabReload = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,<!doctype%20html><title></title>"; + + tabs.open({ url: url, onReady: function onReady(tab) { + tab.removeListener("ready", onReady); + + browser.addEventListener( + "load", + function onLoad() { + browser.removeEventListener("load", onLoad, true); + + browser.addEventListener( + "load", + function onReload() { + browser.removeEventListener("load", onReload, true); + test.pass("the tab was loaded again"); + test.assertEqual(tab.url, url, "the tab has the same URL"); + closeBrowserWindow(window, function() test.done()); + }, + true + ); + tab.reload(); + }, + true + ); + }}); + }); +}; + +// test tab.move() +exports.testTabMove = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,foo"; + + tabs.open({ + url: url, + onOpen: function(tab) { + test.assertEqual(tab.index, 1, "tab index before move matches"); + tab.index = 0; + test.assertEqual(tab.index, 0, "tab index after move matches"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// open tab with default options +exports.testOpen = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,default"; + tabs.open({ + url: url, + onReady: function(tab) { + test.assertEqual(tab.url, url, "URL of the new tab matches"); + test.assertEqual(window.content.location, url, "URL of active tab in the current window matches"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// open pinned tab +exports.testOpenPinned = function(test) { + const xulApp = require("xul-app"); + if (xulApp.versionInRange(xulApp.platformVersion, "2.0b2", "*")) { + // test tab pinning + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,default"; + tabs.open({ + url: url, + isPinned: true, + onOpen: function(tab) { + test.assertEqual(tab.isPinned, true, "The new tab is pinned"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); + } + else { + test.pass("Pinned tabs are not supported in this application."); + } +}; + +// pin/unpin opened tab +exports.testPinUnpin = function(test) { + const xulApp = require("xul-app"); + if (xulApp.versionInRange(xulApp.platformVersion, "2.0b2", "*")) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let url = "data:text/html,default"; + tabs.open({ + url: url, + onOpen: function(tab) { + tab.pin(); + test.assertEqual(tab.isPinned, true, "The tab was pinned correctly"); + tab.unpin(); + test.assertEqual(tab.isPinned, false, "The tab was unpinned correctly"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); + } + else { + test.pass("Pinned tabs are not supported in this application."); + } +}; + +// open tab in background +exports.testInBackground = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let activeUrl = tabs.activeTab.url; + let url = "data:text/html,background"; + test.assertEqual(activeWindow, window, "activeWindow matches this window"); + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + test.assertEqual(tabs.activeTab.url, activeUrl, "URL of active tab has not changed"); + test.assertEqual(tab.url, url, "URL of the new background tab matches"); + test.assertEqual(activeWindow, window, "a new window was not opened"); + test.assertNotEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL"); + closeBrowserWindow(window, function() test.done()); + }); + tabs.open({ + url: url, + inBackground: true + }); + }); +}; + +// open tab in new window +exports.testOpenInNewWindow = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + + let cache = []; + let windowUtils = require("window-utils"); + let wt = new windowUtils.WindowTracker({ + onTrack: function(win) { + cache.push(win); + }, + onUntrack: function(win) { + cache.splice(cache.indexOf(win), 1) + } + }); + let startWindowCount = cache.length; + + let url = "data:text/html,newwindow"; + tabs.open({ + url: url, + inNewWindow: true, + onReady: function(tab) { + let newWindow = cache[cache.length - 1]; + test.assertEqual(cache.length, startWindowCount + 1, "a new window was opened"); + test.assertEqual(activeWindow, newWindow, "new window is active"); + test.assertEqual(tab.url, url, "URL of the new tab matches"); + test.assertEqual(newWindow.content.location, url, "URL of new tab in new window matches"); + test.assertEqual(tabs.activeTab.url, url, "URL of activeTab matches"); + for (var i in cache) cache[i] = null; + wt.unload(); + closeBrowserWindow(newWindow, function() { + closeBrowserWindow(window, function() test.done()); + }); + } + }); + }); +}; + +// onOpen event handler +exports.testTabsEvent_onOpen = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,1"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('open', listener1); + + // add listener via collection add + tabs.on('open', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('open', listener1); + tabs.removeListener('open', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.open(url); + }); +}; + +// onClose event handler +exports.testTabsEvent_onClose = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,onclose"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + } + tabs.on('close', listener1); + + // add listener via collection add + tabs.on('close', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('close', listener1); + tabs.removeListener('close', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + tab.close(); + }); + + tabs.open(url); + }); +}; + +// onClose event handler when a window is closed +exports.testTabsEvent_onCloseWindow = function(test) { + test.waitUntilDone(); + + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + + let closeCount = 0, individualCloseCount = 0; + function listener() { + closeCount++; + } + tabs.on('close', listener); + + // One tab is already open with the window + let openTabs = 1; + function testCasePossiblyLoaded() { + if (++openTabs == 4) { + beginCloseWindow(); + } + } + + tabs.open({ + url: "data:text/html,tab2", + onOpen: function() testCasePossiblyLoaded(), + onClose: function() individualCloseCount++ + }); + + tabs.open({ + url: "data:text/html,tab3", + onOpen: function() testCasePossiblyLoaded(), + onClose: function() individualCloseCount++ + }); + + tabs.open({ + url: "data:text/html,tab4", + onOpen: function() testCasePossiblyLoaded(), + onClose: function() individualCloseCount++ + }); + + function beginCloseWindow() { + closeBrowserWindow(window, function testFinished() { + tabs.removeListener("close", listener); + + test.assertEqual(closeCount, 4, "Correct number of close events received"); + test.assertEqual(individualCloseCount, 3, + "Each tab with an attached onClose listener received a close " + + "event when the window was closed"); + + test.done(); + }); + } + + }); +} + +// onReady event handler +exports.testTabsEvent_onReady = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,onready"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('ready', listener1); + + // add listener via collection add + tabs.on('ready', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('ready', listener1); + tabs.removeListener('ready', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.open(url); + }); +}; + +// onActivate event handler +exports.testTabsEvent_onActivate = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,onactivate"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('activate', listener1); + + // add listener via collection add + tabs.on('activate', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('activate', listener1); + tabs.removeListener('activate', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.open(url); + }); +}; + +// onDeactivate event handler +exports.testTabsEvent_onDeactivate = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,ondeactivate"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('deactivate', listener1); + + // add listener via collection add + tabs.on('deactivate', function listener2(tab) { + test.assertEqual(++eventCount, 2, "both listeners notified"); + tabs.removeListener('deactivate', listener1); + tabs.removeListener('deactivate', listener2); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.on('open', function onOpen(tab) { + tabs.removeListener('open', onOpen); + tabs.open("data:text/html,foo"); + }); + + tabs.open(url); + }); +}; + +exports.testTabsEvent_pinning = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let url = "data:text/html,1"; + + tabs.on('open', function onOpen(tab) { + tabs.removeListener('open', onOpen); + tab.pin(); + }); + + tabs.on('pinned', function onPinned(tab) { + tabs.removeListener('pinned', onPinned); + test.assert(tab.isPinned, "notified tab is pinned"); + tab.unpin(); + }); + + tabs.on('unpinned', function onUnpinned(tab) { + tabs.removeListener('unpinned', onUnpinned); + test.assert(!tab.isPinned, "notified tab is not pinned"); + closeBrowserWindow(window, function() test.done()); + }); + + tabs.open(url); + }); +}; + +// per-tab event handlers +exports.testPerTabEvents = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + var tabs = require("tabs"); + let eventCount = 0; + + tabs.open({ + url: "data:text/html,foo", + onOpen: function(tab) { + // add listener via property assignment + function listener1() { + eventCount++; + }; + tab.on('ready', listener1); + + // add listener via collection add + tab.on('ready', function listener2() { + test.assertEqual(eventCount, 1, "both listeners notified"); + tab.removeListener('ready', listener1); + tab.removeListener('ready', listener2); + closeBrowserWindow(window, function() test.done()); + }); + } + }); + }); +}; + +exports.testAttachOnOpen = function (test) { + // Take care that attach has to be called on tab ready and not on tab open. + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + + tabs.open({ + url: "data:text/html,foobar", + onOpen: function (tab) { + let worker = tab.attach({ + contentScript: 'self.postMessage(document.location.href); ', + onMessage: function (msg) { + test.assertEqual(msg, "about:blank", + "Worker document url is about:blank on open"); + worker.destroy(); + closeBrowserWindow(window, function() test.done()); + } + }); + } + }); + + }); +} + +exports.testAttachOnMultipleDocuments = function (test) { + // Example of attach that process multiple tab documents + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let firstLocation = "data:text/html,foobar"; + let secondLocation = "data:text/html,bar"; + let thirdLocation = "data:text/html,fox"; + let onReadyCount = 0; + let worker1 = null; + let worker2 = null; + let detachEventCount = 0; + tabs.open({ + url: firstLocation, + onReady: function (tab) { + onReadyCount++; + if (onReadyCount == 1) { + worker1 = tab.attach({ + contentScript: 'self.on("message", ' + + ' function () self.postMessage(document.location.href)' + + ');', + onMessage: function (msg) { + test.assertEqual(msg, firstLocation, + "Worker url is equal to the 1st document"); + tab.url = secondLocation; + }, + onDetach: function () { + detachEventCount++; + test.pass("Got worker1 detach event"); + test.assertRaises(function () { + worker1.postMessage("ex-1"); + }, + /The page has been destroyed/, + "postMessage throw because worker1 is destroyed"); + checkEnd(); + } + }); + worker1.postMessage("new-doc-1"); + } + else if (onReadyCount == 2) { + + worker2 = tab.attach({ + contentScript: 'self.on("message", ' + + ' function () self.postMessage(document.location.href)' + + ');', + onMessage: function (msg) { + test.assertEqual(msg, secondLocation, + "Worker url is equal to the 2nd document"); + tab.url = thirdLocation; + }, + onDetach: function () { + detachEventCount++; + test.pass("Got worker2 detach event"); + test.assertRaises(function () { + worker2.postMessage("ex-2"); + }, + /The page has been destroyed/, + "postMessage throw because worker2 is destroyed"); + checkEnd(); + } + }); + worker2.postMessage("new-doc-2"); + } + else if (onReadyCount == 3) { + + tab.close(); + + } + + } + }); + + function checkEnd() { + if (detachEventCount != 2) + return; + + test.pass("Got all detach events"); + + closeBrowserWindow(window, function() test.done()); + } + + }); +} + + +exports.testAttachWrappers = function (test) { + // Check that content script has access to wrapped values by default + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let document = "data:text/html,<script>var globalJSVar = true; " + + " document.getElementById = 3;</script>"; + let count = 0; + + tabs.open({ + url: document, + onReady: function (tab) { + let worker = tab.attach({ + contentScript: 'try {' + + ' self.postMessage(!("globalJSVar" in window));' + + ' self.postMessage(typeof window.globalJSVar == "undefined");' + + '} catch(e) {' + + ' self.postMessage(e.message);' + + '}', + onMessage: function (msg) { + test.assertEqual(msg, true, "Worker has wrapped objects ("+count+")"); + if (count++ == 1) + closeBrowserWindow(window, function() test.done()); + } + }); + } + }); + + }); +} + +/* +// We do not offer unwrapped access to DOM since bug 601295 landed +// See 660780 to track progress of unwrap feature +exports.testAttachUnwrapped = function (test) { + // Check that content script has access to unwrapped values through unsafeWindow + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + let tabs = require("tabs"); + let document = "data:text/html,<script>var globalJSVar=true;</script>"; + let count = 0; + + tabs.open({ + url: document, + onReady: function (tab) { + let worker = tab.attach({ + contentScript: 'try {' + + ' self.postMessage(unsafeWindow.globalJSVar);' + + '} catch(e) {' + + ' self.postMessage(e.message);' + + '}', + onMessage: function (msg) { + test.assertEqual(msg, true, "Worker has access to javascript content globals ("+count+")"); + closeBrowserWindow(window, function() test.done()); + } + }); + } + }); + + }); +} +*/ + +exports['test window focus changes active tab'] = function(test) { + test.waitUntilDone(); + let win1 = openBrowserWindow(function() { + let win2 = openBrowserWindow(function() { + let tabs = require("tabs"); + tabs.on("activate", function onActivate() { + tabs.removeListener("activate", onActivate); + test.pass("activate was called on windows focus change."); + closeBrowserWindow(win1, function() { + closeBrowserWindow(win2, function() { test.done(); }); + }); + }); + win1.focus(); + }, "data:text/html,test window focus changes active tab</br><h1>Window #2"); + }, "data:text/html,test window focus changes active tab</br><h1>Window #1"); +}; + +exports['test ready event on new window tab'] = function(test) { + test.waitUntilDone(); + let uri = encodeURI("data:text/html,Waiting for ready event!"); + + require("tabs").on("ready", function onReady(tab) { + if (tab.url === uri) { + require("tabs").removeListener("ready", onReady); + test.pass("ready event was emitted"); + closeBrowserWindow(window, function() { + test.done(); + }); + } + }); + + let window = openBrowserWindow(function(){}, uri); +}; +/******************* helpers *********************/ + +// Helper for getting the active window +this.__defineGetter__("activeWindow", function activeWindow() { + return Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); +}); + +// Utility function to open a new browser window. +function openBrowserWindow(callback, url) { + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + let urlString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + urlString.data = url; + let window = ww.openWindow(null, "chrome://browser/content/browser.xul", + "_blank", "chrome,all,dialog=no", urlString); + + if (callback) { + window.addEventListener("load", function onLoad(event) { + if (event.target && event.target.defaultView == window) { + window.removeEventListener("load", onLoad, true); + let browsers = window.document.getElementsByTagName("tabbrowser"); + try { + require("timer").setTimeout(function () { + callback(window, browsers[0]); + }, 10); + } catch (e) { console.exception(e); } + } + }, true); + } + + return window; +} + +// Helper for calling code at window close +function closeBrowserWindow(window, callback) { + window.addEventListener("unload", function unload() { + window.removeEventListener("unload", unload, false); + callback(); + }, false); + window.close(); +} + +// 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("tabs"); +} +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("the tabs module does not support this application."); + }; +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-timers.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-timers.js new file mode 100644 index 0000000..90b26bf --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-timers.js @@ -0,0 +1,12 @@ +/* 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/. */ +const timers = require("timers"); + +exports.testTimeout = function (test) { + test.waitUntilDone(); + timers.setTimeout(function () { + test.pass("timers.setTimeout works"); + test.done(); + }, 0); +} diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-widget.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-widget.js new file mode 100644 index 0000000..5d3475b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-widget.js @@ -0,0 +1,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."); + }; +} + diff --git a/tools/addon-sdk-1.7/packages/addon-kit/tests/test-windows.js b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-windows.js new file mode 100644 index 0000000..b85fec7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/addon-kit/tests/test-windows.js @@ -0,0 +1,309 @@ +/* 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/. */ + +const {Cc, Ci} = require("chrome"); +const { setTimeout } = require("timer"); +const { Loader } = require('./helpers'); +const wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); +let browserWindows; + +function getTestRunnerWindow() wm.getMostRecentWindow("test:runner") + +exports.testOpenAndCloseWindow = function(test) { + test.waitUntilDone(); + + test.assertEqual(browserWindows.length, 1, "Only one window open"); + + browserWindows.open({ + url: "data:text/html,<title>windows API test</title>", + onOpen: function(window) { + test.assertEqual(this, browserWindows, + "The 'this' object is the windows object."); + test.assertEqual(window.tabs.length, 1, "Only one tab open"); + test.assertEqual(browserWindows.length, 2, "Two windows open"); + window.tabs.activeTab.on('ready', function onReady(tab) { + tab.removeListener('ready', onReady); + test.assert(window.title.indexOf("windows API test") != -1, + "URL correctly loaded"); + window.close(); + }); + }, + onClose: function(window) { + test.assertEqual(window.tabs.length, 0, "Tabs were cleared"); + test.assertEqual(browserWindows.length, 1, "Only one window open"); + test.done(); + } + }); +}; + +exports.testAutomaticDestroy = function(test) { + + test.waitUntilDone(); + let windows = browserWindows; + + // Create a second windows instance that we will unload + let called = false; + let loader = Loader(module); + let windows2 = loader.require("windows").browserWindows; + windows2.on("open", function() { + called = true; + }); + + loader.unload(); + + // Fire a windows event and check that this unloaded instance is inactive + windows.open({ + url: "data:text/html,foo", + onOpen: function(window) { + setTimeout(function () { + test.assert(!called, + "Unloaded windows instance is destroyed and inactive"); + window.close(function () { + test.done(); + }); + }); + } + }); + +}; + +exports.testOnOpenOnCloseListeners = function(test) { + test.waitUntilDone(); + let windows = browserWindows; + + test.assertEqual(browserWindows.length, 1, "Only one window open"); + + let received = { + listener1: false, + listener2: false, + listener3: false, + listener4: false + } + + function listener1() { + test.assertEqual(this, windows, "The 'this' object is the windows object."); + if (received.listener1) + test.fail("Event received twice"); + received.listener1 = true; + } + + function listener2() { + if (received.listener2) + test.fail("Event received twice"); + received.listener2 = true; + } + + function listener3() { + test.assertEqual(this, windows, "The 'this' object is the windows object."); + if (received.listener3) + test.fail("Event received twice"); + received.listener3 = true; + } + + function listener4() { + if (received.listener4) + test.fail("Event received twice"); + received.listener4 = true; + } + + windows.on('open', listener1); + windows.on('open', listener2); + windows.on('close', listener3); + windows.on('close', listener4); + + function verify() { + test.assert(received.listener1, "onOpen handler called"); + test.assert(received.listener2, "onOpen handler called"); + test.assert(received.listener3, "onClose handler called"); + test.assert(received.listener4, "onClose handler called"); + + windows.removeListener('open', listener1); + windows.removeListener('open', listener2); + windows.removeListener('close', listener3); + windows.removeListener('close', listener4); + test.done(); + } + + + windows.open({ + url: "data:text/html,foo", + onOpen: function(window) { + window.close(verify); + } + }); +}; + +exports.testWindowTabsObject = function(test) { + test.waitUntilDone(); + + browserWindows.open({ + url: "data:text/html,<title>tab 1</title>", + onOpen: function onOpen(window) { + test.assertEqual(window.tabs.length, 1, "Only 1 tab open"); + + window.tabs.open({ + url: "data:text/html,<title>tab 2</title>", + inBackground: true, + onReady: function onReady(newTab) { + test.assertEqual(window.tabs.length, 2, "New tab open"); + test.assertEqual(newTab.title, "tab 2", "Correct new tab title"); + test.assertEqual(window.tabs.activeTab.title, "tab 1", "Correct active tab"); + + let i = 1; + for each (let tab in window.tabs) + test.assertEqual(tab.title, "tab " + i++, "Correct title"); + + window.close(); + } + }); + }, + onClose: function onClose(window) { + test.assertEqual(window.tabs.length, 0, "No more tabs on closed window"); + test.done(); + } + }); +}; + +exports.testActiveWindow = function(test) { + const xulApp = require("xul-app"); + if (xulApp.versionInRange(xulApp.platformVersion, "1.9.2", "1.9.2.*")) { + test.pass("This test is disabled on 3.6. For more information, see bug 598525"); + return; + } + + let windows = browserWindows; + + // API window objects + let window2, window3; + + // Raw window objects + let nonBrowserWindow = getTestRunnerWindow(), rawWindow2, rawWindow3; + + test.waitUntilDone(); + + let testSteps = [ + function() { + test.assertEqual(windows.length, 3, "Correct number of browser windows"); + let count = 0; + for (let window in windows) + count++; + test.assertEqual(count, 3, "Correct number of windows returned by iterator"); + + rawWindow2.focus(); + continueAfterFocus(rawWindow2); + }, + function() { + nonBrowserWindow.focus(); + continueAfterFocus(nonBrowserWindow); + }, + function() { + /** + * Bug 614079: This test fails intermittently on some specific linux + * environnements, without being able to reproduce it in same + * distribution with same window manager. + * Disable it until being able to reproduce it easily. + + // On linux, focus is not consistent, so we can't be sure + // what window will be on top. + // Here when we focus "non-browser" window, + // Any Browser window may be selected as "active". + test.assert(windows.activeWindow == window2 || windows.activeWindow == window3, + "Non-browser windows aren't handled by this module"); + */ + window2.activate(); + continueAfterFocus(rawWindow2); + }, + function() { + test.assertEqual(windows.activeWindow.title, window2.title, "Correct active window - 2"); + window3.activate(); + continueAfterFocus(rawWindow3); + }, + function() { + test.assertEqual(windows.activeWindow.title, window3.title, "Correct active window - 3"); + nonBrowserWindow.focus(); + finishTest(); + } + ]; + + windows.open({ + url: "data:text/html,<title>window 2</title>", + onOpen: function(window) { + window2 = window; + rawWindow2 = wm.getMostRecentWindow("navigator:browser"); + + windows.open({ + url: "data:text/html,<title>window 3</title>", + onOpen: function(window) { + window.tabs.activeTab.on('ready', function onReady() { + window3 = window; + rawWindow3 = wm.getMostRecentWindow("navigator:browser"); + nextStep() + }); + } + }); + } + }); + + function nextStep() { + if (testSteps.length > 0) + testSteps.shift()(); + } + + function continueAfterFocus(targetWindow) { + + // Based on SimpleTest.waitForFocus + var fm = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + + var childTargetWindow = {}; + fm.getFocusedElementForWindow(targetWindow, true, childTargetWindow); + childTargetWindow = childTargetWindow.value; + + var focusedChildWindow = {}; + if (fm.activeWindow) { + fm.getFocusedElementForWindow(fm.activeWindow, true, focusedChildWindow); + focusedChildWindow = focusedChildWindow.value; + } + + var focused = (focusedChildWindow == childTargetWindow); + if (focused) { + nextStep(); + } else { + childTargetWindow.addEventListener("focus", function focusListener() { + childTargetWindow.removeEventListener("focus", focusListener, true); + nextStep(); + }, true); + } + + } + + function finishTest() { + window3.close(function() { + window2.close(function() { + test.done(); + }); + }); + } +}; + +// 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 { + browserWindows = require("windows").browserWindows; +} +catch (err) { + // This bug should be mentioned in the error message. + let bug = "https://bugzilla.mozilla.org/show_bug.cgi?id=571449"; + 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("the windows module does not support this application."); + }; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/README.md b/tools/addon-sdk-1.7/packages/api-utils/README.md new file mode 100644 index 0000000..55adb57 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/README.md @@ -0,0 +1,35 @@ +<!-- 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/. --> + +API Utils provides a basic CommonJS infrastructure for +developing traditional XULRunner add-ons and applications. It is +the basis for the Add-on SDK. + +To address issues present in traditional add-on development, +API Utils provides mechanisms for: + +* writing and executing test cases, inspired by Python's [nose][] + package, +* tracking JS objects of interest to aid in memory profiling and leak + detection, +* registering callbacks that perform cleanup tasks when modules are + unloaded, +* easily reporting errors with full stack tracebacks. + +API Utils also has the following characteristics: + +* Beautiful, concise documentation. +* A rigorous test suite ensuring that the library doesn't break as the + Mozilla platform evolves. +* Solid developer ergonomics ensuring that developers can easily find + out why something they're doing isn't working. + +API Utils is intended to be very small and only contain the bare +minimum of functionality that all add-ons need. + +Note that the API Utils package has not fully stabilized yet, meaning that +we do still expect to make incompatible changes to its APIs in future releases +of the SDK. + + [nose]: http://code.google.com/p/python-nose/ diff --git a/tools/addon-sdk-1.7/packages/api-utils/data/content-proxy.js b/tools/addon-sdk-1.7/packages/api-utils/data/content-proxy.js new file mode 100644 index 0000000..f0a39d3 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/data/content-proxy.js @@ -0,0 +1,847 @@ +/* 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"; + +let Ci = Components.interfaces; + +/** + * Access key that allows privileged code to unwrap proxy wrappers through + * valueOf: + * let xpcWrapper = proxyWrapper.valueOf(UNWRAP_ACCESS_KEY); + * This key should only be used by proxy unit test. + */ + const UNWRAP_ACCESS_KEY = {}; + + + /** + * Returns a closure that wraps arguments before calling the given function, + * which can be given to native functions that accept a function, such that when + * the closure is called, the given function is called with wrapped arguments. + * + * @param fun {Function} + * the function for which to create a closure wrapping its arguments + * @param obj {Object} + * target object from which `fun` comes from + * (optional, for debugging purpose) + * @param name {String} + * name of the attribute from which `fun` is binded on `obj` + * (optional, for debugging purpose) + * + * Example: + * function contentScriptListener(event) {} + * let wrapper = ContentScriptFunctionWrapper(contentScriptListener); + * xray.addEventListener("...", wrapper, false); + * -> Allow to `event` to be wrapped + */ +function ContentScriptFunctionWrapper(fun, obj, name) { + if ("___proxy" in fun && typeof fun.___proxy == "function") + return fun.___proxy; + + let wrappedFun = function () { + let args = []; + for (let i = 0, l = arguments.length; i < l; i++) + args.push(wrap(arguments[i])); + + //console.log("Called from native :"+obj+"."+name); + //console.log(">args "+arguments.length); + //console.log(fun); + + // Native code can execute this callback with `this` being the wrapped + // function. For example, window.mozRequestAnimationFrame. + if (this == wrappedFun) + return fun.apply(fun, args); + + return fun.apply(wrap(this), args); + }; + + Object.defineProperty(fun, "___proxy", {value : wrappedFun, + writable : false, + enumerable : false, + configurable : false}); + + return wrappedFun; +} + +/** + * Returns a closure that unwraps arguments before calling the `fun` function, + * which can be used to build a wrapper for a native function that accepts + * wrapped arguments, since native function only accept unwrapped arguments. + * + * @param fun {Function} + * the function to wrap + * @param originalObject {Object} + * target object from which `fun` comes from + * (optional, for debugging purpose) + * @param name {String} + * name of the attribute from which `fun` is binded on `originalObject` + * (optional, for debugging purpose) + * + * Example: + * wrapper.appendChild = NativeFunctionWrapper(xray.appendChild, xray); + * wrapper.appendChild(anotherWrapper); + * -> Allow to call xray.appendChild with unwrapped version of anotherWrapper + */ +function NativeFunctionWrapper(fun, originalObject, name) { + return function () { + let args = []; + let obj = this && typeof this.valueOf == "function" ? + this.valueOf(UNWRAP_ACCESS_KEY) : this; + + for (let i = 0, l = arguments.length; i < l; i++) + args.push( unwrap(arguments[i], obj, name) ); + + //if (name != "toString") + //console.log(">>calling native ["+(name?name:'#closure#')+"]: \n"+fun.apply+"\n"+obj+"\n("+args.join(', ')+")\nthis :"+obj+"from:"+originalObject+"\n"); + + // Need to use Function.prototype.apply.apply because XMLHttpRequest + // is a function (typeof return 'function') and fun.apply is null :/ + let unwrapResult = Function.prototype.apply.apply(fun, [obj, args]); + let result = wrap(unwrapResult, obj, name); + + //console.log("<< "+rr+" -> "+r); + + return result; + }; +} + +/* + * Unwrap a JS value that comes from the content script. + * Mainly converts proxy wrapper to XPCNativeWrapper. + */ +function unwrap(value, obj, name) { + //console.log("unwrap : "+value+" ("+name+")"); + if (!value) + return value; + let type = typeof value; + + // In case of proxy, unwrap them recursively + // (it should not be recursive, just in case of) + if (["object", "function"].indexOf(type) !== -1 && + "__isWrappedProxy" in value) { + while("__isWrappedProxy" in value) + value = value.valueOf(UNWRAP_ACCESS_KEY); + return value; + } + + // In case of functions we need to return a wrapper that converts native + // arguments applied to this function into proxies. + if (type == "function") + return ContentScriptFunctionWrapper(value, obj, name); + + // We must wrap objects coming from content script too, as they may have + // a function that will be called by a native method. + // For example: + // addEventListener(..., { handleEvent: function(event) {} }, ...); + if (type == "object") + return ContentScriptObjectWrapper(value); + + if (["string", "number", "boolean"].indexOf(type) !== -1) + return value; + //console.log("return non-wrapped to native : "+typeof value+" -- "+value); + return value; +} + +/** + * Returns an XrayWrapper proxy object that allow to wrap any of its function + * though `ContentScriptFunctionWrapper`. These proxies are given to + * XrayWrappers in order to automatically wrap values when they call a method + * of these proxies. So that they are only used internaly and content script, + * nor web page have ever access to them. As a conclusion, we can consider + * this code as being safe regarding web pages overload. + * + * + * @param obj {Object} + * object coming from content script context to wrap + * + * Example: + * let myListener = { handleEvent: function (event) {} }; + * node.addEventListener("click", myListener, false); + * `event` has to be wrapped, so handleEvent has to be wrapped using + * `ContentScriptFunctionWrapper` function. + * In order to do so, we build this new kind of proxies. + */ +function ContentScriptObjectWrapper(obj) { + if ("___proxy" in obj && typeof obj.___proxy == "object") + return obj.___proxy; + + function valueOf(key) { + if (key === UNWRAP_ACCESS_KEY) + return obj; + return this; + } + + let proxy = Proxy.create({ + // Fundamental traps + getPropertyDescriptor: function(name) { + return Object.getOwnPropertyDescriptor(obj, name); + }, + defineProperty: function(name, desc) { + return Object.defineProperty(obj, name, desc); + }, + getOwnPropertyNames: function () { + return Object.getOwnPropertyNames(obj); + }, + delete: function(name) { + return delete obj[name]; + }, + // derived traps + has: function(name) { + return name === "__isXrayWrapperProxy" || + name in obj; + }, + hasOwn: function(name) { + return Object.prototype.hasOwnProperty.call(obj, name); + }, + get: function(receiver, name) { + if (name == "valueOf") + return valueOf; + let value = obj[name]; + if (!value) + return value; + + return unwrap(value); + }, + set: function(receiver, name, val) { + obj[name] = val; + return true; + }, + enumerate: function() { + var result = []; + for each (let name in obj) { + result.push(name); + }; + return result; + }, + keys: function() { + return Object.keys(obj); + } + }); + + Object.defineProperty(obj, "___proxy", {value : proxy, + writable : false, + enumerable : false, + configurable : false}); + + return proxy; +} + +// List of all existing typed arrays. +// Can be found here: +// http://mxr.mozilla.org/mozilla-central/source/js/src/jsapi.cpp#1790 +const typedArraysCtor = [ + ArrayBuffer, + Int8Array, + Uint8Array, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + Uint8ClampedArray +]; + +/* + * Wrap a JS value coming from the document by building a proxy wrapper. + */ +function wrap(value, obj, name, debug) { + if (!value) + return value; + let type = typeof value; + if (type == "object") { + // Bug 671016: Typed arrays don't need to be proxified. + // We avoid checking the whole constructor list on all objects + // by doing this check only on non-extensible objects: + if (!Object.isExtensible(value) && + typedArraysCtor.indexOf(value.constructor) !== -1) + return value; + + // Bug 715755: do not proxify COW wrappers + // These wrappers throw an exception when trying to access + // any attribute that is not in a white list + try { + ("nonExistantAttribute" in value); + } + catch(e) { + if (e.message.indexOf("Permission denied to access property") !== -1) + return value; + } + + // We may have a XrayWrapper proxy. + // For example: + // let myListener = { handleEvent: function () {} }; + // node.addEventListener("click", myListener, false); + // When native code want to call handleEvent, + // we go though ContentScriptFunctionWrapper that calls `wrap(this)` + // `this` is the XrayWrapper proxy of myListener. + // We return this object without building a CS proxy as it is already + // a value coming from the CS. + if ("__isXrayWrapperProxy" in value) + return value.valueOf(UNWRAP_ACCESS_KEY); + + // Unwrap object before wrapping it. + // It should not happen with CS proxy objects. + while("__isWrappedProxy" in value) { + value = value.valueOf(UNWRAP_ACCESS_KEY); + } + + if (XPCNativeWrapper.unwrap(value) !== value) + return getProxyForObject(value); + // In case of Event, HTMLCollection or NodeList or ??? + // XPCNativeWrapper.unwrap(value) === value + // but it's still a XrayWrapper so let's build a proxy + return getProxyForObject(value); + } + if (type == "function") { + if (XPCNativeWrapper.unwrap(value) !== value + || (typeof value.toString === "function" && + value.toString().match(/\[native code\]/))) { + return getProxyForFunction(value, NativeFunctionWrapper(value, obj, name)); + } + return value; + } + if (type == "string") + return value; + if (type == "number") + return value; + if (type == "boolean") + return value; + //console.log("return non-wrapped to wrapped : "+value); + return value; +} + +/* + * Wrap an object from the document to a proxy wrapper + */ +function getProxyForObject(obj) { + if (typeof obj != "object") { + let msg = "tried to proxify something other than an object: " + typeof obj; + console.warn(msg); + throw msg; + } + if ("__isWrappedProxy" in obj) { + return obj; + } + // Check if there is a proxy cached on this wrapper, + // but take care of prototype ___proxy attribute inheritance! + if (obj && obj.___proxy && obj.___proxy.valueOf(UNWRAP_ACCESS_KEY) === obj) { + return obj.___proxy; + } + + let proxy = Proxy.create(handlerMaker(obj)); + + Object.defineProperty(obj, "___proxy", {value : proxy, + writable : false, + enumerable : false, + configurable : false}); + return proxy; +} + +/* + * Wrap a function from the document to a proxy wrapper + */ +function getProxyForFunction(fun, callTrap) { + if (typeof fun != "function") { + let msg = "tried to proxify something other than a function: " + typeof fun; + console.warn(msg); + throw msg; + } + if ("__isWrappedProxy" in fun) + return obj; + if ("___proxy" in fun) + return fun.___proxy; + + let proxy = Proxy.createFunction(handlerMaker(fun), callTrap); + + Object.defineProperty(fun, "___proxy", {value : proxy, + writable : false, + enumerable : false, + configurable : false}); + + return proxy; +} + +/* + * Check if a DOM attribute name is an event name. + */ +function isEventName(id) { + if (id.indexOf("on") != 0 || id.length == 2) + return false; + // Taken from: + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#7616 + switch (id[2]) { + case 'a' : + return (id == "onabort" || + id == "onafterscriptexecute" || + id == "onafterprint"); + case 'b' : + return (id == "onbeforeunload" || + id == "onbeforescriptexecute" || + id == "onblur" || + id == "onbeforeprint"); + case 'c' : + return (id == "onchange" || + id == "onclick" || + id == "oncontextmenu" || + id == "oncopy" || + id == "oncut" || + id == "oncanplay" || + id == "oncanplaythrough"); + case 'd' : + return (id == "ondblclick" || + id == "ondrag" || + id == "ondragend" || + id == "ondragenter" || + id == "ondragleave" || + id == "ondragover" || + id == "ondragstart" || + id == "ondrop" || + id == "ondurationchange"); + case 'e' : + return (id == "onerror" || + id == "onemptied" || + id == "onended"); + case 'f' : + return id == "onfocus"; + case 'h' : + return id == "onhashchange"; + case 'i' : + return (id == "oninput" || + id == "oninvalid"); + case 'k' : + return (id == "onkeydown" || + id == "onkeypress" || + id == "onkeyup"); + case 'l' : + return (id == "onload" || + id == "onloadeddata" || + id == "onloadedmetadata" || + id == "onloadstart"); + case 'm' : + return (id == "onmousemove" || + id == "onmouseout" || + id == "onmouseover" || + id == "onmouseup" || + id == "onmousedown" || + id == "onmessage"); + case 'p' : + return (id == "onpaint" || + id == "onpageshow" || + id == "onpagehide" || + id == "onpaste" || + id == "onpopstate" || + id == "onpause" || + id == "onplay" || + id == "onplaying" || + id == "onprogress"); + case 'r' : + return (id == "onreadystatechange" || + id == "onreset" || + id == "onresize" || + id == "onratechange"); + case 's' : + return (id == "onscroll" || + id == "onselect" || + id == "onsubmit" || + id == "onseeked" || + id == "onseeking" || + id == "onstalled" || + id == "onsuspend"); + case 't': + return id == "ontimeupdate" + /* + // TODO: Make it work for mobile version + || + (nsDOMTouchEvent::PrefEnabled() && + (id == "ontouchstart" || + id == "ontouchend" || + id == "ontouchmove" || + id == "ontouchenter" || + id == "ontouchleave" || + id == "ontouchcancel"))*/; + + case 'u' : + return id == "onunload"; + case 'v': + return id == "onvolumechange"; + case 'w': + return id == "onwaiting"; + } + + return false; +} + +// XrayWrappers miss some attributes. +// Here is a list of functions that return a value when it detects a miss: +const NODES_INDEXED_BY_NAME = ["IMG", "FORM", "APPLET", "EMBED", "OBJECT"]; +const xRayWrappersMissFixes = [ + + // Fix bug with XPCNativeWrapper on HTMLCollection + // We can only access array item once, then it's undefined :o + function (obj, name) { + let i = parseInt(name); + if (obj.toString().match(/HTMLCollection|NodeList/) && + i >= 0 && i < obj.length) { + return wrap(XPCNativeWrapper(obj.wrappedJSObject[name]), obj, name); + } + return null; + }, + + // Trap access to document["form name"] + // that may refer to an existing form node + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#9285 + function (obj, name) { + if ("nodeType" in obj && obj.nodeType == 9) { + let node = obj.wrappedJSObject[name]; + // List of supported tag: + // http://mxr.mozilla.org/mozilla-central/source/content/html/content/src/nsGenericHTMLElement.cpp#1267 + if (node && NODES_INDEXED_BY_NAME.indexOf(node.tagName) != -1) + return wrap(XPCNativeWrapper(node)); + } + return null; + }, + + // Trap access to window["frame name"] and window.frames[i] + // that refer to an (i)frame internal window object + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#6824 + function (obj, name) { + if (typeof obj == "object" && "document" in obj) { + // Ensure that we are on a window object + try { + obj.QueryInterface(Ci.nsIDOMWindow); + } + catch(e) { + return null; + } + + // Integer case: + let i = parseInt(name); + if (i >= 0 && i in obj) { + return wrap(XPCNativeWrapper(obj[i])); + } + + // String name case: + if (name in obj.wrappedJSObject) { + let win = obj.wrappedJSObject[name]; + let nodes = obj.document.getElementsByName(name); + for (let i = 0, l = nodes.length; i < l; i++) { + let node = nodes[i]; + if ("contentWindow" in node && node.contentWindow == win) + return wrap(node.contentWindow); + } + } + } + return null; + }, + + // Trap access to form["node name"] + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMClassInfo.cpp#9477 + function (obj, name) { + if (typeof obj == "object" && obj.tagName == "FORM") { + let match = obj.wrappedJSObject[name]; + let nodes = obj.ownerDocument.getElementsByName(name); + for (let i = 0, l = nodes.length; i < l; i++) { + let node = nodes[i]; + if (node == match) + return wrap(node); + } + } + return null; + }, + + // Fix XPathResult's constants being undefined on XrayWrappers + // these constants are defined here: + // http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/xpath/nsIDOMXPathResult.idl + // and are only numbers. + // The platform bug 665279 was fixed in Gecko 10.0a1. + // FIXME: remove this workaround once the SDK no longer supports Firefox 9. + function (obj, name) { + if (typeof obj == "object" && name in Ci.nsIDOMXPathResult) { + let value = Ci.nsIDOMXPathResult[name]; + if (typeof value == "number" && value === obj.wrappedJSObject[name]) + return value; + } + return null; + } + +]; + +// XrayWrappers have some buggy methods. +// Here is the list of them with functions returning some replacement +// for a given object `obj`: +const xRayWrappersMethodsFixes = { + // postMessage method is checking the Javascript global + // and it expects it to be a window object. + // But in our case, the global object is our sandbox global object. + // See nsGlobalWindow::CallerInnerWindow(): + // http://mxr.mozilla.org/mozilla-central/source/dom/base/nsGlobalWindow.cpp#5893 + // nsCOMPtr<nsPIDOMWindow> win = do_QueryWrappedNative(wrapper); + // win is null + postMessage: function (obj) { + // Ensure that we are on a window object + try { + obj.QueryInterface(Ci.nsIDOMWindow); + } + catch(e) { + return null; + } + // Create a wrapper that is going to call `postMessage` through `eval` + let f = function postMessage(message, targetOrigin) { + message = message.toString().replace(/['\\]/g,"\\$&"); + targetOrigin = targetOrigin.toString().replace(/['\\]/g,"\\$&"); + + let jscode = "this.postMessage('" + message + "', '" + + targetOrigin + "')"; + return this.wrappedJSObject.eval(jscode); + }; + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + + // Fix mozMatchesSelector uses that is broken on XrayWrappers + // when we use document.documentElement.mozMatchesSelector.call(node, expr) + // It's only working if we call mozMatchesSelector on the node itself. + // SEE BUG 658909: mozMatchesSelector returns incorrect results with XrayWrappers + mozMatchesSelector: function (obj) { + // Ensure that we are on an object to expose this buggy method + try { + // Bug 707576 removed nsIDOMNSElement. + // Can be simplified as soon as Firefox 11 become the minversion + obj.QueryInterface("nsIDOMElement" in Ci ? Ci.nsIDOMElement : + Ci.nsIDOMNSElement); + } + catch(e) { + return null; + } + // We can't use `wrap` function as `f` is not a native function, + // so wrap it manually: + let f = function mozMatchesSelector(selectors) { + return this.mozMatchesSelector(selectors); + }; + + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + + // Bug 679054: History API doesn't work with Proxy objects. We have to pass + // regular JS objects on `pushState` and `replaceState` methods. + // In addition, the first argument has to come from the same compartment. + pushState: function (obj) { + // Ensure that we are on an object that expose History API + try { + obj.QueryInterface(Ci.nsIDOMHistory); + } + catch(e) { + return null; + } + let f = function fix() { + // Call native method with JSON objects + // (need to convert `arguments` to an array via `slice`) + return this.pushState.apply(this, JSON.parse(JSON.stringify(Array.slice(arguments)))); + }; + + return getProxyForFunction(f, NativeFunctionWrapper(f)); + }, + replaceState: function (obj) { + // Ensure that we are on an object that expose History API + try { + obj.QueryInterface(Ci.nsIDOMHistory); + } + catch(e) { + return null; + } + let f = function fix() { + // Call native method with JSON objects + // (need to convert `arguments` to an array via `slice`) + return this.replaceState.apply(this, JSON.parse(JSON.stringify(Array.slice(arguments)))); + }; + + return getProxyForFunction(f, NativeFunctionWrapper(f)); + } +}; + +/* + * Generate handler for proxy wrapper + */ +function handlerMaker(obj) { + // Overloaded attributes dictionary + let overload = {}; + // Expando attributes dictionary (i.e. onclick, onfocus, on* ...) + let expando = {}; + // Cache of methods overloaded to fix XrayWrapper bug + let methodFixes = {}; + return { + // Fundamental traps + getPropertyDescriptor: function(name) { + return Object.getOwnPropertyDescriptor(obj, name); + }, + defineProperty: function(name, desc) { + return Object.defineProperty(obj, name, desc); + }, + getOwnPropertyNames: function () { + return Object.getOwnPropertyNames(obj); + }, + delete: function(name) { + delete expando[name]; + delete overload[name]; + return delete obj[name]; + }, + + // derived traps + has: function(name) { + if (name == "___proxy") return false; + if (isEventName(name)) { + // XrayWrappers throw exception when we try to access expando attributes + // even on "name in wrapper". So avoid doing it! + return name in expando; + } + return name in obj || name in overload || name == "__isWrappedProxy" || + undefined !== this.get(null, name); + }, + hasOwn: function(name) { + return Object.prototype.hasOwnProperty.call(obj, name); + }, + get: function(receiver, name) { + if (name == "___proxy") + return undefined; + + // Overload toString in order to avoid returning "[XrayWrapper [object HTMLElement]]" + // or "[object Function]" for function's Proxy + if (name == "toString") { + // Bug 714778: we should not pass obj.wrappedJSObject.toString + // in order to avoid sharing its proxy between two contents scripts. + // (not that `unwrappedObj` can be equal to `obj` when `obj` isn't + // an xraywrapper) + let unwrappedObj = XPCNativeWrapper.unwrap(obj); + return wrap(function () { + return unwrappedObj.toString.call( + this.valueOf(UNWRAP_ACCESS_KEY), arguments); + }, obj, name); + } + + // Offer a way to retrieve XrayWrapper from a proxified node through `valueOf` + if (name == "valueOf") + return function (key) { + if (key === UNWRAP_ACCESS_KEY) + return obj; + return this; + }; + + // Return overloaded value if there is one. + // It allows to overload native methods like addEventListener that + // are not saved, even on the wrapper itself. + // (And avoid some methods like toSource from being returned here! [__proto__ test]) + if (name in overload && + overload[name] != Object.getPrototypeOf(overload)[name] && + name != "__proto__") { + return overload[name]; + } + + // Catch exceptions thrown by XrayWrappers when we try to access on* + // attributes like onclick, onfocus, ... + if (isEventName(name)) { + //console.log("expando:"+obj+" - "+obj.nodeType); + return name in expando ? expando[name].original : undefined; + } + + // Overload some XrayWrappers method in order to fix its bugs + if (name in methodFixes && + methodFixes[name] != Object.getPrototypeOf(methodFixes)[name] && + name != "__proto__") + return methodFixes[name]; + if (Object.keys(xRayWrappersMethodsFixes).indexOf(name) !== -1) { + let fix = xRayWrappersMethodsFixes[name](obj); + if (fix) + return methodFixes[name] = fix; + } + + let o = obj[name]; + + // XrayWrapper miss some attributes, try to catch these and return a value + if (!o) { + for each(let atttributeFixer in xRayWrappersMissFixes) { + let fix = atttributeFixer(obj, name); + if (fix) + return fix; + } + } + + // Generic case + return wrap(o, obj, name); + + }, + + set: function(receiver, name, val) { + + if (isEventName(name)) { + //console.log("SET on* attribute : " + name + " / " + val + "/" + obj); + let shortName = name.replace(/^on/,""); + + // Unregister previously set listener + if (expando[name]) { + obj.removeEventListener(shortName, expando[name], true); + delete expando[name]; + } + + // Only accept functions + if (typeof val != "function") + return false; + + // Register a new listener + let original = val; + val = ContentScriptFunctionWrapper(val); + expando[name] = val; + val.original = original; + obj.addEventListener(name.replace(/^on/, ""), val, true); + return true; + } + + obj[name] = val; + + // Handle native method not overloaded on XrayWrappers: + // obj.addEventListener = val; -> obj.addEventlistener = native method + // And, XPCNativeWrapper bug where nested values appear to be wrapped: + // obj.customNestedAttribute = val -> obj.customNestedAttribute !== val + // obj.customNestedAttribute = "waive wrapper something" + // SEE BUG 658560: Fix identity problem with CrossOriginWrappers + // TODO: check that DOM can't be updated by the document itself and so overloaded value becomes wrong + // but I think such behavior is limited to primitive type + if ((typeof val == "function" || typeof val == "object") && name) { + overload[name] = val; + } + + return true; + }, + + enumerate: function() { + var result = []; + for each (let name in Object.keys(obj)) { + result.push(name); + }; + return result; + }, + + keys: function() { + return Object.keys(obj); + } + }; +}; + + +/* + * Wrap an object from the document to a proxy wrapper. + */ +function create(object) { + if ("wrappedJSObject" in object) + object = object.wrappedJSObject; + let xpcWrapper = XPCNativeWrapper(object); + // If we can't build an XPCNativeWrapper, it doesn't make sense to build + // a proxy. All proxy code is based on having such wrapper that store + // different JS attributes set. + // (we can't build XPCNativeWrapper when object is from the same + // principal/domain) + if (object === xpcWrapper) { + return object; + } + return getProxyForObject(xpcWrapper); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/data/test-content-symbiont.js b/tools/addon-sdk-1.7/packages/api-utils/data/test-content-symbiont.js new file mode 100644 index 0000000..65a2a21 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/data/test-content-symbiont.js @@ -0,0 +1,5 @@ +/* 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/. */ + +// test-content-symbiont diff --git a/tools/addon-sdk-1.7/packages/api-utils/data/test-message-manager.js b/tools/addon-sdk-1.7/packages/api-utils/data/test-message-manager.js new file mode 100644 index 0000000..d647bd8 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/data/test-message-manager.js @@ -0,0 +1,6 @@ +/* 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/. */ + +const TEST_VALUE = 11; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/data/test-trusted-document.html b/tools/addon-sdk-1.7/packages/api-utils/data/test-trusted-document.html new file mode 100644 index 0000000..5845cf5 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/data/test-trusted-document.html @@ -0,0 +1,17 @@ +<!-- 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/. --> + +<html>
+<head>
+ <title>Worker test</title>
+</head>
+<body>
+ <p id="paragraph">Lorem ipsum dolor sit amet.</p>
+ <script>
+ addon.port.on('addon-to-document', function (arg) {
+ addon.port.emit('document-to-addon', arg);
+ });
+ </script>
+</body>
+</html>
diff --git a/tools/addon-sdk-1.7/packages/api-utils/data/worker.js b/tools/addon-sdk-1.7/packages/api-utils/data/worker.js new file mode 100644 index 0000000..aba594d --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/data/worker.js @@ -0,0 +1,247 @@ +/* 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/. */ + +const ContentWorker = Object.freeze({ + // TODO: Bug 727854 Use same implementation than common JS modules, + // i.e. EventEmitter module + + /** + * Create an EventEmitter instance. + */ + createEventEmitter: function createEventEmitter(emit) { + let listeners = Object.create(null); + let eventEmitter = Object.freeze({ + emit: emit, + on: function on(name, callback) { + if (typeof callback !== "function") + return this; + if (!(name in listeners)) + listeners[name] = []; + listeners[name].push(callback); + return this; + }, + once: function once(name, callback) { + eventEmitter.on(name, function onceCallback() { + eventEmitter.removeListener(name, onceCallback); + callback.apply(callback, arguments); + }); + }, + removeListener: function removeListener(name, callback) { + if (!(name in listeners)) + return; + let index = listeners[name].indexOf(name); + if (index == -1) + return; + listeners[name].splice(index, 1); + } + }); + function onEvent(name) { + if (!(name in listeners)) + return []; + let args = Array.slice(arguments, 1); + let results = []; + for each (let callback in listeners[name]) { + results.push(callback.apply(null, args)); + } + return results; + } + function hasListenerFor(name) { + if (!(name in listeners)) + return false; + return listeners[name].length > 0; + } + return { + eventEmitter: eventEmitter, + emit: onEvent, + hasListenerFor: hasListenerFor + }; + }, + + /** + * Create an EventEmitter instance to communicate with chrome module + * by passing only strings between compartments. + * This function expects `emitToChrome` function, that allows to send + * events to the chrome module. It returns the EventEmitter as `pipe` + * attribute, and, `onChromeEvent` a function that allows chrome module + * to send event into the EventEmitter. + * + * pipe.emit --> emitToChrome + * onChromeEvent --> callback registered through pipe.on + */ + createPipe: function createPipe(emitToChrome) { + function onEvent() { + // Convert to real array + let args = Array.slice(arguments); + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + function replacer(k, v) { + return typeof v === "function" ? undefined : v; + } + let str = JSON.stringify(args, replacer); + emitToChrome(str); + } + + let { eventEmitter, emit, hasListenerFor } = + ContentWorker.createEventEmitter(onEvent); + + return { + pipe: eventEmitter, + onChromeEvent: function onChromeEvent(array) { + // We either receive a stringified array, or a real array. + // We still allow to pass an array of objects, in WorkerSandbox.emitSync + // in order to allow sending DOM node reference between content script + // and modules (only used for context-menu API) + let args = typeof array == "string" ? JSON.parse(array) : array; + return emit.apply(null, args); + }, + hasListenerFor: hasListenerFor + }; + }, + + injectConsole: function injectConsole(exports, pipe) { + exports.console = Object.freeze({ + log: pipe.emit.bind(null, "console", "log"), + info: pipe.emit.bind(null, "console", "info"), + warn: pipe.emit.bind(null, "console", "warn"), + error: pipe.emit.bind(null, "console", "error"), + debug: pipe.emit.bind(null, "console", "debug"), + exception: pipe.emit.bind(null, "console", "exception"), + trace: pipe.emit.bind(null, "console", "trace") + }); + }, + + injectTimers: function injectTimers(exports, chromeAPI, pipe, console) { + // wrapped functions from `'timer'` module. + // Wrapper adds `try catch` blocks to the callbacks in order to + // emit `error` event on a symbiont if exception is thrown in + // the Worker global scope. + // @see http://www.w3.org/TR/workers/#workerutils + + // List of all living timeouts/intervals + let _timers = Object.create(null); + + // Keep a reference to original timeout functions + let { + setTimeout: chromeSetTimeout, + setInterval: chromeSetInterval, + clearTimeout: chromeClearTimeout, + clearInterval: chromeClearInterval + } = chromeAPI.timers; + + exports.setTimeout = function ContentScriptSetTimeout(callback, delay) { + let params = Array.slice(arguments, 2); + let id = chromeSetTimeout(function() { + try { + delete _timers[id]; + callback.apply(null, params); + } catch(e) { + console.exception(e); + } + }, delay); + _timers[id] = "timeout"; + return id; + }; + exports.clearTimeout = function ContentScriptClearTimeout(id) { + delete _timers[id]; + return chromeClearTimeout(id); + }; + + exports.setInterval = function ContentScriptSetInterval(callback, delay) { + let params = Array.slice(arguments, 2); + let id = chromeSetInterval(function() { + try { + callback.apply(null, params); + } catch(e) { + console.exception(e); + } + }, delay); + _timers[id] = "interval"; + return id; + }; + exports.clearInterval = function ContentScriptClearInterval(id) { + delete _timers[id]; + return chromeClearInterval(id); + }; + pipe.on("destroy", function clearTimeouts() { + // Unregister all setTimeout/setInterval on page unload + for (let id in _timers) { + let kind = _timers[id]; + if (kind == "timeout") + chromeClearTimeout(id); + else + chromeClearInterval(id); + } + }); + }, + + injectMessageAPI: function injectMessageAPI(exports, pipe) { + + let { eventEmitter: port, emit : portEmit } = + ContentWorker.createEventEmitter(pipe.emit.bind(null, "event")); + pipe.on("event", portEmit); + + let self = Object.freeze({ + port: port, + postMessage: pipe.emit.bind(null, "message"), + on: pipe.on.bind(null), + once: pipe.once.bind(null), + removeListener: pipe.removeListener.bind(null), + }); + Object.defineProperty(exports, "self", { + value: self + }); + + // Deprecated use of on/postMessage from globals + exports.postMessage = function deprecatedPostMessage() { + console.warn("The global `postMessage()` function in content " + + "scripts is deprecated in favor of the " + + "`self.postMessage()` function, which works the same. " + + "Replace calls to `postMessage()` with calls to " + + "`self.postMessage()`." + + "For more info on `self.on`, see " + + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); + return self.postMessage.apply(null, arguments); + }; + exports.on = function deprecatedOn() { + console.warn("The global `on()` function in content scripts is " + + "deprecated in favor of the `self.on()` function, " + + "which works the same. Replace calls to `on()` with " + + "calls to `self.on()`" + + "For more info on `self.on`, see " + + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); + return self.on.apply(null, arguments); + }; + + // Deprecated use of `onMessage` from globals + let onMessage = null; + Object.defineProperty(exports, "onMessage", { + get: function () onMessage, + set: function (v) { + if (onMessage) + self.removeListener("message", onMessage); + console.warn("The global `onMessage` function in content scripts " + + "is deprecated in favor of the `self.on()` function. " + + "Replace `onMessage = function (data){}` definitions " + + "with calls to `self.on('message', function (data){})`. " + + "For more info on `self.on`, see " + + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); + onMessage = v; + if (typeof onMessage == "function") + self.on("message", onMessage); + } + }); + }, + + inject: function (exports, chromeAPI, emitToChrome) { + let { pipe, onChromeEvent, hasListenerFor } = + ContentWorker.createPipe(emitToChrome); + ContentWorker.injectConsole(exports, pipe); + ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console); + ContentWorker.injectMessageAPI(exports, pipe); + return { + emitToContent: onChromeEvent, + hasListenerFor: hasListenerFor + }; + } +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/api-utils.md b/tools/addon-sdk-1.7/packages/api-utils/docs/api-utils.md new file mode 100644 index 0000000..aee88da --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/api-utils.md @@ -0,0 +1,157 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `api-utils` module provides some helpers useful to the SDK's high-level API +implementations. + +Introduction +------------ + +The SDK high-level API design guidelines make a number of recommendations. +This module implements some of those patterns so that your own implementations +don't need to reinvent them. + +For example, public constructors should be callable both with and without the +`new` keyword. Your module can implement this recommendation using the +`publicConstructor` function. + +Options objects or "dictionaries" are also common throughout the high-level +APIs. The guidelines recommend that public constructors should generally define +a single `options` parameter rather than defining many parameters. Since one of +the SDK's principles is to be friendly to developers, ideally all properties on +options dictionaries should be checked for correct type, and informative error +messages should be generated when clients make mistakes. With the +`validateOptions` function, your module can easily do so. + +And objects are sometimes iterable over a custom set of key/value pairs. +Such objects should have custom iterators that let consumers iterate keys, +values, or [key, value] pairs. The `addIterator` function makes it easy to do +so in a way that is consistent with the behavior of default iterators during +`for...in`, `for each...in`, and `for...in Iterator()` loops. + +<api name="publicConstructor"> +@function +Returns a function *C* that creates an instance of `privateConstructor`. *C* +may be called with or without the `new` keyword. + +The prototype of each instance returned from *C* is *C*.`prototype`, and +*C*.`prototype` is an object whose prototype is +`privateConstructor.prototype`. Instances returned from *C* are therefore +instances of both *C* and `privateConstructor`. + +Additionally, the constructor of each instance returned from *C* is *C*. + +Instances returned from *C* are automatically memory tracked using +`memory.track` under the bin name `privateConstructor.name`. + +**Example** + + function MyObject() {} + exports.MyObject = apiUtils.publicConstructor(MyObject); + +@returns {function} +A function that makes new instances of `privateConstructor`. + +@param privateConstructor {constructor} +</api> + +<api name="validateOptions"> +@function +A function to validate an options dictionary according to the specified +constraints. + +`map`, `is`, and `ok` are used in that order. + +The return value is an object whose keys are those keys in `requirements` that +are also in `options` and whose values are the corresponding return values of +`map` or the corresponding values in `options`. Note that any keys not shared +by both `requirements` and `options` are not in the returned object. + +**Examples** + +A typical use: + + var opts = { foo: 1337 }; + var requirements = { + foo: { + map: function (val) val.toString(), + is: ["string"], + ok: function (val) val.length > 0, + msg: "foo must be a non-empty string." + } + }; + var validatedOpts = apiUtils.validateOptions(opts, requirements); + // validatedOpts == { foo: "1337" } + +If the key `foo` is optional and doesn't need to be mapped: + + var opts = { foo: 1337 }; + var validatedOpts = apiUtils.validateOptions(opts, { foo: {} }); + // validatedOpts == { foo: 1337 } + + opts = {}; + validatedOpts = apiUtils.validateOptions(opts, { foo: {} }); + // validatedOpts == {} + +@returns {object} +A validated options dictionary given some requirements. If any of the +requirements are not met, an exception is thrown. + +@param options {object} +The options dictionary to validate. It's not modified. If it's null or +otherwise falsey, an empty object is assumed. + +@param requirements {object} +An object whose keys are the expected keys in `options`. Any key in +`options` that is not present in `requirements` is ignored. Each +value in `requirements` is itself an object describing the requirements +of its key. The keys of that object are the following, and each is optional: + +@prop [map] {function} +A function that's passed the value of the key in the `options`. `map`'s +return value is taken as the key's value in the final validated options, +`is`, and `ok`. If `map` throws an exception it is caught and discarded, +and the key's value is its value in `options`. + +@prop [is] {array} +An array containing the number of `typeof` type names. If the key's value is +none of these types it fails validation. Arrays and nulls are identified by +the special type names "array" and "null"; "object" will not match either. +No type coercion is done. + +@prop [ok] {function} +A function that is passed the key's value. If it returns false, the value +fails validation. + +@prop [msg] {string} +If the key's value fails validation, an exception is thrown. This string +will be used as its message. If undefined, a generic message is used, unless +`is` is defined, in which case the message will state that the value needs to +be one of the given types. +</api> + +<api name="addIterator"> +@function +Adds an iterator to the specified object that iterates keys, values, +or [key, value] pairs depending on how it is invoked, i.e.: + + for (var key in obj) { ... } // iterate keys + for each (var val in obj) { ... } // iterate values + for (var [key, val] in Iterator(obj)) { ... } // iterate pairs + +If your object only iterates either keys or values, you don't need this +function. Simply assign a generator function that iterates the keys/values +to your object's `__iterator__` property instead, f.e.: + + obj.__iterator__ = function () { for each (var i in items) yield i; } + +@param obj {object} +the object to which to add the iterator + +@param keysValsGen {function} +a generator function that yields [key, value] pairs +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/app-strings.md b/tools/addon-sdk-1.7/packages/api-utils/docs/app-strings.md new file mode 100644 index 0000000..c40ddaf --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/app-strings.md @@ -0,0 +1,65 @@ +<!-- 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/. --> + +The `app-strings` module gives you access to the host application's localized +string bundles (`.properties` files). + +The module exports the `StringBundle` constructor function. To access a string +bundle, construct an instance of `StringBundle`, passing it the bundle's URL: + + var StringBundle = require("app-strings").StringBundle; + var bundle = StringBundle("chrome://browser/locale/browser.properties"); + +To get the value of a string, call the object's `get` method, passing it +the name of the string: + + var accessKey = bundle.get("contextMenuSearchText.accesskey"); + // "S" in the en-US locale + +To get the formatted value of a string that accepts arguments, call the object's +`get` method, passing it the name of the string and an array of arguments +with which to format the string: + + var searchText = bundle.get("contextMenuSearchText", + ["universe", "signs of intelligent life"]); + // 'Search universe for "signs of intelligent life"' in the en-US locale + +To get all strings in the bundle, iterate the object, which returns arrays +of the form [name, value]: + + for (var [name, value] in Iterator(bundle)) + console.log(name + " = " + value); + +Iteration +--------- + +<code>for (var name in bundle) { ... }</code> + +Iterate the names of strings in the bundle. + +<code>for each (var val in bundle) { ... }</code> + +Iterate the values of strings in the bundle. + +<code>for (var [name, value] in Iterator(bundle)) { ... }</code> + +Iterate the names and values of strings in the bundle. + + +<api name="StringBundle"> +@class +The `StringBundle` object represents a string bundle. +<api name="StringBundle"> +@constructor +Creates a StringBundle object that gives you access to a string bundle. +@param url {string} the URL of the string bundle +@returns {StringBundle} the string bundle +</api> +<api name="get"> +@method Get the value of the string with the given name. +@param [name] {string} the name of the string to get +@param [args] {array} (optional) strings that replace placeholders in the string +@returns {string} the value of the string +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/base.md b/tools/addon-sdk-1.7/packages/api-utils/docs/base.md new file mode 100644 index 0000000..7a694f2 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/base.md @@ -0,0 +1,207 @@ +<!-- 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/. --> + +### Inheritance ### + +Doing [inheritance in JavaScript](https://developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript) +is both verbose and painful. Reading or writing such code requires requires +sharp eye and lot's of discipline, mainly due to code fragmentation and lots of +machinery exposed: + + // Defining a simple Class + function Dog(name) { + // Classes are for creating instances, calling them without `new` changes + // behavior, which in majority cases you need to handle, so you end up + // with additional boilerplate. + if (!(this instanceof Dog)) return new Dog(name); + + this.name = name; + }; + // To define methods you need to make a dance with a special 'prototype' + // property of the constructor function. This is too much machinery exposed. + Dog.prototype.type = 'dog'; + Dog.prototype.bark = function bark() { + return 'Ruff! Ruff!' + }; + + // Subclassing a `Dog` + function Pet(name, breed) { + // Once again we do our little dance + if (!(this instanceof Pet)) return new Pet(name, breed); + + Dog.call(this, name); + this.breed = breed; + } + // To subclass, you need to make another special dance with special + // 'prototype' properties. + Pet.prototype = Object.create(Dog.prototype); + // If you want correct instanceof behavior you need to make a dance with + // another special `constructor` property of the `prototype` object. + Object.defineProperty(Pet.prototype, 'contsructor', { value: Pet }); + // Finally you can define some properties. + Pet.prototype.call = function(name) { + return this.name === name ? this.bark() : ''; + }; + +An "exemplar" is a factory for instances. Usually exemplars are defined as +(constructor) functions as in examples above. But that does not necessary has +to be the case. Prototype (object) can form far more simpler exemplars. After +all what could be more object oriented than objects that inherit from objects. + + var Dog = { + new: function(name) { + var instance = Object.create(this); + this.initialize.apply(instance, arguments); + return instance; + }, + initialize: function initialize(name) { + this.name = name; + }, + type: 'dog', + bark: function bark() { + return 'Ruff! Ruff!' + } + }; + var fluffy = Dog.new('fluffy'); + + + var Pet = Object.create(Dog); + Pet.initialize = function initialize(name, breed) { + Dog.initialize.call(this, name); + this.breed = breed; + }; + Pet.call = function call(name) { + return this.name === name ? this.bark() : ''; + }; + +While this small trick solves some readability issues, there are still more. To +address them this module exports `Base` exemplar with few methods predefined: + + var Dog = Base.extend({ + initialize: function initialize(name) { + this.name = name; + }, + type: 'dog', + bark: function bark() { + return 'Ruff! Ruff!' + } + }); + + var Pet = Dog.extend({ + initialize: function initialize(name, breed) { + Dog.initialize.call(this, name); + this.breed = breed; + }, + function call(name) { + return this.name === name ? this.bark() : ''; + } + }); + + var fluffy = Dog.new('fluffy'); + dog.bark(); // 'Ruff! Ruff!' + Dog.isPrototypeOf(fluffy); // true + Pet.isPrototypeOf(fluffy); // true + +### Composition ### + +Even though (single) inheritance is very powerful it's not always enough. +Sometimes it's more useful suitable to define reusable pieces of functionality +and then compose bigger pieces out of them: + + var HEX = Base.extend({ + hex: function hex() { + return '#' + this.color + } + }) + + var RGB = Base.extend({ + red: function red() { + return parseInt(this.color.substr(0, 2), 16) + }, + green: function green() { + return parseInt(this.color.substr(2, 2), 16) + }, + blue: function blue() { + return parseInt(this.color.substr(4, 2), 16) + } + }) + + var CMYK = Base.extend(RGB, { + black: function black() { + var color = Math.max(Math.max(this.red(), this.green()), this.blue()) + return (1 - color / 255).toFixed(4) + }, + magenta: function magenta() { + var K = this.black(); + return (((1 - this.green() / 255).toFixed(4) - K) / (1 - K)).toFixed(4) + }, + yellow: function yellow() { + var K = this.black(); + return (((1 - this.blue() / 255).toFixed(4) - K) / (1 - K)).toFixed(4) + }, + cyan: function cyan() { + var K = this.black(); + return (((1 - this.red() / 255).toFixed(4) - K) / (1 - K)).toFixed(4) + } + }) + + // Composing `Color` prototype out of reusable components: + var Color = Base.extend(HEX, RGB, CMYK, { + initialize: function initialize(color) { + this.color = color + } + }) + + var pink = Color.new('FFC0CB') + // RGB + pink.red() // 255 + pink.green() // 192 + pink.blue() // 203 + + // CMYK + pink.magenta() // 0.2471 + pink.yellow() // 0.2039 + pink.cyan() // 0.0000 + +### Combining composition & inheritance ### + +Also it's easy to mix composition with inheritance: + + var Pixel = Color.extend({ + initialize: function initialize(x, y, color) { + Color.initialize.call(this, color) + this.x = x + this.y = y + }, + toString: function toString() { + return this.x + ':' + this.y + '@' + this.hex() + } + }); + + var pixel = Pixel.new(11, 23, 'CC3399'); + pixel.toString() // 11:23@#CC3399 + Pixel.isPrototypeOf(pixel) // true + + // Pixel instances inhertis from `Color` + Color.isPrototypeOf(pixel); // true + + // In fact `Pixel` itself inherits from `Color`, remember just simple and + // pure prototypal inheritance where object inherit from objects. + Color.isPrototypeOf(Pixel); // true + +### Classes ### + +Module exports `Class` function. `Class` takes argument of exemplar object +extending `Base` and returns `constructor` function that can be used for +simulating classes defined by given exemplar. + + var CPixel = Class(Pixel); + var pixel = CPixel(11, 12, '000000'); + pixel instanceof CPixel // true + Pixel.prototypeOf(pixel); // true + + // Use of `new` is optional, but possible. + var p2 = CPixel(17, 2, 'cccccc'); + p2 instanceof CPixel // true + p2.prototypeOf(pixel); // true diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/byte-streams.md b/tools/addon-sdk-1.7/packages/api-utils/docs/byte-streams.md new file mode 100644 index 0000000..1238cd8 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/byte-streams.md @@ -0,0 +1,68 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `byte-streams` module provides streams for reading and writing bytes. + +<api name="ByteReader"> +@class +<api name="ByteReader"> +@constructor + Creates a binary input stream that reads bytes from a backing stream. +@param inputStream {stream} + The backing stream, an <a href="http://mxr.mozilla.org/mozilla-central/ +source/xpcom/io/nsIInputStream.idl"><code>nsIInputStream</code></a>. +</api> +<api name="closed"> +@property {boolean} + True if the stream is closed. +</api> + +<api name="close"> +@method + Closes both the stream and its backing stream. If the stream is already + closed, an exception is thrown. +</api> + +<api name="read"> +@method + Reads a string from the stream. If the stream is closed, an exception is + thrown. +@param [numBytes] {number} + The number of bytes to read. If not given, the remainder of the entire stream + is read. +@returns {string} + A string containing the bytes read. If the stream is at the end, returns the + empty string. +</api> +</api> + +<api name="ByteWriter"> +@class +<api name="ByteWriter"> +@constructor + Creates a binary output stream that writes bytes to a backing stream. +@param outputStream {stream} + The backing stream, an <a href="http://mxr.mozilla.org/mozilla-central/ +source/xpcom/io/nsIOutputStream.idl"><code>nsIOutputStream</code></a>. +</api> +<api name="closed"> +@property {boolean} + True if the stream is closed. +</api> +<api name="close"> +@method + Closes both the stream and its backing stream. If the stream is already + closed, an exception is thrown. +</api> +<api name="write"> +@method + Writes a string to the stream. If the stream is closed, an exception is + thrown. +@param str {string} + The string to write. +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/collection.md b/tools/addon-sdk-1.7/packages/api-utils/docs/collection.md new file mode 100644 index 0000000..1e557c6 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/collection.md @@ -0,0 +1,77 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `collection` module provides a simple list-like class and utilities for +using it. A collection is ordered, like an array, but its items are unique, +like a set. + +<api name="Collection"> +@class +A collection object provides for...in-loop iteration. Items are yielded in the +order they were added. For example, the following code... + + var collection = require("collection"); + var c = new collection.Collection(); + c.add(1); + c.add(2); + c.add(3); + for (item in c) + console.log(item); + +... would print this to the console: + +<pre> + 1 + 2 + 3 +</pre> + +Iteration proceeds over a copy of the collection made before iteration begins, +so it is safe to mutate the collection during iteration; doing so does not +affect the results of the iteration. + +<api name="Collection"> +@constructor +Creates a new collection. The collection is backed by an array. +@param [array] {array} +If *array* is given, it will be used as the backing array. This way the caller +can fully control the collection. Otherwise a new empty array will be used, and +no one but the collection will have access to it. +</api> +<api name="length"> +@property {number} +The number of items in the collection array. +</api> +<api name="add"> +@method +Adds a single item or an array of items to the collection. Any items already +contained in the collection are ignored. +@param itemOrItems {object} An item or array of items. +@returns {Collection} The Collection. +</api> +<api name="remove"> +@method +Removes a single item or an array of items from the collection. Any items not +contained in the collection are ignored. +@param itemOrItems {object} An item or array of items. +@returns {Collection} The Collection. +</api> +</api> + +<api name="addCollectionProperty"> +@function +Adds a collection property to the given object. Setting the property to a +scalar value empties the collection and adds the value. Setting it to an array +empties the collection and adds all the items in the array. +@param object {object} +The property will be defined on this object. +@param propName {string} +The name of the property. +@param [backingArray] {array} +If given, this will be used as the collection's backing array. +</api> + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/content.md b/tools/addon-sdk-1.7/packages/api-utils/docs/content.md new file mode 100644 index 0000000..1c25eb4 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/content.md @@ -0,0 +1,15 @@ +<!-- 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/. --> + +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `content` module exports three different traits [Loader][], [Worker][] and +[Symbiont][]. None of this traits is intended to be used directly by programs. +Rather, they are intended to be used by other modules that provide high +level APIs to programs or libraries. + +[Loader]:packages/api-utils/content/loader.html +[Worker]:packages/api-utils/content/worker.html +[Symbiont]:packages/api-utils/content/symbiont.html + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/content/loader.md b/tools/addon-sdk-1.7/packages/api-utils/docs/content/loader.md new file mode 100644 index 0000000..a460476 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/content/loader.md @@ -0,0 +1,92 @@ +<!-- 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/. --> + +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +Loader is base trait and it provides set of core properties and associated +validations. Trait is useful for all the compositions providing high level +APIs for creating JavaScript contexts that can access web content. + +Loader is composed from the +[EventEmitter](packages/api-utils/events.html) trait, therefore +instances of Loader and their descendants expose all the public properties +exposed by EventEmitter along with additional public properties: + +Value changes on all of the above mentioned properties emit `propertyChange` +events on an instances. + +**Example:** + +The following code creates a wrapper on hidden frame that reloads a web page +in frame every time `contentURL` property is changed: + + var hiddenFrames = require("hidden-frame"); + var { Loader } = require("content"); + var PageLoader = Loader.compose({ + constructor: function PageLoader(options) { + options = options || {}; + if (options.contentURL) + this.contentURL = options.contentURL; + this.on('propertyChange', this._onChange = this._onChange.bind(this)); + let self = this; + hiddenFrames.add(hiddenFrames.HiddenFrame({ + onReady: function onReady() { + let frame = self._frame = this.element; + self._emit('propertyChange', { contentURL: self.contentURL }); + } + })); + }, + _onChange: function _onChange(e) { + if ('contentURL' in e) + this._frame.setAttribute('src', this._contentURL); + } + }); + +<api name="Loader"> +@class +<api name="contentScriptFile"> +@property {array} +The local file URLs of content scripts to load. Content scripts specified by +this property are loaded *before* those specified by the `contentScript` +property. +</api> + +<api name="contentScript"> +@property {array} +The texts of content scripts to load. Content scripts specified by this +property are loaded *after* those specified by the `contentScriptFile` property. +</api> + +<api name="contentScriptWhen"> +@property {string} +When to load the content scripts. This may take one of the following +values: + +* "start": load content scripts immediately after the document +element for the page is inserted into the DOM, but before the DOM content +itself has been loaded +* "ready": load content scripts once DOM content has been loaded, +corresponding to the +[DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) +event +* "end": load content scripts once all the content (DOM, JS, CSS, +images) for the page has been loaded, at the time the +[window.onload event](https://developer.mozilla.org/en/DOM/window.onload) +fires + +</api> + +<api name="contentURL"> +@property {string} +The URL of the content loaded. +</api> + +<api name="allow"> +@property {object} +Permissions for the content, with the following keys: +@prop script {boolean} + Whether or not to execute script in the content. Defaults to true. +</api> +</api> + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/content/proxy.md b/tools/addon-sdk-1.7/packages/api-utils/docs/content/proxy.md new file mode 100644 index 0000000..095e8ff --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/content/proxy.md @@ -0,0 +1,241 @@ +<!-- 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/. --> + +<!-- contributed by Alexandre Poirot [apoirot@mozilla.com] --> + +Content scripts need access to the DOM of the pages they are attached to. +However, those pages should be considered to be hostile environments: we +have no control over any other scripts loaded by the web page that may be +executing in the same context. If the content scripts and scripts loaded +by the web page were to access the same DOM objects, there are two possible +security problems: + +First, a malicious page might redefine functions and properties of DOM +objects so they don't do what the add-on expects. For example, if a +content script calls `document.getElementById()` to retrieve a DOM +element, then a malicious page could redefine its behavior to return +something unexpected: + +<pre><code> +// If the web document contains the following script: +document.getElementById = function (str) { + // Overload indexOf method of all string instances + str.__proto__.indexOf = function () {return -1;}; + // Overload toString method of all object instances + str.__proto__.__proto__.toString = function () {return "evil";}; +}; +// After the following line, the content script will be compromised: +var node = document.getElementById("element"); +// Then your content script is totally out of control. +</code></pre> + +Second, changes the content script made to the DOM objects would be visible +to the page, leaking information to it. + +The general approach to fixing these problems is to wrap DOM objects in +[`XrayWrappers`](https://developer.mozilla.org/en/XPCNativeWrapper) +(also know as `XPCNativeWrapper`). This guarantees that: + +* when the content script accesses DOM properties and functions it gets the +original native version of them, ignoring any modifications made by the web +page +* changes to the DOM made by the content script are not visible to scripts +running in the page. + +However, `XrayWrapper` has some limitations and bugs, which break many +popular web frameworks. In particular, you can't: + +* define attributes like `onclick`: you have to use `addEventListener` syntax +* overload native methods on DOM objects, like this: +<pre><code> +proxy.addEventListener = function () {}; +</code></pre> +* access named elements using properties like `window[framename]` or +`document[formname]` +* use some other features that have bugs in the `XrayWrapper` +implementation, like `mozMatchesSelector` + +The `proxy` module uses `XrayWrapper` in combination with the +experimental +[Proxy API](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Proxy) +to address both the security vulnerabilities of content scripts and the +limitations of `XrayWrapper`. + +<pre> + /--------------------\ /------------------------\ + | Web document | | Content script sandbox | + | http://mozilla.org | | data/worker.js | + | | require('content-proxy'). | | + | window >-----------|- create(window) -|-> window | + \--------------------/ \------------------------/ +</pre> + + +## The Big Picture ## + +The implementation defines two different kinds of proxy: + + 1. Content script proxies that wrap DOM objects that are exposed to + content scripts as described above. + 2. XrayWrapper proxies that wrap objects from content scripts before handing + them over to XrayWrapper functions. These proxies are internal + and are not exposed to content scripts or document content. + +<pre> + /--------------------\ /------------------------\ + | Web document | | Content script sandbox | + | http://mozilla.org | | data/worker.js | + | | /-------|-> myObject = {} | + | | /----------------v--\ | | + | | | XrayWrapper Proxy | | - document | + | | \---------v---------/ \----^-------------------/ + | | v | + | | /-------------\ /----------\ | + | - document >-------|->| XrayWrapper |<-| CS proxy |-/ + \--------------------/ \-------------/ \----------/ +</pre> + +Everything begins with a single call to the `create` function exported by the +content-proxy module: + + // Retrieve the unwrapped reference to the current web page window object + var win = gBrowser.contentDocument.defaultView.wrappedJSObject; + // Or in addon sdk style + var win = require("tab-browser").activeTab.linkedBrowser.contentWindow.wrappedJSObject; + // Now create a content script proxy for the window object + var windowProxy = require("api-utils/content/content-proxy").create(win); + + // We finally use this window object as sandbox prototype, + // so that all web page globals are accessible in CS too: + var contentScriptSandbox = new Cu.Sandbox(win, { + sandboxPrototype: windowProxy + }); + +Then all other proxies are created from this one. Attempts to access DOM +attributes of this proxy are trapped, and the proxy constructs and returns +content script proxies for those attributes: + + // For example, if you simply do this: + var document = window.document; + // accessing the `document` attribute will be trapped by the `window` content script + // proxy, and that proxy will that create another content script proxy for `document` + +So the main responsibility of the content script proxy implementation is to +ensure that we always return content script proxies to the content script. + +## Internal Implementation ## + +Each content script proxy keeps a reference to the `XrayWrapper` that enables +it to be sure of calling native DOM methods. + +There are two internal functions to convert between content script proxy +values and `XrayWrapper` values. + +1. __`wrap`__ takes an XrayWrapper value and wraps it in a content script +proxy if needed. + This method is called when: + * a content script accesses an attribute of a content script proxy. + * XrayWrapper code calls a callback function defined in the content +script, so that arguments passed into the function by the XrayWrapper are +converted into content script proxies. For example, if a content script +calls `addEventListener`, then the listener function will expect any arguments +to be content script proxies. +<br/><br/> +2. __`unwrap`__ takes an object coming from the content script context and: + * if the object is a content script proxy, unwraps it back to an +XrayWrapper reference + * if the object is not a content script proxy, wraps it in an XrayWrapper +proxy. +<br/><br/> +This means we can call a XrayWrapper method either with: + + * a raw XrayWrapper object. + + // The following line doesn't work if child is a content script proxy, + // it has to be a raw XrayWrapper reference + xrayWrapper.appendChild(child) + + * an XrayWrapper proxy when you pass a custom object from the content +script context. + + var myListener = { + handleEvent: function(event) { + // `event` should be a content script proxy + } + }; + // `myListener` has to be another kind of Proxy: XrayWrapper proxy, + // that aims to catch the call to `handleEvent` in order to wrap its + // arguments in a content script proxy. + xrayWrapper.addEventListener("click", myListener, false); + + +## Stack Traces ## + +The following code: + + function listener(event) { + + } + csProxy.addEventListener("message", listener, false); + +generates the following internal calls: + + -> CS Proxy:: get("addEventListener") + -> wrap(xrayWrapper.addEventListener) + -> NativeFunctionWrapper(xrayWrapper.addEventListener) + // NativeFunctionWrapper generates: + function ("message", listener, false) { + return xraywrapper.addEventListener("message", unwrap(listener), false); + } + -> unwrap(listener) + -> ContentScriptFunctionWrapper(listener) + // ContentScriptFunctionWrapper generates: + function (event) { + return listener(wrap(event)); + } + +<br> + + // First, create an object from content script context + var myListener = { + handleEvent: function (event) { + + } + }; + // Then, pass this object as an argument to a CS proxy method + window.addEventListener("message", myListener, false); + + // Generates the following internal calls: + -> CS Proxy:: get("addEventListener") + -> wrap(xrayWrapper.addEventListener) + -> NativeFunctionWrapper(xrayWrapper.addEventListener) + // Generate the following function: + function ("message", myListener, false) { + return xraywrapper.addEventListener("message", unwrap(myListener), false); + } + -> unwrap(myListener) + -> ContentScriptObjectWrapper(myListener) + // Generate an XrayWrapper proxy and give it to xrayWrapper method. + // Then when native code fires an event, the proxy will catch it: + -> XrayWrapper Proxy:: get("handleEvent") + -> unwrap(myListener.handleEvent) + -> ContentScriptFunctionWrapper(myListener.handleEvent) + // Generate following function: + function (event) { + return myListener.handleEvent(wrap(event)); + } + + +<api name="create"> +@function + Create a content script proxy. <br/> + Doesn't create a proxy if we are not able to create a XrayWrapper for + this object: for example, if the object comes from system principal. + +@param object {Object} + The object to proxify. + +@returns {Object} + A content script proxy that wraps `object`. +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/content/symbiont.md b/tools/addon-sdk-1.7/packages/api-utils/docs/content/symbiont.md new file mode 100644 index 0000000..1ff7dd1 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/content/symbiont.md @@ -0,0 +1,140 @@ +<!-- 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/. --> + +<!-- contributed by Myk Melez [myk@mozilla.org] --> +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + + +This module is not intended to be used directly by programs. Rather, it is +intended to be used by other modules that provide APIs to programs. + + +This module exports `Symbiont` trait that can be used for creating JavaScript +contexts that can access web content in host application frames (i.e. XUL +`<iframe>` and `<browser>` elements) and communicate with programs via +asynchronous JSON pipes. It is useful in the construction of APIs that +are compatible with the execution model codenamed "electrolysis" in which +programs run in separate processes from web content. + +Introduction +------------ + +`Symbiont` constructs a content symbiont for a given frame, it loads the +specified contentURL and scripts into it, and plumbs an asynchronous +JSON pipe between the content symbiont object and the content symbiont +context. If frame is not provided hidden frame will be created. + +Examples +-------- + + var { Symbiont } = require('content'); + var Thing = Symbiont.resolve({ constructor: '_init' }).compose({ + constructor: function Thing(options) { + // `getMyFrame` returns the host application frame in which + // the page is loaded. + this._frame = getMyFrame(); + this._init(options) + } + }); + +See the [panel][] module for a real-world example of usage of this module. + +[panel]:packages/addon-kit/panel.html + +Reference +--------- + +<api name="Symbiont"> +@class +Symbiont is composed from the [Worker][] trait, therefore instances +of Symbiont and their descendants expose all the public properties +exposed by [Worker][] along with additional public properties that +are listed below: + +[Worker]:packages/api-utils/content/worker.html + +<api name="Symbiont"> +@constructor +Creates a content symbiont. +@param options {object} + Options for the constructor. Includes all the keys that +the [Worker](packages/api-utils/content/worker.html) +constructor accepts and a few more: + + @prop [frame] {object} + The host application frame in which the page is loaded. + If frame is not provided hidden one will be created. + @prop [contentScriptWhen="end"] {string} + When to load the content scripts. This may take one of the following + values: + + * "start": load content scripts immediately after the document + element for the page is inserted into the DOM, but before the DOM content + itself has been loaded + * "ready": load content scripts once DOM content has been loaded, + corresponding to the + [DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) + event + * "end": load content scripts once all the content (DOM, JS, CSS, + images) for the page has been loaded, at the time the + [window.onload event](https://developer.mozilla.org/en/DOM/window.onload) + fires + + This property is optional and defaults to "end". + + @prop [allow] {object} + Permissions for the content, with the following keys: + @prop [script] {boolean} + Whether or not to execute script in the content. Defaults to true. + Optional. + Optional. +</api> + +<api name="contentScriptFile"> +@property {array} +The local file URLs of content scripts to load. Content scripts specified by +this property are loaded *before* those specified by the `contentScript` +property. +</api> + +<api name="contentScript"> +@property {array} +The texts of content scripts to load. Content scripts specified by this +property are loaded *after* those specified by the `contentScriptFile` property. +</api> + +<api name="contentScriptWhen"> +@property {string} +When to load the content scripts. This may have one of the following +values: + +* "start": load content scripts immediately after the document +element for the page is inserted into the DOM, but before the DOM content +itself has been loaded +* "ready": load content scripts once DOM content has been loaded, +corresponding to the +[DOMContentLoaded](https://developer.mozilla.org/en/Gecko-Specific_DOM_Events) +event +* "end": load content scripts once all the content (DOM, JS, CSS, +images) for the page has been loaded, at the time the +[window.onload event](https://developer.mozilla.org/en/DOM/window.onload) +fires + +</api> + +<api name="contentURL"> +@property {string} +The URL of the content loaded. +</api> + +<api name="allow"> +@property {object} +Permissions for the content, with a single boolean key called `script` which +defaults to true and indicates whether or not to execute scripts in the +content. +</api> + +</api> + + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/content/worker.md b/tools/addon-sdk-1.7/packages/api-utils/docs/content/worker.md new file mode 100644 index 0000000..b3e17ea --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/content/worker.md @@ -0,0 +1,130 @@ +<!-- 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/. --> + +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +This module exports the `Worker` trait, which may be used to construct objects +implementing the [Worker][] interface defined by the W3C, with minor +differences. + +Content workers are message-passing facilities for communication between +[content scripts](dev-guide/guides/content-scripts/index.html) and the main +add-on code. + +It is important to note that unlike "web workers," these workers run in the +same process as web content and browser chrome, so code within workers can +block the UI. + +[Worker]:http://www.w3.org/TR/workers/#worker + +<api name="Worker"> +@class +Worker is composed from the [EventEmitter][] trait, therefore instances +of Worker and their descendants expose all the public properties +exposed by [EventEmitter][] along with additional public properties that +are listed below. + +**Example** + + var workers = require("content/worker"); + let worker = workers.Worker({ + window: require("window-utils").activeWindow, + contentScript: + "self.port.on('hello', function(name) { " + + " self.port.emit('response', window.location); " + + "});" + }); + worker.port.emit("hello", { name: "worker"}); + worker.port.on("response", function (location) { + console.log(location); + }); + +[EventEmitter]:packages/api-utils/events.html + +<api name="Worker"> +@constructor +Creates a content worker. +@param options {object} +Options for the constructor, with the following keys: + @prop window {object} + The content window to create JavaScript sandbox for communication with. + @prop [contentScriptFile] {string,array} + The local file URLs of content scripts to load. Content scripts specified + by this option are loaded *before* those specified by the `contentScript` + option. Optional. + @prop [contentScript] {string,array} + The texts of content scripts to load. Content scripts specified by this + option are loaded *after* those specified by the `contentScriptFile` option. + Optional. + @prop [onMessage] {function} + Functions that will registered as a listener to a 'message' events. + @prop [onError] {function} + Functions that will registered as a listener to an 'error' events. +</api> + +<api name="port"> +@property {EventEmitter} +[EventEmitter](packages/api-utils/events.html) object that allows you to: + +* send customized messages to the worker using the `port.emit` function +* receive events from the worker using the `port.on` function + +</api> + +<api name="postMessage"> +@method +Asynchronously emits `"message"` events in the enclosed worker, where content +script was loaded. +@param data {number,string,JSON} +The data to send. Must be stringifiable to JSON. +</api> + +<api name="destroy"> +@method +Destroy the worker by removing the content script from the page and removing +all registered listeners. A `detach` event is fired just before removal. +</api> + +<api name="url"> +@property {string} +The URL of the content. +</api> + +<api name="tab"> +@property {object} +If this worker is attached to a content document, returns the related +[tab](packages/addon-kit/tabs.html). +</api> + +<api name="message"> +@event +This event allows the content worker to receive messages from its associated +content scripts. Calling the `self.postMessage()` function from a content +script will asynchronously emit the `message` event on the corresponding +worker. + +@argument {value} +The event listener is passed the message, which must be a +<a href = "dev-guide/guides/content-scripts/using-port.html#json_serializable">JSON-serializable value</a>. +</api> + +<api name="error"> +@event +This event allows the content worker to react to an uncaught runtime script +error that occurs in one of the content scripts. + +@argument {Error} +The event listener is passed a single argument which is an +[Error](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error) +object. +</api> + +<api name="detach"> +@event +This event is emitted when the document associated with this worker is unloaded +or the worker's `destroy()` method is called. +</api> + +</api> + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/cortex.md b/tools/addon-sdk-1.7/packages/api-utils/docs/cortex.md new file mode 100644 index 0000000..afae8cb --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/cortex.md @@ -0,0 +1,160 @@ +<!-- 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/. --> + + +## Property Encapsulation ## + +In JavaScript it is not possible to create properties that have limited or +controlled accessibility. It is possible to create non-enumerable and +non-writable properties, but still they can be discovered and accessed. +Usually so called "closure capturing" is used to encapsulate such properties +in lexical scope: + + function Foo() { + var _secret = 'secret'; + this.hello = function hello() { + return 'Hello ' + _secret; + } + } + +This provides desired result, but has side effect of degrading code readability, +especially with object-oriented programs. Another disadvantage with this pattern +is that there is no immediate solution for inheriting access to the privates +(illustrated by the following example): + + function Derived() { + this.hello = function hello() { + return _secret; + } + this.bye = function bye() { + return _secret; + } + } + Derived.prototype = Object.create(Foo.prototype); + +## Facade Objects ## + +Alternatively constructor can returned facade objects - proxies to the +instance's public properties: + + function Foo() { + var foo = Object.create(Foo.prototype); + return { + bar: foo.hello.bind(foo); + } + } + Foo.prototype._secret = 'secret'; + Foo.prototype.hello = function hello() { + return 'Hello ' + this._secret; + } + + function Derived() { + var derived = Object.create(Derived.prototype); + return { + bar: derived.hello.bind(derived); + bye: derived.bye.bind(derived); + } + } + Derived.prototype = Object.create(Foo.prototype); + Derived.prototype.bye = function bye() { + return 'Bye ' + this._secret; + }; + +While this solution solves given issue and provides proper encapsulation for +both own and inherited private properties, it does not addresses following: + + - Privates defined on the `prototype` can be compromised, since they are + accessible through the constructor (`Foo.prototype._secret`). + - Behavior of `instanceof` is broken, since `new Derived() instanceof Derived` + is going to evaluate to `false`. + +## Tamper Proofing with Property Descriptor Maps ## + +In ES5 new property descriptor maps were introduced, which can be used as a +building blocks for defining reusable peace of functionality. To some degree +they are similar to a `prototype` objects, and can be used so to define pieces +of functionality that is considered to be private (In contrast to `prototype` +they are not exposed by default). + + function Foo() { + var foo = Object.create(Foo.prototype, FooDescriptor); + var facade = Object.create(Foo.prototype); + facade.hello = foo.hello.bind(foo); + return facade; + } + Foo.prototype.hello = function hello() { + return 'Hello ' + this._secret; + } + var FooDescriptor = { + _secret: { value: 'secret' }; + } + + function Derived() { + var derived = Object.create(Derived.prototype, DerivedDescriptor); + var facade = Object.create(Derived.prototype); + facade.hello = derived.hello.bind(derived); + facade.bye = derived.bye.bind(derived); + return facade; + } + Derived.prototype = Object.create(Foo.prototype); + Derived.prototype.bye = function bye() { + return 'Bye ' + this._secret; + }; + DerivedDescriptor = {}; + + Object.keys(FooDescriptor).forEach(function(key) { + DerivedDescriptor[key] = FooDescriptor[key]; + }); + +## Cortex Objects ## + +Last approach solves all of the concerns, but adds complexity, verbosity +and decreases code readability. Combination of `Cortex`'s and `Trait`'s +will gracefully solve all these issues and keep code clean: + + var Cortex = require('cortex').Cortex; + var Trait = require('light-traits').Trait; + + var FooTrait = Trait({ + _secret: 'secret', + hello: function hello() { + return 'Hello ' + this._secret; + } + }); + function Foo() { + return Cortex(FooTrait.create(Foo.prototype)); + } + + var DerivedTrait = Trait.compose(FooTrait, Trait({ + bye: function bye() { + return 'Bye ' + this._secret; + } + })); + function Derived() { + var derived = DerivedTrait.create(Derived.prototype); + return Cortex(derived); + } + +Function `Cortex` takes any object and returns a proxy for its public +properties. By default properties are considered to be public if they don't +start with `"_"`, but default behavior can be overridden if needed, by passing +array of public property names as a second argument. + +## Gotchas ## + +`Cortex` is just a utility function to create a proxy object, and it does not +solve the `prototype`-related issues highlighted earlier, but since traits make +use of property descriptor maps instead of `prototype`s, there aren't any +issues with using `Cortex` to wrap objects created from traits. + +If you want to use `Cortex` with an object that uses a `prototype` chain, +however, you should either make sure you don't have any private properties +in the prototype chain or pass the optional third `prototype` argument. + +In the latter case, the returned proxy will inherit from the given prototype, +and the `prototype` chain of the wrapped object will be inaccessible. +However, note that the behavior of the `instanceof` operator will vary, +as `proxy instanceof Constructor` will return false even if the Constructor +function's prototype is in the wrapped object's prototype chain. + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/cuddlefish.md b/tools/addon-sdk-1.7/packages/api-utils/docs/cuddlefish.md new file mode 100644 index 0000000..27daf30 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/cuddlefish.md @@ -0,0 +1,7 @@ +<!-- 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/. --> + +`cuddlefish` is the name of the SDK's module loader. + +This module still needs to be documented. diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/environment.md b/tools/addon-sdk-1.7/packages/api-utils/docs/environment.md new file mode 100644 index 0000000..b5057a2 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/environment.md @@ -0,0 +1,43 @@ +<!-- 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/. --> + +Module provides API to access, set and unset environment variables via exported +`env` object. + + var { env } = require('api-utils/environment'); + +You can get the value of an environment variable, by accessing property that +has name of desired variable: + + var PATH = env.PATH; + +You can check existence of an environment variable by checking if property with +such variable name exists: + + console.log('PATH' in env); // true + console.log('FOO' in env); // false + +You can set value of an environment variable by setting a property: + + env.FOO = 'foo'; + env.PATH += ':/my/path/' + +You can unset environment variable by deleting a property: + + delete env.FOO; + +## Limitations ## + +There is no way to enumerate existing environment variables, also `env` +won't have any enumerable properties: + + console.log(Object.keys(env)); // [] + +Environment variable will be unset, show up as non-existing if it's set +to `null`, `undefined` or `''`. + + env.FOO = null; + console.log('FOO' in env); // false + env.BAR = ''; + console.log(env.BAR); // undefined diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/errors.md b/tools/addon-sdk-1.7/packages/api-utils/docs/errors.md new file mode 100644 index 0000000..98e94ac --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/errors.md @@ -0,0 +1,42 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> + +The `errors` module provides helpers for safely invoking user callbacks. + +<api name="catchAndLog"> +@function + Wraps a callback in a function that when invoked will catch and log any + exception thrown by the callback. +@param callback {function} + The callback to wrap. +@param [defaultResponse] {value} + This value will be returned by the wrapper if `callback` throws an exception. + If not given, `undefined` is used. +@param [logException] {function} + When `callback` throws an exception, it will be passed to this function. If + not given, the exception is logged using `console.exception()`. +@returns {function} + A function that will invoke `callback` when called. The return value of this + function is the return value of `callback` unless `callback` throws an + exception. In that case, `defaultResponse` is returned or `undefined` if + `defaultResponse` is not given. +</api> + +<api name="catchAndLogProps"> +@function + Replaces methods of an object with wrapped versions of those methods returned + by `catchAndLog()`. +@param object {object} + The object whose methods to replace. +@param props {string,array} + The names of the methods of `object` to replace, either a string for a single + method or an array of strings for multiple methods. +@param [defaultResponse] {value} + This value will be returned by any wrapper whose wrapped method throws an + exception. If not given, `undefined` is used. +@param [logException] {function} + See `catchAndLog()`. +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/event/core.md b/tools/addon-sdk-1.7/packages/api-utils/docs/event/core.md new file mode 100644 index 0000000..6ab3317 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/event/core.md @@ -0,0 +1,51 @@ +<!-- 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/. --> + +Module provides core (low level) API for working with events in the SDK. This +API is mainly for implementing higher level event APIs. + +Event `listener` may be registered on any (event `target`) object using +provided `on` function: + + var { on, once, off, emit } = require('api-utils/event/core'); + var target = { name: 'target' }; + on(target, 'message', function listener(event) { + console.log('hello ' + event); + }); + on(target, 'data', console.log); + +Event of specific `type` may be emitted on any event `target` object using +`emit` function. This will call all registered `listener`s for the given `type` +on the given event `target` in the same order they were registered. + + emit(target, 'message', 'event'); + // info: 'hello event' + emit(target, 'data', { type: 'data' }, 'second arg'); + // info: [Object object] 'second arg' + +Registered event listeners may be removed using `off` function: + + off(target, 'message'); + emit(target, 'message', 'bye'); + // info: 'hello bye' + +Sometimes listener only cares about first event of specific `type`. To avoid +hassles of removing such listeners there is convenient `once` function: + + once(target, 'load', function() { + console.log('ready'); + }); + emit(target, 'load') + // info: 'ready' + emit(target, 'load') + +There are also convenient ways to remove registered listeners. All listeners of +the specific type can be easily removed (only two argument must be passed): + + off(target, 'message'); + +Also, removing all registered listeners is possible (only one argument must be +passed): + + off(target); diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/event/target.md b/tools/addon-sdk-1.7/packages/api-utils/docs/event/target.md new file mode 100644 index 0000000..96feb79 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/event/target.md @@ -0,0 +1,95 @@ +<!-- 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/. --> + +Provides an exemplar `EventTarget` object, that implements interface for +adding removing event listeners of a specific type. `EventTarget` is a +base of all objects in SDK on which events are emitted. + +### Instantiation + +It's easy to create event target objects, no special arguments are required. + + const { EventTarget } = require('api-utils/event/target'); + let target = EventTarget.new(); + +For a convenience though optional `options` arguments may be used, in which +case all the function properties with keys like: `onMessage`, `onMyEvent`... +will be auto registered for associated `'message'`, `'myEvent'` events on +the created instance. _All other properties of `options` will be ignored_. + +### Adding listeners + +`EventTarget` interface defines `on` method, that can be used to register +event listeners on them for the given event type: + + target.on('message', function onMessage(message) { + // Note: `this` pseudo variable is an event `target` unless + // intentionally overridden via `.bind()`. + console.log(message); + }); + +Sometimes event listener may care only about very first event of specific +`type`. `EventTarget` interface defines convenience method for adding one +shot event listeners via method `once`. Such listeners are called only once +next time event of the specified type is emitted: + + target.once('ready', function onReady() { + // Do the thing once ready! + }); + +### Removing listeners + +`EventTarget` interface defines API for unregistering event listeners, via +`removeListener` method: + + target.removeListener('message', onMessage); + +### Emitting events + +`EventTarget` interface intentionally does not defines an API for emitting +events. In majority of cases party emitting events is different from party +registering listeners. In order to emit events one needs to use `event/core` +module instead: + + let { emit } = require('api-utils/event/core'); + + target.on('hi', function(person) { console.log(person + 'tells hi'); }); + emit(target, 'hi', 'Mark'); + // info: 'Mark tells hi' + +For more details see **event/core** documentation. + +### More details + +Listeners registered during the event propagation (by one of the listeners) +won't be triggered until next emit of the matching type: + + let { emit } = require('api-utils/event/core'); + + target.on('message', function onMessage(message) { + console.log('listener trigerred'); + target.on('message', function() { + console.log('nested listener triggered'); + }); + }); + + emit(target, 'message'); + // info: 'listener trigerred' + emit(target, 'message'); + // info: 'listener trigerred' + // info: 'nested listener trigerred' + +Exceptions in the listeners can be handled via `'error'` event listeners: + + target.on('boom', function() { + throw Error('Boom!'); + }); + target.once('error', function(error) { + console.log('caught an error: ' + error.message); + }); + emit(target, 'boom'); + // info: caught an error: Boom! + +If there is no listener registered for `error` event or if it also throws +exception then such exceptions are logged into a console. diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/events.md b/tools/addon-sdk-1.7/packages/api-utils/docs/events.md new file mode 100644 index 0000000..76f9efd --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/events.md @@ -0,0 +1,78 @@ +<!-- 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/. --> + +The `events` module provides base API for emitting events. + +This module is not intended to be used directly by programs. Rather, it is +intended to be used by other modules that provide APIs to programs. + +<api name="EventEmitter"> +@class +The EventEmitter is the base building block for all compositions that +would need to broadcast data to multiple consumers. + +Please note that `EventEmitter` does not expose either a method for emitting +events or a list of available event listeners as its public API. Obviously +both are accessible but from the instance itself through the private API. +<api name="EventEmitter"> +@constructor +Creates an EventEmitter object. +</api> + +<api name="on"> +@method +Registers an event `listener` that will be called when events of +specified `type` are emitted. + +If the `listener` is already registered for this `type`, a call to this +method has no effect. + +If the event listener is being registered while an event is being processed, +the event listener is not called during the current emit. + +**Example:** + + // worker is instance of EventEmitter + worker.on('message', function (data) { + console.log('data received: ' + data) + }); + +@param type {String} + The type of the event. +@param listener {Function} + The listener function that processes the event. +</api> + +<api name="once"> +@method +Registers an event `listener` that will only be called once, the next time +an event of the specified `type` is emitted. + +If the event listener is registered while an event of the specified `type` +is being emitted, the event listener will not be called during the current +emit. + +@param type {String} + The type of the event. +@param listener {Function} + The listener function that processes the event. +</api> + +<api name="removeListener"> +@method +Unregisters an event `listener` for the specified event `type`. + +If the `listener` is not registered for this `type`, a call to this +method has no effect. + +If an event listener is removed while an event is being processed, it is +still triggered by the current emit. After it is removed, the event listener +is never invoked again (unless registered again for future processing). + +@param type {String} + The type of the event. +@param listener {Function} + The listener function that processes the event. +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/file.md b/tools/addon-sdk-1.7/packages/api-utils/docs/file.md new file mode 100644 index 0000000..9587353 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/file.md @@ -0,0 +1,151 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `file` module provides access to the local filesystem. + + +Paths +----- + +Path specifications in this API are platform-specific. This means that on +Windows paths are specified using the backslash path separator (`\`), and on +Unix-like systems like Linux and OS X paths are specified using the forward +slash path separator (`/`). + +If your add-on uses literal Windows-style path specifications with this API, +your add-on likely won't work when users run it on Unix-like systems. Likewise, +if your add-on uses literal Unix-style path specifications, it won't work for +users on Windows. + +To ensure your add-on works for everyone, generate paths using the +[`join`](packages/api-utils/file.html#join(...)) function. Unfortunately +this API does not currently provide a way to obtain an absolute base path which +you could then use with `join`. For now, you need to +[`require("chrome")`](dev-guide/tutorials/chrome.html) and use the +XPCOM directory service as described at +[MDN](https://developer.mozilla.org/en/Code_snippets/File_I%2F%2FO#Getting_special_files). + +Note that if you do decide to hardcode Windows-style paths, be sure to escape +backslashes in strings. For example, to specify the file at `C:\Users\Myk`, you +need to use the string `"C:\\Users\\Myk"`, not `"C:\Users\Myk"`. You can read +more about escaping characters in strings at +[MDN](https://developer.mozilla.org/en/JavaScript/Guide/Values,_Variables,_and_Literals#Escaping_Characters). + + +<api name="basename"> +@function + Returns the last component of the given path. For example, + `basename("/foo/bar/baz")` returns `"baz"`. If the path has no components, + the empty string is returned. +@param path {string} + The path of a file. +@returns {string} + The last component of the given path. +</api> + +<api name="dirname"> +@function + Returns the path of the directory containing the given file. If the file is + at the top of the volume, the empty string is returned. +@param path {string} + The path of a file. +@returns {string} + The path of the directory containing the file. +</api> + +<api name="exists"> +@function + Returns true if a file exists at the given path and false otherwise. +@param path {string} + The path of a file. +@returns {boolean} + True if the file exists and false otherwise. +</api> + +<api name="join"> +@function + Takes a variable number of strings, joins them on the file system's path + separator, and returns the result. +@param ... {strings} + A variable number of strings to join. The first string must be an absolute + path. +@returns {string} + A single string formed by joining the strings on the file system's path + separator. +</api> + +<api name="list"> +@function + Returns an array of file names in the given directory. +@param path {string} + The path of the directory. +@returns {array} + An array of file names. Each is a basename, not a full path. +</api> + +<api name="mkpath"> +@function + Makes a new directory named by the given path. Any subdirectories that do not + exist are also created. `mkpath` can be called multiple times on the same + path. +@param path {string} + The path to create. +</api> + +<api name="open"> +@function + Returns a stream providing access to the contents of a file. +@param path {string} + The path of the file to open. +@param [mode] {string} + An optional string, each character of which describes a characteristic of the + returned stream. If the string contains `"r"`, the file is opened in + read-only mode. `"w"` opens the file in write-only mode. `"b"` opens the + file in binary mode. If `"b"` is not present, the file is opened in text + mode, and its contents are assumed to be UTF-8. If *`mode`* is not given, + `"r"` is assumed, and the file is opened in read-only text mode. +@returns {stream} + If the file is opened in text read-only `mode`, a `TextReader` is returned, + and if text write-only mode, a `TextWriter` is returned. See + [`text-streams`](packages/api-utils/text-streams.html) for information on + these text stream objects. If the file is opened in binary read-only `mode`, + a `ByteReader` is returned, and if binary write-only mode, a `ByteWriter` is + returned. See + [`byte-streams`](packages/api-utils/byte-streams.html) for more + information on these byte stream objects. Opened files should always be + closed after use by calling `close` on the returned stream. +</api> + +<api name="read"> +@function + Opens a file and returns a string containing its entire contents. +@param path {string} + The path of the file to read. +@param [mode] {string} + An optional string, each character of which describes a characteristic of the + returned stream. If the string contains `"b"`, the contents will be returned + in binary mode. If `"b"` is not present or `mode` is not given, the file + contents will be returned in text mode. +@returns {string} + A string containing the file's entire contents. +</api> + +<api name="remove"> +@function + Removes a file from the file system. To remove directories, use `rmdir`. +@param path {string} + The path of the file to remove. +</api> + +<api name="rmdir"> +@function + Removes a directory from the file system. If the directory is not empty, an + exception is thrown. +@param path {string} + The path of the directory to remove. +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/frame/utils.md b/tools/addon-sdk-1.7/packages/api-utils/docs/frame/utils.md new file mode 100644 index 0000000..c88d198 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/frame/utils.md @@ -0,0 +1,53 @@ +<!-- 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/. --> + +The `frame/utils` module provides helper functions for working with platform +internals like [frames](https://developer.mozilla.org/en/XUL/iframe) and +[browsers](https://developer.mozilla.org/en/XUL/browser). + +### create + +Module exports `create` function that takes `nsIDOMDocument` of the +[privileged document](https://developer.mozilla.org/en/Working_with_windows_in_chrome_code) +and creates a `browser` element in it's `documentElement`: + + let { open } = require('api-utils/window/utils'); + let { create } = require('api-utils/frame/utils'); + let window = open('data:text/html,Foo'); + let frame = create(window.document); + +Optionally `create` can be passed set of `options` to configure created frame +even further. Following options are supported: + +- `type` +String that defines access type of the document loaded into it. Defaults to +`'content'`. For more details and other possible values see +[documentation on MDN](https://developer.mozilla.org/en/XUL/Attribute/browser.type) + +- `uri` +URI of the document to be loaded into created frame. Defaults to `about:blank`. + +- `remote` +If `true` separate process will be used for this frame, also in such case all +the following options are ignored. + +- `allowAuth` +Whether to allow auth dialogs. Defaults to `false`. + +- `allowJavascript` +Whether to allow Javascript execution. Defaults to `false`. + +- `allowPlugins` +Whether to allow plugin execution. Defaults to `false`. + +Execution of scripts may easily be enabled: + + let { open } = require('api-utils/window/utils'); + let { create } = require('api-utils/frame/utils'); + let window = open('data:text/html,top'); + let frame = create(window.document, { + uri: 'data:text/html,<script>alert("Hello")</script>', + allowJavascript: true + }); + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/globals.md b/tools/addon-sdk-1.7/packages/api-utils/docs/globals.md new file mode 100644 index 0000000..e6df8a8 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/globals.md @@ -0,0 +1,100 @@ +<!-- 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/. --> + +Globals in this section are subject to change in the future and/or are likely +to be of interest to SDK module developers, rather than add-on developers. + +## Components ## + +To access the infamous and powerful `Components` object, see the +[Chrome Authority](dev-guide/tutorials/chrome.html) documentation. + +## \_\_url\_\_ ## + +The `__url__` global is a string identifying the URL from which the code has +been retrieved. If the code has no identifiable URL, this value may be `null`. + +## packaging ## + +<span class="aside"> +For more information on packaging, see the [Package Specification][] appendix. +</span> + +The `packaging` global contains methods and metadata related to +the packages available in the current environment. + +<code>packaging.**getURLForData**(*path*)</code> + +Given a unix-style path relative to the calling package's `data` +directory, returns an absolute URL to the file or directory. + +By "calling package", we mean the package in which the caller's source +code resides. + +Thus, for example, if a package contains a resource at +`data/mydata.dat` and a module at `lib/foo.js`, the module at +`lib/foo.js` may make the following call to retrieve an absolute URL +to `data/mydata.dat`: + + var myDataURL = packaging.getURLForData("/mydata.dat"); + +If the calling package has no `data` directory, an exception is +thrown. + +## memory ## + +`memory` is an object that exposes functionality to track +objects of interest and help diagnose and prevent memory leaks. + +<code>memory.**track**(*object*, [*bin*])</code> + +Marks *object* for being tracked, and categorizes it with the given +bin name. If *bin* isn't specified, the memory tracker attempts to +infer a bin name by first checking the object's +`constructor.name`; if that fails or results in the generic +`Object`, the stack is inspected and the name of the current +function being executed—which is assumed to be a constructor +function—is used. If that fails, then the object is placed in a +bin named `generic`. + +<code>memory.**getObjects**([*bin*])</code> + +Returns an `Array` containing information about tracked objects +that have been categorized with the given bin name. If *bin* isn't +provided, information about all live tracked objects are returned. + +Each element of the array is an object with the following keys: + +<table> + <tr> + <td><code>weakref</code></td> + <td>A weak reference to the object being tracked. Call + <code>get()</code> on this object to retrieve its strong reference; if + a strong reference to the object no longer exists, <code>get()</code> + will return <code>null</code>.</td> + </tr> + <tr> + <td><code>created</code></td> + <td>A <code>Date</code> representing the date and time that + <code>memory.track()</code> was called on the object being + tracked.</td> + </tr> + <tr> + <td><code>filename</code></td> + <td>The name of the file that called <code>memory.track()</code> on + the object being tracked.</td> + </tr> + <tr> + <td><code>lineNo</code></td> + <td>The line number of the file that called + <code>memory.track()</code> on the object being tracked.</td> + </tr> +</table> + +<code>memory.**getBins**()</code> + +Returns an `Array` containing the names of all bins that aren't +currently empty. + + [Package Specification]: dev-guide/package-spec.html
\ No newline at end of file diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/hidden-frame.md b/tools/addon-sdk-1.7/packages/api-utils/docs/hidden-frame.md new file mode 100644 index 0000000..9d16dfa --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/hidden-frame.md @@ -0,0 +1,83 @@ +<!-- 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/. --> + +<!-- contributed by Myk Melez [myk@mozilla.org] --> + +The `hidden-frame` module creates host application frames (i.e. XUL `<iframe>` +elements) that are not displayed to the user. It is useful in the construction +of APIs that load web content not intended to be directly seen or accessed +by users, like `page-worker`. It is also useful in the construction of APIs +that load web content for intermittent display, such as `panel`. + +This module is not intended to be used directly by programs. Rather, it is +intended to be used by other modules that provide APIs to programs. + +Introduction +------------ + +The module exports a constructor function, `HiddenFrame`, and two other +functions, `add` and `remove`. + +`HiddenFrame` constructs a new hidden frame. `add` registers a hidden frame, +preparing it to load content. `remove` unregisters a frame, unloading any +content that was loaded in it. + +Examples +-------- + +The following code creates a hidden frame, loads a web page into it, and then +logs its title: + + var hiddenFrames = require("hidden-frame"); + let hiddenFrame = hiddenFrames.add(hiddenFrames.HiddenFrame({ + onReady: function() { + this.element.contentWindow.location = "http://www.mozilla.org/"; + let self = this; + this.element.addEventListener("DOMContentLoaded", function() { + console.log(self.element.contentDocument.title); + }, true, true); + } + })); + +See the `panel` module for a real-world example of usage of this module. + +Reference +--------- +<api name="HiddenFrame"> +@class +`HiddenFrame` objects represent hidden frames. +<api name="HiddenFrame"> +@constructor +Creates a hidden frame. +@param options {object} + Options for the frame, with the following keys: + @prop onReady {function,array} + Functions to call when the frame is ready to load content. You must specify + an `onReady` callback and refrain from using the hidden frame until + the callback gets called, because hidden frames are not always ready to load + content the moment they are added. +</api> + +<api name="add"> +@function +Register a hidden frame, preparing it to load content. +@param hiddenFrame {HiddenFrame} the frame to add +</api> + +<api name="remove"> +@function +Unregister a hidden frame, unloading any content that was loaded in it. +@param hiddenFrame {HiddenFrame} the frame to remove +</api> + +<api name="element"> +@property {DOMElement} +The host application frame in which the page is loaded. +</api> + +<api name="onReady"> +@property {array} +Functions to call when the frame is ready to load content. +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/httpd.md b/tools/addon-sdk-1.7/packages/api-utils/docs/httpd.md new file mode 100644 index 0000000..3f131f9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/httpd.md @@ -0,0 +1,31 @@ +<!-- 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/. --> + +Provides an HTTP server written in JavaScript for the Mozilla platform, which +can be used in unit tests. + +The most basic usage is: + + var {startServerAsync} = require("httpd"); + var srv = startServerAsync(port, basePath); + require("unload").when(function cleanup() { + srv.stop(function() { // you should continue execution from this point. + }) + }); + +This starts a server in background (assuming you're running this code in an +application that has an event loop, such as Firefox). The server listens at +http://localhost:port/ and serves files from the specified directory. You +can serve static content or use SJS scripts, as described in documentation +on developer.mozilla.org. + +You can also use `nsHttpServer` to start the server manually: + + var {nsHttpServer} = require("httpd"); + var srv = new nsHttpServer(); + // further documentation on developer.mozilla.org + +See +[HTTP server for unit tests](https://developer.mozilla.org/En/HTTP_server_for_unit_tests) +for general information. diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/light-traits.md b/tools/addon-sdk-1.7/packages/api-utils/docs/light-traits.md new file mode 100644 index 0000000..367a6a9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/light-traits.md @@ -0,0 +1,295 @@ +<!-- 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/. --> + + +[Traits](http://en.wikipedia.org/wiki/Trait_%28computer_science%29) are a simple +mechanism for structuring object-oriented programs. They represent reusable and +composable building blocks of functionality that factor out the common +attributes and behavior of objects. + +They are a more robust alternative to +[mixins](http://en.wikipedia.org/wiki/Mixins) and +[multiple inheritance](http://en.wikipedia.org/wiki/Multiple_inheritance), +because name clashes must be explicitly resolved and composition is commutative +and associative (i.e. the order of traits in a composition is irrelevant). + +Use traits to share functionality between similar objects without duplicating +code or creating complex inheritance chains. + +## Trait Creation ## + +To create a trait, call the `Trait` constructor function exported by this +module, passing it a JavaScript object that specifies the properties of the +trait. + + let Trait = require('light-traits').Trait; + let t = Trait({ + foo: "foo", + bar: function bar() { + return "Hi!" + }, + baz: Trait.required + }); + +Traits can both provide and require properties. A *provided* property is a +property for which the trait itself provides a value. A *required* property is a +property that the trait needs in order to function correctly but for which +it doesn't provide a value. + +Required properties must be provided by another trait or by an object with a +trait. Creation of an object with a trait will fail if required properties are +not provided. Specify a required property by setting the value of the property +to `Trait.required`. + +## Object Creation ## + +Create objects with a single trait by calling the trait's `create` method. The +method takes a single argument, the object to serve as the new object's +prototype. If no prototype is specified, the new object's prototype will be +`Object.prototype`. + + let t = Trait({ + foo: 'foo', + bar: 2 + }); + let foo1 = t.create(); + let foo2 = t.create({ name: 'Super' }); + +## Trait Composition ## + +Traits are designed to be composed with other traits to create objects with the +properties of multiple traits. To compose an object with multiple traits, you +first create a composite trait and then use it to create the object. A composite +trait is a trait that contains all of the properties of the traits from which it +is composed. In the following example, MagnitudeTrait is a composite trait. + + let EqualityTrait = Trait({ + equal: Trait.required, + notEqual: function notEqual(x) { + return !this.equal(x) + } + }); + + let ComparisonTrait = Trait({ + less: Trait.required, + notEqual: Trait.required, + greater: function greater(x) { + return !this.less(x) && this.notEqual(x) + } + }); + + let MagnitudeTrait = Trait.compose(EqualityTrait, ComparisonTrait); + +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-11 121 490 190" width="490px" height="190px"> + <defs> + <marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="SharpArrow_Marker" viewBox="-4 -4 10 8" markerWidth="10" markerHeight="8" color="black"> + <g> + <path d="M 5 0 L -3 -3 L 0 0 L 0 0 L -3 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1px"/> + </g> + </marker> + </defs> + <g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"> + <g> + <rect x="9" y="165.33334" width="141" height="14"/> + <rect x="9" y="165.33334" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 165.33334)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="11" textLength="47.373047">notEqual</tspan> + </text> + <rect x="9" y="151.33334" width="141" height="14"/> + <rect x="9" y="151.33334" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 151.33334)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="29.361328">equal</tspan> + </text> + <rect x="9" y="137.33334" width="141" height="14"/> + <rect x="9" y="137.33334" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 137.33334)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="bold" x="38.49707" y="11" textLength="54.00586">EqualityTrait</tspan> + </text> + <rect x="9" y="273" width="141" height="14"/> + <rect x="9" y="273" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 273)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="11" textLength="38.021484">greater</tspan> + </text> + <rect x="9" y="259" width="141" height="14"/> + <rect x="9" y="259" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 259)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="47.373047">notEqual</tspan> + </text> + <rect x="9" y="245" width="141" height="14"/> + <rect x="9" y="245" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 245)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="21.339844">less</tspan> + </text> + <rect x="9" y="231" width="141" height="14"/> + <rect x="9" y="231" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(14 231)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="bold" x=".15332031" y="11" textLength="112.67578">ComparisonTrait</tspan> + </text> + <rect x="317.75" y="235.5" width="141" height="14"/> + <rect x="317.75" y="235.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 235.5)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="11" textLength="38.021484">greater</tspan> + </text> + <rect x="317.75" y="221.5" width="141" height="14"/> + <rect x="317.75" y="221.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 221.5)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="21.339844">less</tspan> + </text> + <rect x="317.75" y="207.5" width="141" height="14"/> + <rect x="317.75" y="207.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 207.5)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" x="0" y="11" textLength="47.373047">notEqual</tspan> + </text> + <rect x="317.75" y="193.5" width="141" height="14"/> + <rect x="317.75" y="193.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 193.5)" fill="red"> + <tspan font-family="Helvetica" font-size="12" font-weight="500" fill="red" x="0" y="11" textLength="29.361328">equal</tspan> + </text> + <rect x="317.75" y="179.5" width="141" height="14"/> + <rect x="317.75" y="179.5" width="141" height="14" stroke="black" stroke-width="1px"/> + <text transform="translate(322.75 179.5)" fill="black"> + <tspan font-family="Helvetica" font-size="12" font-weight="bold" x="31.83789" y="11" textLength="67.32422">MagnitudeTrait</tspan> + </text> + <path d="M 150 248.83887 L 158.89999 248.83887 L 235.9 248.83887 L 235.9 224.66113 L 308.85 224.66113 L 310.85 224.66113" marker-end="url(#SharpArrow_Marker)" stroke="black" stroke-linecap="butt" stroke-linejoin="miter" stroke-width="1px"/> + <path d="M 150 171.15845 L 158.89999 171.15845 L 233.9 171.15845 L 233.9 201.6749 L 308.85 201.6749 L 310.85 201.6749" marker-end="url(#SharpArrow_Marker)" stroke="black" stroke-linecap="butt" stroke-linejoin="miter" stroke-width="1px"/> + </g> + </g> +</svg> + +## Trait Resolution ## + +Composite traits have conflicts when two of the traits in the composition +provide properties with the same name but different values (when compared using +the `===` strict equality operator). In the following example, `TC` has a +conflict because `T1` and `T2` both define a `foo` property: + + let T1 = Trait({ + foo: function () { + // do something + }, + bar: 'bar', + t1: 1 + }); + + let T2 = Trait({ + foo: function() { + // do something else + }, + bar: 'bar', + t2: 2 + }); + + let TC = Trait.compose(T1, T2); + +Attempting to create an object from a composite trait with conflicts throws a +`remaining conflicting property` exception. To create objects from such traits, +you must resolve the conflict. + +You do so by excluding or renaming the conflicting property of one of the +traits. Excluding a property removes it from the composition, so the composition +only acquires the property from the other trait. Renaming a property gives it a +new, non-conflicting name at which it can be accessed. + +In both cases, you call the `resolve` method on the trait whose property you +want to exclude or rename, passing it an object. Each key in the object is the +name of a conflicting property; each value is either `null` to exclude the +property or a string representing the new name of the property. + +For example, the conflict in the previous example could be resolved by excluding +the `foo` property of the second trait. + + let TC = Trait(T1, T2.resolve({ foo: null })); + +It could also be resolved by renaming the `foo` property of the first trait to +`foo2`: + + let TC = Trait(T1.resolve({ foo: "foo2" }), T2); + +When you resolve a conflict, the same-named property of the other trait (the one +that wasn't excluded or renamed) remains available in the composition under its +original name. + +## Constructor Functions ## + +When your code is going to create more than one object with traits, you may want +to define a constructor function to create them. To do so, create a composite +trait representing the traits the created objects should have, then define a +constructor function that creates objects with that trait and whose prototype is +the prototype of the constructor: + + let PointTrait = Trait.compose(T1, T2, T3); + function Point(options) { + let point = PointTrait.create(Point.prototype); + return point; + } + +## Property Descriptor Maps ## + +Traits are designed to work with the new object manipulation APIs defined in +[ECMAScript-262, Edition +5](http://www.ecma-international.org/publications/standards/Ecma-262.htm) (ES5). +Traits are also property descriptor maps that inherit from `Trait.prototype` to +expose methods for creating objects and resolving conflicts. + +The following trait definition: + + let FooTrait = Trait({ + foo: "foo", + bar: function bar() { + return "Hi!" + }, + baz: Trait.required + }); + +Creates the following property descriptor map: + + { + foo: { + value: 'foo', + enumerable: true, + configurable: true, + writable: true + }, + + bar: { + value: function b() { + return 'bar' + }, + enumerable: true, + configurable: true, + writable: true + }, + + baz: { + get baz() { throw new Error('Missing required property: `baz`') } + set baz() { throw new Error('Missing required property: `baz`') } + }, + + __proto__: Trait.prototype + } + +Since Traits are also property descriptor maps, they can be used with built-in +`Object.*` methods that accept such maps: + + Object.create(proto, FooTrait); + Object.defineProperties(myObject, FooTrait); + +Note that conflicting and required properties won't cause exceptions to be +thrown when traits are used with the `Object.*` methods, since those methods are +not aware of those constraints. However, such exceptions will be thrown when the +property with the conflict or the required but missing property is accessed. + +Property descriptor maps can also be used in compositions. This may be useful +for defining non-enumerable properties, for example: + + let TC = Trait.compose( + Trait({ foo: 'foo' }), + { bar: { value: 'bar', enumerable: false } } + ); + +_When using property descriptor maps in this way, make sure the map is not the +only argument to `Trait.compose`, since in that case it will be interpreted as +an object literal with properties to be defined._ + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/list.md b/tools/addon-sdk-1.7/packages/api-utils/docs/list.md new file mode 100644 index 0000000..fd70698 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/list.md @@ -0,0 +1,98 @@ +<!-- 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/. --> + +<!-- contributed by Irakli Gozalishvili [gozala@mozilla.com] --> + +The `"list"` module provides base building blocks for composing lists. + +<api name="Iterable"> +@class +Base trait that can be used to compose traits with non-standard +enumeration behaviors. + +This trait is supposed to be used as part of a composition, since it only +provides custom enumeration behavior to a composed object. +It defines one required `_keyValueMap` property, that is used as a hash of +"key-values" to iterate on during enumeration. + +<api name="Iterable"> +@constructor +Constructs an `Iterable` object. +</api> + +<api name="_keyValueMap"> +@property {Object} +Hash map of key-values to iterate over. _Required_ property: that is, the +property must be supplied by objects that compose this trait. +_Note: That this property can be a getter if you need dynamic behavior._ +</api> + +</api> + +<api name="List"> +@class +An ordered collection (also known as a sequence) disallowing duplicate +elements. List is composed out of `Iterable`, therefore it provides custom +enumeration behavior that is similar to array (enumerates only on the +elements of the list). + +List is a base trait and is meant to be part of a +composition, since all of its API is private except for the `length` property. + +**Examples:** + + var MyList = List.compose({ + add: function add(item1, item2, /*item3...*/) { + Array.slice(arguments).forEach(this._add.bind(this)); + }, + remove: function remove(item1, item2, /*item3...*/) { + Array.slice(arguments).forEach(this._remove.bind(this)); + } + }); + MyList('foo', 'bar', 'baz').length == 3; // true + new MyList('new', 'keyword').length == 2; // true + MyList.apply(null, [1, 2, 3]).length == 3; // true + let list = MyList(); + list.length == 0; // true + list.add(1, 2, 3) == 3; // true + +<api name="List"> +@constructor +Constructor can takes any number of elements and creates an instance of +`List` populated with the specified elements. +@param [element1] {Object|String|Number} +@param [element2] {Object|String|Number} +@param [...] {Object|String|Number} +</api> + +<api name="length"> +@property {Number} +Number of elements in this list. +</api> + +<api name="_has"> +@method +@param element {Object|Number|String} +Returns `true` if this list contains the specified `element`. +</api> +<api name="_add"> +@method +@param element {Object|Number|String} +Appends the specified `element` to the end of this list, if it doesn't +contain it. + +_Ignores the call if `element` is already contained._ +</api> +<api name="_remove"> +@method +@param element {Object|Number|String} +Removes specified `element` from this list, if it contains it. + +_Ignores the call if `element` is not contained._ +</api> +<api name="_clear"> +@method +Removes all of the elements from this list. +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/match-pattern.md b/tools/addon-sdk-1.7/packages/api-utils/docs/match-pattern.md new file mode 100644 index 0000000..0222e65 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/match-pattern.md @@ -0,0 +1,246 @@ +<!-- 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/. --> + +The `match-pattern` module can be used to test strings containing URLs +against simple patterns. + +## Specifying Patterns ## + +There are three ways you can specify patterns: + +* as an exact match string +* using a wildcard in a string +* using a regular expression + +### Exact Matches ### + +**A URL** matches only that URL. The URL must start with a scheme, end with a +slash, and contain no wildcards. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>"http://example.com/"</code></td> + <td><code>http://example.com/</code></td> + <td><code>http://example.com</code><br> + <code>http://example.com/foo</code><br> + <code>https://example.com/</code><br> + <code>http://foo.example.com/</code></td> + </tr> + +</table> + +### Wildcards ### + +**A single asterisk** matches any URL with an `http`, `https`, or `ftp` +scheme. For other schemes like `file`, use a scheme followed by an +asterisk, as below. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>"*"</code></td> + <td><code>http://example.com/</code><br> + <code>https://example.com/</code><br> + <code>ftp://example.com/</code><br> + <code>http://bar.com/foo.js</code><br> + <code>http://foo.com/</code></td> + <td><code>file://example.js</code></td> + </tr> + +</table> + +**A domain name prefixed with an asterisk and dot** matches any URL of that +domain or a subdomain, using any of `http`, `https`, `ftp`. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>"*.example.com"</code></td> + <td><code>http://example.com/</code><br> + <code>http://foo.example.com/</code><br> + <code>https://example.com/</code><br> + <code>http://example.com/foo</code><br> + <code>ftp://foo.example.com/</code></td> + <td><code>ldap://example.com</code><br> + <code>http://example.foo.com/</code></td> + </tr> + +</table> + +**A URL followed by an asterisk** matches that URL and any URL prefixed with +the pattern. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>"https://foo.com/*"</code></td> + <td><code>https://foo.com/</code><br> + <code>https://foo.com/bar</code></td> + <td><code>http://foo.com/</code><br> + <code>https://foo.com</code><br> + <code>https://bar.foo.com/</code></td> + </tr> + +</table> + +**A scheme followed by an asterisk** matches all URLs with that scheme. To +match local files, use `file://*`. + +<table> + + <colgroup> + <col width="30%"> + <col width="70%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + </tr> + + <tr> + <td><code>"file://*"</code></td> + <td><code>file://C:/file.html</code><br> + <code>file:///home/file.png</code></td> + </tr> + +</table> + +### Regular Expressions ### + +You can specify patterns using a +[regular expression](https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions): + + var { MatchPattern } = require("match-pattern"); + var pattern = new MatchPattern(/.*example.*/); + +The regular expression is subject to restrictions based on those applied to the +[HTML5 pattern attribute](http://dev.w3.org/html5/spec/common-input-element-attributes.html#attr-input-pattern). In particular: + +* The pattern must match the entire value, not just any subset. For example, the +pattern `/moz.*/` will not match the URL `http://mozilla.org`. + +* The expression is compiled with the `global`, `ignoreCase`, and `multiline` flags + disabled. The `MatchPattern` constructor will throw an exception + if you try to set any of these flags. + +<table> + + <colgroup> + <col width="30%"> + <col width="35%"> + <col width="35%"> + </colgroup> + + <tr> + <th>Example pattern</th> + <th>Example matching URLs</th> + <th>Example non-matching URLs</th> + </tr> + + <tr> + <td><code>/.*moz.*/</code></td> + <td><code>http://foo.mozilla.org/</code><br> + <code>http://mozilla.org</code><br> + <code>https://mozilla.org</code><br> + <code>http://foo.com/mozilla</code><br> + <code>http://hemozoon.org</code><br> + <code>mozscheme://foo.org</code><br></td> + <td><code>http://foo.org</code><br> + </tr> + + <tr> + <td><code>/http:\/\/moz.*/</code></td> + <td><code>http://mozilla.org</code><br> + <code>http://mozzarella.com</code></td> + <td><code>https://mozilla.org</code><br> + <code>http://foo.mozilla.org/</code><br> + <code>http://foo.com/moz</code></td> + </tr> + + <tr> + <td><code>/http.*moz.*/</code><br></td> + <td><code>http://foo.mozilla.org/</code><br> + <code>http://mozilla.org</code><br> + <code>http://hemozoon.org/</code></td> + <td><code>ftp://http/mozilla.org</code></td> + </tr> + +</table> + +## Examples ## + + var { MatchPattern } = require("match-pattern"); + var pattern = new MatchPattern("http://example.com/*"); + console.log(pattern.test("http://example.com/")); // true + console.log(pattern.test("http://example.com/foo")); // true + console.log(pattern.test("http://foo.com/")); // false! + +<api name="MatchPattern"> +@class +<api name="MatchPattern"> +@constructor + This constructor creates match pattern objects that can be used to test URLs. +@param pattern {string} + The pattern to use. See Patterns above. +</api> + +<api name="test"> +@method + Tests a URL against the match pattern. +@param url {string} + The URL to test. +@returns {boolean} + True if the URL matches the pattern and false otherwise. +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/memory.md b/tools/addon-sdk-1.7/packages/api-utils/docs/memory.md new file mode 100644 index 0000000..04de606 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/memory.md @@ -0,0 +1,7 @@ +<!-- 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/. --> + +The `memory` module provides a concrete default implementation for the SDK's +`memory` global. For documentation on the `memory` global, see the +[Globals](packages/api-utils/globals.html) reference. diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/message-manager.md b/tools/addon-sdk-1.7/packages/api-utils/docs/message-manager.md new file mode 100644 index 0000000..522c613 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/message-manager.md @@ -0,0 +1,13 @@ +<!-- 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/. --> + +<!-- contributed by Matteo Ferretti [zer0@mozilla.com] --> + +Overview +-------- +The `message-manager` module provides a minimalist implementation +of the [Message Manager](https://developer.mozilla.org/en/The_message_manager) +APIs, in a single process environment. + +It's mainly used internally for Fennec Birch support.
\ No newline at end of file diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/namespace.md b/tools/addon-sdk-1.7/packages/api-utils/docs/namespace.md new file mode 100644 index 0000000..07ec96e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/namespace.md @@ -0,0 +1,71 @@ +<!-- 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/. --> + +Provides an API for creating namespaces for any given objects, which +effectively may be used for creating fields that are not part of objects +public API. + + let { ns } = require('api-utils/namespace'); + let aNamespace = ns(); + + aNamespace(publicAPI).secret = secret; + +One namespace may be used with multiple objects: + + let { ns } = require('api-utils/namespace'); + let dom = ns(); + + function View(element) { + let view = Object.create(View.prototype); + dom(view).element = element; + // .... + } + View.prototype.destroy = function destroy() { + let { element } = dom(this); + element.parentNode.removeChild(element); + // ... + }; + // ... + exports.View = View; + // ... + +Also, multiple namespaces can be used with one object: + + // ./widget.js + + let { Cu } = require('chrome'); + let { ns } = require('api-utils/namespace'); + let { View } = require('./view'); + + // Note this is completely independent from View's internal Namespace object. + let sandboxes = ns(); + + function Widget(options) { + let { element, contentScript } = options; + let widget = Object.create(Widget.prototype); + View.call(widget, options.element); + sandboxes(widget).sandbox = Cu.Sandbox(element.ownerDocument.defaultView); + // ... + } + Widget.prototype = Object.create(View.prototype); + Widget.prototype.postMessage = function postMessage(message) { + let { sandbox } = sandboxes(this); + sandbox.postMessage(JSON.stringify(JSON.parse(message))); + ... + }; + Widget.prototype.destroy = function destroy() { + View.prototype.destroy.call(this); + // ... + delete sandboxes(this).sandbox; + }; + exports.Widget = Widget; + +In addition access to the namespace can be shared with other code by just +handing them a namespace accessor function. + + let { dom } = require('./view'); + Widget.prototype.setInnerHTML = function setInnerHTML(html) { + dom(this).element.innerHTML = String(html); + }; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/observer-service.md b/tools/addon-sdk-1.7/packages/api-utils/docs/observer-service.md new file mode 100644 index 0000000..4d7cb58 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/observer-service.md @@ -0,0 +1,73 @@ +<!-- 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/. --> + +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `observer-service` module provides access to the +application-wide observer service singleton. + +For a list of common observer topics across a variety of Mozilla-based +applications, see the MDC page on +[Observer Notifications](https://developer.mozilla.org/en/Observer_Notifications). + +## Observer Callbacks ## + +Observer callbacks are functions of the following form: + + function callback(subject, data) { + /* Respond to the event notification here... */ + } + +In the above example, `subject` is any JavaScript object, as is +`data`. The particulars of what the two contain are specific +to the notification topic. + +<api name="add"> +@function + Adds an observer callback to be triggered whenever a notification matching the + topic is broadcast throughout the application. + +@param topic {string} + The topic to observe. + +@param callback {function,object} + Either a function or an object that implements [`nsIObserver`](http://mxr.mozilla.org/mozilla-central/source/xpcom/ds/nsIObserver.idl). + If a function, then it is called when the notification occurs. If an object, + then its `observe()` method is called when the notification occurs. + +@param [thisObject] {object} + An optional object to use as `this` when a function callback is called. +</api> + +<api name="remove"> +@function + Unsubscribes a callback from being triggered whenever a notification + matching the topic is broadcast throughout the application. + +@param topic {string} + The topic being observed by the previous call to `add()`. + +@param callback {function,object} + The callback subscribed in the previous call to `add()`, either a function or + object. + +@param [thisObject] {object} + If `thisObject` was passed to the previous call to `add()`, it should be + passed to `remove()` as well. +</api> + +<api name="notify"> +@function + Broadcasts a notification event for a topic, passing a subject and data to all + applicable observers in the application. + +@param topic {string} + The topic about which to broadcast a notification. + +@param [subject] {value} + Optional information about the topic. This can be any JS object or primitive. + If you have multiple values to pass to observers, wrap them in an object, + e.g., `{ foo: 1, bar: "some string", baz: myObject }`. +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/plain-text-console.md b/tools/addon-sdk-1.7/packages/api-utils/docs/plain-text-console.md new file mode 100644 index 0000000..d139587 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/plain-text-console.md @@ -0,0 +1,7 @@ +<!-- 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/. --> + +The `plain-text-console` module provides a minimalist implementation +of the [console](dev-guide/console.html) global, +which simply logs all messages to standard output. diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/preferences-service.md b/tools/addon-sdk-1.7/packages/api-utils/docs/preferences-service.md new file mode 100644 index 0000000..03b4725 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/preferences-service.md @@ -0,0 +1,116 @@ +<!-- 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/. --> + +<!-- contributed by Myk Melez [myk@mozilla.org] --> +<!-- contributed by Daniel Aquino [mr.danielaquino@gmail.com] --> +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `preferences-service` module provides access to the +application-wide preferences service singleton. + + +<api name="set"> +@function +Sets the application preference `name` to `value`. +@param name {string} Preference name. +@param value {string,number,bool} Preference value. + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + require("preferences-service").set(name, false); +</api> + + +<api name="get"> +@function +Gets the application preference `name`. +@param name {string} +@param defaultValue {string,number,bool} Preference value. +@returns {string,number,bool} Preference value, returns a default value if no +preference is set. + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + var nightlyCompatChk = require("preferences-service").get(name); +</api> + + +<api name="has"> +@function +@param name {string} Preference name. +@returns {bool} Returns whether or not the application preference `name` exists. + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + if (require("preferences-service").has(name)) { + // ... + } +</api> + + +<api name="isSet"> +@function +@param name {string} Preference name. +@returns {bool} +Returns whether or not the application preference `name` both exists +and has been set to a non-default value by the user (or a program +acting on the user's behalf). + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + if (require("preferences-service").isSet(name)) { + // ... + } +</api> + + +<api name="reset"> +@function +Clears a non-default, user-set value from the application preference +`name`. If no user-set value is defined on `name`, the function +does nothing. If no default value exists the preference will cease to exist. +@param name {string} Preference name. + +**Example:** + + var name = "extensions.checkCompatibility.nightly"; + require("preferences-service").reset(name); +</api> + +<api name="getLocalized"> +@function +Gets the localized value for an application preference `name`. +@param name {string} +@param defaultValue {string} Preference value. +@returns {string} Localized preference value, returns a default value if no +preference is set. Some preferences refer to a properties file. +So that `prefs.get` returns the properties file URL whereas +`prefs.getLocalized` returns the value defined in the properties file. + +**Example:** + + var prefs = require("preferences-service"); + var name = "general.useragent.locale"; + prefs.get(name); // is equal to "chrome://global/locale/intl.properties" + prefs.getLocalized(name) // is equal to "en-US" + +</api> + +<api name="setLocalized"> +@function +Sets the localized application preference `name` to `value`. +@param name {string} Preference name. +@param value {string} Preference value, a URL to a properties file + +**Example:** + + require("preferences-service").set("general.useragent.locale", + "chrome://global/locale/intl.properties"); + +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/promise.md b/tools/addon-sdk-1.7/packages/api-utils/docs/promise.md new file mode 100644 index 0000000..d67b820 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/promise.md @@ -0,0 +1,394 @@ +<!-- 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/. --> + +## Rationale + +Most of the JS APIs are asynchronous complementing it's non-blocking nature. +While this has a good reason and many advantages, it comes with a price. +Instead of structuring our programs into logical black boxes: + + function blackbox(a, b) { + var c = assemble(a); + return combine(b, c); + } + + +We're forced into continuation passing style, involving lot's of machinery: + + function sphagetti(a, b, callback) { + assemble(a, function continueWith(error, c) { + if (error) callback(error); + else combine(b, c, callback); + }); + } + +This style also makes doing things in sequence hard: + + widget.on('click', function onClick() { + promptUserForTwitterHandle(function continueWith(error, handle) { + if (error) return ui.displayError(error); + twitter.getTweetsFor(handle, funtion continueWith(error, tweets) { + if (error) return ui.displayError(error); + ui.showTweets(tweets); + }); + }); + }); + +Doing things in parallel is even harder: + + var tweets, answers, checkins; + twitter.getTweetsFor(user, function continueWith(result) { + tweets = result; + somethingFinished(); + }); + + stackOverflow.getAnswersFor(question, function continueWith(result) { + answers = result; + somethingFinished(); + }); + + fourSquare.getCheckinsBy(user, function continueWith(result) { + checkins=result; + somethingFinished(); + }); + + var finished = 0; + functions somethingFinished() { + if (++finished === 3) + ui.show(tweets, answers, checkins); + } + +This also makes error handling quite of an adventure. + +## Promises + +Consider another approach, where instead of continuation passing via `callback`, +function returns an object, that represents eventual result, either successful +or failed. This object is a promise, both figuratively and by name, to +eventually resolve. We can call a function on the promise to observe +either its fulfillment or rejection. If the promise is rejected and the +rejection is not explicitly observed, any derived promises will be implicitly +rejected for the same reason. + +In the Add-on SDK we follow +[CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) specification +and model a promise as an object with a `then` method, which can be used to get +the eventual return (fulfillment) value or thrown exception (rejection): + + foo().then(function success(value) { + // ... + }, function failure(reason) { + // ... + }); + +If `foo` returns a promise that gets fulfilled with the `value`, `success` +callback (the value handler) will be called with that `value`. However, +if the returned promise gets rejected, the `failure` callback (the error +handler) will be called with the `reason` of an error. + +## Propagation + +The `then` method of a promise returns a new promise that is resolved with the +return value of either handler. Since function can either return value or throw +an exception, only one handler will be ever called. + + + var bar = foo().then(function success(value) { + // compute something from a value... + }, function failure(reason) { + // handle an error... + }); + +In this example `bar` is a promise and it's fulfilled by one of two handlers +that are responsible for: + + - If handler returns a value, `bar` will be resolved with it. + - If handler throws an exception, `bar` will be rejected with it. + - If handler returns a **promise**, `bar` will "become" that promise. To be + more precise it will be resolved with a resolution value of the returned + promise, which will appear and feel as if it was that returned promise. + +If the `foo()` promise gets rejected and you omit the error handler, the +**error** will propagate to `bar` (`bar` will be rejected with that error): + + var bar = foo().then(function success(value) { + // compute something out of the value... + }); + +If the `foo()` promise gets fulfilled and you omit the value handler, the +**value** will propagate to `bar` (`bar` will be fulfilled with that value): + + var bar = foo().then(null, function failure(error) { + // handle error... + }); + + +## Chaining + +There are two ways to chain promise operations. You can chain them using either +inside or outside handlers. + +### Flat chaining + +You can use `then` for chaining intermediate operations on promises +(`var data = readAsync().then(parse).then(extract)`). You can chain multiple +`then` functions, because `then` returns a promise resolved to a return value +of an operation and errors propagate through the promise chains. In general +good rule of thumb is to prefer `then` based flat chaining. It makes code +easier to read and make changes later: + + var data = readAsync(url). // read content of url asynchronously + then(parse). // parse content from the url + then(extractQuery). // extract SQL query + then(readDBAsync); // exectue extracted query against DB + +### Nested chaining + +Flat chaining is not always an option though, as in some cases you may want to +capture an intermediate values of the chain: + + var result = readAsync(url).then(function(source) { + var json = parse(source) + return readDBAsync(extractQuery(json)).then(function(data) { + return writeAsync(json.url, data); + }); + }); + +In general, nesting is useful for computing values from more then one promise: + + function eventualAdd(a, b) { + return a.then(function (a) { + return b.then(function (b) { + return a + b; + }); + }); + } + + var c = eventualAdd(aAsync(), bAsync()); + +## Error handling + +One sometimes-unintuitive aspect of promises is that if you throw an exception +in the value handler, it will not be be caught by the error handler. + + readAsync(url).then(function (value) { + throw new Error("Can't bar."); + }, function (error) { + // We only get here if `readAsync` fails. + }); + +To see why this is, consider the parallel between promises and `try`/`catch`. +We are `try`-ing to execute `readAsync()`: the error handler represents a +`catch` for `readAsync()`, while the value handler represents code that happens +*after* the `try`/`catch` block. That code then needs its own `try`/`catch` +block to handle errors there. + +In terms of promises, this means chaining your error handler: + + readAsync(url). + then(parse). + then(null, function handleParseError(error) { + // handle here both `readAsync` and `parse` errors. + }); + + +# Consuming promises + +In general, whole purpose of promises is to avoid a callback spaghetti in the +code. As a matter of fact it would be great if we could convert any synchronous +functions to asynchronous by making it aware of promises. Module exports +`promised` function to do exactly that: + + const { promised } = require('api-utils/promise'); + function sum(x, y) { return x + y } + var sumAsync = promised(sum); + + var c = sum(a, b); + var cAsync = asyncSum(aAsync(), bAsinc()); + +`promised` takes normal function and composes new promise aware version of it +that may take both normal values and promises as arguments and returns promise +that will resolve to value that would have being returned by an original +function if it was called with fulfillment values of given arguments. + +This technique is so powerful that it can replace most of the promise utility +functions provided by other promise libraries. For example grouping promises +to observe single resolution of all of them is as simple as this: + + var group = promised(Array); + var abc = group(aAsync, bAsync, cAsync).then(function(items) { + return items[0] + items[1] + items[2]; + }); + +# Making promises + +Everything above assumes you get a promise from somewhere else. This +is the common case, but every once in a while, you will need to create a +promise from scratch. Add-on SDK's `promise` module provides API for doing +that. + +## defer + +Module exports `defer` function, which is where all promises ultimately +come from. Lets see implementation of `readAsync` that we used in lot's +of examples above: + + const { defer } = require('api-utils/promise'); + function readAsync(url) { + var deferred = defer(); + + let xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.onload = function() { + deferred.resolve(xhr.responseText); + } + xhr.onerror = function(event) { + deferred.reject(event); + } + xhr.send(); + + return deferred.promise; + } + +So `defer` returns an object that contains `promise` and two `resolve`, `reject` +functions that can be used to resolve / reject that `promise`. **Note:** that +promise can be rejected or resolved and only once. All subsequent attempts will be +ignored. + +Another simple example may be `delay` function that returns promise which +is fulfilled with a given `value` in a given `ms`, kind of promise based +alternative to `setTimeout`: + + function delay(ms, value) { + let { promise, resolve } = defer(); + setTimeout(resolve, ms, value); + return promise; + } + + delay(10, 'Hello world').then(console.log); + // After 10ms => 'Helo world' + +# Advanced usage + +If general `defer` and `promised` should be enough to doing almost anything +you may think of with promises, but once you start using promises extensively +you may discover some missing pieces and this section of documentation may +help you to discover them. + +## Doing things concurrently + +So far we have being playing with promises that do things sequentially, but +there are bunch of cases where one would need to do things concurrently. In the +following example we implement functions that takes multiple promises and +returns one that resolves to first on being fulfilled: + + function race() { + let { promise, resolve } = defer(); + Array.slice(arguments).forEach(function(promise) { + promise.then(resolve); + }); + return promise; + } + + var asyncAorB = race(readAsync(urlA), readAsync(urlB)); + +*Note: that this implementation forgives failures and would fail if all +promises fail to resolve.* + +There are cases when promise may or may not be fulfilled in a reasonable time. +In such cases it's useful to put a timer on such tasks: + + function timeout(promise, ms) { + let deferred = defer(); + promise.then(deferred.resolve, deferred.reject); + delay(ms, 'timeout').then(deferred.reject); + return deferred.promise; + } + + var tweets = readAsync(url); + timeout(tweets, 20).then(function(data) { + ui.display(data); + }, function() { + alert('Network is being too slow, try again later'); + }); + +## Alternative promise APIs + +There may be a cases where you will want to provide more than just `then` +method on your promises. In fact some other promise frameworks do that. +Such use cases are also supported. Earlier described `defer` may be passed +optional `prototype` argument, in order to make returned promise and all +the subsequent promises decedents of that `prototype`: + + let { promise, resolve } = defer({ + get: function get(name) { + return this.then(function(value) { + return value[name]; + }) + } + }); + + promise.get('foo').get('bar').then(console.log); + resolve({ foo: { bar: 'taram !!' } }); + + // => 'taram !!' + +Also `promised` function maybe passed second optional `prototype` argument to +achieve same effect. + +## Treat all values as promises + +Module provides a simple function for wrapping values into promises: + + const { resolve } = require('api-utils/promise'); + + var a = resolve(5).then(function(value) { + return value + 2 + }); + a.then(console.log); // => 7 + +Also `resolve` not only takes values, but also promises. If you pass it +a promise it will return new identical one: + + const { resolve } = require('api-utils/promise'); + + resolve(resolve(resolve(3))).then(console.log); // => 3 + +If this construct may look strange at first, but it becomes quite handy +when writing functions that deal with both promises and values. In such +cases it's usually easier to wrap value into promise than branch on value +type: + + function or(a, b) { + var second = resolve(b).then(function(bValue) { return !!bValue }); + return resolve(a).then(function(aValue) { + return !!aValue || second; + }, function() { + return second; + }) + } + +*Note: We could not use `promised` function here, as they reject returned +promise if any of the given arguments is rejected.* + +If you need to customize your promises even further you may pass `resolve` a +second optional `prototype` argument that will have same effect as with `defer`. + +## Treat errors as promises + +Now that we can create all kinds of eventual values, it's useful to have a +way to create eventual errors. Module exports `reject` exactly for that. +It takes anything as an argument and returns a promise that is rejected with +it. + + const { reject } = require('api-utils/promise'); + + var boom = reject(Error('boom!')); + + future(function() { + return Math.random() < 0.5 ? boom : value + }) + +As with rest of the APIs error may be given second optional `prototype` +argument to customize resulting promise to your needs. diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/querystring.md b/tools/addon-sdk-1.7/packages/api-utils/docs/querystring.md new file mode 100644 index 0000000..30f4117 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/querystring.md @@ -0,0 +1,37 @@ +<!-- 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/. --> + +Module exports utility functions for working with query strings. + +### stringify + +Object may be serialize to a query string via exported `stringify` function: + + querystring.stringify({ foo: 'bar', baz: 4 }); // => 'foo=bar&baz=4' + +Optionally `separator` and `assignment` arguments may be passed to +override default `'&'` and`'='` characters: + + querystring.stringify({ foo: 'bar', baz: 4 }, ';', ':'); // => 'foo:bar;baz:4' + +### parse + +Query string may be deserialized to an object via exported `parse` +function: + + querystring.parse('foo=bar&baz=bla') // => { foo: 'bar', baz: 'bla' } + +Optionally `separator` and `assignment` arguments may be passed to +override default `'&'` and `'='` characters: + + querystring.parse('foo:bar|baz:bla', '|', ':') // => { foo: 'bar', baz: 'bla' } + +### escape + +The escape function used by `stringify` to encodes a string safely +matching RFC 3986 for `application/x-www-form-urlencoded`. + +### unescape + +The unescape function used by `parse` to decode a string safely. diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/runtime.md b/tools/addon-sdk-1.7/packages/api-utils/docs/runtime.md new file mode 100644 index 0000000..c8d8ed3 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/runtime.md @@ -0,0 +1,75 @@ +<!-- 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/. --> + +<!-- contributed by Wes Kocher [kwierso@gmail.com] --> + +The `runtime` module provides access to information about Firefox's +runtime environment. All properties exposed are read-only. + +For more information, see [nsIXULRuntime][nsIXULRuntime]. +[nsIXULRuntime]: https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIXULRuntime + +<api name="inSafeMode"> +@property {boolean} + This value is `true` if Firefox was started in safe mode, + otherwise `false`. +</api> + +<api name="OS"> +@property {string} + A string identifying the current operating system. For example, . + `"WINNT"`, `"Darwin"`, or `"Linux"`. See [OS_TARGET][OS_TARGET] + for a more complete list of possible values. + +[OS_TARGET]: https://developer.mozilla.org/en/OS_TARGET +</api> + +<api name="processType"> +@property {long} + The type of the caller's process, which will be one of these constants\: +<table> + <tr> + <th>Constant</th> + <th>Value</th> + <th>Description</th> + </tr> + + <tr> + <td>PROCESS_TYPE_DEFAULT</td> + <td>0</td> + <td>The default (chrome) process.</td> + </tr> + + <tr> + <td>PROCESS_TYPE_PLUGIN</td> + <td>1</td> + <td>A plugin subprocess.</td> + </tr> + + <tr> + <td>PROCESS_TYPE_CONTENT</td> + <td>2</td> + <td>A content subprocess.</td> + </tr> + + <tr> + <td>PROCESS_TYPE_IPDLUNITTEST</td> + <td>3</td> + <td>An IPDL unit testing subprocess.</td> + </tr> +</table> +</api> + +<api name="widgetToolkit"> +@property {string} + A string identifying the target widget toolkit in use. +</api> + +<api name="XPCOMABI"> +@property {string} + A string identifying the [ABI][ABI] of the current processor and compiler vtable. + This string takes the form \<`processor`\>-\<`compilerABI`\>, + for example\: "`x86-msvc`" or "`ppc-gcc3`". +[ABI]: https://developer.mozilla.org/en/XPCOM_ABI +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/sandbox.md b/tools/addon-sdk-1.7/packages/api-utils/docs/sandbox.md new file mode 100644 index 0000000..026b569 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/sandbox.md @@ -0,0 +1,51 @@ +<!-- 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/. --> + +Provides an API for creating javascript sandboxes and for executing scripts +in them. + +### Create a sandbox ### + +For the starting point you need to create a sandbox: + + const { sandbox, evaluate, load } = require("api-utils/sandbox"); + let scope = sandbox('http://example.com'); + +Argument passed to the sandbox defines it's privileges. Argument may be an URL +string, in which case sandbox will get exact same privileges as a scripts +loaded from that URL. Argument also could be a DOM window object, to inherit +privileges from the window being passed. Finally if argument is omitted or is +`null` sandbox will have a chrome privileges giving it access to all the XPCOM +components. Optionally `sandbox` function can be passed a second optional +argument (See [sandbox documentation on MDN](https://developer.mozilla.org/en/Components.utils.Sandbox#Optional_parameter) +for details). + +### Evaluate code ### + +Module provides `evaluate` function that allows executing code in the given +sandbox: + + evaluate(scope, 'var a = 5;'); + evaluate(scope, 'a + 2;'); //=> 7 + +More details about evaluated script may be passed via optional arguments that +may improve an exception reporting: + + // Evaluate code as if it was loaded from 'http://foo.com/bar.js' and + // start from 2nd line. + evaluate(scope, 'a ++', 'http://foo.com/bar.js', 2); + +Version of JavaScript can be also specified via optional argument: + + evaluate(scope, 'let b = 2;', 'bar.js', 1, '1.5'); + // throws cause `let` is not defined in JS 1.5. + +### Loading scripts ### + +API provides limited API for loading scripts right form the local URLs, +but data: URLs are supported. + + load(scope, 'resource://path/to/my/script.js'); + load(scope, 'file:///path/to/script.js'); + load(scope, 'data:,var a = 5;');
\ No newline at end of file diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/tab-browser.md b/tools/addon-sdk-1.7/packages/api-utils/docs/tab-browser.md new file mode 100644 index 0000000..295fcc7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/tab-browser.md @@ -0,0 +1,140 @@ +<!-- 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/. --> + +<!-- contributed by Dietrich Ayala [dietrich@mozilla.com] --> + +The `tab-browser` module is a low-level API that provides privileged +access to browser tab events and actions. + +Introduction +------------ + +The `tab-browser` module contains helpers for tracking tabbrowser elements +and tabs, as well as a few utilities for actions such as opening a new +tab, and catching all tab content loads. + +This is a low-level API that has full privileges, and is intended to be used +by SDK internal modules. If you just need easy access to tab events for your +add-on, use the Tabs module (JEP 110). + +<api name="activeTab"> +@property {element} +The XUL tab element of the currently active tab. +</api> + +<api name="addTab"> +@function +Adds a new tab. + +**Example** + + var tabBrowser = require("tab-browser"); + tabBrowser.addTab("http://google.com"); + + var tabBrowser = require("tab-browser"); + tabBrowser.addTab("http://google.com", { + inBackground: true + }); + + var tabBrowser = require("tab-browser"); + tabBrowser.addTab("http://google.com", { + inNewWindow: true, + onLoad: function(tab) { + console.log("tab is open."); + } + }); + +@returns {element} +The XUL tab element of the newly created tab. + +@param URL {string} +The URL to be opened in the new tab. + +@param options {object} +Options for how and where to open the new tab. + +@prop [inNewWindow] {boolean} +An optional parameter whose key can be set in `options`. +If true, the tab is opened in a new window. Default is false. + +@prop [inBackground] {boolean} +An optional parameter whose key can be set in `options`. +If true, the tab is opened adjacent to the active tab, but not +switched to. Default is false. + +@prop [onLoad] {function} +An optional parameter whose key can be set in `options`. +A callback function that is called once the tab has loaded. +The XUL element for the tab is passed as a parameter to +this function. +</api> + +<api name="Tracker"> +@function +Register a delegate object to be notified when tabbrowsers are created +and destroyed. + +The onTrack method will be called once per pre-existing tabbrowser, upon +tracker registration. + +**Example** + + var tabBrowser = require("tab-browser"); + let tracker = { + onTrack: function(tabbrowser) { + console.log("A new tabbrowser is being tracked."); + }, + onUntrack: function(tabbrowser) { + console.log("A tabbrowser is no longer being tracked."); + } + }; + tabBrowser.Tracker(tracker); + +@param delegate {object} +Delegate object to be notified each time a tabbrowser is created or destroyed. +The object should contain the following methods: + +@prop [onTrack] {function} +Method of delegate that is called when a new tabbrowser starts to be tracked. +The tabbrowser element is passed as a parameter to this method. + +@prop [onUntrack] {function} +Method of delegate that is called when a tabbrowser stops being tracked. +The tabbrowser element is passed as a parameter to this method. +</api> + +<api name="TabTracker"> +@function +Register a delegate object to be notified when tabs are opened and closed. + + +The onTrack method will be called once per pre-existing tab, upon +tracker registration. + +**Example** + + var tabBrowser = require("tab-browser"); + let tracker = { + onTrack: function(tab) { + console.log("A new tab is being tracked."); + }, + onUntrack: function(tab) { + console.log("A tab is no longer being tracked."); + } + }; + tabBrowser.TabTracker(tracker); + +@param delegate {object} +Delegate object to be notified each time a tab is opened or closed. +The object should contain the following methods: + +@prop [onTrack] {function} +Method of delegate that is called when a new tab starts to be tracked. +The tab element is passed as a parameter to this method. + +@prop [onUntrack] {function} +Method of delegate that is called when a tab stops being tracked. +The tab element is passed as a parameter to this method. +</api> + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/text-streams.md b/tools/addon-sdk-1.7/packages/api-utils/docs/text-streams.md new file mode 100644 index 0000000..55f177c --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/text-streams.md @@ -0,0 +1,102 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `text-streams` module provides streams for reading and writing text using +particular character encodings. + +<api name="TextReader"> +@class +<api name="TextReader"> +@constructor + Creates a buffered input stream that reads text from a backing stream using a + given text encoding. +@param inputStream {stream} + The backing stream, an [`nsIInputStream`](http://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsIInputStream.idl). + It must already be opened. +@param [charset] {string} + `inputStream` is expected to be in the character encoding named by this value. + If not specified, "UTF-8" is assumed. See [`nsICharsetConverterManager.idl`](http://mxr.mozilla.org/mozilla-central/source/intl/uconv/idl/nsICharsetConverterManager.idl) + for documentation on how to determine other valid values for this. +</api> + +<api name="closed"> +@property {boolean} + True if the stream is closed. +</api> + +<api name="close"> +@method + Closes both the stream and its backing stream. +</api> + +<api name="read"> +@method + Reads and returns a string from the stream. If the stream is closed, an + exception is thrown. +@param [numChars] {number} + The number of characters to read. If not given, the remainder of the stream + is read. +@returns {string} + The string read. If the stream is at the end, the empty string is returned. +</api> + +</api> + + +<api name="TextWriter"> +@class +<api name="TextWriter"> +@constructor + Creates a buffered output stream that writes text to a backing stream using a + given text encoding. +@param outputStream {stream} + The backing stream, an [`nsIOutputStream`](http://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsIOutputStream.idl). + It must already be opened. +@param [charset] {string} + Text will be written to `outputStream` using the character encoding named by + this value. If not specified, "UTF-8" is assumed. See [`nsICharsetConverterManager.idl`](http://mxr.mozilla.org/mozilla-central/source/intl/uconv/idl/nsICharsetConverterManager.idl) + for documentation on how to determine other valid values for this. +</api> + +<api name="closed"> +@property {boolean} + True if the stream is closed. +</api> + +<api name="close"> +@method + Flushes the backing stream's buffer and closes both the stream and the backing + stream. If the stream is already closed, an exception is thrown. +</api> + +<api name="flush"> +@method + Flushes the backing stream's buffer. +</api> + +<api name="write"> +@method + Writes a string to the stream. If the stream is closed, an exception is + thrown. +@param str {string} + The string to write. +</api> + +<api name="writeAsync"> +@method + Writes a string on a background thread. After the write completes, the + backing stream's buffer is flushed, and both the stream and the backing stream + are closed, also on the background thread. If the stream is already closed, + an exception is thrown immediately. +@param str {string} + The string to write. +@param [callback] {callback} + *`callback`*, if given, must be a function. It's called as `callback(error)` + when the write completes. `error` is an `Error` object or undefined if there + was no error. Inside *`callback`*, `this` is the `TextWriter` object. +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/traceback.md b/tools/addon-sdk-1.7/packages/api-utils/docs/traceback.md new file mode 100644 index 0000000..b52c183 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/traceback.md @@ -0,0 +1,66 @@ +<!-- 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/. --> + +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + + +The `traceback` module contains functionality similar to +Python's [traceback](http://docs.python.org/library/traceback.html) module. + +## JSON Traceback Objects ## + +Tracebacks are stored in JSON format. The stack is represented as an +array in which the most recent stack frame is the last element; each +element thus represents a stack frame and has the following keys: + +<table> + <tr> + <td><code>filename</code></td> + <td>The name of the file that the stack frame takes place in.</td> + </tr> + <tr> + <td><code>lineNo</code></td> + <td>The line number is being executed at the stack frame.</td> + </tr> + <tr> + <td><code>funcName</code></td> + <td>The name of the function being executed at the stack frame, or + <code>null</code> if the function is anonymous or the stack frame is + being executed in a top-level script or module.</td> + </tr> +</table> + +<api name="fromException"> +@function + Attempts to extract the traceback from *`exception`*. + +@returns {traceback} + JSON representation of the traceback or `null` if not found. + +@param exception {exception} + exception where exception is an `nsIException`. +</api> + +See [nsIException](https://developer.mozilla.org/en/NsIException) for more +information. + +<api name="get"> +@function + +@returns {JSON} + Returns the JSON representation of the stack at the point that this + function is called. +</api> + +<api name="format"> +@function +Given a JSON representation of the stack or an exception instance, +returns a formatted plain text representation of it, similar to +Python's formatted stack tracebacks. If no argument is provided, the +stack at the point this function is called is used. + +@param [tbOrException] {object} +</api> + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/traits.md b/tools/addon-sdk-1.7/packages/api-utils/docs/traits.md new file mode 100644 index 0000000..910cd9a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/traits.md @@ -0,0 +1,244 @@ +<!-- 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/. --> + +<!-- contributed by Irakli Gozalishvil [gozala@mozilla.com] --> + +The `traits` module provides base building blocks for secure object +composition. It exports base trait / constructor function that +constructs an instance of `Trait`. + +[Traits](http://en.wikipedia.org/wiki/Trait_%28computer_science%29) are a +simple composition mechanism for structuring object-oriented programs. Traits +are similar to +[interfaces](http://en.wikipedia.org/wiki/Interface_%28object-oriented_programming%29), +except that they often define only a part of an object's data and behavior and +are intended to be used in conjunction with other traits to completely define +the object. + +Traits are also considered to be a more robust alternative to +[mixins](http://en.wikipedia.org/wiki/Mixins) because, name conflicts have to +be resolved explicitly by composer & because trait composition is +order-independent (hence more declarative). + + +There are some other implementations of traits in JavaScript & some ideas / +APIs are borrowed from them: + +- [traitsjs](http://www.traitsjs.org/) +- [joose](http://code.google.com/p/joose-js/) + +Object-capability security model +-------------------------------- + +Implementation uses an +[object-capability security model](http://en.wikipedia.org/wiki/Object-capability_model) +to allow protection of private APIs. At the same private APIs can be shared +between among trait composition parties. To put it simply: All the properties +whose names start with `"_"` are considered to be **private**, and are +unaccessible from anywhere except other **public** methods / accessors of the +instance that had been defined during composition. + +<api name="Trait"> +@class +<api name="Trait"> +@constructor +Creates an instance of Trait and returns it if it has no `constructor` method +defined. If instance has `constructor` method, then it is called with all the +arguments passed to this function and returned value is returned instead, +unless it's `undefined`. In that case instance is returned. + +`Trait` function represents a base trait. As with any other trait it represents +a constructor function for creating instances of its own & a placeholder +for a trait compositions functions. +</api> + +<api name="compose"> +@method +Composes new trait out of itself and traits / property maps passed as an +arguments. If two or more traits / property maps have properties with the same +name, the new trait will contain a "conflict" property for that name (see +examples in Examples section to find out more about "conflict" properties). +This is a commutative and associative operation, and the order of its +arguments is not significant. + +**Examples:** + +Let's say we want to define a reusable piece of code for a lists of elements. + + var { Trait } = require('traits'); + var List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.splice(index, 1); + } + }); + +Instances of `List` can be created by calling `List` function with or without +`new` keyword. + + let l1 = List(); + l1 instanceof List; // true + let l2 = new List(); + l2 instanceof List; // true + +As you can see `add` and `remove` functions are capable of accessing private +`_list` property, but thats about it, there's nothing else that will be able +to access this property: + + '_list' in l1; // false + '_list' in l2; // false + '_list' in List.protoype; // false + l1.has = function(name) name in this + l1.has('_list'); // false + l1.length; // 0 + l1.add('test') + l1.length // 1 + +@param trait1 {Object|Function} + Trait or property map to compose new trait from. +@param trait2 {Object|Function} + Trait or property map to compose new trait from. +@param ... {Object|Function} + Traits or property maps to compose new trait from. + +@returns {Function} + New trait containing the combined properties of all the traits. +</api> + +<api name="required"> +@property {Object} +Singleton, used during trait composition to define "required" properties. + +**Example:** + + var Enumerable = Trait.compose({ + list: Trait.required, + forEach: function forEach(consumer) { + return this.list.forEach(consumer); + } + }); + + let c1 = Enumerable(); // Error: Missing required property: list + + var EnumerableList = List.compose({ + get list() this._list.slice(0) + }, Enumerable); + + let c2 = EnumerableList(); + c2.add('test') + c2.length // 1 + c2.list[0] // 'test' + c2.forEach(console.log) // > info: 'test 0 test' + +</api> + + +<api name="resolve"> +@method +Composes a new trait that has all the same properties +as the trait on which it is called, except that each property listed +in the `resolutions` argument will be renamed from the name +of the property in the `resolutions` argument to its value. +And if its value is `null`, the property will become required. + +**Example:** + + var Range = List.resolve({ + constructor: null, + add: '_add', + }).compose({ + min: null, + max: null, + get list() this._list.slice(0), + constructor: function Range(min, max) { + this.min = min; + this.max = max; + this._list = []; + }, + add: function(item) { + if (item <= this.max && item >= this.min) + this._add(item) + } + }); + + + let r = Range(0, 10); + r.min; // 0 + r.max; // 10 + r.length; // 0; + r.add(5); + r.length; // 1 + r.add(12); + r.length; // 1 (12 was not in a range) + +@param resolutions {Object} +@returns {Function} + New resolved trait. +</api> + +<api name="override"> +@method +Composes a new trait with all of the combined properties of `this` and the +argument traits. In contrast to `compose`, `override` immediately resolves +all conflicts resulting from this composition by overriding the properties of +later traits. Trait priority is from left to right. I.e. the properties of +the leftmost trait are never overridden. + +**Example:** + + // will compose trait with conflict property 'constructor' + var ConstructableList = List.compose({ + constructor: function List() this._list = Array.slice(arguments) + }); + // throws error with message 'Remaining conflicting property: constructor' + ConstructableList(1, 2, 3); + + var ConstructableList = List.override({ + constructor: function List() this._list = Array.slice(arguments) + }); + ConstructableList(1, 2, 3).length // 3 + +@param trait1 {Object|Function} + Trait or property map to compose new trait from. +@param trait2 {Object|Function} + Trait or property map to compose new trait from. +@param ... {Object|Function} + Traits or property maps to compose new trait from. + +@returns {Function} + New trait containing the combined properties of all the traits. +</api> + +<api name="_public"> +@property {Object} +Internal property of instance representing public API that is exposed to the +consumers of an instance. +</api> + +<api name='toString'> +@method +Textual representation of an object. All the traits will return: +`'[object Trait]'` string, unless they have `constructor` property, in that +case string `'Trait'` is replaced with the name of `constructor` property. + +**Example:** + + var MyTrait = Trait.compose({ + constructor: function MyTrait() { + // do your initialization here + } + }); + MyTrait().toString(); // [object MyTrait] + +</api> +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/unit-test.md b/tools/addon-sdk-1.7/packages/api-utils/docs/unit-test.md new file mode 100644 index 0000000..6962c41 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/unit-test.md @@ -0,0 +1,393 @@ +<!-- 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/. --> + +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> +<!-- edited by Shane Tomlinson[stomlinson@mozilla.com] --> + +The `unit-test` module makes it easy to find and run unit tests. + +<api name="test"> +@class +Each function which represents a test case is passed a single argument +`test`, which represents the test runner. + +<api name="pass"> +@method + Marks a test as passing, with the given optional message. + +@param [message] {string} + Optional passing message. +</api> + + +<api name="fail"> +@method + Marks a test as failing, with the given optional message. + +@param [message] {string} + Optional failure message. +</api> + +<api name="expectFail"> +@method + *experimental* Expect the test enclosed within `func` to fail. + +@param func {function} + A function that should contain a test that is expected to fail. +</api> + +<api name="exception"> +@method + Marks a test as failing due to the given exception having been thrown. + This can be put in a `catch` clause. + +@param e {exception} + An exception. +</api> + +<api name="assert"> +@method + Ensures that `a` has a truthy value. + +@param a {value} + Value to verify. +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="assertEqual"> +@method + Ensures that `a == b` without recursing into `a` or `b`. + +@param a {value} + A value. + +@param b {value} + Another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + +<api name="assertNotEqual"> +@method + Ensures that `a != b` without recursing into `a` or `b`. + +@param a {value} + A value. + +@param b {value} + Another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + +<api name="assertStrictEqual"> +@method + Ensures that `a === b` without recursing into `a` or `b`. + +@param a {value} + A value. + +@param b {value} + Another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="assertNotStrictEqual"> +@method + Ensures that `a !== b` without recursing into `a` or `b`. + +@param a {value} + A value. + +@param b {value} + Another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + +<api name="assertMatches"> +@method + Ensures that the given string matches the given regular expression. + If it does, marks a test as passing, otherwise marks a test as + failing. + +@param string {string} + The string to test. + +@param regexp {regexp} + The string should match this regular expression. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="assertRaises"> +@method + Calls the function `func` with no arguments, expecting an exception + to be raised. If nothing is raised, marks the test as failing. If an + exception is raised, the exception's `message` property is + compared with `predicate`: if `predicate` is a string, then a + simple equality comparison is done with `message`. Otherwise, + if `predicate` is a regular expression, `message` is tested + against it. + +@param func {function} + A function that should raise an exception when called. + +@param predicate {string,regexp} + A string or regular expression to compare to the exception's message. + +@param [message] {string} + Depending on the outcome, a test is marked as passing or failing, and + *message* is logged. +</api> + + +<api name="assertFunction"> +@method + Ensures that `a` is a function. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertUndefined"> +@method + Ensures that `a` is `undefined`. `null`, `0`, and `false` will all fail. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertNotUndefined"> +@method + Ensures that `a` is not `undefined`. `null`, `0`, and `false` will all pass. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertNull"> +@method + Ensures that `a` is `null`. `undefined`, `0`, and `false` will all fail. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertNotNull"> +@method + Ensures that `a` is not `null`. `undefined`, `0`, and `false` will all pass. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertObject"> +@method + Ensures that `a` is an object. A function, string, or number will fail. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertString"> +@method + Ensures that `a` is a string. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertArray"> +@method + Ensures that `a` is an array. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="assertNumber"> +@method + Ensures that `a` is a number. + +@param a {value} + A value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. + +</api> + + +<api name="waitUntilDone"> +@method + Puts the test runner into asynchronous testing mode, waiting up to + *timeout* milliseconds for `test.done()` to be called. This + is intended for use in situations where a test suite schedules a + callback, calls `test.waitUntilDone()`, and then calls + `test.done()` in the callback. + +@param [timeout] {integer} + If this number of milliseconds elapses and `test.done()` has not yet been + called, the test is marked as failing. +</api> + + +<api name="done"> +@method + Marks a test as being complete. Assumes a previous call to + `test.waitUntilDone()`. +</api> + +</api> + + +<api name="waitUntil"> +@method + Ensures that `a` returns a truthy value within a reasonable amount of time. + +@param a {function} + Function that returns the value to verify. +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="waitUntilEqual"> +@method + Ensures that `a == b` returned values or values without without recursing + into `a` or `b`. + +@param a {Function} + A value, or a function that returns a value. + +@param b {value} + Another value, or a function that returns value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + +<api name="waitUntilNotEqual"> +@method + Ensures that `a != b` without recursing into `a` or `b`. + +@param a {Function} + A value, or a function that returns a value. + +@param b {value} + Another value, or a function that returns another value. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + +<api name="waitUntilMatches"> +@method + Ensures that the given string matches the given regular expression. + If it does, marks the test as passing, otherwise marks the test as + failing. + +@param string {Function} + A function that returns the string to test. + +@param regexp {regexp} + The string should match this regular expression. + +@param [message] {string} + The test is marked as passing or failing depending on the result, logging + *message* with it. +</api> + + + +<api name="findAndRunTests"> +@function + The list of directories is searched for SecurableModules that start + with the prefix `test-`. Each module matching this criteria is + expected to export functions that are test cases or a suite of test + cases; each is called with a single argument, which is a Test Runner + Object. + +@param options {object} + An object with the following properties: + @prop dirs {string} + A list of absolute paths representing directories to search + for tests in. It's assumed that all of these directories are also + in the module search path, i.e. any JS files found in them are + SecurableModules that can be loaded via a call to + `require()`. + @prop onDone {function} + A function to call when testing is complete. +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/unload.md b/tools/addon-sdk-1.7/packages/api-utils/docs/unload.md new file mode 100644 index 0000000..8a76b3f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/unload.md @@ -0,0 +1,61 @@ +<!-- 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/. --> + +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `unload` module allows modules to register callbacks that are called +when they are unloaded. It is similar to the CommonJS module of the same +name in the [Narwhal][] platform. + +[Narwhal]: http://narwhaljs.org/ + +<api name="ensure"> +@function + Calling `ensure()` on an object does two things: + + 1. It replaces a destructor method with a wrapper method that will never call + the destructor more than once. + 2. It ensures that this wrapper method is called when `send()` is + called. + + Therefore, when you register an object with `ensure()`, you can call its + destructor method yourself, you can let it happen for you, or you can do both. + + The destructor will be called with a single argument describing the reason + for the unload; see `when()`. If `object` does not have the expected + destructor method, then an exception is thrown when `ensure()` is called. + +@param object {object} + An object that defines a destructor method. +@param [name] {string} + Optional name of the destructor method. Default is `unload`. +</api> + +<api name="when"> +@function + Registers a function to be called when `send()` is called. + +@param callback {function} + A function that will be called when `send()` is called. It is called with a + single argument, one of the following strings describing the reason for + unload: `"uninstall"`, `"disable"`, `"shutdown"`, `"upgrade"`, or + `"downgrade"`. (On Gecko 1.9.2-based applications such as Firefox 3.6, + `"upgrade"` and `"downgrade"` are not available, and `"shutdown"` will be sent + in their place.) If a reason could not be determined, `undefined` will be + passed instead. Note that if an add-on is unloaded with reason `"disable"`, + it will not be notified about `"uninstall"` while it is disabled. A solution + to this issue is being investigated; see bug 571049. +</api> + +<api name="send"> +@function + Sends an "unload signal", thereby triggering all callbacks registered via + `when()`. In general, this function need not be manually called; it is + automatically triggered by the embedder. + +@param [reason] {string} + An optional string describing the reason for unload; see `unload.when()`. +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/url.md b/tools/addon-sdk-1.7/packages/api-utils/docs/url.md new file mode 100644 index 0000000..2974797 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/url.md @@ -0,0 +1,85 @@ +<!-- 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/. --> + +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + + +The `url` module provides functionality for the parsing and retrieving of URLs. + +<api name="URL"> +@class +<api name="URL"> +@constructor + The URL constructor creates an object that represents a URL, verifying that + the provided string is a valid URL in the process. Any API in the SDK which + has a URL parameter will accept `URL` objects, not raw strings, unless + otherwise noted. + +@param source {string} + A string to be converted into a URL. If `source` is not a valid URI, this + constructor will throw an exception. + +@param [base] {string} + An optional string used to resolve relative `source` URLs into absolute ones. +</api> + +<api name="scheme"> +@property {string} + The name of the protocol in the URL. +</api> + +<api name="userPass"> +@property {string} + The username:password part of the URL, `null` if not present. +</api> + +<api name="host"> +@property {string} + The host of the URL, `null` if not present. +</api> + +<api name="port"> +@property {integer} + The port number of the URL, `null` if none was specified. +</api> + +<api name="path"> +@property {string} + The path component of the URL. +</api> + +<api name="toString"> +@method + Returns a string representation of the URL. +@returns {string} + The URL as a string. +</api> +</api> + +<api name="toFilename"> +@function + Attempts to convert the given URL to a native file path. This function will + automatically attempt to resolve non-file protocols, such as the `resource:` + protocol, to their place on the file system. An exception is raised if the URL + can't be converted; otherwise, the native file path is returned as a string. + +@param url {string} + The URL, as a string, to be converted. + +@returns {string} + The converted native file path as a string. +</api> + +<api name="fromFilename"> +@function + Converts the given native file path to a `file:` URL. + +@param path {string} + The native file path, as a string, to be converted. + +@returns {string} + The converted URL as a string. +</api> + diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/uuid.md b/tools/addon-sdk-1.7/packages/api-utils/docs/uuid.md new file mode 100644 index 0000000..429e3a0 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/uuid.md @@ -0,0 +1,27 @@ +<!-- 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/. --> + +Module `uuid` provides low level API for generating / parsing UUID, that may +be necessary when hacking on internals of the platform. + + +## Generate UUID + +Module exports `uuid` function. When called without arguments it will uses +platform-specific methods to obtain a `nsID` that can be considered to be +globally unique. + + let uuid = require('api-utils/uuid').uuid() + +## Parsing UUID + +Sometimes one might need to create `nsID` from an existing UUID string. Same +`uuid` function may be used to parse such UUID strings into an `nsID`: + + let { uuid } = require('api-utils/uuid'); + let firefoxUUID = uuid('{ec8030f7-c20a-464f-9b0e-13a3a9e97384}'); + +For more details about UUID representations and what they are used for by the +platform see MDN documentation for +[JSID](https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIJSID) diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/window-utils.md b/tools/addon-sdk-1.7/packages/api-utils/docs/window-utils.md new file mode 100644 index 0000000..1435fd2 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/window-utils.md @@ -0,0 +1,88 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> +<!-- edited by Erik Vold [erikvvold@gmail.com] --> + +The `window-utils` module provides helpers for accessing and tracking +application windows. These windows implement the [`nsIDOMWindow`][nsIDOMWindow] +interface. + +[nsIDOMWindow]: http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIDOMWindow.idl + +<api name="WindowTracker"> +@class +`WindowTracker` objects make it easy to "monkeypatch" windows when a program is +loaded and "un-monkeypatch" those windows when the program is unloaded. For +example, if a Firefox add-on needs to add a status bar icon to all browser +windows, it can use a single `WindowTracker` object to gain access to windows +when they are opened and closed and also when the add-on is loaded and unloaded. + +When a window is opened or closed, a `WindowTracker` notifies its delegate +object, which is passed to the constructor. The delegate is also notified of +all windows that are open at the time that the `WindowTracker` is created and +all windows that are open at the time that the `WindowTracker` is unloaded. The +caller can therefore use the same code to act on all windows, regardless of +whether they are currently open or are opened in the future, or whether they are +closed while the parent program is loaded or remain open when the program is +unloaded. + +When a window is opened or when a window is open at the time that the +`WindowTracker` is created, the delegate's `onTrack()` method is called and +passed the window. + +When a window is closed or when a window is open at the time that the +`WindowTracker` is unloaded, the delegate's `onUntrack()` method is called and +passed the window. (The `WindowTracker` is unloaded when its its `unload()` +method is called, or when its parent program is unloaded, disabled, or +uninstalled, whichever comes first.) + +**Example** + + var delegate = { + onTrack: function (window) { + console.log("Tracking a window: " + window.location); + // Modify the window! + }, + onUntrack: function (window) { + console.log("Untracking a window: " + window.location); + // Undo your modifications! + } + }; + var winUtils = require("window-utils"); + var tracker = new winUtils.WindowTracker(delegate); + +<api name="WindowTracker"> +@constructor + A `WindowTracker` object listens for openings and closings of application + windows. +@param delegate {object} + An object that implements `onTrack()` and `onUntrack()` methods. +@prop onTrack {function} + A function to be called when a window is open or loads, with the window as the + first and only argument. +@prop [onUntrack] {function} + A function to be called when a window unloads, with the window as the first + and only argument. +</api> +</api> + +<api name="windowIterator"> +@function + An iterator for windows currently open in the application. + +**Example** + + var winUtils = require("window-utils"); + for (window in winUtils.windowIterator()) + console.log("An open window! " + window.location); + +</api> + +<api name="closeOnUnload"> +@function + Marks an application window to be closed when the program is unloaded. +@param window {window} + The window to close. +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/window/utils.md b/tools/addon-sdk-1.7/packages/api-utils/docs/window/utils.md new file mode 100644 index 0000000..276a17a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/window/utils.md @@ -0,0 +1,90 @@ +<!-- 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/. --> + +The `window/utils` module provides helper functions for working with +application windows. + +### getInnerId + +Returns the ID of the given window's current inner window. + +### getOuterId + +Returns the ID of the given window's outer window. + +### getXULWindow + +Module provides `getXULWindow` function that can be used get access +[nsIXULWindow](https://developer.mozilla.org/en/nsIDOMWindow) for the given +[nsIDOMWindow](https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIXULWindow): + + let { Ci } = require('chrome'); + let utils = require('api-utils/window-utils'); + let active = utils.activeBrowserWindow; + active instanceof Ci.nsIXULWindow // => false + utils.getXULWindow(active) instanceof Ci.nsIXULWindow // => true + +### getBaseWindow + +Module provides `getBaseWindow` function that can be used get access +[nsIBaseWindow](http://mxr.mozilla.org/mozilla-central/source/widget/nsIBaseWindow.idl) +for the given [nsIDOMWindow](https://developer.mozilla.org/en/nsIDOMWindow): + + let { Ci } = require('chrome'); + let utils = require('api-utils/window-utils'); + let active = utils.activeBrowserWindow; + active instanceof Ci.nsIBaseWindow // => false + utils.getBaseWindow(active) instanceof Ci.nsIBaseWindow // => true + +### open + +Module exports `open` function that may be used to open top level +(application) windows. Function takes `uri` of the window document as a first +argument and optional hash of `options` as second argument. + + let { open } = require('api-utils/window-utils'); + let window = open('data:text/html,Hello Window'); + +Following options may be provided to configure created window behavior: + +- `parent` +If provided must be `nsIDOMWindow` and will be used as parent for the created +window. + +- `name` +Optional name that will be assigned to the window. + +- `features` +Hash of options that will be serialized to features string. See +[features documentation](https://developer.mozilla.org/en/DOM/window.open#Position_and_size_features) +for more details. + + let { open } = require('api-utils/window/utils'); + let window = open('data:text/html,Hello Window', { + name: 'jetpack window', + features: { + width: 200, + height: 50, + popup: true + } + }); + +### backgroundify + +Module exports `backgroundify` function that takes `nsIDOMWindow` and +removes it from the application's window registry, so that they won't appear +in the OS specific window lists for the application. + + let { backgroundify, open } = require('api-utils/window/utils'); + let bgwin = backgroundify(open('data:text/html,Hello backgroundy')); + +Optionally more configuration options via second `options` argument. If +`options.close` is `false` unregistered window won't automatically +be closed on application quit, preventing application from quitting. While this +is possible you should make sure to close all such windows manually: + + let { backgroundify, open } = require('api-utils/window-utils'); + let bgwin = backgroundify(open('data:text/html,Foo'), { + close: false + }); diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/xhr.md b/tools/addon-sdk-1.7/packages/api-utils/docs/xhr.md new file mode 100644 index 0000000..d0381c0 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/xhr.md @@ -0,0 +1,95 @@ +<!-- 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/. --> + +<!-- contributed by Atul Varma [atul@mozilla.com] --> +<!-- edited by Noelle Murata [fiveinchpixie@gmail.com] --> + +The `xhr` module provides access to `XMLHttpRequest` functionality, also known +as AJAX. + +## Limitations ## + +The `XMLHttpRequest` object is currently fairly limited, and does not +yet implement the `addEventListener()` or `removeEventListener()` +methods. It also doesn't yet implement the `upload` property. + +Furthermore, the `XMLHttpRequest` object does not currently support +the `mozBackgroundRequest` property. All security UI, such as +username/password prompts, are automatically suppressed, so if +required authentication information isn't passed to the `open()` +method, the request will fail. + +## Resource Use ## + +Whenever this module is unloaded, all in-progress requests are immediately +aborted. + +## Security Concerns ## + +By default, the `XMLHttpRequest` object grants full access to any +protocol scheme, which means that it can be used to read from (but not +write to) the host system's entire filesystem. It also has unfettered +access to any local area networks, VPNs, and the internet. + +### Threat Model ### + +The `XMLHttpRequest` object can be used by an add-on to "phone +home" and transmit potentially sensitive user data to third +parties. + +If access to the filesystem isn't prevented, it could easily be used +to access sensitive user data, though this may be inconsequential if +the client can't access the network. + +If access to local area networks isn't prevented, malicious code could access +sensitive data. + +If transmission of cookies isn't prevented, malicious code could access +sensitive data. + +Attenuating access based on a regular expression may be ineffective if +it's easy to write a regular expression that *looks* safe but contains +a special character or two that makes it far less secure than it seems +at first glance. + +### Possible Attenuations ### + +<span class="aside"> +We may also want to consider attenuating further based on domain name +and possibly even restricting the protocol to `https:` only, to reduce +risk. +</span> + +Before being exposed to unprivileged code, this object needs +to be attenuated in such a way that, at the very least, it can't +access the user's filesystem. This can probably be done most securely +by white-listing the protocols that can be used in the URL passed to +the `open()` method, and limiting them to `http:`, `https:`, and +possibly a special scheme that can be used to access the add-on's +packaged, read-only resources. + +Finally, we need to also consider attenuating http/https requests such +that they're "sandboxed" and don't communicate potentially sensitive +cookie information. + +<api name="XMLHttpRequest"> +@class + +<api name="XMLHttpRequest"> +@constructor + Creates an `XMLHttpRequest`. This is a constructor, so its use should always + be preceded by the `new` operator. For more information about + `XMLHttpRequest` objects, see the MDC page on + [Using XMLHttpRequest](https://developer.mozilla.org/En/Using_XMLHttpRequest) + and the Limitations section in this page. +</api> +</api> + +<api name="getRequestCount"> +@function + Returns the number of `XMLHttpRequest` objects that are alive (i.e., currently + active or about to be). +@returns {integer} + The number of live `XMLHttpRequest` objects. +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/xpcom.md b/tools/addon-sdk-1.7/packages/api-utils/docs/xpcom.md new file mode 100644 index 0000000..6d96650 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/xpcom.md @@ -0,0 +1,216 @@ +<!-- 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/. --> + +Module `xpcom` provides low level API for implementing and registering / +unregistering various XCOM interfaces. + +## Implementing XPCOM interfaces + +Module exports `Unknow` exemplar object, that may be extended to implement +specific XCOM interface(s). For example [nsIObserver] +(https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIObserver) may be +implemented as follows: + + const { Unknown } = require('api-utils/xpcom'); + const { Cc, Ci } = require('chrome') + const observerService = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + + // Create my observer exemplar + const SleepObserver = Unknown.extend({ + interfaces: [ 'nsIObserver' ], // Interfaces component implements + topic: 'sleep_notification', + initialize: function(fn) { this.fn = fn }, + register: function register() { + observerService.addObserver(this, this.topic, false); + }, + unregister: function() { + addObserver.removeObserver(this, this.topic, false); + }, + observe: function observe(subject, topic, data) { + this.fn({ + subject: subject, + type: topic, + data: data + }); + } + }); + + // create instances of observers + let observer = SleepObserver.new(function(event) { + console.log('Going to sleep!') + }); + // register observer + observer.register(); + +## Implementing XCOM factories + +Module exports `Factory` exemplar, object that may be used to create objects +implementing +[nsIFactory](https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIFactory) +interface: + + const { Factory } = require('api-utils/xpcom'); + let Request = Unknown.extend({ + interfaces: [ 'nsIRequest' ], + initialize: function initialize() { + this.pending = false; + // Do some initialization + }, + isPending: function() { return this.pending }, + resume: function() { /* Implementation */ }, + suspend: function() { /* Implementation */ }, + cancel: function() { /* Implementation */ }, + initiate: function() { + this.pending = true; + /* Implementation */ + } + }); + + let requestFactory = Factory.new({ component: Request }); + +Factories registered into a runtime may be accessed from the rest of the +application via standard XPCOM API using factory's auto generated `id` +(optionally you could specify specific `id` by passing it as an option): + + let request = Components.classesByID[requestFactory.id]. + createInstance(Ci.nsIRequest); + request.isPending() // => false + +Be aware that traditional XPCOM API will always return a wrapped JS objects +exposing only properties defined by a given interface (`nsIRequest`) in our +case: + + request.initiate() // TypeError: request.initiate is not a function + +You still can expose unwrapped JS object, by a special `wrappedJSObject` +property of the component: + + let Request = Unknown.extend({ + get wrappedJSObject() this, + interfaces: [ 'nsIRequest' ], + initialize: function initialize() { + this.pending = false; + // Do some initialization + }, + isPending: function() { return this.pending }, + resume: function() { /* Implementation */ }, + suspend: function() { /* Implementation */ }, + cancel: function() { /* Implementation */ }, + initiate: function() { + this.pending = true; + /* Implementation */ + } + }); + + let requestFactory = Factory.new({ component: Request }); + let request = Components.classesByID[requestFactory.id]. + createInstance(Ci.nsIRequest); + request.isPending(); // => false + request.wrappedJSObject.initiate(); + request.isPending(); // => true + +Optionally `Factory.new` may be passed globally unique string in a format of: +`'@domain.com/unique/identifier;1'` as a `contract` option in order to +associate it with it: + + let namedFactory = Factory.new({ + contract: '@examples.com/request/factory;1', + component: Request + }); + +Such factories when registered can be accessed form rest of the application by +human readable `contract` strings: + + let request = Components.classes['@examples.com/request/factory;1']. + createInstance(Components.interfaces.nsIRequest); + +In addition factories associated with a given `contract` may be replaced at +runtime: + + let renewedFactory = Factory.new({ + contract: '@examples.com/request/factory;1', + component: Unknown.extend({ /* Implementation */ }) + }) + +Unfortunately commonly used `Components.classes` won't get updated at runtime +but there is an alternative, more verbose way to access last registered factory +for a given `contract`: + + let id = Components.manager.QueryInterface(Ci.nsIComponentRegistrar). + contractIDToCID('@examples.com/request/factory;1'); + Components.classesByID[requestFactory.id]. + createInstance(Ci.nsISupports); + +Module also exports `factoryByContract` function to simplify this: + + factoryByContract('@examples.com/request/factory;1'). + createInstance(Ci.nsISupports); + +It's also recommended to construct factories with an optional `description` +property, providing human readable description of it: + + let factory = Factory.new({ + contract: '@examples.com/request/factory;1', + description: 'Super duper request factory', + component: Request + }); + +## Registering / Unregistering factories + +All factories created using `Factory.new` get automatically registered into +runtime unless explicitly specified otherwise by setting `register` option to +`false`: + + var factoryToRegister = Factory.new({ + register: false, + component: Unknown.extend({ /* Implementation */ }) + }); + +Such factories still may be registered manually using exported `register` +function: + + const { register } = require('api-utils/xpcom'); + register(factoryToRegister); + +All factories created using `Factory.new` also get unregistered automatically +when add-on is unloaded. This also can be disabled by setting `unregister` +option to `false`. + + var factoryToUnregister = Service.new({ + unregister: false, + component: Unknown.extend({ /* Implementation */ }) + }); + +All registered services may be unregistered at any time using exported +`unregister` function: + + unregister(factoryToUnregister); + +## Implementing XCOM services + +Module exports `Service` exemplar object, that has exact same API as `Factory` +and can be used to register services: + + const { Service } = require('api-utils/xpcom'); + let service = Service.new({ + contract: '@examples/demo/service;1', + description: 'My demo service', + component: Unknown.extend({ + // Implementation + get wrappedJSObject() this + }) + }); + +Registered services can be accessed through the rest of the application via +standard XPCOM API: + + let s = Components.classes['@examples/demo/service;1']. + getService(Components.interfaces.nsISupports); + +In contrast to factories, services do not create instances of enclosed +components, they expose component itself. Also please note that idiomatic way +to work with a service is via `getService` method: + + s.wrappedJSObject === service.component // => true diff --git a/tools/addon-sdk-1.7/packages/api-utils/docs/xul-app.md b/tools/addon-sdk-1.7/packages/api-utils/docs/xul-app.md new file mode 100644 index 0000000..597af56 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/docs/xul-app.md @@ -0,0 +1,76 @@ +<!-- 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/. --> + +<!-- contributed by Drew Willcoxon [adw@mozilla.com] --> + +The `xul-app` module provides facilities for introspecting the application on +which your program is running. + +With the exception of `ids`, each of these properties exposes the attribute of +the same name on the [`nsIXULAppInfo`][nsIXULAppInfo] interface. For more +information, see the [MDC documentation][]. + +[nsIXULAppInfo]: http://mxr.mozilla.org/mozilla-central/source/xpcom/system/nsIXULAppInfo.idl +[MDC documentation]: https://developer.mozilla.org/en/nsIXULAppInfo + +<api name="ID"> +@property {string} + The GUID of the host application. For example, for Firefox this value is + `"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"`. +</api> + +<api name="name"> +@property {string} + The host application name. For example, `"Firefox"`. +</api> + +<api name="version"> +@property {string} + The host application version. +</api> + +<api name="platformVersion"> +@property {string} + The Gecko/XULRunner platform version. +</api> + +<api name="ids"> +@property {object} + A mapping of application names to their IDs. For example, + `ids["Firefox"] == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"`. +</api> + +<api name="is"> +@function + Checks whether the host application is the given application. +@param name {string} + A host application name. +@returns {boolean} + True if the host application is `name` and false otherwise. +</api> + +<api name="isOneOf"> +@function + Checks whether the host application is one of the given applications. +@param names {array} + An array of host application names. +@returns {boolean} + True if the host application is one of the `names` and false otherwise. +</api> + +<api name="versionInRange"> +@function + Compares a given version to a version range. See the [MDC documentation](https://developer.mozilla.org/en/Toolkit_version_format#Comparing_versions) + for details on version comparisons. +@param version {string} + The version to compare. +@param lowInclusive {string} + The lower bound of the version range to compare. The range includes this + bound. +@param highExclusive {string} + The upper bound of the version range to compare. The range does not include + this bound. +@returns {boolean} + True if `version` falls in the given range and false otherwise. +</api> diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/api-utils.js b/tools/addon-sdk-1.7/packages/api-utils/lib/api-utils.js new file mode 100644 index 0000000..94d13b7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/api-utils.js @@ -0,0 +1,153 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 memory = require("api-utils/memory"); +// The possible return values of getTypeOf. +const VALID_TYPES = [ + "array", + "boolean", + "function", + "null", + "number", + "object", + "string", + "undefined", +]; + +/** + * Returns a function C that creates instances of privateCtor. C may be called + * with or without the new keyword. The prototype of each instance returned + * from C is C.prototype, and C.prototype is an object whose prototype is + * privateCtor.prototype. Instances returned from C will therefore be instances + * of both C and privateCtor. Additionally, the constructor of each instance + * returned from C is C. + * + * @param privateCtor + * A constructor. + * @return A function that makes new instances of privateCtor. + */ +exports.publicConstructor = function publicConstructor(privateCtor) { + function PublicCtor() { + let obj = { constructor: PublicCtor, __proto__: PublicCtor.prototype }; + memory.track(obj, privateCtor.name); + privateCtor.apply(obj, arguments); + return obj; + } + PublicCtor.prototype = { __proto__: privateCtor.prototype }; + return PublicCtor; +}; + +/** + * Returns a validated options dictionary given some requirements. If any of + * the requirements are not met, an exception is thrown. + * + * @param options + * An object, the options dictionary to validate. It's not modified. + * If it's null or otherwise falsey, an empty object is assumed. + * @param requirements + * An object whose keys are the expected keys in options. Any key in + * options that is not present in requirements is ignored. Each value + * in requirements is itself an object describing the requirements of + * its key. There are four optional keys in this object: + * map: A function that's passed the value of the key in options. + * map's return value is taken as the key's value in the final + * validated options, is, and ok. If map throws an exception + * it's caught and discarded, and the key's value is its value in + * options. + * is: An array containing any number of the typeof type names. If + * the key's value is none of these types, it fails validation. + * Arrays and null are identified by the special type names + * "array" and "null"; "object" will not match either. No type + * coercion is done. + * ok: A function that's passed the key's value. If it returns + * false, the value fails validation. + * msg: If the key's value fails validation, an exception is thrown. + * This string will be used as its message. If undefined, a + * generic message is used, unless is is defined, in which case + * the message will state that the value needs to be one of the + * given types. + * @return An object whose keys are those keys in requirements that are also in + * options and whose values are the corresponding return values of map + * or the corresponding values in options. Note that any keys not + * shared by both requirements and options are not in the returned + * object. + */ +exports.validateOptions = function validateOptions(options, requirements) { + options = options || {}; + let validatedOptions = {}; + let mapThrew = false; + + for (let [key, req] in Iterator(requirements)) { + let [optsVal, keyInOpts] = (key in options) ? + [options[key], true] : + [undefined, false]; + if (req.map) { + try { + optsVal = req.map(optsVal); + } + catch (err) { + mapThrew = true; + } + } + if (req.is) { + // Sanity check the caller's type names. + req.is.forEach(function (typ) { + if (VALID_TYPES.indexOf(typ) < 0) { + let msg = 'Internal error: invalid requirement type "' + typ + '".'; + throw new Error(msg); + } + }); + if (req.is.indexOf(getTypeOf(optsVal)) < 0) + throw requirementError(key, req); + } + if (req.ok && !req.ok(optsVal)) + throw requirementError(key, req); + + if (keyInOpts || (req.map && !mapThrew)) + validatedOptions[key] = optsVal; + } + + return validatedOptions; +}; + +exports.addIterator = function addIterator(obj, keysValsGenerator) { + obj.__iterator__ = function(keysOnly, keysVals) { + let keysValsIterator = keysValsGenerator.call(this); + + // "for (.. in ..)" gets only keys, "for each (.. in ..)" gets values, + // and "for (.. in Iterator(..))" gets [key, value] pairs. + let index = keysOnly ? 0 : 1; + while (true) + yield keysVals ? keysValsIterator.next() : keysValsIterator.next()[index]; + }; +}; + +// Similar to typeof, except arrays and null are identified by "array" and +// "null", not "object". +let getTypeOf = exports.getTypeOf = function getTypeOf(val) { + let typ = typeof(val); + if (typ === "object") { + if (!val) + return "null"; + if (Array.isArray(val)) + return "array"; + } + return typ; +} + +// Returns a new Error with a nice message. +function requirementError(key, requirement) { + let msg = requirement.msg; + if (!msg) { + msg = 'The option "' + key + '" '; + msg += requirement.is ? + "must be one of the following types: " + requirement.is.join(", ") : + "is invalid."; + } + return new Error(msg); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/app-strings.js b/tools/addon-sdk-1.7/packages/api-utils/lib/app-strings.js new file mode 100644 index 0000000..c03ae7c --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/app-strings.js @@ -0,0 +1,63 @@ +/* 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 apiUtils = require("./api-utils"); + +/** + * A bundle of strings. + * + * @param url {String} + * the URL of the string bundle + */ +exports.StringBundle = apiUtils.publicConstructor(function StringBundle(url) { + + let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(url); + + this.__defineGetter__("url", function () url); + + /** + * Get a string from the bundle. + * + * @param name {String} + * the name of the string to get + * @param args {array} [optional] + * an array of arguments that replace occurrences of %S in the string + * + * @returns {String} the value of the string + */ + this.get = function strings_get(name, args) { + try { + if (args) + return stringBundle.formatStringFromName(name, args, args.length); + else + return stringBundle.GetStringFromName(name); + } + catch(ex) { + // f.e. "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) + // [nsIStringBundle.GetStringFromName]" + throw new Error("String '" + name + "' could not be retrieved from the " + + "bundle due to an unknown error (it doesn't exist?)."); + } + }, + + /** + * Iterate the strings in the bundle. + * + */ + apiUtils.addIterator( + this, + function keysValsGen() { + let enumerator = stringBundle.getSimpleEnumeration(); + while (enumerator.hasMoreElements()) { + let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + yield [elem.key, elem.value]; + } + } + ); +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/array.js b/tools/addon-sdk-1.7/packages/api-utils/lib/array.js new file mode 100644 index 0000000..dadcfa6 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/array.js @@ -0,0 +1,69 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +/** + * Returns `true` if given `array` contain given `element` or `false` + * otherwise. + * @param {Array} array + * Target array. + * @param {Object|String|Number|Boolean} element + * Element being looked up. + * @returns {Boolean} + */ +var has = exports.has = function has(array, element) { + // shorter and faster equivalent of `array.indexOf(element) >= 0` + return !!~array.indexOf(element); +}; + +/** + * Adds given `element` to the given `array` if it does not contain it yet. + * `true` is returned if element was added otherwise `false` is returned. + * @param {Array} array + * Target array. + * @param {Object|String|Number|Boolean} element + * Element to be added. + * @returns {Boolean} + */ +var add = exports.add = function add(array, element) { + var result; + if ((result = !has(array, element))) + array.push(element); + + return result; +}; + +/** + * Removes first occurrence of the given `element` from the given `array`. If + * `array` does not contain given `element` `false` is returned otherwise + * `true` is returned. + * @param {Array} array + * Target array. + * @param {Object|String|Number|Boolean} element + * Element to be removed. + * @returns {Boolean} + */ +exports.remove = function remove(array, element) { + var result; + if ((result = has(array, element))) + array.splice(array.indexOf(element), 1); + + return result; +}; + +/** + * Produces a duplicate-free version of the given `array`. + * @param {Array} array + * Source array. + * @returns {Array} + */ +exports.unique = function unique(array) { + var value = []; + return array.forEach(function(element) { + add(value, element); + }); + return value; +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/base.js b/tools/addon-sdk-1.7/packages/api-utils/lib/base.js new file mode 100644 index 0000000..63acabc --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/base.js @@ -0,0 +1,177 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* 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"; + +// Instead of inheriting from `Object.prototype` we copy all interesting +// properties from it and then freeze. This way we can guarantee integrity +// of components build on top. +exports.Base = Object.freeze(Object.create(null, { + toString: { value: Object.prototype.toString }, + toLocaleString: { value: Object.prototype.toLocaleString }, + toSource: { value: Object.prototype.toSource }, + valueOf: { value: Object.prototype.valueOf }, + isPrototypeOf: { value: Object.prototype.isPrototypeOf }, + /** + * Creates an object that inherits from `this` object (Analog of + * `new Object()`). + * @examples + * + * var Dog = Base.extend({ + * bark: function bark() { + * return 'Ruff! Ruff!' + * } + * }); + * var dog = Dog.new(); + */ + new: { value: function create() { + var object = Object.create(this); + object.initialize.apply(object, arguments); + return object; + }}, + /** + * When new instance of the this prototype is created it's `initialize` + * method is called with all the arguments passed to the `new`. You can + * override `initialize` to set up an instance. + */ + initialize: { value: function initialize() { + }}, + /** + * Merges all the properties of the passed objects into `this` instance (This + * method can be used on instances only as prototype objects are frozen). + * + * If two or more argument objects have own properties with the same name, + * the property is overridden, with precedence from right to left, implying, + * that properties of the object on the left are overridden by a same named + * property of the object on the right. + * + * @examples + * + * var Pet = Dog.extend({ + * initialize: function initialize(options) { + * // this.name = options.name -> would have thrown (frozen prototype) + * this.merge(options) // will override all properties. + * }, + * call: function(name) { + * return this.name === name ? this.bark() : '' + * }, + * name: null + * }) + * var pet = Pet.new({ name: 'Benzy', breed: 'Labrador' }) + * pet.call('Benzy') // 'Ruff! Ruff!' + */ + merge: { value: function merge() { + var descriptor = {}; + Array.prototype.forEach.call(arguments, function (properties) { + Object.getOwnPropertyNames(properties).forEach(function(name) { + descriptor[name] = Object.getOwnPropertyDescriptor(properties, name); + }); + }); + Object.defineProperties(this, descriptor); + return this; + }}, + /** + * Takes any number of argument objects and returns frozen, composite object + * that inherits from `this` object and combines all of the own properties of + * the argument objects. (Objects returned by this function are frozen as + * they are intended to be used as types). + * + * If two or more argument objects have own properties with the same name, + * the property is overridden, with precedence from right to left, implying, + * that properties of the object on the left are overridden by a same named + * property of the object on the right. + * @examples + * + * // ## Object composition ## + * + * var HEX = Base.extend({ + * hex: function hex() { + * return '#' + this.color; + * } + * }) + * + * var RGB = Base.extend({ + * red: function red() { + * return parseInt(this.color.substr(0, 2), 16); + * }, + * green: function green() { + * return parseInt(this.color.substr(2, 2), 16); + * }, + * blue: function blue() { + * return parseInt(this.color.substr(4, 2), 16); + * } + * }) + * + * var CMYK = Base.extend(RGB, { + * black: function black() { + * var color = Math.max(Math.max(this.red(), this.green()), this.blue()); + * return (1 - color / 255).toFixed(4); + * }, + * cyan: function cyan() { + * var K = this.black(); + * return (((1 - this.red() / 255).toFixed(4) - K) / (1 - K)).toFixed(4); + * }, + * magenta: function magenta() { + * var K = this.black(); + * return (((1 - this.green() / 255).toFixed(4) - K) / (1 - K)).toFixed(4); + * }, + * yellow: function yellow() { + * var K = this.black(); + * return (((1 - this.blue() / 255).toFixed(4) - K) / (1 - K)).toFixed(4); + * } + * }) + * + * var Color = Base.extend(HEX, RGB, CMYK, { + * initialize: function Color(color) { + * this.color = color; + * } + * }); + * + * // ## Prototypal inheritance ## + * + * var Pixel = Color.extend({ + * initialize: function Pixel(x, y, hex) { + * Color.initialize.call(this, hex); + * this.x = x; + * this.y = y; + * }, + * toString: function toString() { + * return this.x + ':' + this.y + '@' + this.hex(); + * } + * }); + * + * var pixel = Pixel.new(11, 23, 'CC3399') + * pixel.toString(); // 11:23@#CC3399 + * + * pixel.red(); // 204 + * pixel.green(); // 51 + * pixel.blue(); // 153 + * + * pixel.cyan(); // 0.0000 + * pixel.magenta(); // 0.7500 + * pixel.yellow(); // 0.2500 + * + */ + extend: { value: function extend() { + return Object.freeze(this.merge.apply(Object.create(this), arguments)); + }} +})); + +/** + * Function takes prototype object that implements `initialize` method, and + * returns `constructor` function (with correct prototype property), that can + * be used for simulating classes for given prototypes. + */ +exports.Class = Object.freeze(function Class(prototype) { + function constructor() { + var instance = Object.create(prototype); + prototype.initialize.apply(instance, arguments); + return instance; + } + return Object.freeze(Object.defineProperties(constructor, { + prototype: { value: prototype }, + new: { value: constructor } + })); +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/byte-streams.js b/tools/addon-sdk-1.7/packages/api-utils/lib/byte-streams.js new file mode 100644 index 0000000..edc8407 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/byte-streams.js @@ -0,0 +1,102 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * 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"; + +exports.ByteReader = ByteReader; +exports.ByteWriter = ByteWriter; + +const {Cc, Ci} = require("chrome"); + +// This just controls the maximum number of bytes we read in at one time. +const BUFFER_BYTE_LEN = 0x8000; + +function ByteReader(inputStream) { + const self = this; + + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(inputStream); + + let manager = new StreamManager(this, stream); + + this.read = function ByteReader_read(numBytes) { + manager.ensureOpened(); + if (typeof(numBytes) !== "number") + numBytes = Infinity; + + let data = ""; + let read = 0; + try { + while (true) { + let avail = stream.available(); + let toRead = Math.min(numBytes - read, avail, BUFFER_BYTE_LEN); + if (toRead <= 0) + break; + data += stream.readBytes(toRead); + read += toRead; + } + } + catch (err) { + throw new Error("Error reading from stream: " + err); + } + + return data; + }; +} + +function ByteWriter(outputStream) { + const self = this; + + let stream = Cc["@mozilla.org/binaryoutputstream;1"]. + createInstance(Ci.nsIBinaryOutputStream); + stream.setOutputStream(outputStream); + + let manager = new StreamManager(this, stream); + + this.write = function ByteWriter_write(str) { + manager.ensureOpened(); + try { + stream.writeBytes(str, str.length); + } + catch (err) { + throw new Error("Error writing to stream: " + err); + } + }; +} + + +// This manages the lifetime of stream, a ByteReader or ByteWriter. It defines +// closed and close() on stream and registers an unload listener that closes +// rawStream if it's still opened. It also provides ensureOpened(), which +// throws an exception if the stream is closed. +function StreamManager(stream, rawStream) { + const self = this; + this.rawStream = rawStream; + this.opened = true; + + stream.__defineGetter__("closed", function stream_closed() { + return !self.opened; + }); + + stream.close = function stream_close() { + self.ensureOpened(); + self.unload(); + }; + + require("./unload").ensure(this); +} + +StreamManager.prototype = { + ensureOpened: function StreamManager_ensureOpened() { + if (!this.opened) + throw new Error("The stream is closed and cannot be used."); + }, + unload: function StreamManager_unload() { + this.rawStream.close(); + this.opened = false; + } +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/channel.js b/tools/addon-sdk-1.7/packages/api-utils/lib/channel.js new file mode 100644 index 0000000..a888af8 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/channel.js @@ -0,0 +1,46 @@ +/* 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/. */ + +const { jetpackID } = require('@packaging'); +const { when } = require('./unload'); + +// TODO: Create a bug report and remove this workaround once it's fixed. +// Only function needs defined in the context of the message manager window +// can be registered via `addMessageListener`. +function listener(callee) { + return function listener() { return callee.apply(this, arguments); }; +} +function messageListener(scope, callee) { + return scope ? scope.eval('(' + listener + ')')(callee) : callee +} + +exports.channel = function channel(scope, messageManager, address, raw) { + address = jetpackID + ':' + address + return { + input: function input(next, stop) { + let listener = messageListener(scope, function onMessage(message) { + if (false === next(raw ? message : message.json) && listener) { + messageManager.removeMessageListener(address, listener); + listener = null; + } + }); + messageManager.addMessageListener(address, listener); + + // Bug 724433: do not leak `listener` on addon disabling + when(function () { + if (listener) { + messageManager.removeMessageListener(address, listener); + listener = null; + } + }); + }, + output: function output(data) { + messageManager.sendAsyncMessage(address, data); + }, + sync: !messageManager.sendSyncMessage ? null : function sync(data) { + messageManager.sendSyncMessage(address, data); + } + }; +}; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/collection.js b/tools/addon-sdk-1.7/packages/api-utils/lib/collection.js new file mode 100644 index 0000000..b9dd352 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/collection.js @@ -0,0 +1,108 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +exports.Collection = Collection; + +/** + * Adds a collection property to the given object. Setting the property to a + * scalar value empties the collection and adds the value. Setting it to an + * array empties the collection and adds all the items in the array. + * + * @param obj + * The property will be defined on this object. + * @param propName + * The name of the property. + * @param array + * If given, this will be used as the collection's backing array. + */ +exports.addCollectionProperty = function addCollProperty(obj, propName, array) { + array = array || []; + let publicIface = new Collection(array); + + obj.__defineSetter__(propName, function (itemOrItems) { + array.splice(0, array.length); + publicIface.add(itemOrItems); + }); + + obj.__defineGetter__(propName, function () { + return publicIface; + }); +}; + +/** + * A collection is ordered, like an array, but its items are unique, like a set. + * + * @param array + * The collection is backed by an array. If this is given, it will be + * used as the backing array. This way the caller can fully control the + * collection. Otherwise a new empty array will be used, and no one but + * the collection will have access to it. + */ +function Collection(array) { + array = array || []; + + /** + * Provides iteration over the collection. Items are yielded in the order + * they were added. + */ + this.__iterator__ = function Collection___iterator__() { + let items = array.slice(); + for (let i = 0; i < items.length; i++) + yield items[i]; + }; + + /** + * The number of items in the collection. + */ + this.__defineGetter__("length", function Collection_get_length() { + return array.length; + }); + + /** + * Adds a single item or an array of items to the collection. Any items + * already contained in the collection are ignored. + * + * @param itemOrItems + * An item or array of items. + * @return The collection. + */ + this.add = function Collection_add(itemOrItems) { + let items = toArray(itemOrItems); + for (let i = 0; i < items.length; i++) { + let item = items[i]; + if (array.indexOf(item) < 0) + array.push(item); + } + return this; + }; + + /** + * Removes a single item or an array of items from the collection. Any items + * not contained in the collection are ignored. + * + * @param itemOrItems + * An item or array of items. + * @return The collection. + */ + this.remove = function Collection_remove(itemOrItems) { + let items = toArray(itemOrItems); + for (let i = 0; i < items.length; i++) { + let idx = array.indexOf(items[i]); + if (idx >= 0) + array.splice(idx, 1); + } + return this; + }; +}; + +function toArray(itemOrItems) { + let isArr = itemOrItems && + itemOrItems.constructor && + itemOrItems.constructor.name === "Array"; + return isArr ? itemOrItems : [itemOrItems]; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/content.js b/tools/addon-sdk-1.7/packages/api-utils/lib/content.js new file mode 100644 index 0000000..a4acb94 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/content.js @@ -0,0 +1,11 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +exports.Loader = require('./content/loader').Loader; +exports.Symbiont = require('./content/symbiont').Symbiont; +exports.Worker = require('./content/worker').Worker; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/content/loader.js b/tools/addon-sdk-1.7/packages/api-utils/lib/content/loader.js new file mode 100644 index 0000000..62c3ad0 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/content/loader.js @@ -0,0 +1,179 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { EventEmitter } = require('../events'); +const { validateOptions } = require('../api-utils'); +const { URL } = require('../url'); +const file = require('../file'); + +const LOCAL_URI_SCHEMES = ['resource', 'data']; + +// Returns `null` if `value` is `null` or `undefined`, otherwise `value`. +function ensureNull(value) { + return value == null ? null : value; +} + +// map of property validations +const valid = { + contentURL: { + ok: function (value) { + try { + URL(value); + } + catch(e) { + return false; + } + return true; + }, + msg: 'The `contentURL` option must be a valid URL.' + }, + contentScriptFile: { + is: ['undefined', 'null', 'string', 'array'], + map: ensureNull, + ok: function(value) { + if (value === null) + return true; + + value = [].concat(value); + + // Make sure every item is a local file URL. + return value.every(function (item) { + try { + return ~LOCAL_URI_SCHEMES.indexOf(URL(item).scheme); + } + catch(e) { + return false; + } + }); + + }, + msg: 'The `contentScriptFile` option must be a local URL or an array of URLs.' + }, + contentScript: { + is: ['undefined', 'null', 'string', 'array'], + map: ensureNull, + ok: function(value) { + return !Array.isArray(value) || value.every( + function(item) { return typeof item === 'string' } + ); + }, + msg: 'The `contentScript` option must be a string or an array of strings.' + }, + contentScriptWhen: { + is: ['string'], + ok: function(value) { return ~['start', 'ready', 'end'].indexOf(value) }, + map: function(value) { + return value || 'end'; + }, + msg: 'The `contentScriptWhen` option must be either "start", "ready" or "end".' + } +}; +exports.validationAttributes = valid; + +/** + * Shortcut function to validate property with validation. + * @param {Object|Number|String} suspect + * value to validate + * @param {Object} validation + * validation rule passed to `api-utils` + */ +function validate(suspect, validation) validateOptions( + { $: suspect }, + { $: validation } +).$ + +function Allow(script) ({ + get script() script, + set script(value) script = !!value +}) + +/** + * Trait is intended to be used in some composition. It provides set of core + * properties and bounded validations to them. Trait is useful for all the + * compositions providing high level APIs for interaction with content. + * Property changes emit `"propertyChange"` events on instances. + */ +const Loader = EventEmitter.compose({ + /** + * Permissions for the content, with the following keys: + * @property {Object} [allow = { script: true }] + * @property {Boolean} [allow.script = true] + * Whether or not to execute script in the content. Defaults to true. + */ + get allow() this._allow || (this._allow = Allow(true)), + set allow(value) this.allow.script = value && value.script, + _allow: null, + /** + * The content to load. Either a string of HTML or a URL. + * @type {String} + */ + get contentURL() this._contentURL, + set contentURL(value) { + value = validate(value, valid.contentURL); + if (this._contentURL != value) { + this._emit('propertyChange', { + contentURL: this._contentURL = value + }); + } + }, + _contentURL: null, + /** + * When to load the content scripts. + * Possible values are "end" (default), which loads them once all page + * contents have been loaded, "ready", which loads them once DOM nodes are + * ready (ie like DOMContentLoaded event), and "start", which loads them once + * the `window` object for the page has been created, but before any scripts + * specified by the page have been loaded. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {'start'|'ready'|'end'} + */ + get contentScriptWhen() this._contentScriptWhen, + set contentScriptWhen(value) { + value = validate(value, valid.contentScriptWhen); + if (value !== this._contentScriptWhen) { + this._emit('propertyChange', { + contentScriptWhen: this._contentScriptWhen = value + }); + } + }, + _contentScriptWhen: 'end', + /** + * The URLs of content scripts. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {String[]} + */ + get contentScriptFile() this._contentScriptFile, + set contentScriptFile(value) { + value = validate(value, valid.contentScriptFile); + if (value != this._contentScriptFile) { + this._emit('propertyChange', { + contentScriptFile: this._contentScriptFile = value + }); + } + }, + _contentScriptFile: null, + /** + * The texts of content script. + * Property change emits `propertyChange` event on instance with this key + * and new value. + * @type {String|undefined} + */ + get contentScript() this._contentScript, + set contentScript(value) { + value = validate(value, valid.contentScript); + if (value != this._contentScript) { + this._emit('propertyChange', { + contentScript: this._contentScript = value + }); + } + }, + _contentScript: null +}); +exports.Loader = Loader; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/content/symbiont.js b/tools/addon-sdk-1.7/packages/api-utils/lib/content/symbiont.js new file mode 100644 index 0000000..e7c959c --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/content/symbiont.js @@ -0,0 +1,183 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { Worker } = require('./worker'); +const { Loader } = require('./loader'); +const hiddenFrames = require('../hidden-frame'); +const observers = require('../observer-service'); +const unload = require('../unload'); + +/** + * This trait is layered on top of `Worker` and in contrast to symbiont + * Worker constructor requires `content` option that represents content + * that will be loaded in the provided frame, if frame is not provided + * Worker will create hidden one. + */ +const Symbiont = Worker.resolve({ + constructor: '_initWorker', + destroy: '_workerDestroy' + }).compose(Loader, { + + /** + * The constructor requires all the options that are required by + * `require('content').Worker` with the difference that the `frame` option + * is optional. If `frame` is not provided, `contentURL` is expected. + * @param {Object} options + * @param {String} options.contentURL + * URL of a content to load into `this._frame` and create worker for. + * @param {Element} [options.frame] + * iframe element that is used to load `options.contentURL` into. + * if frame is not provided hidden iframe will be created. + */ + constructor: function Symbiont(options) { + options = options || {}; + + if ('contentURL' in options) + this.contentURL = options.contentURL; + if ('contentScriptWhen' in options) + this.contentScriptWhen = options.contentScriptWhen; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('allow' in options) + this.allow = options.allow; + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + if ('frame' in options) { + this._initFrame(options.frame); + } + else { + let self = this; + this._hiddenFrame = hiddenFrames.HiddenFrame({ + onReady: function onFrame() { + self._initFrame(this.element); + } + }); + hiddenFrames.add(this._hiddenFrame); + } + + unload.ensure(this._public, "destroy"); + }, + + destroy: function destroy() { + this._workerDestroy(); + this._unregisterListener(); + this._frame = null; + if (this._hiddenFrame) { + hiddenFrames.remove(this._hiddenFrame); + this._hiddenFrame = null; + } + }, + + /** + * XUL iframe or browser elements with attribute `type` being `content`. + * Used to create `ContentSymbiont` from. + * @type {nsIFrame|nsIBrowser} + */ + _frame: null, + + /** + * Listener to the `'frameReady"` event (emitted when `iframe` is ready). + * Removes listener, sets right permissions to the frame and loads content. + */ + _initFrame: function _initFrame(frame) { + if (this._loadListener) + this._unregisterListener(); + + this._frame = frame; + frame.docShell.allowJavascript = this.allow.script; + frame.setAttribute("src", this._contentURL); + + // Inject `addon` object in document if we load a document from + // one of our addon folder and if no content script are defined. bug 612726 + let isDataResource = + typeof this._contentURL == "string" && + this._contentURL.indexOf(require("@packaging").uriPrefix) == 0; + let hasContentScript = + (Array.isArray(this.contentScript) ? this.contentScript.length > 0 + : !!this.contentScript) || + (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0 + : !!this.contentScriptFile); + // If we have to inject `addon` we have to do it before document + // script execution, so during `start`: + this._injectInDocument = isDataResource && !hasContentScript; + if (this._injectInDocument) + this.contentScriptWhen = "start"; + + if ((frame.contentDocument.readyState == "complete" || + (frame.contentDocument.readyState == "interactive" && + this.contentScriptWhen != 'end' )) && + frame.contentDocument.location == this._contentURL) { + // In some cases src doesn't change and document is already ready + // (for ex: when the user moves a widget while customizing toolbars.) + this._onInit(); + return; + } + + let self = this; + + if ('start' == this.contentScriptWhen) { + this._loadEvent = 'start'; + observers.add('document-element-inserted', + this._loadListener = function onStart(doc) { + + let window = doc.defaultView; + if (window && window == frame.contentWindow) { + self._unregisterListener(); + self._onInit(); + } + + }); + return; + } + + let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded'; + let self = this; + this._loadEvent = eventName; + frame.addEventListener(eventName, + this._loadListener = function _onReady(event) { + + if (event.target != frame.contentDocument) + return; + self._unregisterListener(); + + self._onInit(); + + }, true); + + }, + + /** + * Unregister listener that watchs for document being ready to be injected. + * This listener is registered in `Symbiont._initFrame`. + */ + _unregisterListener: function _unregisterListener() { + if (!this._loadListener) + return; + if (this._loadEvent == "start") { + observers.remove('document-element-inserted', this._loadListener); + } + else { + this._frame.removeEventListener(this._loadEvent, this._loadListener, + true); + } + this._loadListener = null; + }, + + /** + * Called by Symbiont itself when the frame is ready to load + * content scripts according to contentScriptWhen. Overloaded by Panel. + */ + _onInit: function () { + this._initWorker({ window: this._frame.contentWindow }); + } + +}); +exports.Symbiont = Symbiont; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/content/worker.js b/tools/addon-sdk-1.7/packages/api-utils/lib/content/worker.js new file mode 100644 index 0000000..c6f35d9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/content/worker.js @@ -0,0 +1,491 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { Trait } = require('../traits'); +const { EventEmitter, EventEmitterTrait } = require('../events'); +const { Ci, Cu, Cc } = require('chrome'); +const timer = require('../timer'); +const { URL } = require('../url'); +const unload = require('../unload'); +const observers = require('../observer-service'); +const { Cortex } = require('../cortex'); +const self = require("self"); +const { sandbox, evaluate, load } = require("../sandbox"); +const { merge } = require('../utils/object'); + +const CONTENT_PROXY_URL = self.data.url("content-proxy.js"); +const CONTENT_WORKER_URL = self.data.url("worker.js"); + +const JS_VERSION = '1.8'; + +const ERR_DESTROYED = + "The page has been destroyed and can no longer be used."; + +/** + * This key is not exported and should only be used for proxy tests. + * The following `PRIVATE_KEY` is used in addon module scope in order to tell + * Worker API to expose `UNWRAP_ACCESS_KEY` in content script. + * This key allows test-content-proxy.js to unwrap proxy with valueOf: + * let xpcWrapper = proxyWrapper.valueOf(UNWRAP_ACCESS_KEY); + */ +const PRIVATE_KEY = {}; + + +const WorkerSandbox = EventEmitter.compose({ + + /** + * Emit a message to the worker content sandbox + */ + emit: function emit() { + // First ensure having a regular array + // (otherwise, `arguments` would be mapped to an object by `stringify`) + let array = Array.slice(arguments); + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + function replacer(k, v) { + return typeof v === "function" ? undefined : v; + } + // Ensure having an asynchronous behavior + let self = this; + timer.setTimeout(function () { + self._emitToContent(JSON.stringify(array, replacer)); + }, 0); + }, + + /** + * Synchronous version of `emit`. + * /!\ Should only be used when it is strictly mandatory /!\ + * Doesn't ensure passing only JSON values. + * Mainly used by context-menu in order to avoid breaking it. + */ + emitSync: function emitSync() { + return this._emitToContent(Array.slice(arguments)); + }, + + /** + * Tells if content script has at least one listener registered for one event, + * through `self.on('xxx', ...)`. + * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API. + */ + hasListenerFor: function hasListenerFor(name) { + return this._hasListenerFor(name); + }, + + /** + * Method called by the worker sandbox when it needs to send a message + */ + _onContentEvent: function onContentEvent(args) { + // As `emit`, we ensure having an asynchronous behavior + let self = this; + timer.setTimeout(function () { + // We emit event to chrome/addon listeners + self._emit.apply(self, JSON.parse(args)); + }, 0); + }, + + /** + * Configures sandbox and loads content scripts into it. + * @param {Worker} worker + * content worker + */ + constructor: function WorkerSandbox(worker) { + this._addonWorker = worker; + + // Ensure that `emit` has always the right `this` + this.emit = this.emit.bind(this); + this.emitSync = this.emitSync.bind(this); + + // We receive a wrapped window, that may be an xraywrapper if it's content + let window = worker._window; + let proto = window; + + // Instantiate trusted code in another Sandbox in order to prevent content + // script from messing with standard classes used by proxy and API code. + let apiSanbox = sandbox(window, { wantXrays: true }); + + // Build content proxies only if the document has a non-system principal + if (window.wrappedJSObject) { + apiSanbox.console = console; + // Execute the proxy code + load(apiSanbox, CONTENT_PROXY_URL); + // Get a reference of the window's proxy + proto = apiSanbox.create(window); + } + + // Create the sandbox and bind it to window in order for content scripts to + // have access to all standard globals (window, document, ...) + let content = this._sandbox = sandbox(window, { + sandboxPrototype: proto, + wantXrays: true + }); + merge(content, { + // We need "this === window === top" to be true in toplevel scope: + get window() content, + get top() content, + // Use the Greasemonkey naming convention to provide access to the + // unwrapped window object so the content script can access document + // JavaScript values. + // NOTE: this functionality is experimental and may change or go away + // at any time! + get unsafeWindow() window.wrappedJSObject + }); + + // Load trusted code that will inject content script API. + // We need to expose JS objects defined in same principal in order to + // avoid having any kind of wrapper. + load(apiSanbox, CONTENT_WORKER_URL); + + // Then call `inject` method and communicate with this script + // by trading two methods that allow to send events to the other side: + // - `onEvent` called by content script + // - `result.emitToContent` called by addon script + let chromeAPI = { + timers: { + setTimeout: timer.setTimeout, + setInterval: timer.setInterval, + clearTimeout: timer.clearTimeout, + clearInterval: timer.clearInterval + } + }; + let onEvent = this._onContentEvent.bind(this); + // `ContentWorker` is defined in CONTENT_WORKER_URL file + let result = apiSanbox.ContentWorker.inject(content, chromeAPI, onEvent); + this._emitToContent = result.emitToContent; + this._hasListenerFor = result.hasListenerFor; + + // Handle messages send by this script: + let self = this; + // console.xxx calls + this.on("console", function consoleListener(kind) { + console[kind].apply(console, Array.slice(arguments, 1)); + }); + + // self.postMessage calls + this.on("message", function postMessage(data) { + self._addonWorker._emit('message', data); + }); + + // self.port.emit calls + this.on("event", function portEmit(name, args) { + self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments); + }); + + // Internal feature that is only used by SDK tests: + // Expose unlock key to content script context. + // See `PRIVATE_KEY` definition for more information. + if (apiSanbox && worker._expose_key) + content.UNWRAP_ACCESS_KEY = apiSanbox.UNWRAP_ACCESS_KEY; + + // Inject `addon` global into target document if document is trusted, + // `addon` in document is equivalent to `self` in content script. + if (worker._injectInDocument) { + let win = window.wrappedJSObject ? window.wrappedJSObject : window; + Object.defineProperty(win, "addon", { + value: content.self + } + ); + } + + // The order of `contentScriptFile` and `contentScript` evaluation is + // intentional, so programs can load libraries like jQuery from script URLs + // and use them in scripts. + let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile + : null, + contentScript = ('contentScript' in worker) ? worker.contentScript : null; + + if (contentScriptFile) { + if (Array.isArray(contentScriptFile)) + this._importScripts.apply(this, contentScriptFile); + else + this._importScripts(contentScriptFile); + } + if (contentScript) { + this._evaluate( + Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript + ); + } + }, + destroy: function destroy() { + this.emitSync("destroy"); + this._sandbox = null; + this._addonWorker = null; + }, + + /** + * JavaScript sandbox where all the content scripts are evaluated. + * {Sandbox} + */ + _sandbox: null, + + /** + * Reference to the addon side of the worker. + * @type {Worker} + */ + _addonWorker: null, + + /** + * Evaluates code in the sandbox. + * @param {String} code + * JavaScript source to evaluate. + * @param {String} [filename='javascript:' + code] + * Name of the file + */ + _evaluate: function(code, filename) { + try { + evaluate(this._sandbox, code, filename || 'javascript:' + code); + } + catch(e) { + this._addonWorker._emit('error', e); + } + }, + /** + * Imports scripts to the sandbox by reading files under urls and + * evaluating its source. If exception occurs during evaluation + * `"error"` event is emitted on the worker. + * This is actually an analog to the `importScript` method in web + * workers but in our case it's not exposed even though content + * scripts may be able to do it synchronously since IO operation + * takes place in the UI process. + */ + _importScripts: function _importScripts(url) { + let urls = Array.slice(arguments, 0); + for each (let contentScriptFile in urls) { + try { + let uri = URL(contentScriptFile); + if (uri.scheme === 'resource') + load(this._sandbox, String(uri)); + else + throw Error("Unsupported `contentScriptFile` url: " + String(uri)); + } + catch(e) { + this._addonWorker._emit('error', e); + } + } + } +}); + +/** + * Message-passing facility for communication between code running + * in the content and add-on process. + * @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/content/worker + */ +const Worker = EventEmitter.compose({ + on: Trait.required, + _removeAllListeners: Trait.required, + + /** + * Sends a message to the worker's global scope. Method takes single + * argument, which represents data to be sent to the worker. The data may + * be any primitive type value or `JSON`. Call of this method asynchronously + * emits `message` event with data value in the global scope of this + * symbiont. + * + * `message` event listeners can be set either by calling + * `self.on` with a first argument string `"message"` or by + * implementing `onMessage` function in the global scope of this worker. + * @param {Number|String|JSON} data + */ + postMessage: function postMessage(data) { + if (!this._contentWorker) + throw new Error(ERR_DESTROYED); + this._contentWorker.emit("message", data); + }, + + /** + * EventEmitter, that behaves (calls listeners) asynchronously. + * A way to send customized messages to / from the worker. + * Events from in the worker can be observed / emitted via + * worker.on / worker.emit. + */ + get port() { + // We generate dynamically this attribute as it needs to be accessible + // before Worker.constructor gets called. (For ex: Panel) + + // create an event emitter that receive and send events from/to the worker + let self = this; + this._port = EventEmitterTrait.create({ + emit: function () self._emitEventToContent(Array.slice(arguments)) + }); + + // expose wrapped port, that exposes only public properties: + // We need to destroy this getter in order to be able to set the + // final value. We need to update only public port attribute as we never + // try to access port attribute from private API. + delete this._public.port; + this._public.port = Cortex(this._port); + // Replicate public port to the private object + delete this.port; + this.port = this._public.port; + + return this._port; + }, + + /** + * Same object than this.port but private API. + * Allow access to _emit, in order to send event to port. + */ + _port: null, + + /** + * Emit a custom event to the content script, + * i.e. emit this event on `self.port` + */ + _emitEventToContent: function _emitEventToContent(args) { + // We need to save events that are emitted before the worker is + // initialized + if (!this._inited) { + this._earlyEvents.push(args); + return; + } + + // We throw exception when the worker has been destroyed + if (!this._contentWorker) { + throw new Error(ERR_DESTROYED); + } + + // Forward the event to the WorkerSandbox object + this._contentWorker.emit.apply(null, ["event"].concat(args)); + }, + + // Is worker connected to the content worker sandbox ? + _inited: false, + + // List of custom events fired before worker is initialized + get _earlyEvents() { + delete this._earlyEvents; + this._earlyEvents = []; + return this._earlyEvents; + }, + + constructor: function Worker(options) { + options = options || {}; + + if ('window' in options) + this._window = options.window; + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('onError' in options) + this.on('error', options.onError); + if ('onMessage' in options) + this.on('message', options.onMessage); + if ('onDetach' in options) + this.on('detach', options.onDetach); + + // Internal feature that is only used by SDK unit tests. + // See `PRIVATE_KEY` definition for more information. + if ('exposeUnlockKey' in options && options.exposeUnlockKey === PRIVATE_KEY) + this._expose_key = true; + + // Track document unload to destroy this worker. + // We can't watch for unload event on page's window object as it + // prevents bfcache from working: + // https://developer.mozilla.org/En/Working_with_BFCache + this._windowID = this._window. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils). + currentInnerWindowID; + observers.add("inner-window-destroyed", + this._documentUnload = this._documentUnload.bind(this)); + + unload.ensure(this._public, "destroy"); + + // Ensure that worker._port is initialized for contentWorker to be able + // to send use event during WorkerSandbox(this) + this.port; + + // will set this._contentWorker pointing to the private API: + this._contentWorker = WorkerSandbox(this); + + // Mainly enable worker.port.emit to send event to the content worker + this._inited = true; + + // Flush all events that have been fired before the worker is initialized. + this._earlyEvents.forEach((function (args) this._emitEventToContent(args)). + bind(this)); + }, + + _documentUnload: function _documentUnload(subject, topic, data) { + let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (innerWinID != this._windowID) return false; + this._workerCleanup(); + return true; + }, + + get url() { + // this._window will be null after detach + return this._window ? this._window.document.location.href : null; + }, + + get tab() { + if (this._window) { + let tab = require("../tabs/tab"); + // this._window will be null after detach + return tab.getTabForWindow(this._window); + } + return null; + }, + + /** + * Tells content worker to unload itself and + * removes all the references from itself. + */ + destroy: function destroy() { + this._workerCleanup(); + this._removeAllListeners(); + }, + + /** + * Remove all internal references to the attached document + * Tells _port to unload itself and removes all the references from itself. + */ + _workerCleanup: function _workerCleanup() { + // maybe unloaded before content side is created + // As Symbiont call worker.constructor on document load + if (this._contentWorker) + this._contentWorker.destroy(); + this._contentWorker = null; + this._window = null; + // This method may be called multiple times, + // avoid dispatching `detach` event more than once + if (this._windowID) { + this._windowID = null; + observers.remove("inner-window-destroyed", this._documentUnload); + this._earlyEvents.slice(0, this._earlyEvents.length); + this._emit("detach"); + } + }, + + /** + * Receive an event from the content script that need to be sent to + * worker.port. Provide a way for composed object to catch all events. + */ + _onContentScriptEvent: function _onContentScriptEvent() { + this._port._emit.apply(this._port, arguments); + }, + + /** + * Reference to the content side of the worker. + * @type {WorkerGlobalScope} + */ + _contentWorker: null, + + /** + * Reference to the window that is accessible from + * the content scripts. + * @type {Object} + */ + _window: null, + + /** + * Flag to enable `addon` object injection in document. (bug 612726) + * @type {Boolean} + */ + _injectInDocument: false +}); +exports.Worker = Worker; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/cortex.js b/tools/addon-sdk-1.7/packages/api-utils/lib/cortex.js new file mode 100644 index 0000000..1c3a1b3 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/cortex.js @@ -0,0 +1,109 @@ +/* vim:set ts=2 sw=2 sts=2 + * 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"; + +// `var` is being used in the module in order to make it reusable in +// environments in which `let` and `const` is not yet supported. + +// Returns `object`'s property value, where `name` is a name of the property. +function get(object, name) { + return object[name]; +} + +// Assigns `value` to the `object`'s property, where `name` is the name of the +// property. +function set(object, name, value) { + return object[name] = value; +} + +/** + * Given an `object` containing a property with the given `name`, create + * a property descriptor that can be used to define alias/proxy properties + * on other objects. A change in the value of an alias will propagate + * to the aliased property and vice versa. + */ +function createAliasProperty(object, name) { + // Getting own property descriptor of an `object` for the given `name` as + // we are going to create proxy analog. + var property = Object.getOwnPropertyDescriptor(object, name); + var descriptor = { + configurable: property.configurable, + enumerable: property.enumerable, + alias: true + }; + + // If the original property has a getter and/or setter, bind a + // corresponding getter/setter in the alias descriptor to the original + // object, so the `this` object in the getter/setter is the original object + // rather than the alias. + if ("get" in property && property.get) + descriptor.get = property.get.bind(object); + if ("set" in property && property.set) + descriptor.set = property.set.bind(object); + + // If original property was a value property. + if ("value" in property) { + // If original property is a method using it's `object` bounded copy. + if (typeof property.value === "function") { + descriptor.value = property.value.bind(object); + // Also preserving writability of the original property. + descriptor.writable = property.writable; + } + + // If the original property was just a data property, we create proxy + // accessors using our custom get/set functions to propagate changes to the + // original `object` and vice versa. + else { + descriptor.get = get.bind(null, object, name); + descriptor.set = set.bind(null, object, name); + } + } + return descriptor; +} + +// Defines property on `object` object with a name `alias` if given if not +// defaults to `name` that represents an alias of `source[name]`. If aliased +// property was an assessor or a method `this` pseudo-variable will be `source` +// when invoked. If aliased property was a data property changes on any of the +// aliases will propagate to the `source[name]` and also other way round. +function defineAlias(source, target, name, alias) { + return Object.defineProperty(target, alias || name, + createAliasProperty(source, name)); +} + +/** + * Function takes any `object` and returns a proxy for its own public + * properties. By default properties are considered to be public if they don't + * start with `"_"`, but default behavior can be overridden if needed, by + * passing array of public property `names` as a second argument. By default + * returned object will be direct decedent of the given `object`'s prototype, + * but this can be overridden by passing third optional argument, that will be + * used as `prototype` instead. + * @param {Object} object + * Object to create cortex for. + * @param {String[]} [names] + * Optional array of public property names. + * @param {Object} [prototype] + * Optional argument that will be used as `prototype` of the returned object, + * if not provided `Object.getPrototypeOf(object)` is used instead. + */ +exports.Cortex = function Cortex(object, names, prototype) { + // Creating a cortex object from the given `prototype`, if one was not + // provided then `prototype` of a given `object` is used. This allows + // consumer to define expected behavior `instanceof`. In common case + // `prototype` argument can be omitted to preserve same behavior of + // `instanceof` as on original `object`. + var cortex = Object.create(prototype || Object.getPrototypeOf(object)); + // Creating alias properties on the `cortex` object for all the own + // properties of the original `object` that are contained in `names` array. + // If `names` array is not provided then all the properties that don't + // start with `"_"` are aliased. + Object.getOwnPropertyNames(object).forEach(function (name) { + if ((!names && "_" !== name.charAt(0)) || (names && ~names.indexOf(name))) + defineAlias(object, cortex, name); + }); + return cortex; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/cuddlefish.js b/tools/addon-sdk-1.7/packages/api-utils/lib/cuddlefish.js new file mode 100644 index 0000000..ac01aee --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/cuddlefish.js @@ -0,0 +1,302 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* 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/. */ +var EXPORTED_SYMBOLS = [ 'Loader' ]; + +!function(exports) { + +"use strict"; + +const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu, + results: Cr, manager: Cm } = Components; +const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); +const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. + getService(Ci.mozIJSSubScriptLoader); + +const Sandbox = { + new: function ({ principal, prototype, name, existingSandbox }) { + let options = { sandboxPrototype: prototype || Sandbox.prototype, + wantXrays: Sandbox.wantXrays }; + if (name) + options.sandboxName = name; + if (existingSandbox) + options.sameGroupAs = existingSandbox.sandbox; + let sandbox = Object.create(Sandbox, { + sandbox: { value: Cu.Sandbox(principal || Sandbox.principal, options) } + }); + // There are few properties (dump, Iterator) that by default appear in + // sandboxes shadowing properties provided by a prototype. To workaround + // this we override all such properties by copying them directly to the + // sandbox. + Object.keys(prototype).forEach(function onEach(key) { + if (sandbox.sandbox[key] !== prototype[key]) + sandbox.sandbox[key] = prototype[key]; + }); + return sandbox; + }, + evaluate: function evaluate(source, uri, lineNumber) { + return Cu.evalInSandbox( + source, + this.sandbox, + this.version, + uri, + lineNumber || this.lineNumber + ); + }, + load: function load(uri) { + scriptLoader.loadSubScript(uri, this.sandbox, 'UTF-8'); + }, + merge: function merge(properties) { + Object.getOwnPropertyNames(properties).forEach(function(name) { + Object.defineProperty(this.sandbox, name, + Object.getOwnPropertyDescriptor(properties, name)); + }, this); + }, + principal: systemPrincipal, + version: '1.8', + lineNumber: 1, + wantXrays: false, + prototype: {} +}; + +// the Module object made available to CommonJS modules when they are +// evaluated, along with 'exports' and 'uri' +const Module = { + new: function(id, path, uri) { + let module = Object.create(this); + + module.id = id; + module.path = path; + module.uri = uri; + module.exports = {}; + + return module; + }, + // TODO: I'd like to remove this, it's not used adds complexity and does + // not has much adoption in commonjs either. + setExports: function setExports(exports) { + this.exports = exports; + } +}; + +const Loader = { + new: function (options) { + let loader = Object.create(Loader, { + // Manifest generated by a linker, containing map of module url's mapped + // to it's requirements, comes from harness-options.json + manifest: { value: options.manifest || {} }, + + // Following property may be passed in (usually for mocking purposes) in + // order to override default modules cache. + modules: { value: options.modules || Object.create(Loader.modules) }, + globals: { value: options.globals || {} }, + + uriPrefix: { value: options.uriPrefix }, + + sandboxes: { value: {} } + }); + loader.require = this.require.bind(loader, options.loader); + + // some 'magic' modules, that have no corresponding .js file + loader.modules['@packaging'] = Object.freeze({ + id: '@packaging', + exports: JSON.parse(JSON.stringify(options)) + }); + loader.modules['@loader'] = Object.freeze({ + exports: Object.freeze({ Loader: Loader }), + id: '@loader' + }); + + // This special module defines globals which will be added to every + // module this loader creates + let globals = loader.require('api-utils/globals!'); + Object.getOwnPropertyNames(globals).forEach(function(name) { + Object.defineProperty(loader.globals, name, + Object.getOwnPropertyDescriptor(globals, name)); + }); + // Freeze globals so that modules won't have a chance to mutate scope of + // other modules. + Object.freeze(globals); + + // Override global `dump` so that it behaves same as in any other module ( + // currently we override dump to write to a file instead of `stdout` so that + // python can read it on windows). + dump = globals.dump; + + return Object.freeze(loader); + }, + modules: { + 'chrome': Object.freeze({ + exports: Object.freeze({ + Cc: Cc, + CC: CC, + Ci: Ci, + Cu: Cu, + Cr: Cr, + Cm: Cm, + components: Components, + messageManager: 'addMessageListener' in exports ? exports : null + }), + id: 'chrome' + }), + 'self': function self(loader, requirer) { + return loader.require('api-utils/self!').create(requirer.path); + }, + }, + + // populate a Module by evaluating the CommonJS module code in the sandbox + load: function load(module) { + let require = Loader.require.bind(this, module.path); + require.main = this.main; + + // Get an existing module sandbox, if any, so we can reuse its compartment + // when creating the new one to reduce memory consumption. + let existingSandbox = [this.sandboxes[p] for (p in this.sandboxes)][0]; + + // XXX Always set "principal" to work around bug 705795, which generates + // 'reference to undefined property "principal"' warnings when the argument + // is deconstructed in the "new" function's parameter list. + // FIXME: stop setting "principal" once bug 705795 is fixed. + let sandbox = this.sandboxes[module.path] = + Sandbox.new({ principal: null, + prototype: this.globals, + name: module.uri, + existingSandbox: existingSandbox }); + sandbox.merge({ + require: require, + module: module, + exports: module.exports + }); + + sandbox.load(module.uri); + + // Workaround for bug 674195. Freezing objects from other sandboxes fail, + // so we create descendant and freeze it instead. + if (typeof(module.exports) === 'object') { + module.exports = Object.prototype.isPrototypeOf(module.exports) ? + Object.freeze(module.exports) : + Object.freeze(Object.create(module.exports)); + } + }, + + // this require() is the main entry point for regular CommonJS modules. The + // bind() in load (above) causes those modules to get a very restricted + // form of this require(): one which can only ever reference this one + // loader, and which always uses their URI as a "base" (so they're limited + // to their own manifest entries, and can't import anything off the + // manifest). + require: function require(base, id) { + let module, manifest = this.manifest[base], requirer = this.modules[base]; + + if (!id) + throw Error("you must provide a module name when calling require() from " + + (requirer && requirer.id), base, id); + + // If we have a manifest for requirer, then all it's requirements have been + // registered by linker and we should have a `path` to the required module. + // Even pseudo-modules like 'chrome', 'self', '@packaging', and '@loader' + // have pseudo-paths: exactly those same names. + // details see: Bug-697422. + let requirement = manifest && manifest.requirements[id]; + if (!requirement) + throw Error("Module: " + (requirer && requirer.id) + ' located at ' + + base + " has no authority to load: " + id); + let path = requirement.path; + + if (path in this.modules) { + module = this.modules[path]; + } + else { + let uri = this.uriPrefix + path; + module = this.modules[path] = Module.new(id, path, uri); + this.load(module); + Object.freeze(module); + } + + // "magic" modules which have contents that depend upon who imports them + // (like "self") are expressed in the Loader's pre-populated 'modules' + // table as callable functions, which are given the reference to this + // Loader and a copy of the importer's URI + // + // TODO: Find a better way to implement `self`. + // Maybe something like require('self!path/to/data') + if (typeof(module) === 'function') + module = module(this, requirer); + + return module.exports; + }, + + // process.process() will eventually cause a call to main() to be evaluated + // in the addon's context. This function loads and executes the addon's + // entry point module. + main: function main(id, path) { + try { + let uri = this.uriPrefix + path; + let module = this.modules[path] = Module.new(id, path, uri); + this.load(module); // this is where the addon's main.js finally runs + let program = Object.freeze(module).exports; + + if (typeof(program.onUnload) === 'function') + this.require('api-utils/unload').when(program.onUnload); + + if (program.main) { + let { exit, staticArgs } = this.require('api-utils/system'); + let { loadReason } = this.require('@packaging'); + program.main({ loadReason: loadReason, staticArgs: staticArgs }, + { print: function($) dump($ + '\n'), quit: exit }); + } + } catch (error) { + Cu.reportError(error); + if (this.globals.console) this.globals.console.exception(error); + throw error; + } + }, + + // This is the main entry-point: bootstrap.js calls this when the add-on is + // installed. The order of calls is a bit confusing, but here's what + // happens (in temporal order): + // * process.spawn creates a new XUL 'browser' element which will house the + // main addon code. When e10s is active, this uses a real separate OS + // process. When e10s is disabled, this element lives in the one original + // process. Either way, its API is the same. + // * Grab the channel named "require!" and attach a handler which will load + // modules (in the chrome process) when requested to by the addon + // process. This handler uses Loader.require to import the module, then + // calls the module's .initialize() function to connect a new channel. + // The remote caller winds up with a channel reference, which they can + // use to send messages to the newly loaded module. This is for e10s. + // * After the channel handler is attached, process.process() (invoked by + // process.spawn()) will use loadScript() to evaluate code in the + // 'browser' element (which is where the main addon code starts running), + // to do the following: + // * create a Loader, initialized with the same manifest and + // harness-options.json that we've got + // * invoke it's main() method, with the name and path of the addon's + // entry module (which comes from linker via harness-options.js, and is + // usually main.js). That executes main(), above. + // * main() loads the addon's main.js, which executes all top-level + // forms. If the module defines an "exports.main=" function, we invoke + // that too. This is where the addon finally gets to run. + spawn: function spawn(id, path) { + let loader = this; + let process = this.require('api-utils/process'); + process.spawn(id, path)(function(addon) { + // Listen to `require!` channel's input messages from the add-on process + // and load modules being required. + addon.channel('require!').input(function({ requirer: { path }, id }) { + try { + Loader.require.call(loader, path, id).initialize(addon.channel(id)); + } catch (error) { + this.globals.console.exception(error); + } + }); + }); + }, + unload: function unload(reason, callback) { + this.require('api-utils/unload').send(reason, callback); + } +}; +exports.Loader = Loader; + +}(this); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/dom/events.js b/tools/addon-sdk-1.7/packages/api-utils/lib/dom/events.js new file mode 100644 index 0000000..2168f42 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/dom/events.js @@ -0,0 +1,136 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +// Utility function that returns copy of the given `text` with last character +// removed if it is `"s"`. +function singularify(text) { + return text[text.length - 1] === "s" ? text.substr(0, text.length - 1) : text; +} + +// Utility function that takes event type, argument is passed to +// `document.createEvent` and returns name of the initializer method of the +// given event. Please note that there are some event types whose initializer +// methods can't be guessed by this function. For more details see following +// link: https://developer.mozilla.org/En/DOM/Document.createEvent +function getInitializerName(category) { + return "init" + singularify(category); +} + +/** + * Registers an event `listener` on a given `element`, that will be called + * when events of specified `type` is dispatched on the `element`. + * @param {Element} element + * Dom element to register listener on. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type) to + * listen for. + * @param {Function} listener + * Function that is called whenever an event of the specified `type` + * occurs. + * @param {Boolean} capture + * If true, indicates that the user wishes to initiate capture. After + * initiating capture, all events of the specified type will be dispatched + * to the registered listener before being dispatched to any `EventTarget`s + * beneath it in the DOM tree. Events which are bubbling upward through + * the tree will not trigger a listener designated to use capture. + * See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) + * for a detailed explanation. + */ +function on(element, type, listener, capture) { + // `capture` defaults to `false`. + capture = capture || false; + element.addEventListener(type, listener, capture); +} +exports.on = on; + +/** + * Registers an event `listener` on a given `element`, that will be called + * only once, next time event of specified `type` is dispatched on the + * `element`. + * @param {Element} element + * Dom element to register listener on. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type) to + * listen for. + * @param {Function} listener + * Function that is called whenever an event of the specified `type` + * occurs. + * @param {Boolean} capture + * If true, indicates that the user wishes to initiate capture. After + * initiating capture, all events of the specified type will be dispatched + * to the registered listener before being dispatched to any `EventTarget`s + * beneath it in the DOM tree. Events which are bubbling upward through + * the tree will not trigger a listener designated to use capture. + * See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) + * for a detailed explanation. + */ +function once(element, type, listener, capture) { + on(element, type, function selfRemovableListener(event) { + removeListener(element, type, selfRemovableListener, capture); + listener.apply(this, arguments); + }, capture); +} +exports.once = once; + +/** + * Unregisters an event `listener` on a given `element` for the events of the + * specified `type`. + * + * @param {Element} element + * Dom element to unregister listener from. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type) to + * listen for. + * @param {Function} listener + * Function that is called whenever an event of the specified `type` + * occurs. + * @param {Boolean} capture + * If true, indicates that the user wishes to initiate capture. After + * initiating capture, all events of the specified type will be dispatched + * to the registered listener before being dispatched to any `EventTarget`s + * beneath it in the DOM tree. Events which are bubbling upward through + * the tree will not trigger a listener designated to use capture. + * See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) + * for a detailed explanation. + */ +function removeListener(element, type, listener, capture) { + element.removeEventListener(type, listener, capture); +} +exports.removeListener = removeListener; + +/** + * Emits event of the specified `type` and `category` on the given `element`. + * Specified `settings` are used to initialize event before dispatching it. + * @param {Element} element + * Dom element to dispatch event on. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type). + * @param {Object} options + * Options object containing following properties: + * - `category`: String passed to the `document.createEvent`. Option is + * optional and defaults to "UIEvents". + * - `initializer`: If passed it will be used as name of the method used + * to initialize event. If omitted name will be generated from the + * `category` field by prefixing it with `"init"` and removing last + * character if it matches `"s"`. + * - `settings`: Array of settings that are forwarded to the event + * initializer after firs `type` argument. + * @see https://developer.mozilla.org/En/DOM/Document.createEvent + */ +function emit(element, type, { category, initializer, settings }) { + category = category || "UIEvents"; + initializer = initializer || getInitializerName(category); + let document = element.ownerDocument; + let event = document.createEvent(category); + event[initializer].apply(event, [type].concat(settings)); + element.dispatchEvent(event); +}; +exports.emit = emit; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/dom/events/keys.js b/tools/addon-sdk-1.7/packages/api-utils/lib/dom/events/keys.js new file mode 100644 index 0000000..53107ae --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/dom/events/keys.js @@ -0,0 +1,60 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { emit } = require("../events"); +const { getCodeForKey, toJSON } = require("../../keyboard/utils"); +const { has } = require("../../array"); +const { isString } = require("../../type"); + +const INITIALIZER = "initKeyEvent"; +const CATEGORY = "KeyboardEvent"; + +function Options(options) { + if (!isString(options)) + return options; + + var { key, modifiers } = toJSON(options); + return { + key: key, + control: has(modifiers, "control"), + alt: has(modifiers, "alt"), + shift: has(modifiers, "shift"), + meta: has(modifiers, "meta") + }; +} + +var keyEvent = exports.keyEvent = function keyEvent(element, type, options) { + + emit(element, type, { + initializer: INITIALIZER, + category: CATEGORY, + settings: [ + !("bubbles" in options) || options.bubbles !== false, + !("cancelable" in options) || options.cancelable !== false, + "window" in options && options.window ? options.window : null, + "control" in options && !!options.control, + "alt" in options && !!options.alt, + "shift" in options && !!options.shift, + "meta" in options && !!options.meta, + getCodeForKey(options.key) || 0, + options.key.length === 1 ? options.key.charCodeAt(0) : 0 + ] + }); +} + +exports.keyDown = function keyDown(element, options) { + keyEvent(element, "keydown", Options(options)); +}; + +exports.keyUp = function keyUp(element, options) { + keyEvent(element, "keyup", Options(options)); +}; + +exports.keyPress = function keyPress(element, options) { + keyEvent(element, "keypress", Options(options)); +}; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/env!.js b/tools/addon-sdk-1.7/packages/api-utils/lib/env!.js new file mode 100644 index 0000000..f7144ad --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/env!.js @@ -0,0 +1,20 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* 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/. */ + +const { messageManager } = require("chrome"); +const { channel } = require("./channel"); + +module.exports = function load(module) { + return { + require: function require(id) { + // Load required module on the chrome process. + channel(messageManager, messageManager, 'require!').sync({ + requirer: module, + id: id + }); + return channel(messageManager, messageManager, id); + } + }; +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/environment.js b/tools/addon-sdk-1.7/packages/api-utils/lib/environment.js new file mode 100644 index 0000000..fd95c67 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/environment.js @@ -0,0 +1,54 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* 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 { get, set, exists } = Cc['@mozilla.org/process/environment;1']. + getService(Ci.nsIEnvironment); + +exports.env = Proxy.create({ + // XPCOM does not provides a way to enumerate environment variables, so we + // just don't support enumeration. + getPropertyNames: function() [], + getOwnPropertyNames: function() [], + enumerate: function() [], + keys: function() [], + // We do not support freezing, cause it would make it impossible to set new + // environment variables. + fix: function() undefined, + // We present all environment variables as own properties of this object, + // so we just delegate this call to `getOwnPropertyDescriptor`. + getPropertyDescriptor: function(name) this.getOwnPropertyDescriptor(name), + // If environment variable with this name is defined, we generate proprety + // descriptor for it, otherwise fall back to `undefined` so that for consumer + // this property does not exists. + getOwnPropertyDescriptor: function(name) { + return !exists(name) ? undefined : { + value: get(name), + enumerable: false, // Non-enumerable as we don't support enumeration. + configurable: true, // Configurable as it may be deleted. + writable: true // Writable as we do support set. + } + }, + + // New environment variables can be defined just by defining properties + // on this object. + defineProperty: function(name, { value }) set(name, value), + delete: function(name) set(name, null), + + // We present all properties as own, there for we just delegate to `hasOwn`. + has: function(name) this.hasOwn(name), + // We do support checks for existence of an environment variable, via `in` + // operator on this object. + hasOwn: function(name) exists(name), + + // On property get / set we do read / write appropriate environment variables, + // please note though, that variables with names of standard object properties + // intentionally (so that this behaves as normal object) can not be + // read / set. + get: function(proxy, name) Object.prototype[name] || get(name) || undefined, + set: function(proxy, name, value) Object.prototype[name] || set(name, value) +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/errors.js b/tools/addon-sdk-1.7/packages/api-utils/lib/errors.js new file mode 100644 index 0000000..7029158 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/errors.js @@ -0,0 +1,60 @@ +/* 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"; + +function logToConsole(e) { + console.exception(e); +} + +var catchAndLog = exports.catchAndLog = function(callback, + defaultResponse, + logException) { + if (!logException) + logException = logToConsole; + + return function() { + try { + return callback.apply(this, arguments); + } catch (e) { + logException(e); + return defaultResponse; + } + }; +}; + +exports.catchAndLogProps = function catchAndLogProps(object, + props, + defaultResponse, + logException) { + if (typeof(props) == "string") + props = [props]; + props.forEach( + function(property) { + object[property] = catchAndLog(object[property], + defaultResponse, + logException); + }); +}; + +/** + * Catch and return an exception while calling the callback. If the callback + * doesn't throw, return the return value of the callback in a way that makes it + * possible to distinguish between a return value and an exception. + * + * This function is useful when you need to pass the result of a call across + * a process boundary (across which exceptions don't propagate). It probably + * doesn't need to be factored out into this module, since it is only used by + * a single caller, but putting it here works around bug 625560. + */ +exports.catchAndReturn = function(callback) { + return function() { + try { + return { returnValue: callback.apply(this, arguments) }; + } + catch (exception) { + return { exception: exception }; + } + }; +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/event/core.js b/tools/addon-sdk-1.7/packages/api-utils/lib/event/core.js new file mode 100644 index 0000000..ed2a0d4 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/event/core.js @@ -0,0 +1,147 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.'; +const BAD_LISTENER = 'The event listener must be a function.'; + +const { ns } = require('../namespace'); + +const event = ns(); + +// Utility function to access given event `target` object's event listeners for +// the specific event `type`. If listeners for this type does not exists they +// will be created. +const observers = function observers(target, type) { + let listeners = event(target); + return type in listeners ? listeners[type] : listeners[type] = []; +}; + +/** + * Registers an event `listener` that is called every time events of + * specified `type` is emitted on the given event `target`. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ +function on(target, type, listener) { + if (typeof(listener) !== 'function') + throw new Error(BAD_LISTENER); + + let listeners = observers(target, type); + if (!~listeners.indexOf(listener)) + listeners.push(listener); +} +exports.on = on; + +/** + * Registers an event `listener` that is called only the next time an event + * of the specified `type` is emitted on the given event `target`. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of the event. + * @param {Function} listener + * The listener function that processes the event. + */ +function once(target, type, listener) { + on(target, type, function observer() { + off(target, type, observer); + listener.apply(target, arguments); + }); +} +exports.once = once; + +/** + * Execute each of the listeners in order with the supplied arguments. + * All the exceptions that are thrown by listeners during the emit + * are caught and can be handled by listeners of 'error' event. Thrown + * exceptions are passed as an argument to an 'error' event listener. + * If no 'error' listener is registered exception will be logged into an + * error console. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of event. + * @params {Object|Number|String|Boolean} message + * First argument that will be passed to listeners. + * @params {Object|Number|String|Boolean} ... + * More arguments that will be passed to listeners. + */ +function emit(target, type, message /*, ...*/) { + for each (let item in emit.lazy.apply(emit.lazy, arguments)) + item; +} + +/** + * This is very experimental feature that you should not use unless absolutely + * need it. Also it may be removed at any point without any further notice. + * + * Creates lazy iterator of return values of listeners. You can think of it + * as lazy array of return values of listeners for the `emit` with the given + * arguments. + */ +emit.lazy = function lazy(target, type, message /*, ...*/) { + let args = Array.slice(arguments, 2) + let listeners = observers(target, type).slice() + while (listeners.length) { + try { + yield listeners.shift().apply(target, args); + } + catch (error) { + // If exception is not thrown by a error listener and error listener is + // registered emit `error` event. Otherwise dump exception to the console. + if (type !== 'error' && observers(target, 'error').length) + emit(target, 'error', error); + else + console.exception(error); + } + } +} +exports.emit = emit; + +/** + * Removes an event `listener` for the given event `type` on the given event + * `target`. If no `listener` is passed removes all listeners of the given + * `type`. If `type` is not passed removes all the listeners of the given + * event `target`. + * @param {Object} target + * The event target object. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ +function off(target, type, listener) { + let length = arguments.length; + if (length === 3) { + let listeners = observers(target, type); + let index = listeners.indexOf(listener); + if (~index) + listeners.splice(index, 1); + } + else if (length === 2) { + observers(target, type).splice(0); + } + else if (length === 1) { + let listeners = event(target); + Object.keys(listeners).forEach(function(type) delete listeners[type]); + } +} +exports.off = off; + +/** + * Returns a number of event listeners registered for the given event `type` + * on the given event `target`. + */ +function count(target, type) { + return observers(target, type).length; +} +exports.count = count; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/event/target.js b/tools/addon-sdk-1.7/packages/api-utils/lib/event/target.js new file mode 100644 index 0000000..60b19d2 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/event/target.js @@ -0,0 +1,76 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { on, once, off } = require('./core'); +const { method } = require('../functional'); +const { Base } = require('../base'); + +const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/; + +/** + * `EventTarget` is an exemplar for creating an objects that can be used to + * add / remove event listeners on them. Events on these objects may be emitted + * via `emit` function exported by 'event/core' module. + */ +const EventTarget = Base.extend({ + /** + * Method initializes `this` event source. It goes through properties of a + * given `options` and registers listeners for the ones that look like an + * event listeners. + */ + initialize: function initialize(options) { + options = options || {}; + // Go through each property and registers event listeners for those + // that have a name matching following pattern (`onEventType`). + Object.keys(options).forEach(function onEach(key) { + let match = EVENT_TYPE_PATTERN.exec(key); + let type = match && match[1].toLowerCase(); + let listener = options[key]; + + if (type && typeof(listener) === 'function') + this.on(type, listener); + }, this); + }, + /** + * Registers an event `listener` that is called every time events of + * specified `type` are emitted. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + * @example + * worker.on('message', function (data) { + * console.log('data received: ' + data) + * }) + */ + on: method(on), + /** + * Registers an event `listener` that is called once the next time an event + * of the specified `type` is emitted. + * @param {String} type + * The type of the event. + * @param {Function} listener + * The listener function that processes the event. + */ + once: method(once), + /** + * Removes an event `listener` for the given event `type`. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ + removeListener: function removeListener(type, listener) { + // Note: We can't just wrap `off` in `method` as we do it for other methods + // cause skipping a second or third argument will behave very differently + // than intended. This way we make sure all arguments are passed and only + // one listener is removed at most. + off(this, type, listener); + } +}); +exports.EventTarget = EventTarget; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/events.js b/tools/addon-sdk-1.7/packages/api-utils/lib/events.js new file mode 100644 index 0000000..531a7a3 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/events.js @@ -0,0 +1,178 @@ +/* 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 ERROR_TYPE = 'error', + UNCAUGHT_ERROR = 'An error event was dispatched for which there was' + + ' no listener.', + BAD_LISTENER = 'The event listener must be a function.'; +/** + * This object is used to create an `EventEmitter` that, useful for composing + * objects that emit events. It implements an interface like `EventTarget` from + * DOM Level 2, which is implemented by Node objects in implementations that + * support the DOM Event Model. + * @see http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget + * @see http://nodejs.org/api.html#EventEmitter + * @see http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/EventDispatcher.html + */ +const eventEmitter = { + /** + * Registers an event `listener` that is called every time events of + * specified `type` are emitted. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + * @example + * worker.on('message', function (data) { + * console.log('data received: ' + data) + * }) + */ + on: function on(type, listener) { + if ('function' !== typeof listener) + throw new Error(BAD_LISTENER); + let listeners = this._listeners(type); + if (0 > listeners.indexOf(listener)) + listeners.push(listener); + // Use of `_public` is required by the legacy traits code that will go away + // once bug-637633 is fixed. + return this._public || this; + }, + + /** + * Registers an event `listener` that is called once the next time an event + * of the specified `type` is emitted. + * @param {String} type + * The type of the event. + * @param {Function} listener + * The listener function that processes the event. + */ + once: function once(type, listener) { + this.on(type, function selfRemovableListener() { + this.removeListener(type, selfRemovableListener); + listener.apply(this, arguments); + }); + }, + + /** + * Unregister `listener` for the specified event type. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ + removeListener: function removeListener(type, listener) { + if ('function' !== typeof listener) + throw new Error(BAD_LISTENER); + let listeners = this._listeners(type), + index = listeners.indexOf(listener); + if (0 <= index) + listeners.splice(index, 1); + // Use of `_public` is required by the legacy traits code, that will go away + // once bug-637633 is fixed. + return this._public || this; + }, + + /** + * Hash of listeners on this EventEmitter. + */ + _events: null, + + /** + * Returns an array of listeners for the specified event `type`. This array + * can be manipulated, e.g. to remove listeners. + * @param {String} type + * The type of event. + */ + _listeners: function listeners(type) { + let events = this._events || (this._events = {}); + return events[type] || (events[type] = []); + }, + + /** + * Execute each of the listeners in order with the supplied arguments. + * Returns `true` if listener for this event was called, `false` if there are + * no listeners for this event `type`. + * + * All the exceptions that are thrown by listeners during the emit + * are caught and can be handled by listeners of 'error' event. Thrown + * exceptions are passed as an argument to an 'error' event listener. + * If no 'error' listener is registered exception will propagate to a + * caller of this method. + * + * **It's recommended to have a default 'error' listener in all the complete + * composition that in worst case may dump errors to the console.** + * + * @param {String} type + * The type of event. + * @params {Object|Number|String|Boolean} + * Arguments that will be passed to listeners. + * @returns {Boolean} + */ + _emit: function _emit(type, event) { + let args = Array.slice(arguments); + // Use of `_public` is required by the legacy traits code that will go away + // once bug-637633 is fixed. + args.unshift(this._public || this); + return this._emitOnObject.apply(this, args); + }, + + /** + * A version of _emit that lets you specify the object on which listeners are + * called. This is a hack that is sometimes necessary when such an object + * (exports, for example) cannot be an EventEmitter for some reason, but other + * object(s) managing events for the object are EventEmitters. Once bug + * 577782 is fixed, this method shouldn't be necessary. + * + * @param {object} targetObj + * The object on which listeners will be called. + * @param {string} type + * The event name. + * @param {value} event + * The first argument to pass to listeners. + * @param {value} ... + * More arguments to pass to listeners. + * @returns {boolean} + */ + _emitOnObject: function _emitOnObject(targetObj, type, event /* , ... */) { + let listeners = this._listeners(type).slice(0); + // If there is no 'error' event listener then throw. + if (type === ERROR_TYPE && !listeners.length) + console.exception(event); + if (!listeners.length) + return false; + let params = Array.slice(arguments, 2); + for each (let listener in listeners) { + try { + listener.apply(targetObj, params); + } catch(e) { + // Bug 726967: Ignore exceptions being throws while notifying the error + // in order to avoid infinite loops. + if (type !== ERROR_TYPE) + this._emit(ERROR_TYPE, e); + else + console.exception("Exception in error event listener " + e); + } + } + return true; + }, + + /** + * Removes all the event listeners for the specified event `type`. + * @param {String} type + * The type of event. + */ + _removeAllListeners: function _removeAllListeners(type) { + if (typeof type == "undefined") { + this._events = null; + return this; + } + + this._listeners(type).splice(0); + return this; + } +}; +exports.EventEmitter = require("./traits").Trait.compose(eventEmitter); +exports.EventEmitterTrait = require('./light-traits').Trait(eventEmitter); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/events/assembler.js b/tools/addon-sdk-1.7/packages/api-utils/lib/events/assembler.js new file mode 100644 index 0000000..5d11be2 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/events/assembler.js @@ -0,0 +1,53 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { Trait } = require("../light-traits"); +const { removeListener, on } = require("../dom/events"); + +/** + * Trait may be used for building objects / composing traits that wish to handle + * multiple dom events from multiple event targets in one place. Event targets + * can be added / removed by calling `observe / ignore` methods. Composer should + * provide array of event types it wishes to handle as property + * `supportedEventsTypes` and function for handling all those events as + * `handleEvent` property. + */ +exports.DOMEventAssembler = Trait({ + /** + * Function that is supposed to handle all the supported events (that are + * present in the `supportedEventsTypes`) from all the observed + * `eventTargets`. + * @param {Event} event + * Event being dispatched. + */ + handleEvent: Trait.required, + /** + * Array of supported event names. + * @type {String[]} + */ + supportedEventsTypes: Trait.required, + /** + * Adds `eventTarget` to the list of observed `eventTarget`s. Listeners for + * supported events will be registered on the given `eventTarget`. + * @param {EventTarget} eventTarget + */ + observe: function observe(eventTarget) { + this.supportedEventsTypes.forEach(function(eventType) { + on(eventTarget, eventType, this); + }, this); + }, + /** + * Removes `eventTarget` from the list of observed `eventTarget`s. Listeners + * for all supported events will be unregistered from the given `eventTarget`. + * @param {EventTarget} eventTarget + */ + ignore: function ignore(eventTarget) { + this.supportedEventsTypes.forEach(function(eventType) { + removeListener(eventTarget, eventType, this); + }, this); + } +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/file.js b/tools/addon-sdk-1.7/packages/api-utils/lib/file.js new file mode 100644 index 0000000..0b4eac9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/file.js @@ -0,0 +1,192 @@ +/* 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,Cr} = require("chrome"); +const byteStreams = require("./byte-streams"); +const textStreams = require("./text-streams"); + +// Flags passed when opening a file. See nsprpub/pr/include/prio.h. +const OPEN_FLAGS = { + RDONLY: parseInt("0x01"), + WRONLY: parseInt("0x02"), + CREATE_FILE: parseInt("0x08"), + APPEND: parseInt("0x10"), + TRUNCATE: parseInt("0x20"), + EXCL: parseInt("0x80") +}; + +var dirsvc = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + +function MozFile(path) { + var file = Cc['@mozilla.org/file/local;1'] + .createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return file; +} + +function ensureReadable(file) { + if (!file.isReadable()) + throw new Error("path is not readable: " + file.path); +} + +function ensureDir(file) { + ensureExists(file); + if (!file.isDirectory()) + throw new Error("path is not a directory: " + file.path); +} + +function ensureFile(file) { + ensureExists(file); + if (!file.isFile()) + throw new Error("path is not a file: " + file.path); +} + +function ensureExists(file) { + if (!file.exists()) + throw friendlyError(Cr.NS_ERROR_FILE_NOT_FOUND, file.path); +} + +function friendlyError(errOrResult, filename) { + var isResult = typeof(errOrResult) === "number"; + var result = isResult ? errOrResult : errOrResult.result; + switch (result) { + case Cr.NS_ERROR_FILE_NOT_FOUND: + return new Error("path does not exist: " + filename); + } + return isResult ? new Error("XPCOM error code: " + errOrResult) : errOrResult; +} + +exports.exists = function exists(filename) { + return MozFile(filename).exists(); +}; + +exports.isFile = function isFile(filename) { + return MozFile(filename).isFile(); +}; + +exports.read = function read(filename, mode) { + if (typeof(mode) !== "string") + mode = ""; + + // Ensure mode is read-only. + mode = /b/.test(mode) ? "b" : ""; + + var stream = exports.open(filename, mode); + try { + var str = stream.read(); + } + finally { + stream.close(); + } + + return str; +}; + +exports.join = function join(base) { + if (arguments.length < 2) + throw new Error("need at least 2 args"); + base = MozFile(base); + for (var i = 1; i < arguments.length; i++) + base.append(arguments[i]); + return base.path; +}; + +exports.dirname = function dirname(path) { + var parent = MozFile(path).parent; + return parent ? parent.path : ""; +}; + +exports.basename = function basename(path) { + var leafName = MozFile(path).leafName; + + // On Windows, leafName when the path is a volume letter and colon ("c:") is + // the path itself. But such a path has no basename, so we want the empty + // string. + return leafName == path ? "" : leafName; +}; + +exports.list = function list(path) { + var file = MozFile(path); + ensureDir(file); + ensureReadable(file); + + var entries = file.directoryEntries; + var entryNames = []; + while(entries.hasMoreElements()) { + var entry = entries.getNext(); + entry.QueryInterface(Ci.nsIFile); + entryNames.push(entry.leafName); + } + return entryNames; +}; + +exports.open = function open(filename, mode) { + var file = MozFile(filename); + if (typeof(mode) !== "string") + mode = ""; + + // File opened for write only. + if (/w/.test(mode)) { + if (file.exists()) + ensureFile(file); + var stream = Cc['@mozilla.org/network/file-output-stream;1']. + createInstance(Ci.nsIFileOutputStream); + var openFlags = OPEN_FLAGS.WRONLY | + OPEN_FLAGS.CREATE_FILE | + OPEN_FLAGS.TRUNCATE; + var permFlags = parseInt("0644"); // u+rw go+r + try { + stream.init(file, openFlags, permFlags, 0); + } + catch (err) { + throw friendlyError(err, filename); + } + return /b/.test(mode) ? + new byteStreams.ByteWriter(stream) : + new textStreams.TextWriter(stream); + } + + // File opened for read only, the default. + ensureFile(file); + stream = Cc['@mozilla.org/network/file-input-stream;1']. + createInstance(Ci.nsIFileInputStream); + try { + stream.init(file, OPEN_FLAGS.RDONLY, 0, 0); + } + catch (err) { + throw friendlyError(err, filename); + } + return /b/.test(mode) ? + new byteStreams.ByteReader(stream) : + new textStreams.TextReader(stream); +}; + +exports.remove = function remove(path) { + var file = MozFile(path); + ensureFile(file); + file.remove(false); +}; + +exports.mkpath = function mkpath(path) { + var file = MozFile(path); + if (!file.exists()) + file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755")); // u+rwx go+rx + else if (!file.isDirectory()) + throw new Error("The path already exists and is not a directory: " + path); +}; + +exports.rmdir = function rmdir(path) { + var file = MozFile(path); + ensureDir(file); + try { + file.remove(false); + } + catch (err) { + // Bug 566950 explains why we're not catching a specific exception here. + throw new Error("The directory is not empty: " + path); + } +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/find-tests.js b/tools/addon-sdk-1.7/packages/api-utils/lib/find-tests.js new file mode 100644 index 0000000..60550bf --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/find-tests.js @@ -0,0 +1,5 @@ +/* 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/. */ + +// this file left intentionally blank diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/frame/utils.js b/tools/addon-sdk-1.7/packages/api-utils/lib/frame/utils.js new file mode 100644 index 0000000..5c9da38 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/frame/utils.js @@ -0,0 +1,59 @@ +/* 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 XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + +/** + * Creates a XUL `browser` element in a privileged document. + * @params {nsIDOMDocument} document + * @params {String} options.type + * By default is 'content' for possible values see: + * https://developer.mozilla.org/en/XUL/iframe#a-browser.type + * @params {String} options.uri + * URI of the document to be loaded into created frame. + * @params {Boolean} options.remote + * If `true` separate process will be used for this frame, also in such + * case all the following options are ignored. + * @params {Boolean} options.allowAuth + * Whether to allow auth dialogs. Defaults to `false`. + * @params {Boolean} options.allowJavascript + * Whether to allow Javascript execution. Defaults to `false`. + * @params {Boolean} options.allowPlugins + * Whether to allow plugin execution. Defaults to `false`. + */ +function create(document, options) { + options = options || {}; + let remote = 'remote' in options && options.remote === true; + + let frame = document.createElementNS(XUL, 'browser'); + // Type="content" is mandatory to enable stuff here: + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1776 + frame.setAttribute('type', options.type || 'content'); + frame.setAttribute('src', options.uri || 'about:blank'); + + // Load in separate process if `options.remote` is `true`. + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1347 + if (remote) { + // We remove XBL binding to avoid execution of code that is not going to + // work because browser has no docShell attribute in remote mode + // (for example) + frame.setAttribute('style', '-moz-binding: none;'); + frame.setAttribute('remote', 'true'); + } + + document.documentElement.appendChild(frame); + + // If browser is remote it won't have a `docShell`. + if (!remote) { + let docShell = frame.docShell; + docShell.allowAuth = options.allowAuth || false; + docShell.allowJavascript = options.allowJavascript || false; + docShell.allowPlugins = options.allowPlugins || false; + } + + return frame; +} +exports.create = create; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/functional.js b/tools/addon-sdk-1.7/packages/api-utils/lib/functional.js new file mode 100644 index 0000000..61738d9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/functional.js @@ -0,0 +1,159 @@ +/* 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/. */ + +// Disclaimer: Most of the functions in this module implement APIs from +// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for +// those goes to him. + +"use strict"; + +const { setTimeout } = require("./timer"); + +/** + * Takes `lambda` function and returns a method. When returned method is + * invoked it calls wrapped `lambda` and passes `this` as a first argument + * and given argument as rest. + */ +function method(lambda) { + return function method() { + return lambda.apply(null, [this].concat(Array.slice(arguments))); + } +} +exports.method = method; + +/** + * Takes a function and returns a wrapped one instead, calling which will call + * original function in the next turn of event loop. This is basically utility + * to do `setTimeout(function() { ... }, 0)`, with a difference that returned + * function is reused, instead of creating a new one each time. This also allows + * to use this functions as event listeners. + */ +function defer(f) { + return function deferred() + setTimeout(invoke, 0, f, arguments, this); +} +exports.defer = defer; +// Exporting `remit` alias as `defer` may conflict with promises. +exports.remit = defer; + +/** + * Invokes `callee` by passing `params` as an arguments and `self` as `this` + * pseudo-variable. Returns value that is returned by a callee. + * @param {Function} callee + * Function to invoke. + * @param {Array} params + * Arguments to invoke function with. + * @param {Object} self + * Object to be passed as a `this` pseudo variable. + */ +function invoke(callee, params, self) callee.apply(self, params); +exports.invoke = invoke; + +/** + * Curries a function with the arguments given. + * + * @param {Function} fn + * The function to curry + * + * @returns The function curried + */ +function curry(fn) { + if (typeof fn !== "function") + throw new TypeError(String(fn) + " is not a function"); + + let args = Array.slice(arguments, 1); + + return function() fn.apply(this, args.concat(Array.slice(arguments))); +} +exports.curry = curry; + +/** + * Returns the composition of a list of functions, where each function consumes + * the return value of the function that follows. In math terms, composing the + * functions `f()`, `g()`, and `h()` produces `f(g(h()))`. + * @example + * + * var greet = function(name) { return "hi: " + name; }; + * var exclaim = function(statement) { return statement + "!"; }; + * var welcome = compose(exclaim, greet); + * + * welcome('moe'); // => 'hi: moe!' + */ +function compose() { + let lambdas = Array.slice(arguments); + return function composed() { + let args = Array.slice(arguments), index = lambdas.length; + while (0 <= --index) + args = [ lambdas[index].apply(this, args) ]; + return args[0]; + }; +} +exports.compose = compose; + +/* + * Returns the first function passed as an argument to the second, + * allowing you to adjust arguments, run code before and after, and + * conditionally execute the original function. + * @example + * + * var hello = function(name) { return "hello: " + name; }; + * hello = wrap(hello, function(f) { + * return "before, " + f("moe") + ", after"; + * }); + * + * hello(); // => 'before, hello: moe, after' + */ +function wrap(f, wrapper) { + return function wrapped() + wrapper.apply(this, [ f ].concat(Array.slice(arguments))) +}; +exports.wrap = wrap; + +/** + * Returns the same value that is used as the argument. In math: f(x) = x + */ +function identity(value) value +exports.identity = identity; + +/** + * Memoizes a given function by caching the computed result. Useful for + * speeding up slow-running computations. If passed an optional hashFunction, + * it will be used to compute the hash key for storing the result, based on + * the arguments to the original function. The default hashFunction just uses + * the first argument to the memoized function as the key. + */ +function memoize(f, hasher) { + let memo = Object.create(null); + hasher = hasher || identity; + return function memoizer() { + let key = hasher.apply(this, arguments); + return key in memo ? memo[key] : (memo[key] = f.apply(this, arguments)); + }; +} +exports.memoize = memoize; + +/** + * Much like setTimeout, invokes function after wait milliseconds. If you pass + * the optional arguments, they will be forwarded on to the function when it is + * invoked. + */ +function delay(f, ms) { + let args = Array.slice(arguments, 2); + setTimeout(function(context) { return f.apply(context, args); }, ms, this); +}; +exports.delay = delay; + +/** + * Creates a version of the function that can only be called one time. Repeated + * calls to the modified function will have no effect, returning the value from + * the original call. Useful for initialization functions, instead of having to + * set a boolean flag and then check it later. + */ +function once(f) { + let ran = false, cache; + return function() ran ? cache : (ran = true, cache = f.apply(this, arguments)) +}; +exports.once = once; +// export cache as once will may be conflicting with event once a lot. +exports.cache = once; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/globals!.js b/tools/addon-sdk-1.7/packages/api-utils/lib/globals!.js new file mode 100644 index 0000000..01e85b7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/globals!.js @@ -0,0 +1,93 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* 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"; + +let { Cc, Ci } = require('chrome'); +let { PlainTextConsole } = require('./plain-text-console'); +let options = require('@packaging'); +let consoleService = Cc['@mozilla.org/consoleservice;1'].getService(). + QueryInterface(Ci.nsIConsoleService); + +// On windows dump does not writes into stdout so cfx can't read thous dumps. +// To workaround this issue we write to a special file from which cfx will +// read and print to the console. +// For more details see: bug-673383 +exports.dump = (function define(global) { + const PR_WRONLY = 0x02; + const PR_CREATE_FILE = 0x08; + const PR_APPEND = 0x10; + let print = Object.getPrototypeOf(global).dump + if (print) return print; + if ('logFile' in options) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(options.logFile); + let stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(file, PR_WRONLY|PR_CREATE_FILE|PR_APPEND, -1, 0); + + return function print(message) { + message = String(message); + stream.write(message, message.length); + stream.flush(); + }; + } + return dump; +})(this); + +// Override the default Iterator function with one that passes +// a second argument to custom iterator methods that identifies +// the call as originating from an Iterator function so the custom +// iterator method can return [key, value] pairs just like default +// iterators called via the default Iterator function. +exports.Iterator = (function(DefaultIterator) { + return function Iterator(obj, keysOnly) { + if ("__iterator__" in obj && !keysOnly) + return obj.__iterator__.call(obj, false, true); + return DefaultIterator(obj, keysOnly); + }; +})(Iterator); + +// Bug 718230: We need to send console messages to stdout and JS Console +function forsakenConsoleDump(msg, level) { + exports.dump(msg); + + if (level === "error") { + let err = Cc["@mozilla.org/scripterror;1"]. + createInstance(Ci.nsIScriptError); + msg = msg.replace(/^error: /, ""); + err.init(msg, null, null, 0, 0, 0, "Add-on SDK"); + consoleService.logMessage(err); + } + else + consoleService.logStringMessage(msg); +}; +exports.console = new PlainTextConsole(forsakenConsoleDump); + +// Provide CommonJS `define` to allow authoring modules in a format that can be +// loaded both into jetpack and into browser via AMD loaders. +Object.defineProperty(exports, 'define', { + // `define` is provided as a lazy getter that binds below defined `define` + // function to the module scope, so that require, exports and module + // variables remain accessible. + configurable: true, + get: (function() { + function define(factory) { + factory = Array.slice(arguments).pop(); + factory.call(this, this.require, this.exports, this.module); + } + + return function getter() { + // Redefine `define` as a static property to make sure that module + // gets access to the same function so that `define === define` is + // `true`. + Object.defineProperty(this, 'define', { + configurable: false, + value: define.bind(this) + }); + return this.define; + } + })() +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/hidden-frame.js b/tools/addon-sdk-1.7/packages/api-utils/lib/hidden-frame.js new file mode 100644 index 0000000..6f0016b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/hidden-frame.js @@ -0,0 +1,166 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 errors = require("./errors"); +const apiUtils = require("./api-utils"); +const timer = require("./timer"); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +let hostFrame, hostDocument, hiddenWindow, isHostFrameReady = false; + +if (!require("./xul-app").isOneOf(["Firefox", "Fennec", "Thunderbird"])) { + throw new Error([ + "The hidden-frame module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")); +} + +let appShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); +hiddenWindow = appShellService.hiddenDOMWindow; + +if (!hiddenWindow) { + throw new Error([ + "The hidden-frame module needs an app that supports a hidden window. ", + "We would like it to support other applications, however. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more information." + ].join("")); +} + +// Check if we can use the hidden window itself to host our iframes. +// If it's not a suitable host, the hostFrame will be lazily created +// by the first HiddenFrame instance. +if (hiddenWindow.location.protocol == "chrome:" && + (hiddenWindow.document.contentType == "application/vnd.mozilla.xul+xml" || + hiddenWindow.document.contentType == "application/xhtml+xml")) { + hostFrame = hiddenWindow; + hostDocument = hiddenWindow.document; + isHostFrameReady = true; +} + +function setHostFrameReady() { + hostDocument = hostFrame.contentDocument; + hostFrame.removeEventListener("DOMContentLoaded", setHostFrameReady, false); + isHostFrameReady = true; +} + +// This cache is used to access friend properties between functions +// without exposing them on the public API. +let cache = []; + +exports.HiddenFrame = apiUtils.publicConstructor(HiddenFrame); + +function HiddenFrame(options) { + options = options || {}; + let self = this; + + for each (let [key, val] in Iterator(apiUtils.validateOptions(options, { + onReady: { + is: ["undefined", "function", "array"], + ok: function(v) { + if (apiUtils.getTypeOf(v) === "array") { + // make sure every item is a function + return v.every(function (item) typeof(item) === "function") + } + return true; + } + } + }))) { + if (typeof(val) != "undefined") + options[key] = val; + } + + require("./collection").addCollectionProperty(this, "onReady"); + if (options.onReady) + this.onReady.add(options.onReady); + + if (!hostFrame) { + hostFrame = hiddenWindow.document.createElement("iframe"); + + // ugly ugly hack. This is the most lightweight chrome:// file I could find on the tree + // This hack should be removed by proper platform support on bug 565388 + hostFrame.setAttribute("src", "chrome://global/content/mozilla.xhtml"); + hostFrame.addEventListener("DOMContentLoaded", setHostFrameReady, false); + + hiddenWindow.document.body.appendChild(hostFrame); + } + + this.toString = function toString() "[object Frame]"; +} + +exports.add = function JP_SDK_Frame_add(frame) { + if (!(frame instanceof HiddenFrame)) + throw new Error("The object to be added must be a HiddenFrame."); + + // This instance was already added. + if (cache.filter(function (v) v.frame === frame)[0]) + return frame; + + function createElement() { + hostFrame.removeEventListener("DOMContentLoaded", createElement, false); + + let element = hostDocument.createElementNS(XUL_NS, "iframe"); + + element.setAttribute("type", "content"); + hostDocument.documentElement.appendChild(element); + + /* Public API: hiddenFrame.element */ + frame.__defineGetter__("element", function () element); + + // Notify consumers that the frame is ready. + function onReadyListener(event) { + element.removeEventListener("DOMContentLoaded", onReadyListener, false); + if (event.target == element.contentDocument) { + for (let handler in frame.onReady) + errors.catchAndLog(function () handler.call(frame))(); + } + } + element.addEventListener("DOMContentLoaded", onReadyListener, false); + + cache.push({ + frame: frame, + element: element, + unload: function unload() { + hostDocument.documentElement.removeChild(element); + } + }); + } + + /* Begin element construction or schedule it for later */ + if (isHostFrameReady) { + createElement(); + } else { + hostFrame.addEventListener("DOMContentLoaded", createElement, false); + } + + return frame; +} + +exports.remove = function remove(frame) { + if (!(frame instanceof HiddenFrame)) + throw new Error("The object to be removed must be a HiddenFrame."); + + let entry = cache.filter(function (v) v.frame === frame)[0]; + if (!entry) + return; + + entry.unload(); + cache.splice(cache.indexOf(entry), 1); +} + +require("./unload").when(function () { + for each (let entry in cache.slice()) + exports.remove(entry.frame); + + if (hostFrame && hostFrame !== hiddenWindow) + hiddenWindow.document.body.removeChild(hostFrame); +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/httpd.js b/tools/addon-sdk-1.7/packages/api-utils/lib/httpd.js new file mode 100644 index 0000000..7f6a363 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/httpd.js @@ -0,0 +1,5166 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* +* An implementation of an HTTP server both as a loadable script and as an XPCOM +* component. See the accompanying README file for user documentation on +* httpd.js. +*/ + + +var {components,Cc,Ci,Cr,Cu} = require("chrome"); +components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +const CC = components.Constructor; + +const PR_UINT32_MAX = Math.pow(2, 32) - 1; + +/** True if debugging output is enabled, false otherwise. */ +var DEBUG = false; // non-const *only* so tweakable in server tests + +/** True if debugging output should be timestamped. */ +var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests + +var gGlobalObject = Cc["@mozilla.org/systemprincipal;1"].createInstance(); + +/** +* Asserts that the given condition holds. If it doesn't, the given message is +* dumped, a stack trace is printed, and an exception is thrown to attempt to +* stop execution (which unfortunately must rely upon the exception not being +* accidentally swallowed by the code that uses it). +*/ +function NS_ASSERT(cond, msg) +{ + if (DEBUG && !cond) + { + dumpn("###!!!"); + dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!")); + dumpn("###!!! Stack follows:"); + + var stack = new Error().stack.split(/\n/); + dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n")); + + throw Cr.NS_ERROR_ABORT; + } +} + +/** Constructs an HTTP error object. */ +function HttpError(code, description) +{ + this.code = code; + this.description = description; +} +HttpError.prototype = +{ + toString: function() + { + return this.code + " " + this.description; + } +}; + +/** +* Errors thrown to trigger specific HTTP server responses. +*/ +const HTTP_400 = new HttpError(400, "Bad Request"); +const HTTP_401 = new HttpError(401, "Unauthorized"); +const HTTP_402 = new HttpError(402, "Payment Required"); +const HTTP_403 = new HttpError(403, "Forbidden"); +const HTTP_404 = new HttpError(404, "Not Found"); +const HTTP_405 = new HttpError(405, "Method Not Allowed"); +const HTTP_406 = new HttpError(406, "Not Acceptable"); +const HTTP_407 = new HttpError(407, "Proxy Authentication Required"); +const HTTP_408 = new HttpError(408, "Request Timeout"); +const HTTP_409 = new HttpError(409, "Conflict"); +const HTTP_410 = new HttpError(410, "Gone"); +const HTTP_411 = new HttpError(411, "Length Required"); +const HTTP_412 = new HttpError(412, "Precondition Failed"); +const HTTP_413 = new HttpError(413, "Request Entity Too Large"); +const HTTP_414 = new HttpError(414, "Request-URI Too Long"); +const HTTP_415 = new HttpError(415, "Unsupported Media Type"); +const HTTP_417 = new HttpError(417, "Expectation Failed"); + +const HTTP_500 = new HttpError(500, "Internal Server Error"); +const HTTP_501 = new HttpError(501, "Not Implemented"); +const HTTP_502 = new HttpError(502, "Bad Gateway"); +const HTTP_503 = new HttpError(503, "Service Unavailable"); +const HTTP_504 = new HttpError(504, "Gateway Timeout"); +const HTTP_505 = new HttpError(505, "HTTP Version Not Supported"); + +/** Creates a hash with fields corresponding to the values in arr. */ +function array2obj(arr) +{ + var obj = {}; + for (var i = 0; i < arr.length; i++) + obj[arr[i]] = arr[i]; + return obj; +} + +/** Returns an array of the integers x through y, inclusive. */ +function range(x, y) +{ + var arr = []; + for (var i = x; i <= y; i++) + arr.push(i); + return arr; +} + +/** An object (hash) whose fields are the numbers of all HTTP error codes. */ +const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505))); + + +/** +* The character used to distinguish hidden files from non-hidden files, a la +* the leading dot in Apache. Since that mechanism also hides files from +* easy display in LXR, ls output, etc. however, we choose instead to use a +* suffix character. If a requested file ends with it, we append another +* when getting the file on the server. If it doesn't, we just look up that +* file. Therefore, any file whose name ends with exactly one of the character +* is "hidden" and available for use by the server. +*/ +const HIDDEN_CHAR = "^"; + +/** +* The file name suffix indicating the file containing overridden headers for +* a requested file. +*/ +const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR; + +/** Type used to denote SJS scripts for CGI-like functionality. */ +const SJS_TYPE = "sjs"; + +/** Base for relative timestamps produced by dumpn(). */ +var firstStamp = 0; + +/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */ +function dumpn(str) +{ + if (DEBUG) + { + var prefix = "HTTPD-INFO | "; + if (DEBUG_TIMESTAMP) + { + if (firstStamp === 0) + firstStamp = Date.now(); + + var elapsed = Date.now() - firstStamp; // milliseconds + var min = Math.floor(elapsed / 60000); + var sec = (elapsed % 60000) / 1000; + + if (sec < 10) + prefix += min + ":0" + sec.toFixed(3) + " | "; + else + prefix += min + ":" + sec.toFixed(3) + " | "; + } + + dump(prefix + str + "\n"); + } +} + +/** Dumps the current JS stack if DEBUG. */ +function dumpStack() +{ + // peel off the frames for dumpStack() and Error() + var stack = new Error().stack.split(/\n/).slice(2); + stack.forEach(dumpn); +} + + +/** The XPCOM thread manager. */ +var gThreadManager = null; + +/** The XPCOM prefs service. */ +var gRootPrefBranch = null; +function getRootPrefBranch() +{ + if (!gRootPrefBranch) + { + gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + } + return gRootPrefBranch; +} + +/** +* JavaScript constructors for commonly-used classes; precreating these is a +* speedup over doing the same from base principles. See the docs at +* http://developer.mozilla.org/en/docs/components.Constructor for details. +*/ +const ServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init"); +const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init"); +const Pipe = CC("@mozilla.org/pipe;1", + "nsIPipe", + "init"); +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init"); +const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1", + "nsIConverterInputStream", + "init"); +const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1", + "nsIWritablePropertyBag2"); +const SupportsString = CC("@mozilla.org/supports-string;1", + "nsISupportsString"); + +/* These two are non-const only so a test can overwrite them. */ +var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); +var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream"); + +/** +* Returns the RFC 822/1123 representation of a date. +* +* @param date : Number +* the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT +* @returns string +* the representation of the given date +*/ +function toDateString(date) +{ + // + // rfc1123-date = wkday "," SP date1 SP time SP "GMT" + // date1 = 2DIGIT SP month SP 4DIGIT + // ; day month year (e.g., 02 Jun 1982) + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + // ; 00:00:00 - 23:59:59 + // wkday = "Mon" | "Tue" | "Wed" + // | "Thu" | "Fri" | "Sat" | "Sun" + // month = "Jan" | "Feb" | "Mar" | "Apr" + // | "May" | "Jun" | "Jul" | "Aug" + // | "Sep" | "Oct" | "Nov" | "Dec" + // + + const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + /** +* Processes a date and returns the encoded UTC time as a string according to +* the format specified in RFC 2616. +* +* @param date : Date +* the date to process +* @returns string +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" +*/ + function toTime(date) + { + var hrs = date.getUTCHours(); + var rv = (hrs < 10) ? "0" + hrs : hrs; + + var mins = date.getUTCMinutes(); + rv += ":"; + rv += (mins < 10) ? "0" + mins : mins; + + var secs = date.getUTCSeconds(); + rv += ":"; + rv += (secs < 10) ? "0" + secs : secs; + + return rv; + } + + /** +* Processes a date and returns the encoded UTC date as a string according to +* the date1 format specified in RFC 2616. +* +* @param date : Date +* the date to process +* @returns string +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" +*/ + function toDate1(date) + { + var day = date.getUTCDate(); + var month = date.getUTCMonth(); + var year = date.getUTCFullYear(); + + var rv = (day < 10) ? "0" + day : day; + rv += " " + monthStrings[month]; + rv += " " + year; + + return rv; + } + + date = new Date(date); + + const fmtString = "%wkday%, %date1% %time% GMT"; + var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]); + rv = rv.replace("%time%", toTime(date)); + return rv.replace("%date1%", toDate1(date)); +} + +/** +* Prints out a human-readable representation of the object o and its fields, +* omitting those whose names begin with "_" if showMembers != true (to ignore +* "private" properties exposed via getters/setters). +*/ +function printObj(o, showMembers) +{ + var s = "******************************\n"; + s += "o = {\n"; + for (var i in o) + { + if (typeof(i) != "string" || + (showMembers || (i.length > 0 && i[0] != "_"))) + s+= " " + i + ": " + o[i] + ",\n"; + } + s += " };\n"; + s += "******************************"; + dumpn(s); +} + +/** +* Instantiates a new HTTP server. +*/ +function nsHttpServer() +{ + if (!gThreadManager) + gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + + /** The port on which this server listens. */ + this._port = undefined; + + /** The socket associated with this. */ + this._socket = null; + + /** The handler used to process requests to this server. */ + this._handler = new ServerHandler(this); + + /** Naming information for this server. */ + this._identity = new ServerIdentity(); + + /** +* Indicates when the server is to be shut down at the end of the request. +*/ + this._doQuit = false; + + /** +* True if the socket in this is closed (and closure notifications have been +* sent and processed if the socket was ever opened), false otherwise. +*/ + this._socketClosed = true; + + /** +* Used for tracking existing connections and ensuring that all connections +* are properly cleaned up before server shutdown; increases by 1 for every +* new incoming connection. +*/ + this._connectionGen = 0; + + /** +* Hash of all open connections, indexed by connection number at time of +* creation. +*/ + this._connections = {}; +} +nsHttpServer.prototype = +{ + classID: components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"), + + // NSISERVERSOCKETLISTENER + + /** +* Processes an incoming request coming in on the given socket and contained +* in the given transport. +* +* @param socket : nsIServerSocket +* the socket through which the request was served +* @param trans : nsISocketTransport +* the transport for the request/response +* @see nsIServerSocketListener.onSocketAccepted +*/ + onSocketAccepted: function(socket, trans) + { + dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")"); + + dumpn(">>> new connection on " + trans.host + ":" + trans.port); + + const SEGMENT_SIZE = 8192; + const SEGMENT_COUNT = 1024; + try + { + var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) + .QueryInterface(Ci.nsIAsyncInputStream); + var output = trans.openOutputStream(0, 0, 0); + } + catch (e) + { + dumpn("*** error opening transport streams: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + var connectionNumber = ++this._connectionGen; + + try + { + var conn = new Connection(input, output, this, socket.port, trans.port, + connectionNumber); + var reader = new RequestReader(conn); + + // XXX add request timeout functionality here! + + // Note: must use main thread here, or we might get a GC that will cause + // threadsafety assertions. We really need to fix XPConnect so that + // you can actually do things in multi-threaded JS. :-( + input.asyncWait(reader, 0, 0, gThreadManager.mainThread); + } + catch (e) + { + // Assume this connection can't be salvaged and bail on it completely; + // don't attempt to close it so that we can assert that any connection + // being closed is in this._connections. + dumpn("*** error in initial request-processing stages: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + this._connections[connectionNumber] = conn; + dumpn("*** starting connection " + connectionNumber); + }, + + /** +* Called when the socket associated with this is closed. +* +* @param socket : nsIServerSocket +* the socket being closed +* @param status : nsresult +* the reason the socket stopped listening (NS_BINDING_ABORTED if the server +* was stopped using nsIHttpServer.stop) +* @see nsIServerSocketListener.onStopListening +*/ + onStopListening: function(socket, status) + { + dumpn(">>> shutting down server on port " + socket.port); + this._socketClosed = true; + if (!this._hasOpenConnections()) + { + dumpn("*** no open connections, notifying async from onStopListening"); + + // Notify asynchronously so that any pending teardown in stop() has a + // chance to run first. + var self = this; + var stopEvent = + { + run: function() + { + dumpn("*** _notifyStopped async callback"); + self._notifyStopped(); + } + }; + gThreadManager.currentThread + .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + // NSIHTTPSERVER + + // + // see nsIHttpServer.start + // + start: function(port) + { + this._start(port, "localhost") + }, + + _start: function(port, host) + { + if (this._socket) + throw Cr.NS_ERROR_ALREADY_INITIALIZED; + + this._port = port; + this._doQuit = this._socketClosed = false; + + this._host = host; + + // The listen queue needs to be long enough to handle + // network.http.max-connections-per-server concurrent connections, + // plus a safety margin in case some other process is talking to + // the server as well. + var prefs = getRootPrefBranch(); + var maxConnections = + prefs.getIntPref("network.http.max-connections-per-server") + 5; + + try + { + var loopback = true; + if (this._host != "127.0.0.1" && this._host != "localhost") { + var loopback = false; + } + + var socket = new ServerSocket(this._port, + loopback, // true = localhost, false = everybody + maxConnections); + dumpn(">>> listening on port " + socket.port + ", " + maxConnections + + " pending connections"); + socket.asyncListen(this); + this._identity._initialize(port, host, true); + this._socket = socket; + } + catch (e) + { + dumpn("!!! could not start server on port " + port + ": " + e); + throw Cr.NS_ERROR_NOT_AVAILABLE; + } + }, + + // + // see nsIHttpServer.stop + // + stop: function(callback) + { + if (!callback) + throw Cr.NS_ERROR_NULL_POINTER; + if (!this._socket) + throw Cr.NS_ERROR_UNEXPECTED; + + this._stopCallback = typeof callback === "function" + ? callback + : function() { callback.onStopped(); }; + + dumpn(">>> stopping listening on port " + this._socket.port); + this._socket.close(); + this._socket = null; + + // We can't have this identity any more, and the port on which we're running + // this server now could be meaningless the next time around. + this._identity._teardown(); + + this._doQuit = false; + + // socket-close notification and pending request completion happen async + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (file && (!file.exists() || file.isDirectory())) + throw Cr.NS_ERROR_INVALID_ARG; + + this._handler.registerFile(path, file); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // XXX true path validation! + if (path.charAt(0) != "/" || + path.charAt(path.length - 1) != "/" || + (directory && + (!directory.exists() || !directory.isDirectory()))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping + // exists! + + this._handler.registerDirectory(path, directory); + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + this._handler.registerPathHandler(path, handler); + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(code, handler) + { + this._handler.registerErrorHandler(code, handler); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + this._handler.setIndexHandler(handler); + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + this._handler.registerContentType(ext, type); + }, + + // + // see nsIHttpServer.serverIdentity + // + get identity() + { + return this._identity; + }, + + // + // see nsIHttpServer.getState + // + getState: function(path, k) + { + return this._handler._getState(path, k); + }, + + // + // see nsIHttpServer.setState + // + setState: function(path, k, v) + { + return this._handler._setState(path, k, v); + }, + + // + // see nsIHttpServer.getSharedState + // + getSharedState: function(k) + { + return this._handler._getSharedState(k); + }, + + // + // see nsIHttpServer.setSharedState + // + setSharedState: function(k, v) + { + return this._handler._setSharedState(k, v); + }, + + // + // see nsIHttpServer.getObjectState + // + getObjectState: function(k) + { + return this._handler._getObjectState(k); + }, + + // + // see nsIHttpServer.setObjectState + // + setObjectState: function(k, v) + { + return this._handler._setObjectState(k, v); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIServerSocketListener) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NON-XPCOM PUBLIC API + + /** +* Returns true iff this server is not running (and is not in the process of +* serving any requests still to be processed when the server was last +* stopped after being run). +*/ + isStopped: function() + { + return this._socketClosed && !this._hasOpenConnections(); + }, + + // PRIVATE IMPLEMENTATION + + /** True if this server has any open connections to it, false otherwise. */ + _hasOpenConnections: function() + { + // + // If we have any open connections, they're tracked as numeric properties on + // |this._connections|. The non-standard __count__ property could be used + // to check whether there are any properties, but standard-wise, even + // looking forward to ES5, there's no less ugly yet still O(1) way to do + // this. + // + for (var n in this._connections) + return true; + return false; + }, + + /** Calls the server-stopped callback provided when stop() was called. */ + _notifyStopped: function() + { + NS_ASSERT(this._stopCallback !== null, "double-notifying?"); + NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now"); + + // + // NB: We have to grab this now, null out the member, *then* call the + // callback here, or otherwise the callback could (indirectly) futz with + // this._stopCallback by starting and immediately stopping this, at + // which point we'd be nulling out a field we no longer have a right to + // modify. + // + var callback = this._stopCallback; + this._stopCallback = null; + try + { + callback(); + } + catch (e) + { + // not throwing because this is specified as being usually (but not + // always) asynchronous + dump("!!! error running onStopped callback: " + e + "\n"); + } + }, + + /** +* Notifies this server that the given connection has been closed. +* +* @param connection : Connection +* the connection that was closed +*/ + _connectionClosed: function(connection) + { + NS_ASSERT(connection.number in this._connections, + "closing a connection " + this + " that we never added to the " + + "set of open connections?"); + NS_ASSERT(this._connections[connection.number] === connection, + "connection number mismatch? " + + this._connections[connection.number]); + delete this._connections[connection.number]; + + // Fire a pending server-stopped notification if it's our responsibility. + if (!this._hasOpenConnections() && this._socketClosed) + this._notifyStopped(); + }, + + /** +* Requests that the server be shut down when possible. +*/ + _requestQuit: function() + { + dumpn(">>> requesting a quit"); + dumpStack(); + this._doQuit = true; + } +}; + + +// +// RFC 2396 section 3.2.2: +// +// host = hostname | IPv4address +// hostname = *( domainlabel "." ) toplabel [ "." ] +// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum +// toplabel = alpha | alpha *( alphanum | "-" ) alphanum +// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit +// + +const HOST_REGEX = + new RegExp("^(?:" + + // *( domainlabel "." ) + "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" + + // toplabel + "[a-z](?:[a-z0-9-]*[a-z0-9])?" + + "|" + + // IPv4 address + "\\d+\\.\\d+\\.\\d+\\.\\d+" + + ")$", + "i"); + + +/** +* Represents the identity of a server. An identity consists of a set of +* (scheme, host, port) tuples denoted as locations (allowing a single server to +* serve multiple sites or to be used behind both HTTP and HTTPS proxies for any +* host/port). Any incoming request must be to one of these locations, or it +* will be rejected with an HTTP 400 error. One location, denoted as the +* primary location, is the location assigned in contexts where a location +* cannot otherwise be endogenously derived, such as for HTTP/1.0 requests. +* +* A single identity may contain at most one location per unique host/port pair; +* other than that, no restrictions are placed upon what locations may +* constitute an identity. +*/ +function ServerIdentity() +{ + /** The scheme of the primary location. */ + this._primaryScheme = "http"; + + /** The hostname of the primary location. */ + this._primaryHost = "127.0.0.1" + + /** The port number of the primary location. */ + this._primaryPort = -1; + + /** +* The current port number for the corresponding server, stored so that a new +* primary location can always be set if the current one is removed. +*/ + this._defaultPort = -1; + + /** +* Maps hosts to maps of ports to schemes, e.g. the following would represent +* https://example.com:789/ and http://example.org/: +* +* { +* "xexample.com": { 789: "https" }, +* "xexample.org": { 80: "http" } +* } +* +* Note the "x" prefix on hostnames, which prevents collisions with special +* JS names like "prototype". +*/ + this._locations = { "xlocalhost": {} }; +} +ServerIdentity.prototype = +{ + // NSIHTTPSERVERIDENTITY + + // + // see nsIHttpServerIdentity.primaryScheme + // + get primaryScheme() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryScheme; + }, + + // + // see nsIHttpServerIdentity.primaryHost + // + get primaryHost() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryHost; + }, + + // + // see nsIHttpServerIdentity.primaryPort + // + get primaryPort() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryPort; + }, + + // + // see nsIHttpServerIdentity.add + // + add: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + this._locations["x" + host] = entry = {}; + + entry[port] = scheme; + }, + + // + // see nsIHttpServerIdentity.remove + // + remove: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return false; + + var present = port in entry; + delete entry[port]; + + if (this._primaryScheme == scheme && + this._primaryHost == host && + this._primaryPort == port && + this._defaultPort !== -1) + { + // Always keep at least one identity in existence at any time, unless + // we're in the process of shutting down (the last condition above). + this._primaryPort = -1; + this._initialize(this._defaultPort, host, false); + } + + return present; + }, + + // + // see nsIHttpServerIdentity.has + // + has: function(scheme, host, port) + { + this._validate(scheme, host, port); + + return "x" + host in this._locations && + scheme === this._locations["x" + host][port]; + }, + + // + // see nsIHttpServerIdentity.has + // + getScheme: function(host, port) + { + this._validate("http", host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return ""; + + return entry[port] || ""; + }, + + // + // see nsIHttpServerIdentity.setPrimary + // + setPrimary: function(scheme, host, port) + { + this._validate(scheme, host, port); + + this.add(scheme, host, port); + + this._primaryScheme = scheme; + this._primaryHost = host; + this._primaryPort = port; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** +* Initializes the primary name for the corresponding server, based on the +* provided port number. +*/ + _initialize: function(port, host, addSecondaryDefault) + { + this._host = host; + if (this._primaryPort !== -1) + this.add("http", host, port); + else + this.setPrimary("http", "localhost", port); + this._defaultPort = port; + + // Only add this if we're being called at server startup + if (addSecondaryDefault && host != "127.0.0.1") + this.add("http", "127.0.0.1", port); + }, + + /** +* Called at server shutdown time, unsets the primary location only if it was +* the default-assigned location and removes the default location from the +* set of locations used. +*/ + _teardown: function() + { + if (this._host != "127.0.0.1") { + // Not the default primary location, nothing special to do here + this.remove("http", "127.0.0.1", this._defaultPort); + } + + // This is a *very* tricky bit of reasoning here; make absolutely sure the + // tests for this code pass before you commit changes to it. + if (this._primaryScheme == "http" && + this._primaryHost == this._host && + this._primaryPort == this._defaultPort) + { + // Make sure we don't trigger the readding logic in .remove(), then remove + // the default location. + var port = this._defaultPort; + this._defaultPort = -1; + this.remove("http", this._host, port); + + // Ensure a server start triggers the setPrimary() path in ._initialize() + this._primaryPort = -1; + } + else + { + // No reason not to remove directly as it's not our primary location + this.remove("http", this._host, this._defaultPort); + } + }, + + /** +* Ensures scheme, host, and port are all valid with respect to RFC 2396. +* +* @throws NS_ERROR_ILLEGAL_VALUE +* if any argument doesn't match the corresponding production +*/ + _validate: function(scheme, host, port) + { + if (scheme !== "http" && scheme !== "https") + { + dumpn("*** server only supports http/https schemes: '" + scheme + "'"); + dumpStack(); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (!HOST_REGEX.test(host)) + { + dumpn("*** unexpected host: '" + host + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (port < 0 || port > 65535) + { + dumpn("*** unexpected port: '" + port + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + } +}; + + +/** +* Represents a connection to the server (and possibly in the future the thread +* on which the connection is processed). +* +* @param input : nsIInputStream +* stream from which incoming data on the connection is read +* @param output : nsIOutputStream +* stream to write data out the connection +* @param server : nsHttpServer +* the server handling the connection +* @param port : int +* the port on which the server is running +* @param outgoingPort : int +* the outgoing port used by this connection +* @param number : uint +* a serial number used to uniquely identify this connection +*/ +function Connection(input, output, server, port, outgoingPort, number) +{ + dumpn("*** opening new connection " + number + " on port " + outgoingPort); + + /** Stream of incoming data. */ + this.input = input; + + /** Stream for outgoing data. */ + this.output = output; + + /** The server associated with this request. */ + this.server = server; + + /** The port on which the server is running. */ + this.port = port; + + /** The outgoing poort used by this connection. */ + this._outgoingPort = outgoingPort; + + /** The serial number of this connection. */ + this.number = number; + + /** +* The request for which a response is being generated, null if the +* incoming request has not been fully received or if it had errors. +*/ + this.request = null; + + /** State variables for debugging. */ + this._closed = this._processed = false; +} +Connection.prototype = +{ + /** Closes this connection's input/output streams. */ + close: function() + { + dumpn("*** closing connection " + this.number + + " on port " + this._outgoingPort); + + this.input.close(); + this.output.close(); + this._closed = true; + + var server = this.server; + server._connectionClosed(this); + + // If an error triggered a server shutdown, act on it now + if (server._doQuit) + server.stop(function() { /* not like we can do anything better */ }); + }, + + /** +* Initiates processing of this connection, using the data in the given +* request. +* +* @param request : Request +* the request which should be processed +*/ + process: function(request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + + this.request = request; + this.server._handler.handleResponse(this); + }, + + /** +* Initiates processing of this connection, generating a response with the +* given HTTP error code. +* +* @param code : uint +* an HTTP code, so in the range [0, 1000) +* @param request : Request +* incomplete data about the incoming request (since there were errors +* during its processing +*/ + processError: function(code, request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + this.request = request; + this.server._handler.handleError(code, this); + }, + + /** Converts this to a string for debugging purposes. */ + toString: function() + { + return "<Connection(" + this.number + + (this.request ? ", " + this.request.path : "") +"): " + + (this._closed ? "closed" : "open") + ">"; + } +}; + + + +/** Returns an array of count bytes from the given input stream. */ +function readBytes(inputStream, count) +{ + return new BinaryInputStream(inputStream).readByteArray(count); +} + + + +/** Request reader processing states; see RequestReader for details. */ +const READER_IN_REQUEST_LINE = 0; +const READER_IN_HEADERS = 1; +const READER_IN_BODY = 2; +const READER_FINISHED = 3; + + +/** +* Reads incoming request data asynchronously, does any necessary preprocessing, +* and forwards it to the request handler. Processing occurs in three states: +* +* READER_IN_REQUEST_LINE Reading the request's status line +* READER_IN_HEADERS Reading headers in the request +* READER_IN_BODY Reading the body of the request +* READER_FINISHED Entire request has been read and processed +* +* During the first two stages, initial metadata about the request is gathered +* into a Request object. Once the status line and headers have been processed, +* we start processing the body of the request into the Request. Finally, when +* the entire body has been read, we create a Response and hand it off to the +* ServerHandler to be given to the appropriate request handler. +* +* @param connection : Connection +* the connection for the request being read +*/ +function RequestReader(connection) +{ + /** Connection metadata for this request. */ + this._connection = connection; + + /** +* A container providing line-by-line access to the raw bytes that make up the +* data which has been read from the connection but has not yet been acted +* upon (by passing it to the request handler or by extracting request +* metadata from it). +*/ + this._data = new LineData(); + + /** +* The amount of data remaining to be read from the body of this request. +* After all headers in the request have been read this is the value in the +* Content-Length header, but as the body is read its value decreases to zero. +*/ + this._contentLength = 0; + + /** The current state of parsing the incoming request. */ + this._state = READER_IN_REQUEST_LINE; + + /** Metadata constructed from the incoming request for the request handler. */ + this._metadata = new Request(connection.port); + + /** +* Used to preserve state if we run out of line data midway through a +* multi-line header. _lastHeaderName stores the name of the header, while +* _lastHeaderValue stores the value we've seen so far for the header. +* +* These fields are always either both undefined or both strings. +*/ + this._lastHeaderName = this._lastHeaderValue = undefined; +} +RequestReader.prototype = +{ + // NSIINPUTSTREAMCALLBACK + + /** +* Called when more data from the incoming request is available. This method +* then reads the available data from input and deals with that data as +* necessary, depending upon the syntax of already-downloaded data. +* +* @param input : nsIAsyncInputStream +* the stream of incoming data from the connection +*/ + onInputStreamReady: function(input) + { + dumpn("*** onInputStreamReady(input=" + input + ") on thread " + + gThreadManager.currentThread + " (main is " + + gThreadManager.mainThread + ")"); + dumpn("*** this._state == " + this._state); + + // Handle cases where we get more data after a request error has been + // discovered but *before* we can close the connection. + var data = this._data; + if (!data) + return; + + try + { + data.appendBytes(readBytes(input, input.available())); + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** WARNING: unexpected error when reading from socket; will " + + "be treated as if the input stream had been closed"); + dumpn("*** WARNING: actual error was: " + e); + } + + // We've lost a race -- input has been closed, but we're still expecting + // to read more data. available() will throw in this case, and since + // we're dead in the water now, destroy the connection. + dumpn("*** onInputStreamReady called on a closed input, destroying " + + "connection"); + this._connection.close(); + return; + } + + switch (this._state) + { + default: + NS_ASSERT(false, "invalid state: " + this._state); + break; + + case READER_IN_REQUEST_LINE: + if (!this._processRequestLine()) + break; + /* fall through */ + + case READER_IN_HEADERS: + if (!this._processHeaders()) + break; + /* fall through */ + + case READER_IN_BODY: + this._processBody(); + } + + if (this._state != READER_FINISHED) + input.asyncWait(this, 0, 0, gThreadManager.currentThread); + }, + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIInputStreamCallback) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE API + + /** +* Processes unprocessed, downloaded data as a request line. +* +* @returns boolean +* true iff the request line has been fully processed +*/ + _processRequestLine: function() + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + // Servers SHOULD ignore any empty line(s) received where a Request-Line + // is expected (section 4.1). + var data = this._data; + var line = {}; + var readSuccess; + while ((readSuccess = data.readLine(line)) && line.value == "") + dumpn("*** ignoring beginning blank line..."); + + // if we don't have a full line, wait until we do + if (!readSuccess) + return false; + + // we have the first non-blank line + try + { + this._parseRequestLine(line.value); + this._state = READER_IN_HEADERS; + return true; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Processes stored data, assuming it is either at the beginning or in +* the middle of processing request headers. +* +* @returns boolean +* true iff header data in the request has been fully processed +*/ + _processHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + // XXX things to fix here: + // + // - need to support RFC 2047-encoded non-US-ASCII characters + + try + { + var done = this._parseHeaders(); + if (done) + { + var request = this._metadata; + + // XXX this is wrong for requests with transfer-encodings applied to + // them, particularly chunked (which by its nature can have no + // meaningful Content-Length header)! + this._contentLength = request.hasHeader("Content-Length") + ? parseInt(request.getHeader("Content-Length"), 10) + : 0; + dumpn("_processHeaders, Content-length=" + this._contentLength); + + this._state = READER_IN_BODY; + } + return done; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Processes stored data, assuming it is either at the beginning or in +* the middle of processing the request body. +* +* @returns boolean +* true iff the request body has been fully processed +*/ + _processBody: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + // XXX handle chunked transfer-coding request bodies! + + try + { + if (this._contentLength > 0) + { + var data = this._data.purge(); + var count = Math.min(data.length, this._contentLength); + dumpn("*** loading data=" + data + " len=" + data.length + + " excess=" + (data.length - count)); + + var bos = new BinaryOutputStream(this._metadata._bodyOutputStream); + bos.writeByteArray(data, count); + this._contentLength -= count; + } + + dumpn("*** remaining body data len=" + this._contentLength); + if (this._contentLength == 0) + { + this._validateRequest(); + this._state = READER_FINISHED; + this._handleResponse(); + return true; + } + + return false; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Does various post-header checks on the data in this request. +* +* @throws : HttpError +* if the request was malformed in some way +*/ + _validateRequest: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + dumpn("*** _validateRequest"); + + var metadata = this._metadata; + var headers = metadata._headers; + + // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header + var identity = this._connection.server.identity; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + { + if (!headers.hasHeader("Host")) + { + dumpn("*** malformed HTTP/1.1 or greater request with no Host header!"); + throw HTTP_400; + } + + // If the Request-URI wasn't absolute, then we need to determine our host. + // We have to determine what scheme was used to access us based on the + // server identity data at this point, because the request just doesn't + // contain enough data on its own to do this, sadly. + if (!metadata._host) + { + var host, port; + var hostPort = headers.getHeader("Host"); + var colon = hostPort.indexOf(":"); + if (colon < 0) + { + host = hostPort; + port = ""; + } + else + { + host = hostPort.substring(0, colon); + port = hostPort.substring(colon + 1); + } + + // NB: We allow an empty port here because, oddly, a colon may be + // present even without a port number, e.g. "example.com:"; in this + // case the default port applies. + if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) + { + dumpn("*** malformed hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + // If we're not given a port, we're stuck, because we don't know what + // scheme to use to look up the correct port here, in general. Since + // the HTTPS case requires a tunnel/proxy and thus requires that the + // requested URI be absolute (and thus contain the necessary + // information), let's assume HTTP will prevail and use that. + port = +port || 80; + + var scheme = identity.getScheme(host, port); + if (!scheme) + { + dumpn("*** unrecognized hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + } + } + else + { + NS_ASSERT(metadata._host === undefined, + "HTTP/1.0 doesn't allow absolute paths in the request line!"); + + metadata._scheme = identity.primaryScheme; + metadata._host = identity.primaryHost; + metadata._port = identity.primaryPort; + } + + NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port), + "must have a location we recognize by now!"); + }, + + /** +* Handles responses in case of error, either in the server or in the request. +* +* @param e +* the specific error encountered, which is an HttpError in the case where +* the request is in some way invalid or cannot be fulfilled; if this isn't +* an HttpError we're going to be paranoid and shut down, because that +* shouldn't happen, ever +*/ + _handleError: function(e) + { + // Don't fall back into normal processing! + this._state = READER_FINISHED; + + var server = this._connection.server; + if (e instanceof HttpError) + { + var code = e.code; + } + else + { + dumpn("!!! UNEXPECTED ERROR: " + e + + (e.lineNumber ? ", line " + e.lineNumber : "")); + + // no idea what happened -- be paranoid and shut down + code = 500; + server._requestQuit(); + } + + // make attempted reuse of data an error + this._data = null; + + this._connection.processError(code, this._metadata); + }, + + /** +* Now that we've read the request line and headers, we can actually hand off +* the request to be handled. +* +* This method is called once per request, after the request line and all +* headers and the body, if any, have been received. +*/ + _handleResponse: function() + { + NS_ASSERT(this._state == READER_FINISHED); + + // We don't need the line-based data any more, so make attempted reuse an + // error. + this._data = null; + + this._connection.process(this._metadata); + }, + + + // PARSING + + /** +* Parses the request line for the HTTP request associated with this. +* +* @param line : string +* the request line +*/ + _parseRequestLine: function(line) + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + dumpn("*** _parseRequestLine('" + line + "')"); + + var metadata = this._metadata; + + // clients and servers SHOULD accept any amount of SP or HT characters + // between fields, even though only a single SP is required (section 19.3) + var request = line.split(/[ \t]+/); + if (!request || request.length != 3) + throw HTTP_400; + + metadata._method = request[0]; + + // get the HTTP version + var ver = request[2]; + var match = ver.match(/^HTTP\/(\d+\.\d+)$/); + if (!match) + throw HTTP_400; + + // determine HTTP version + try + { + metadata._httpVersion = new nsHttpVersion(match[1]); + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) + throw "unsupported HTTP version"; + } + catch (e) + { + // we support HTTP/1.0 and HTTP/1.1 only + throw HTTP_501; + } + + + var fullPath = request[1]; + var serverIdentity = this._connection.server.identity; + + var scheme, host, port; + + if (fullPath.charAt(0) != "/") + { + // No absolute paths in the request line in HTTP prior to 1.1 + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + throw HTTP_400; + + try + { + var uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(fullPath, null, null); + fullPath = uri.path; + scheme = uri.scheme; + host = metadata._host = uri.asciiHost; + port = uri.port; + if (port === -1) + { + if (scheme === "http") + port = 80; + else if (scheme === "https") + port = 443; + else + throw HTTP_400; + } + } + catch (e) + { + // If the host is not a valid host on the server, the response MUST be a + // 400 (Bad Request) error message (section 5.2). Alternately, the URI + // is malformed. + throw HTTP_400; + } + + if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/") + throw HTTP_400; + } + + var splitter = fullPath.indexOf("?"); + if (splitter < 0) + { + // _queryString already set in ctor + metadata._path = fullPath; + } + else + { + metadata._path = fullPath.substring(0, splitter); + metadata._queryString = fullPath.substring(splitter + 1); + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + }, + + /** +* Parses all available HTTP headers in this until the header-ending CRLFCRLF, +* adding them to the store of headers in the request. +* +* @throws +* HTTP_400 if the headers are malformed +* @returns boolean +* true if all headers have now been processed, false otherwise +*/ + _parseHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + dumpn("*** _parseHeaders"); + + var data = this._data; + + var headers = this._metadata._headers; + var lastName = this._lastHeaderName; + var lastVal = this._lastHeaderValue; + + var line = {}; + while (true) + { + NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)), + lastName === undefined ? + "lastVal without lastName? lastVal: '" + lastVal + "'" : + "lastName without lastVal? lastName: '" + lastName + "'"); + + if (!data.readLine(line)) + { + // save any data we have from the header we might still be processing + this._lastHeaderName = lastName; + this._lastHeaderValue = lastVal; + return false; + } + + var lineText = line.value; + var firstChar = lineText.charAt(0); + + // blank line means end of headers + if (lineText == "") + { + // we're finished with the previous header + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** e == " + e); + throw HTTP_400; + } + } + else + { + // no headers in request -- valid for HTTP/1.0 requests + } + + // either way, we're done processing headers + this._state = READER_IN_BODY; + return true; + } + else if (firstChar == " " || firstChar == "\t") + { + // multi-line header if we've already seen a header line + if (!lastName) + { + // we don't have a header to continue! + throw HTTP_400; + } + + // append this line's text to the value; starts with SP/HT, so no need + // for separating whitespace + lastVal += lineText; + } + else + { + // we have a new header, so set the old one (if one existed) + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** e == " + e); + throw HTTP_400; + } + } + + var colon = lineText.indexOf(":"); // first colon must be splitter + if (colon < 1) + { + // no colon or missing header field-name + throw HTTP_400; + } + + // set header name, value (to be set in the next loop, usually) + lastName = lineText.substring(0, colon); + lastVal = lineText.substring(colon + 1); + } // empty, continuation, start of header + } // while (true) + } +}; + + +/** The character codes for CR and LF. */ +const CR = 0x0D, LF = 0x0A; + +/** +* Calculates the number of characters before the first CRLF pair in array, or +* -1 if the array contains no CRLF pair. +* +* @param array : Array +* an array of numbers in the range [0, 256), each representing a single +* character; the first CRLF is the lowest index i where +* |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|, +* if such an |i| exists, and -1 otherwise +* @returns int +* the index of the first CRLF if any were present, -1 otherwise +*/ +function findCRLF(array) +{ + for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1)) + { + if (array[i + 1] == LF) + return i; + } + return -1; +} + + +/** +* A container which provides line-by-line access to the arrays of bytes with +* which it is seeded. +*/ +function LineData() +{ + /** An array of queued bytes from which to get line-based characters. */ + this._data = []; +} +LineData.prototype = +{ + /** +* Appends the bytes in the given array to the internal data cache maintained +* by this. +*/ + appendBytes: function(bytes) + { + Array.prototype.push.apply(this._data, bytes); + }, + + /** +* Removes and returns a line of data, delimited by CRLF, from this. +* +* @param out +* an object whose "value" property will be set to the first line of text +* present in this, sans CRLF, if this contains a full CRLF-delimited line +* of text; if this doesn't contain enough data, the value of the property +* is undefined +* @returns boolean +* true if a full line of data could be read from the data in this, false +* otherwise +*/ + readLine: function(out) + { + var data = this._data; + var length = findCRLF(data); + if (length < 0) + return false; + + // + // We have the index of the CR, so remove all the characters, including + // CRLF, from the array with splice, and convert the removed array into the + // corresponding string, from which we then strip the trailing CRLF. + // + // Getting the line in this matter acknowledges that substring is an O(1) + // operation in SpiderMonkey because strings are immutable, whereas two + // splices, both from the beginning of the data, are less likely to be as + // cheap as a single splice plus two extra character conversions. + // + var line = String.fromCharCode.apply(null, data.splice(0, length + 2)); + out.value = line.substring(0, length); + + return true; + }, + + /** +* Removes the bytes currently within this and returns them in an array. +* +* @returns Array +* the bytes within this when this method is called +*/ + purge: function() + { + var data = this._data; + this._data = []; + return data; + } +}; + + + +/** +* Creates a request-handling function for an nsIHttpRequestHandler object. +*/ +function createHandlerFunc(handler) +{ + return function(metadata, response) { handler.handle(metadata, response); }; +} + + +/** +* The default handler for directories; writes an HTML response containing a +* slightly-formatted directory listing. +*/ +function defaultIndexHandler(metadata, response) +{ + response.setHeader("Content-Type", "text/html", false); + + var path = htmlEscape(decodeURI(metadata.path)); + + // + // Just do a very basic bit of directory listings -- no need for too much + // fanciness, especially since we don't have a style sheet in which we can + // stick rules (don't want to pollute the default path-space). + // + + var body = '<html>\ +<head>\ +<title>' + path + '</title>\ +</head>\ +<body>\ +<h1>' + path + '</h1>\ +<ol style="list-style-type: none">'; + + var directory = metadata.getProperty("directory").QueryInterface(Ci.nsILocalFile); + NS_ASSERT(directory && directory.isDirectory()); + + var fileList = []; + var files = directory.directoryEntries; + while (files.hasMoreElements()) + { + var f = files.getNext().QueryInterface(Ci.nsIFile); + var name = f.leafName; + if (!f.isHidden() && + (name.charAt(name.length - 1) != HIDDEN_CHAR || + name.charAt(name.length - 2) == HIDDEN_CHAR)) + fileList.push(f); + } + + fileList.sort(fileSort); + + for (var i = 0; i < fileList.length; i++) + { + var file = fileList[i]; + try + { + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + var sep = file.isDirectory() ? "/" : ""; + + // Note: using " to delimit the attribute here because encodeURIComponent + // passes through '. + var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' + + htmlEscape(name) + sep + + '</a></li>'; + + body += item; + } + catch (e) { /* some file system error, ignore the file */ } + } + + body += ' </ol>\ +</body>\ +</html>'; + + response.bodyOutputStream.write(body, body.length); +} + +/** +* Sorts a and b (nsIFile objects) into an aesthetically pleasing order. +*/ +function fileSort(a, b) +{ + var dira = a.isDirectory(), dirb = b.isDirectory(); + + if (dira && !dirb) + return -1; + if (dirb && !dira) + return 1; + + var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase(); + return nameb > namea ? -1 : 1; +} + + +/** +* Converts an externally-provided path into an internal path for use in +* determining file mappings. +* +* @param path +* the path to convert +* @param encoded +* true if the given path should be passed through decodeURI prior to +* conversion +* @throws URIError +* if path is incorrectly encoded +*/ +function toInternalPath(path, encoded) +{ + if (encoded) + path = decodeURI(path); + + var comps = path.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) + comps[i] = comp + HIDDEN_CHAR; + } + return comps.join("/"); +} + + +/** +* Adds custom-specified headers for the given file to the given response, if +* any such headers are specified. +* +* @param file +* the file on the disk which is to be written +* @param metadata +* metadata about the incoming request +* @param response +* the Response to which any specified headers/data should be written +* @throws HTTP_500 +* if an error occurred while processing custom-specified headers +*/ +function maybeAddHeaders(file, metadata, response) +{ + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + + var headerFile = file.parent; + headerFile.append(name + HEADERS_SUFFIX); + + if (!headerFile.exists()) + return; + + const PR_RDONLY = 0x01; + var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + var line = {value: ""}; + var more = lis.readLine(line); + + if (!more && line.value == "") + return; + + + // request line + + var status = line.value; + if (status.indexOf("HTTP ") == 0) + { + status = status.substring(5); + var space = status.indexOf(" "); + var code, description; + if (space < 0) + { + code = status; + description = ""; + } + else + { + code = status.substring(0, space); + description = status.substring(space + 1, status.length); + } + + response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description); + + line.value = ""; + more = lis.readLine(line); + } + + // headers + while (more || line.value != "") + { + var header = line.value; + var colon = header.indexOf(":"); + + response.setHeader(header.substring(0, colon), + header.substring(colon + 1, header.length), + false); // allow overriding server-set headers + + line.value = ""; + more = lis.readLine(line); + } + } + catch (e) + { + dumpn("WARNING: error in headers for " + metadata.path + ": " + e); + throw HTTP_500; + } + finally + { + fis.close(); + } +} + + +/** +* An object which handles requests for a server, executing default and +* overridden behaviors as instructed by the code which uses and manipulates it. +* Default behavior includes the paths / and /trace (diagnostics), with some +* support for HTTP error pages for various codes and fallback to HTTP 500 if +* those codes fail for any reason. +* +* @param server : nsHttpServer +* the server in which this handler is being used +*/ +function ServerHandler(server) +{ + // FIELDS + + /** +* The nsHttpServer instance associated with this handler. +*/ + this._server = server; + + /** +* A FileMap object containing the set of path->nsILocalFile mappings for +* all directory mappings set in the server (e.g., "/" for /var/www/html/, +* "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2). +* +* Note carefully: the leading and trailing "/" in each path (not file) are +* removed before insertion to simplify the code which uses this. You have +* been warned! +*/ + this._pathDirectoryMap = new FileMap(); + + /** +* Custom request handlers for the server in which this resides. Path-handler +* pairs are stored as property-value pairs in this property. +* +* @see ServerHandler.prototype._defaultPaths +*/ + this._overridePaths = {}; + + /** +* Custom request handlers for the error handlers in the server in which this +* resides. Path-handler pairs are stored as property-value pairs in this +* property. +* +* @see ServerHandler.prototype._defaultErrors +*/ + this._overrideErrors = {}; + + /** +* Maps file extensions to their MIME types in the server, overriding any +* mapping that might or might not exist in the MIME service. +*/ + this._mimeMappings = {}; + + /** +* The default handler for requests for directories, used to serve directories +* when no index file is present. +*/ + this._indexHandler = defaultIndexHandler; + + /** Per-path state storage for the server. */ + this._state = {}; + + /** Entire-server state storage. */ + this._sharedState = {}; + + /** Entire-server state storage for nsISupports values. */ + this._objectState = {}; +} +ServerHandler.prototype = +{ + // PUBLIC API + + /** +* Handles a request to this server, responding to the request appropriately +* and initiating server shutdown if necessary. +* +* This method never throws an exception. +* +* @param connection : Connection +* the connection for this request +*/ + handleResponse: function(connection) + { + var request = connection.request; + var response = new Response(connection); + + var path = request.path; + dumpn("*** path == " + path); + + try + { + try + { + if (path in this._overridePaths) + { + // explicit paths first, then files based on existing directory mappings, + // then (if the file doesn't exist) built-in server default paths + dumpn("calling override for " + path); + this._overridePaths[path](request, response); + } + else + { + this._handleDefault(request, response); + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + if (!(e instanceof HttpError)) + { + dumpn("*** unexpected error: e == " + e); + throw HTTP_500; + } + if (e.code !== 404) + throw e; + + dumpn("*** default: " + (path in this._defaultPaths)); + + response = new Response(connection); + if (path in this._defaultPaths) + this._defaultPaths[path](request, response); + else + throw HTTP_404; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + var errorCode = "internal"; + + try + { + if (!(e instanceof HttpError)) + throw e; + + errorCode = e.code; + dumpn("*** errorCode == " + errorCode); + + response = new Response(connection); + if (e.customErrorHandling) + e.customErrorHandling(response); + this._handleError(errorCode, request, response); + return; + } + catch (e2) + { + dumpn("*** error handling " + errorCode + " error: " + + "e2 == " + e2 + ", shutting down server"); + + connection.server._requestQuit(); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (!file) + { + dumpn("*** unregistering '" + path + "' mapping"); + delete this._overridePaths[path]; + return; + } + + dumpn("*** registering '" + path + "' as mapping to " + file.path); + file = file.clone(); + + var self = this; + this._overridePaths[path] = + function(request, response) + { + if (!file.exists()) + throw HTTP_404; + + response.setStatusLine(request.httpVersion, 200, "OK"); + self._writeFileResponse(request, file, response, 0, file.fileSize); + }; + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + // XXX true path validation! + if (path.charAt(0) != "/") + throw Cr.NS_ERROR_INVALID_ARG; + + this._handlerToField(handler, this._overridePaths, path); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // strip off leading and trailing '/' so that we can use lastIndexOf when + // determining exactly how a path maps onto a mapped directory -- + // conditional is required here to deal with "/".substring(1, 0) being + // converted to "/".substring(0, 1) per the JS specification + var key = path.length == 1 ? "" : path.substring(1, path.length - 1); + + // the path-to-directory mapping code requires that the first character not + // be "/", or it will go into an infinite loop + if (key.charAt(0) == "/") + throw Cr.NS_ERROR_INVALID_ARG; + + key = toInternalPath(key, false); + + if (directory) + { + dumpn("*** mapping '" + path + "' to the location " + directory.path); + this._pathDirectoryMap.put(key, directory); + } + else + { + dumpn("*** removing mapping for '" + path + "'"); + this._pathDirectoryMap.put(key, null); + } + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(err, handler) + { + if (!(err in HTTP_ERROR_CODES)) + dumpn("*** WARNING: registering non-HTTP/1.1 error code " + + "(" + err + ") handler -- was this intentional?"); + + this._handlerToField(handler, this._overrideErrors, err); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + if (!handler) + handler = defaultIndexHandler; + else if (typeof(handler) != "function") + handler = createHandlerFunc(handler); + + this._indexHandler = handler; + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + if (!type) + delete this._mimeMappings[ext]; + else + this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type); + }, + + // PRIVATE API + + /** +* Sets or remove (if handler is null) a handler in an object with a key. +* +* @param handler +* a handler, either function or an nsIHttpRequestHandler +* @param dict +* The object to attach the handler to. +* @param key +* The field name of the handler. +*/ + _handlerToField: function(handler, dict, key) + { + // for convenience, handler can be a function if this is run from xpcshell + if (typeof(handler) == "function") + dict[key] = handler; + else if (handler) + dict[key] = createHandlerFunc(handler); + else + delete dict[key]; + }, + + /** +* Handles a request which maps to a file in the local filesystem (if a base +* path has already been set; otherwise the 404 error is thrown). +* +* @param metadata : Request +* metadata for the incoming request +* @param response : Response +* an uninitialized Response to the given request, to be initialized by a +* request handler +* @throws HTTP_### +* if an HTTP error occurred (usually HTTP_404); note that in this case the +* calling code must handle post-processing of the response +*/ + _handleDefault: function(metadata, response) + { + dumpn("*** _handleDefault()"); + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + + var path = metadata.path; + NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">"); + + // determine the actual on-disk file; this requires finding the deepest + // path-to-directory mapping in the requested URL + var file = this._getFileForPath(path); + + // the "file" might be a directory, in which case we either serve the + // contained index.html or make the index handler write the response + if (file.exists() && file.isDirectory()) + { + file.append("index.html"); // make configurable? + if (!file.exists() || file.isDirectory()) + { + metadata._ensurePropertyBag(); + metadata._bag.setPropertyAsInterface("directory", file.parent); + this._indexHandler(metadata, response); + return; + } + } + + // alternately, the file might not exist + if (!file.exists()) + throw HTTP_404; + + var start, end; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) && + metadata.hasHeader("Range") && + this._getTypeFromFile(file) !== SJS_TYPE) + { + var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/); + if (!rangeMatch) + throw HTTP_400; + + if (rangeMatch[1] !== undefined) + start = parseInt(rangeMatch[1], 10); + + if (rangeMatch[2] !== undefined) + end = parseInt(rangeMatch[2], 10); + + if (start === undefined && end === undefined) + throw HTTP_400; + + // No start given, so the end is really the count of bytes from the + // end of the file. + if (start === undefined) + { + start = Math.max(0, file.fileSize - end); + end = file.fileSize - 1; + } + + // start and end are inclusive + if (end === undefined || end >= file.fileSize) + end = file.fileSize - 1; + + if (start !== undefined && start >= file.fileSize) { + var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable"); + HTTP_416.customErrorHandling = function(errorResponse) + { + maybeAddHeaders(file, metadata, errorResponse); + }; + throw HTTP_416; + } + + if (end < start) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + start = 0; + end = file.fileSize - 1; + } + else + { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize; + response.setHeader("Content-Range", contentRange); + } + } + else + { + start = 0; + end = file.fileSize - 1; + } + + // finally... + dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " + + start + " to " + end + " inclusive"); + this._writeFileResponse(metadata, file, response, start, end - start + 1); + }, + + /** +* Writes an HTTP response for the given file, including setting headers for +* file metadata. +* +* @param metadata : Request +* the Request for which a response is being generated +* @param file : nsILocalFile +* the file which is to be sent in the response +* @param response : Response +* the response to which the file should be written +* @param offset: uint +* the byte offset to skip to when writing +* @param count: uint +* the number of bytes to write +*/ + _writeFileResponse: function(metadata, file, response, offset, count) + { + const PR_RDONLY = 0x01; + + var type = this._getTypeFromFile(file); + if (type === SJS_TYPE) + { + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var sis = new ScriptableInputStream(fis); + var s = Cu.Sandbox(gGlobalObject); + s.importFunction(dump, "dump"); + + // Define a basic key-value state-preservation API across requests, with + // keys initially corresponding to the empty string. + var self = this; + var path = metadata.path; + s.importFunction(function getState(k) + { + return self._getState(path, k); + }); + s.importFunction(function setState(k, v) + { + self._setState(path, k, v); + }); + s.importFunction(function getSharedState(k) + { + return self._getSharedState(k); + }); + s.importFunction(function setSharedState(k, v) + { + self._setSharedState(k, v); + }); + s.importFunction(function getObjectState(k, callback) + { + callback(self._getObjectState(k)); + }); + s.importFunction(function setObjectState(k, v) + { + self._setObjectState(k, v); + }); + s.importFunction(function registerPathHandler(p, h) + { + self.registerPathHandler(p, h); + }); + + // Make it possible for sjs files to access their location + this._setState(path, "__LOCATION__", file.path); + + try + { + // Alas, the line number in errors dumped to console when calling the + // request handler is simply an offset from where we load the SJS file. + // Work around this in a reasonably non-fragile way by dynamically + // getting the line number where we evaluate the SJS file. Don't + // separate these two lines! + var line = new Error().lineNumber; + Cu.evalInSandbox(sis.read(file.fileSize), s); + } + catch (e) + { + dumpn("*** syntax error in SJS at " + file.path + ": " + e); + throw HTTP_500; + } + + try + { + s.handleRequest(metadata, response); + } + catch (e) + { + dump("*** error running SJS at " + file.path + ": " + + e + " on line " + + (e instanceof Error + ? e.lineNumber + " in httpd.js" + : (e.lineNumber - line)) + "\n"); + throw HTTP_500; + } + } + finally + { + fis.close(); + } + } + else + { + try + { + response.setHeader("Last-Modified", + toDateString(file.lastModifiedTime), + false); + } + catch (e) { /* lastModifiedTime threw, ignore */ } + + response.setHeader("Content-Type", type, false); + maybeAddHeaders(file, metadata, response); + response.setHeader("Content-Length", "" + count, false); + + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + offset = offset || 0; + count = count || file.fileSize; + NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset"); + NS_ASSERT(count >= 0, "bad count"); + NS_ASSERT(offset + count <= file.fileSize, "bad total data size"); + + try + { + if (offset !== 0) + { + // Seek (or read, if seeking isn't supported) to the correct offset so + // the data sent to the client matches the requested range. + if (fis instanceof Ci.nsISeekableStream) + fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset); + else + new ScriptableInputStream(fis).read(offset); + } + } + catch (e) + { + fis.close(); + throw e; + } + + function writeMore() + { + gThreadManager.currentThread + .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL); + } + + var input = new BinaryInputStream(fis); + var output = new BinaryOutputStream(response.bodyOutputStream); + var writeData = + { + run: function() + { + var chunkSize = Math.min(65536, count); + count -= chunkSize; + NS_ASSERT(count >= 0, "underflow"); + + try + { + var data = input.readByteArray(chunkSize); + NS_ASSERT(data.length === chunkSize, + "incorrect data returned? got " + data.length + + ", expected " + chunkSize); + output.writeByteArray(data, data.length); + if (count === 0) + { + fis.close(); + response.finish(); + } + else + { + writeMore(); + } + } + catch (e) + { + try + { + fis.close(); + } + finally + { + response.finish(); + } + throw e; + } + } + }; + + writeMore(); + + // Now that we know copying will start, flag the response as async. + response.processAsync(); + } + }, + + /** +* Get the value corresponding to a given key for the given path for SJS state +* preservation across requests. +* +* @param path : string +* the path from which the given state is to be retrieved +* @param k : string +* the key whose corresponding value is to be returned +* @returns string +* the corresponding value, which is initially the empty string +*/ + _getState: function(path, k) + { + var state = this._state; + if (path in state && k in state[path]) + return state[path][k]; + return ""; + }, + + /** +* Set the value corresponding to a given key for the given path for SJS state +* preservation across requests. +* +* @param path : string +* the path from which the given state is to be retrieved +* @param k : string +* the key whose corresponding value is to be set +* @param v : string +* the value to be set +*/ + _setState: function(path, k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + var state = this._state; + if (!(path in state)) + state[path] = {}; + state[path][k] = v; + }, + + /** +* Get the value corresponding to a given key for SJS state preservation +* across requests. +* +* @param k : string +* the key whose corresponding value is to be returned +* @returns string +* the corresponding value, which is initially the empty string +*/ + _getSharedState: function(k) + { + var state = this._sharedState; + if (k in state) + return state[k]; + return ""; + }, + + /** +* Set the value corresponding to a given key for SJS state preservation +* across requests. +* +* @param k : string +* the key whose corresponding value is to be set +* @param v : string +* the value to be set +*/ + _setSharedState: function(k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + this._sharedState[k] = v; + }, + + /** +* Returns the object associated with the given key in the server for SJS +* state preservation across requests. +* +* @param k : string +* the key whose corresponding object is to be returned +* @returns nsISupports +* the corresponding object, or null if none was present +*/ + _getObjectState: function(k) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + return this._objectState[k] || null; + }, + + /** +* Sets the object associated with the given key in the server for SJS +* state preservation across requests. +* +* @param k : string +* the key whose corresponding object is to be set +* @param v : nsISupports +* the object to be associated with the given key; may be null +*/ + _setObjectState: function(k, v) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + if (typeof v !== "object") + throw new Error("non-object value passed"); + if (v && !("QueryInterface" in v)) + { + throw new Error("must pass an nsISupports; use wrappedJSObject to ease " + + "pain when using the server from JS"); + } + + this._objectState[k] = v; + }, + + /** +* Gets a content-type for the given file, first by checking for any custom +* MIME-types registered with this handler for the file's extension, second by +* asking the global MIME service for a content-type, and finally by failing +* over to application/octet-stream. +* +* @param file : nsIFile +* the nsIFile for which to get a file type +* @returns string +* the best content-type which can be determined for the file +*/ + _getTypeFromFile: function(file) + { + try + { + var name = file.leafName; + var dot = name.lastIndexOf("."); + if (dot > 0) + { + var ext = name.slice(dot + 1); + if (ext in this._mimeMappings) + return this._mimeMappings[ext]; + } + return Cc["@mozilla.org/uriloader/external-helper-app-service;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + } + catch (e) + { + return "application/octet-stream"; + } + }, + + /** +* Returns the nsILocalFile which corresponds to the path, as determined using +* all registered path->directory mappings and any paths which are explicitly +* overridden. +* +* @param path : string +* the server path for which a file should be retrieved, e.g. "/foo/bar" +* @throws HttpError +* when the correct action is the corresponding HTTP error (i.e., because no +* mapping was found for a directory in path, the referenced file doesn't +* exist, etc.) +* @returns nsILocalFile +* the file to be sent as the response to a request for the path +*/ + _getFileForPath: function(path) + { + // decode and add underscores as necessary + try + { + path = toInternalPath(path, true); + } + catch (e) + { + throw HTTP_400; // malformed path + } + + // next, get the directory which contains this path + var pathMap = this._pathDirectoryMap; + + // An example progression of tmp for a path "/foo/bar/baz/" might be: + // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", "" + var tmp = path.substring(1); + while (true) + { + // do we have a match for current head of the path? + var file = pathMap.get(tmp); + if (file) + { + // XXX hack; basically disable showing mapping for /foo/bar/ when the + // requested path was /foo/bar, because relative links on the page + // will all be incorrect -- we really need the ability to easily + // redirect here instead + if (tmp == path.substring(1) && + tmp.length != 0 && + tmp.charAt(tmp.length - 1) != "/") + file = null; + else + break; + } + + // if we've finished trying all prefixes, exit + if (tmp == "") + break; + + tmp = tmp.substring(0, tmp.lastIndexOf("/")); + } + + // no mapping applies, so 404 + if (!file) + throw HTTP_404; + + + // last, get the file for the path within the determined directory + var parentFolder = file.parent; + var dirIsRoot = (parentFolder == null); + + // Strategy here is to append components individually, making sure we + // never move above the given directory; this allows paths such as + // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling"; + // this component-wise approach also means the code works even on platforms + // which don't use "/" as the directory separator, such as Windows + var leafPath = path.substring(tmp.length + 1); + var comps = leafPath.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + + if (comp == "..") + file = file.parent; + else if (comp == "." || comp == "") + continue; + else + file.append(comp); + + if (!dirIsRoot && file.equals(parentFolder)) + throw HTTP_403; + } + + return file; + }, + + /** +* Writes the error page for the given HTTP error code over the given +* connection. +* +* @param errorCode : uint +* the HTTP error code to be used +* @param connection : Connection +* the connection on which the error occurred +*/ + handleError: function(errorCode, connection) + { + var response = new Response(connection); + + dumpn("*** error in request: " + errorCode); + + this._handleError(errorCode, new Request(connection.port), response); + }, + + /** +* Handles a request which generates the given error code, using the +* user-defined error handler if one has been set, gracefully falling back to +* the x00 status code if the code has no handler, and failing to status code +* 500 if all else fails. +* +* @param errorCode : uint +* the HTTP error which is to be returned +* @param metadata : Request +* metadata for the request, which will often be incomplete since this is an +* error +* @param response : Response +* an uninitialized Response should be initialized when this method +* completes with information which represents the desired error code in the +* ideal case or a fallback code in abnormal circumstances (i.e., 500 is a +* fallback for 505, per HTTP specs) +*/ + _handleError: function(errorCode, metadata, response) + { + if (!metadata) + throw Cr.NS_ERROR_NULL_POINTER; + + var errorX00 = errorCode - (errorCode % 100); + + try + { + if (!(errorCode in HTTP_ERROR_CODES)) + dumpn("*** WARNING: requested invalid error: " + errorCode); + + // RFC 2616 says that we should try to handle an error by its class if we + // can't otherwise handle it -- if that fails, we revert to handling it as + // a 500 internal server error, and if that fails we throw and shut down + // the server + + // actually handle the error + try + { + if (errorCode in this._overrideErrors) + this._overrideErrors[errorCode](metadata, response); + else + this._defaultErrors[errorCode](metadata, response); + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + // don't retry the handler that threw + if (errorX00 == errorCode) + throw HTTP_500; + + dumpn("*** error in handling for error code " + errorCode + ", " + + "falling back to " + errorX00 + "..."); + response = new Response(response._connection); + if (errorX00 in this._overrideErrors) + this._overrideErrors[errorX00](metadata, response); + else if (errorX00 in this._defaultErrors) + this._defaultErrors[errorX00](metadata, response); + else + throw HTTP_500; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(); + return; + } + + // we've tried everything possible for a meaningful error -- now try 500 + dumpn("*** error in handling for error code " + errorX00 + ", falling " + + "back to 500..."); + + try + { + response = new Response(response._connection); + if (500 in this._overrideErrors) + this._overrideErrors[500](metadata, response); + else + this._defaultErrors[500](metadata, response); + } + catch (e2) + { + dumpn("*** multiple errors in default error handlers!"); + dumpn("*** e == " + e + ", e2 == " + e2); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // FIELDS + + /** +* This object contains the default handlers for the various HTTP error codes. +*/ + _defaultErrors: + { + 400: function(metadata, response) + { + // none of the data in metadata is reliable, so hard-code everything here + response.setStatusLine("1.1", 400, "Bad Request"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Bad request\n"; + response.bodyOutputStream.write(body, body.length); + }, + 403: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 403, "Forbidden"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>403 Forbidden</title></head>\ +<body>\ +<h1>403 Forbidden</h1>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 404: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>404 Not Found</title></head>\ +<body>\ +<h1>404 Not Found</h1>\ +<p>\ +<span style='font-family: monospace;'>" + + htmlEscape(metadata.path) + + "</span> was not found.\ +</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 416: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 416, + "Requested Range Not Satisfiable"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head>\ +<title>416 Requested Range Not Satisfiable</title></head>\ +<body>\ +<h1>416 Requested Range Not Satisfiable</h1>\ +<p>The byte range was not valid for the\ +requested resource.\ +</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 500: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 500, + "Internal Server Error"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>500 Internal Server Error</title></head>\ +<body>\ +<h1>500 Internal Server Error</h1>\ +<p>Something's broken in this server and\ +needs to be fixed.</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 501: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 501, "Not Implemented"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>501 Not Implemented</title></head>\ +<body>\ +<h1>501 Not Implemented</h1>\ +<p>This server is not (yet) Apache.</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + }, + 505: function(metadata, response) + { + response.setStatusLine("1.1", 505, "HTTP Version Not Supported"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>505 HTTP Version Not Supported</title></head>\ +<body>\ +<h1>505 HTTP Version Not Supported</h1>\ +<p>This server only supports HTTP/1.0 and HTTP/1.1\ +connections.</p>\ +</body>\ +</html>"; + response.bodyOutputStream.write(body, body.length); + } + }, + + /** +* Contains handlers for the default set of URIs contained in this server. +*/ + _defaultPaths: + { + "/": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + + var body = "<html>\ +<head><title>httpd.js</title></head>\ +<body>\ +<h1>httpd.js</h1>\ +<p>If you're seeing this page, httpd.js is up and\ +serving requests! Now set a base path and serve some\ +files!</p>\ +</body>\ +</html>"; + + response.bodyOutputStream.write(body, body.length); + }, + + "/trace": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Request-URI: " + + metadata.scheme + "://" + metadata.host + ":" + metadata.port + + metadata.path + "\n\n"; + body += "Request (semantically equivalent, slightly reformatted):\n\n"; + body += metadata.method + " " + metadata.path; + + if (metadata.queryString) + body += "?" + metadata.queryString; + + body += " HTTP/" + metadata.httpVersion + "\r\n"; + + var headEnum = metadata.headers; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n"; + } + + response.bodyOutputStream.write(body, body.length); + } + } +}; + + +/** +* Maps absolute paths to files on the local file system (as nsILocalFiles). +*/ +function FileMap() +{ + /** Hash which will map paths to nsILocalFiles. */ + this._map = {}; +} +FileMap.prototype = +{ + // PUBLIC API + + /** +* Maps key to a clone of the nsILocalFile value if value is non-null; +* otherwise, removes any extant mapping for key. +* +* @param key : string +* string to which a clone of value is mapped +* @param value : nsILocalFile +* the file to map to key, or null to remove a mapping +*/ + put: function(key, value) + { + if (value) + this._map[key] = value.clone(); + else + delete this._map[key]; + }, + + /** +* Returns a clone of the nsILocalFile mapped to key, or null if no such +* mapping exists. +* +* @param key : string +* key to which the returned file maps +* @returns nsILocalFile +* a clone of the mapped file, or null if no mapping exists +*/ + get: function(key) + { + var val = this._map[key]; + return val ? val.clone() : null; + } +}; + + +// Response CONSTANTS + +// token = *<any CHAR except CTLs or separators> +// CHAR = <any US-ASCII character (0-127)> +// CTL = <any US-ASCII control character (0-31) and DEL (127)> +// separators = "(" | ")" | "<" | ">" | "@" +// | "," | ";" | ":" | "\" | <"> +// | "/" | "[" | "]" | "?" | "=" +// | "{" | "}" | SP | HT +const IS_TOKEN_ARRAY = + [0, 0, 0, 0, 0, 0, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, // 16 + 0, 0, 0, 0, 0, 0, 0, 0, // 24 + + 0, 1, 0, 1, 1, 1, 1, 1, // 32 + 0, 0, 1, 1, 0, 1, 1, 0, // 40 + 1, 1, 1, 1, 1, 1, 1, 1, // 48 + 1, 1, 0, 0, 0, 0, 0, 0, // 56 + + 0, 1, 1, 1, 1, 1, 1, 1, // 64 + 1, 1, 1, 1, 1, 1, 1, 1, // 72 + 1, 1, 1, 1, 1, 1, 1, 1, // 80 + 1, 1, 1, 0, 0, 0, 1, 1, // 88 + + 1, 1, 1, 1, 1, 1, 1, 1, // 96 + 1, 1, 1, 1, 1, 1, 1, 1, // 104 + 1, 1, 1, 1, 1, 1, 1, 1, // 112 + 1, 1, 1, 0, 1, 0, 1]; // 120 + + +/** +* Determines whether the given character code is a CTL. +* +* @param code : uint +* the character code +* @returns boolean +* true if code is a CTL, false otherwise +*/ +function isCTL(code) +{ + return (code >= 0 && code <= 31) || (code == 127); +} + +/** +* Represents a response to an HTTP request, encapsulating all details of that +* response. This includes all headers, the HTTP version, status code and +* explanation, and the entity itself. +* +* @param connection : Connection +* the connection over which this response is to be written +*/ +function Response(connection) +{ + /** The connection over which this response will be written. */ + this._connection = connection; + + /** +* The HTTP version of this response; defaults to 1.1 if not set by the +* handler. +*/ + this._httpVersion = nsHttpVersion.HTTP_1_1; + + /** +* The HTTP code of this response; defaults to 200. +*/ + this._httpCode = 200; + + /** +* The description of the HTTP code in this response; defaults to "OK". +*/ + this._httpDescription = "OK"; + + /** +* An nsIHttpHeaders object in which the headers in this response should be +* stored. This property is null after the status line and headers have been +* written to the network, and it may be modified up until it is cleared, +* except if this._finished is set first (in which case headers are written +* asynchronously in response to a finish() call not preceded by +* flushHeaders()). +*/ + this._headers = new nsHttpHeaders(); + + /** +* Set to true when this response is ended (completely constructed if possible +* and the connection closed); further actions on this will then fail. +*/ + this._ended = false; + + /** +* A stream used to hold data written to the body of this response. +*/ + this._bodyOutputStream = null; + + /** +* A stream containing all data that has been written to the body of this +* response so far. (Async handlers make the data contained in this +* unreliable as a way of determining content length in general, but auxiliary +* saved information can sometimes be used to guarantee reliability.) +*/ + this._bodyInputStream = null; + + /** +* A stream copier which copies data to the network. It is initially null +* until replaced with a copier for response headers; when headers have been +* fully sent it is replaced with a copier for the response body, remaining +* so for the duration of response processing. +*/ + this._asyncCopier = null; + + /** +* True if this response has been designated as being processed +* asynchronously rather than for the duration of a single call to +* nsIHttpRequestHandler.handle. +*/ + this._processAsync = false; + + /** +* True iff finish() has been called on this, signaling that no more changes +* to this may be made. +*/ + this._finished = false; + + /** +* True iff powerSeized() has been called on this, signaling that this +* response is to be handled manually by the response handler (which may then +* send arbitrary data in response, even non-HTTP responses). +*/ + this._powerSeized = false; +} +Response.prototype = +{ + // PUBLIC CONSTRUCTION API + + // + // see nsIHttpResponse.bodyOutputStream + // + get bodyOutputStream() + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + if (!this._bodyOutputStream) + { + var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX, + null); + this._bodyOutputStream = pipe.outputStream; + this._bodyInputStream = pipe.inputStream; + if (this._processAsync || this._powerSeized) + this._startAsyncProcessor(); + } + + return this._bodyOutputStream; + }, + + // + // see nsIHttpResponse.write + // + write: function(data) + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + var dataAsString = String(data); + this.bodyOutputStream.write(dataAsString, dataAsString.length); + }, + + // + // see nsIHttpResponse.setStatusLine + // + setStatusLine: function(httpVersion, code, description) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + if (!(code >= 0 && code < 1000)) + throw Cr.NS_ERROR_INVALID_ARG; + + try + { + var httpVer; + // avoid version construction for the most common cases + if (!httpVersion || httpVersion == "1.1") + httpVer = nsHttpVersion.HTTP_1_1; + else if (httpVersion == "1.0") + httpVer = nsHttpVersion.HTTP_1_0; + else + httpVer = new nsHttpVersion(httpVersion); + } + catch (e) + { + throw Cr.NS_ERROR_INVALID_ARG; + } + + // Reason-Phrase = *<TEXT, excluding CR, LF> + // TEXT = <any OCTET except CTLs, but including LWS> + // + // XXX this ends up disallowing octets which aren't Unicode, I think -- not + // much to do if description is IDL'd as string + if (!description) + description = ""; + for (var i = 0; i < description.length; i++) + if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") + throw Cr.NS_ERROR_INVALID_ARG; + + // set the values only after validation to preserve atomicity + this._httpDescription = description; + this._httpCode = code; + this._httpVersion = httpVer; + }, + + // + // see nsIHttpResponse.setHeader + // + setHeader: function(name, value, merge) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + this._headers.setHeader(name, value, merge); + }, + + // + // see nsIHttpResponse.processAsync + // + processAsync: function() + { + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._processAsync) + return; + this._ensureAlive(); + + dumpn("*** processing connection " + this._connection.number + " async"); + this._processAsync = true; + + /* +* Either the bodyOutputStream getter or this method is responsible for +* starting the asynchronous processor and catching writes of data to the +* response body of async responses as they happen, for the purpose of +* forwarding those writes to the actual connection's output stream. +* If bodyOutputStream is accessed first, calling this method will create +* the processor (when it first is clear that body data is to be written +* immediately, not buffered). If this method is called first, accessing +* bodyOutputStream will create the processor. If only this method is +* called, we'll write nothing, neither headers nor the nonexistent body, +* until finish() is called. Since that delay is easily avoided by simply +* getting bodyOutputStream or calling write(""), we don't worry about it. +*/ + if (this._bodyOutputStream && !this._asyncCopier) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.seizePower + // + seizePower: function() + { + if (this._processAsync) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + return; + this._ensureAlive(); + + dumpn("*** forcefully seizing power over connection " + + this._connection.number + "..."); + + // Purge any already-written data without sending it. We could as easily + // swap out the streams entirely, but that makes it possible to acquire and + // unknowingly use a stale reference, so we require there only be one of + // each stream ever for any response to avoid this complication. + if (this._asyncCopier) + this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED); + this._asyncCopier = null; + if (this._bodyOutputStream) + { + var input = new BinaryInputStream(this._bodyInputStream); + var avail; + while ((avail = input.available()) > 0) + input.readByteArray(avail); + } + + this._powerSeized = true; + if (this._bodyOutputStream) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.finish + // + finish: function() + { + if (!this._processAsync && !this._powerSeized) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._finished) + return; + + dumpn("*** finishing connection " + this._connection.number); + this._startAsyncProcessor(); // in case bodyOutputStream was never accessed + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + this._finished = true; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // POST-CONSTRUCTION API (not exposed externally) + + /** +* The HTTP version number of this, as a string (e.g. "1.1"). +*/ + get httpVersion() + { + this._ensureAlive(); + return this._httpVersion.toString(); + }, + + /** +* The HTTP status code of this response, as a string of three characters per +* RFC 2616. +*/ + get httpCode() + { + this._ensureAlive(); + + var codeString = (this._httpCode < 10 ? "0" : "") + + (this._httpCode < 100 ? "0" : "") + + this._httpCode; + return codeString; + }, + + /** +* The description of the HTTP status code of this response, or "" if none is +* set. +*/ + get httpDescription() + { + this._ensureAlive(); + + return this._httpDescription; + }, + + /** +* The headers in this response, as an nsHttpHeaders object. +*/ + get headers() + { + this._ensureAlive(); + + return this._headers; + }, + + // + // see nsHttpHeaders.getHeader + // + getHeader: function(name) + { + this._ensureAlive(); + + return this._headers.getHeader(name); + }, + + /** +* Determines whether this response may be abandoned in favor of a newly +* constructed response. A response may be abandoned only if it is not being +* sent asynchronously and if raw control over it has not been taken from the +* server. +* +* @returns boolean +* true iff no data has been written to the network +*/ + partiallySent: function() + { + dumpn("*** partiallySent()"); + return this._processAsync || this._powerSeized; + }, + + /** +* If necessary, kicks off the remaining request processing needed to be done +* after a request handler performs its initial work upon this response. +*/ + complete: function() + { + dumpn("*** complete()"); + if (this._processAsync || this._powerSeized) + { + NS_ASSERT(this._processAsync ^ this._powerSeized, + "can't both send async and relinquish power"); + return; + } + + NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?"); + + this._startAsyncProcessor(); + + // Now make sure we finish processing this request! + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + }, + + /** +* Abruptly ends processing of this response, usually due to an error in an +* incoming request but potentially due to a bad error handler. Since we +* cannot handle the error in the usual way (giving an HTTP error page in +* response) because data may already have been sent (or because the response +* might be expected to have been generated asynchronously or completely from +* scratch by the handler), we stop processing this response and abruptly +* close the connection. +* +* @param e : Error +* the exception which precipitated this abort, or null if no such exception +* was generated +*/ + abort: function(e) + { + dumpn("*** abort(<" + e + ">)"); + + // This response will be ended by the processor if one was created. + var copier = this._asyncCopier; + if (copier) + { + // We dispatch asynchronously here so that any pending writes of data to + // the connection will be deterministically written. This makes it easier + // to specify exact behavior, and it makes observable behavior more + // predictable for clients. Note that the correctness of this depends on + // callbacks in response to _waitToReadData in WriteThroughCopier + // happening asynchronously with respect to the actual writing of data to + // bodyOutputStream, as they currently do; if they happened synchronously, + // an event which ran before this one could write more data to the + // response body before we get around to canceling the copier. We have + // tests for this in test_seizepower.js, however, and I can't think of a + // way to handle both cases without removing bodyOutputStream access and + // moving its effective write(data, length) method onto Response, which + // would be slower and require more code than this anyway. + gThreadManager.currentThread.dispatch({ + run: function() + { + dumpn("*** canceling copy asynchronously..."); + copier.cancel(Cr.NS_ERROR_UNEXPECTED); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + } + else + { + this.end(); + } + }, + + /** +* Closes this response's network connection, marks the response as finished, +* and notifies the server handler that the request is done being processed. +*/ + end: function() + { + NS_ASSERT(!this._ended, "ending this response twice?!?!"); + + this._connection.close(); + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + + this._finished = true; + this._ended = true; + }, + + // PRIVATE IMPLEMENTATION + + /** +* Sends the status line and headers of this response if they haven't been +* sent and initiates the process of copying data written to this response's +* body to the network. +*/ + _startAsyncProcessor: function() + { + dumpn("*** _startAsyncProcessor()"); + + // Handle cases where we're being called a second time. The former case + // happens when this is triggered both by complete() and by processAsync(), + // while the latter happens when processAsync() in conjunction with sent + // data causes abort() to be called. + if (this._asyncCopier || this._ended) + { + dumpn("*** ignoring second call to _startAsyncProcessor"); + return; + } + + // Send headers if they haven't been sent already and should be sent, then + // asynchronously continue to send the body. + if (this._headers && !this._powerSeized) + { + this._sendHeaders(); + return; + } + + this._headers = null; + this._sendBody(); + }, + + /** +* Signals that all modifications to the response status line and headers are +* complete and then sends that data over the network to the client. Once +* this method completes, a different response to the request that resulted +* in this response cannot be sent -- the only possible action in case of +* error is to abort the response and close the connection. +*/ + _sendHeaders: function() + { + dumpn("*** _sendHeaders()"); + + NS_ASSERT(this._headers); + NS_ASSERT(!this._powerSeized); + + // request-line + var statusLine = "HTTP/" + this.httpVersion + " " + + this.httpCode + " " + + this.httpDescription + "\r\n"; + + // header post-processing + + var headers = this._headers; + headers.setHeader("Connection", "close", false); + headers.setHeader("Server", "httpd.js", false); + if (!headers.hasHeader("Date")) + headers.setHeader("Date", toDateString(Date.now()), false); + + // Any response not being processed asynchronously must have an associated + // Content-Length header for reasons of backwards compatibility with the + // initial server, which fully buffered every response before sending it. + // Beyond that, however, it's good to do this anyway because otherwise it's + // impossible to test behaviors that depend on the presence or absence of a + // Content-Length header. + if (!this._processAsync) + { + dumpn("*** non-async response, set Content-Length"); + + var bodyStream = this._bodyInputStream; + var avail = bodyStream ? bodyStream.available() : 0; + + // XXX assumes stream will always report the full amount of data available + headers.setHeader("Content-Length", "" + avail, false); + } + + + // construct and send response + dumpn("*** header post-processing completed, sending response head..."); + + // request-line + var preambleData = [statusLine]; + + // headers + var headEnum = headers.enumerator; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + var values = headers.getHeaderValues(fieldName); + for (var i = 0, sz = values.length; i < sz; i++) + preambleData.push(fieldName + ": " + values[i] + "\r\n"); + } + + // end request-line/headers + preambleData.push("\r\n"); + + var preamble = preambleData.join(""); + + var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null); + responseHeadPipe.outputStream.write(preamble, preamble.length); + + var response = this; + var copyObserver = + { + onStartRequest: function(request, cx) + { + dumpn("*** preamble copying started"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** preamble copying complete " + + "[status=0x" + statusCode.toString(16) + "]"); + + if (!components.isSuccessCode(statusCode)) + { + dumpn("!!! header copying problems: non-success statusCode, " + + "ending response"); + + response.end(); + } + else + { + response._sendBody(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + var headerCopier = this._asyncCopier = + new WriteThroughCopier(responseHeadPipe.inputStream, + this._connection.output, + copyObserver, null); + + responseHeadPipe.outputStream.close(); + + // Forbid setting any more headers or modifying the request line. + this._headers = null; + }, + + /** +* Asynchronously writes the body of the response (or the entire response, if +* seizePower() has been called) to the network. +*/ + _sendBody: function() + { + dumpn("*** _sendBody"); + + NS_ASSERT(!this._headers, "still have headers around but sending body?"); + + // If no body data was written, we're done + if (!this._bodyInputStream) + { + dumpn("*** empty body, response finished"); + this.end(); + return; + } + + var response = this; + var copyObserver = + { + onStartRequest: function(request, context) + { + dumpn("*** onStartRequest"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]"); + + if (statusCode === Cr.NS_BINDING_ABORTED) + { + dumpn("*** terminating copy observer without ending the response"); + } + else + { + if (!components.isSuccessCode(statusCode)) + dumpn("*** WARNING: non-success statusCode in onStopRequest"); + + response.end(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + dumpn("*** starting async copier of body data..."); + this._asyncCopier = + new WriteThroughCopier(this._bodyInputStream, this._connection.output, + copyObserver, null); + }, + + /** Ensures that this hasn't been ended. */ + _ensureAlive: function() + { + NS_ASSERT(!this._ended, "not handling response lifetime correctly"); + } +}; + +/** +* Size of the segments in the buffer used in storing response data and writing +* it to the socket. +*/ +Response.SEGMENT_SIZE = 8192; + +/** Serves double duty in WriteThroughCopier implementation. */ +function notImplemented() +{ + throw Cr.NS_ERROR_NOT_IMPLEMENTED; +} + +/** Returns true iff the given exception represents stream closure. */ +function streamClosed(e) +{ + return e === Cr.NS_BASE_STREAM_CLOSED || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED); +} + +/** Returns true iff the given exception represents a blocked stream. */ +function wouldBlock(e) +{ + return e === Cr.NS_BASE_STREAM_WOULD_BLOCK || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK); +} + +/** +* Copies data from source to sink as it becomes available, when that data can +* be written to sink without blocking. +* +* @param source : nsIAsyncInputStream +* the stream from which data is to be read +* @param sink : nsIAsyncOutputStream +* the stream to which data is to be copied +* @param observer : nsIRequestObserver +* an observer which will be notified when the copy starts and finishes +* @param context : nsISupports +* context passed to observer when notified of start/stop +* @throws NS_ERROR_NULL_POINTER +* if source, sink, or observer are null +*/ +function WriteThroughCopier(source, sink, observer, context) +{ + if (!source || !sink || !observer) + throw Cr.NS_ERROR_NULL_POINTER; + + /** Stream from which data is being read. */ + this._source = source; + + /** Stream to which data is being written. */ + this._sink = sink; + + /** Observer watching this copy. */ + this._observer = observer; + + /** Context for the observer watching this. */ + this._context = context; + + /** +* True iff this is currently being canceled (cancel has been called, the +* callback may not yet have been made). +*/ + this._canceled = false; + + /** +* False until all data has been read from input and written to output, at +* which point this copy is completed and cancel() is asynchronously called. +*/ + this._completed = false; + + /** Required by nsIRequest, meaningless. */ + this.loadFlags = 0; + /** Required by nsIRequest, meaningless. */ + this.loadGroup = null; + /** Required by nsIRequest, meaningless. */ + this.name = "response-body-copy"; + + /** Status of this request. */ + this.status = Cr.NS_OK; + + /** Arrays of byte strings waiting to be written to output. */ + this._pendingData = []; + + // start copying + try + { + observer.onStartRequest(this, context); + this._waitToReadData(); + this._waitForSinkClosure(); + } + catch (e) + { + dumpn("!!! error starting copy: " + e + + ("lineNumber" in e ? ", line " + e.lineNumber : "")); + dumpn(e.stack); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + } +} +WriteThroughCopier.prototype = +{ + /* nsISupports implementation */ + + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIInputStreamCallback) || + iid.equals(Ci.nsIOutputStreamCallback) || + iid.equals(Ci.nsIRequest) || + iid.equals(Ci.nsISupports)) + { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NSIINPUTSTREAMCALLBACK + + /** +* Receives a more-data-in-input notification and writes the corresponding +* data to the output. +* +* @param input : nsIAsyncInputStream +* the input stream on whose data we have been waiting +*/ + onInputStreamReady: function(input) + { + if (this._source === null) + return; + + dumpn("*** onInputStreamReady"); + + // + // Ordinarily we'll read a non-zero amount of data from input, queue it up + // to be written and then wait for further callbacks. The complications in + // this method are the cases where we deviate from that behavior when errors + // occur or when copying is drawing to a finish. + // + // The edge cases when reading data are: + // + // Zero data is read + // If zero data was read, we're at the end of available data, so we can + // should stop reading and move on to writing out what we have (or, if + // we've already done that, onto notifying of completion). + // A stream-closed exception is thrown + // This is effectively a less kind version of zero data being read; the + // only difference is that we notify of completion with that result + // rather than with NS_OK. + // Some other exception is thrown + // This is the least kind result. We don't know what happened, so we + // act as though the stream closed except that we notify of completion + // with the result NS_ERROR_UNEXPECTED. + // + + var bytesWanted = 0, bytesConsumed = -1; + try + { + input = new BinaryInputStream(input); + + bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE); + dumpn("*** input wanted: " + bytesWanted); + + if (bytesWanted > 0) + { + var data = input.readByteArray(bytesWanted); + bytesConsumed = data.length; + this._pendingData.push(String.fromCharCode.apply(String, data)); + } + + dumpn("*** " + bytesConsumed + " bytes read"); + + // Handle the zero-data edge case in the same place as all other edge + // cases are handled. + if (bytesWanted === 0) + throw Cr.NS_BASE_STREAM_CLOSED; + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** input stream closed"); + e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED; + } + else + { + dumpn("!!! unexpected error reading from input, canceling: " + e); + e = Cr.NS_ERROR_UNEXPECTED; + } + + this._doneReadingSource(e); + return; + } + + var pendingData = this._pendingData; + + NS_ASSERT(bytesConsumed > 0); + NS_ASSERT(pendingData.length > 0, "no pending data somehow?"); + NS_ASSERT(pendingData[pendingData.length - 1].length > 0, + "buffered zero bytes of data?"); + + NS_ASSERT(this._source !== null); + + // Reading has gone great, and we've gotten data to write now. What if we + // don't have a place to write that data, because output went away just + // before this read? Drop everything on the floor, including new data, and + // cancel at this point. + if (this._sink === null) + { + pendingData.length = 0; + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we've read the data, and we know we have a place to write it. We + // need to queue up the data to be written, but *only* if none is queued + // already -- if data's already queued, the code that actually writes the + // data will make sure to wait on unconsumed pending data. + try + { + if (pendingData.length === 1) + this._waitToWriteData(); + } + catch (e) + { + dumpn("!!! error waiting to write data just read, swallowing and " + + "writing only what we already have: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Whee! We successfully read some data, and it's successfully queued up to + // be written. All that remains now is to wait for more data to read. + try + { + this._waitToReadData(); + } + catch (e) + { + dumpn("!!! error waiting to read more data: " + e); + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + } + }, + + + // NSIOUTPUTSTREAMCALLBACK + + /** +* Callback when data may be written to the output stream without blocking, or +* when the output stream has been closed. +* +* @param output : nsIAsyncOutputStream +* the output stream on whose writability we've been waiting, also known as +* this._sink +*/ + onOutputStreamReady: function(output) + { + if (this._sink === null) + return; + + dumpn("*** onOutputStreamReady"); + + var pendingData = this._pendingData; + if (pendingData.length === 0) + { + // There's no pending data to write. The only way this can happen is if + // we're waiting on the output stream's closure, so we can respond to a + // copying failure as quickly as possible (rather than waiting for data to + // be available to read and then fail to be copied). Therefore, we must + // be done now -- don't bother to attempt to write anything and wrap + // things up. + dumpn("!!! output stream closed prematurely, ending copy"); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + + NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?"); + + // + // Write out the first pending quantum of data. The possible errors here + // are: + // + // The write might fail because we can't write that much data + // Okay, we've written what we can now, so re-queue what's left and + // finish writing it out later. + // The write failed because the stream was closed + // Discard pending data that we can no longer write, stop reading, and + // signal that copying finished. + // Some other error occurred. + // Same as if the stream were closed, but notify with the status + // NS_ERROR_UNEXPECTED so the observer knows something was wonky. + // + + try + { + var quantum = pendingData[0]; + + // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on + // undefined behavior! We're only using this because writeByteArray + // is unusably broken for asynchronous output streams; see bug 532834 + // for details. + var bytesWritten = output.write(quantum, quantum.length); + if (bytesWritten === quantum.length) + pendingData.shift(); + else + pendingData[0] = quantum.substring(bytesWritten); + + dumpn("*** wrote " + bytesWritten + " bytes of data"); + } + catch (e) + { + if (wouldBlock(e)) + { + NS_ASSERT(pendingData.length > 0, + "stream-blocking exception with no data to write?"); + NS_ASSERT(pendingData[0].length > 0, + "stream-blocking exception with empty quantum?"); + this._waitToWriteData(); + return; + } + + if (streamClosed(e)) + dumpn("!!! output stream prematurely closed, signaling error..."); + else + dumpn("!!! unknown error: " + e + ", quantum=" + quantum); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // The day is ours! Quantum written, now let's see if we have more data + // still to write. + try + { + if (pendingData.length > 0) + { + this._waitToWriteData(); + return; + } + } + catch (e) + { + dumpn("!!! unexpected error waiting to write pending data: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we have no more pending data to write -- but might we get more in + // the future? + if (this._source !== null) + { + /* +* If we might, then wait for the output stream to be closed. (We wait +* only for closure because we have no data to write -- and if we waited +* for a specific amount of data, we would get repeatedly notified for no +* reason if over time the output stream permitted more and more data to +* be written to it without blocking.) +*/ + this._waitForSinkClosure(); + } + else + { + /* +* On the other hand, if we can't have more data because the input +* stream's gone away, then it's time to notify of copy completion. +* Victory! +*/ + this._sink = null; + this._cancelOrDispatchCancelCallback(Cr.NS_OK); + } + }, + + + // NSIREQUEST + + /** Returns true if the cancel observer hasn't been notified yet. */ + isPending: function() + { + return !this._completed; + }, + + /** Not implemented, don't use! */ + suspend: notImplemented, + /** Not implemented, don't use! */ + resume: notImplemented, + + /** +* Cancels data reading from input, asynchronously writes out any pending +* data, and causes the observer to be notified with the given error code when +* all writing has finished. +* +* @param status : nsresult +* the status to pass to the observer when data copying has been canceled +*/ + cancel: function(status) + { + dumpn("*** cancel(" + status.toString(16) + ")"); + + if (this._canceled) + { + dumpn("*** suppressing a late cancel"); + return; + } + + this._canceled = true; + this.status = status; + + // We could be in the middle of absolutely anything at this point. Both + // input and output might still be around, we might have pending data to + // write, and in general we know nothing about the state of the world. We + // therefore must assume everything's in progress and take everything to its + // final steady state (or so far as it can go before we need to finish + // writing out remaining data). + + this._doneReadingSource(status); + }, + + + // PRIVATE IMPLEMENTATION + + /** +* Stop reading input if we haven't already done so, passing e as the status +* when closing the stream, and kick off a copy-completion notice if no more +* data remains to be written. +* +* @param e : nsresult +* the status to be used when closing the input stream +*/ + _doneReadingSource: function(e) + { + dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")"); + + this._finishSource(e); + if (this._pendingData.length === 0) + this._sink = null; + else + NS_ASSERT(this._sink !== null, "null output?"); + + // If we've written out all data read up to this point, then it's time to + // signal completion. + if (this._sink === null) + { + NS_ASSERT(this._pendingData.length === 0, "pending data still?"); + this._cancelOrDispatchCancelCallback(e); + } + }, + + /** +* Stop writing output if we haven't already done so, discard any data that +* remained to be sent, close off input if it wasn't already closed, and kick +* off a copy-completion notice. +* +* @param e : nsresult +* the status to be used when closing input if it wasn't already closed +*/ + _doneWritingToSink: function(e) + { + dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")"); + + this._pendingData.length = 0; + this._sink = null; + this._doneReadingSource(e); + }, + + /** +* Completes processing of this copy: either by canceling the copy if it +* hasn't already been canceled using the provided status, or by dispatching +* the cancel callback event (with the originally provided status, of course) +* if it already has been canceled. +* +* @param status : nsresult +* the status code to use to cancel this, if this hasn't already been +* canceled +*/ + _cancelOrDispatchCancelCallback: function(status) + { + dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")"); + + NS_ASSERT(this._source === null, "should have finished input"); + NS_ASSERT(this._sink === null, "should have finished output"); + NS_ASSERT(this._pendingData.length === 0, "should have no pending data"); + + if (!this._canceled) + { + this.cancel(status); + return; + } + + var self = this; + var event = + { + run: function() + { + dumpn("*** onStopRequest async callback"); + + self._completed = true; + try + { + self._observer.onStopRequest(self, self._context, self.status); + } + catch (e) + { + NS_ASSERT(false, + "how are we throwing an exception here? we control " + + "all the callers! " + e); + } + } + }; + + gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** +* Kicks off another wait for more data to be available from the input stream. +*/ + _waitToReadData: function() + { + dumpn("*** _waitToReadData"); + this._source.asyncWait(this, 0, Response.SEGMENT_SIZE, + gThreadManager.mainThread); + }, + + /** +* Kicks off another wait until data can be written to the output stream. +*/ + _waitToWriteData: function() + { + dumpn("*** _waitToWriteData"); + + var pendingData = this._pendingData; + NS_ASSERT(pendingData.length > 0, "no pending data to write?"); + NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?"); + + this._sink.asyncWait(this, 0, pendingData[0].length, + gThreadManager.mainThread); + }, + + /** +* Kicks off a wait for the sink to which data is being copied to be closed. +* We wait for stream closure when we don't have any data to be copied, rather +* than waiting to write a specific amount of data. We can't wait to write +* data because the sink might be infinitely writable, and if no data appears +* in the source for a long time we might have to spin quite a bit waiting to +* write, waiting to write again, &c. Waiting on stream closure instead means +* we'll get just one notification if the sink dies. Note that when data +* starts arriving from the sink we'll resume waiting for data to be written, +* dropping this closure-only callback entirely. +*/ + _waitForSinkClosure: function() + { + dumpn("*** _waitForSinkClosure"); + + this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0, + gThreadManager.mainThread); + }, + + /** +* Closes input with the given status, if it hasn't already been closed; +* otherwise a no-op. +* +* @param status : nsresult +* status code use to close the source stream if necessary +*/ + _finishSource: function(status) + { + dumpn("*** _finishSource(" + status.toString(16) + ")"); + + if (this._source !== null) + { + this._source.closeWithStatus(status); + this._source = null; + } + } +}; + + +/** +* A container for utility functions used with HTTP headers. +*/ +const headerUtils = +{ + /** +* Normalizes fieldName (by converting it to lowercase) and ensures it is a +* valid header field name (although not necessarily one specified in RFC +* 2616). +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not match the field-name production in RFC 2616 +* @returns string +* fieldName converted to lowercase if it is a valid header, for characters +* where case conversion is possible +*/ + normalizeFieldName: function(fieldName) + { + if (fieldName == "") + throw Cr.NS_ERROR_INVALID_ARG; + + for (var i = 0, sz = fieldName.length; i < sz; i++) + { + if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) + { + dumpn(fieldName + " is not a valid header field name!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + } + + return fieldName.toLowerCase(); + }, + + /** +* Ensures that fieldValue is a valid header field value (although not +* necessarily as specified in RFC 2616 if the corresponding field name is +* part of the HTTP protocol), normalizes the value if it is, and +* returns the normalized value. +* +* @param fieldValue : string +* a value to be normalized as an HTTP header field value +* @throws NS_ERROR_INVALID_ARG +* if fieldValue does not match the field-value production in RFC 2616 +* @returns string +* fieldValue as a normalized HTTP header field value +*/ + normalizeFieldValue: function(fieldValue) + { + // field-value = *( field-content | LWS ) + // field-content = <the OCTETs making up the field-value + // and consisting of either *TEXT or combinations + // of token, separators, and quoted-string> + // TEXT = <any OCTET except CTLs, + // but including LWS> + // LWS = [CRLF] 1*( SP | HT ) + // + // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + // qdtext = <any TEXT except <">> + // quoted-pair = "\" CHAR + // CHAR = <any US-ASCII character (octets 0 - 127)> + + // Any LWS that occurs between field-content MAY be replaced with a single + // SP before interpreting the field value or forwarding the message + // downstream (section 4.2); we replace 1*LWS with a single SP + var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " "); + + // remove leading/trailing LWS (which has been converted to SP) + val = val.replace(/^ +/, "").replace(/ +$/, ""); + + // that should have taken care of all CTLs, so val should contain no CTLs + for (var i = 0, len = val.length; i < len; i++) + if (isCTL(val.charCodeAt(i))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly + // normalize, however, so this can be construed as a tightening of the + // spec and not entirely as a bug + return val; + } +}; + + + +/** +* Converts the given string into a string which is safe for use in an HTML +* context. +* +* @param str : string +* the string to make HTML-safe +* @returns string +* an HTML-safe version of str +*/ +function htmlEscape(str) +{ + // this is naive, but it'll work + var s = ""; + for (var i = 0; i < str.length; i++) + s += "&#" + str.charCodeAt(i) + ";"; + return s; +} + + +/** +* Constructs an object representing an HTTP version (see section 3.1). +* +* @param versionString +* a string of the form "#.#", where # is an non-negative decimal integer with +* or without leading zeros +* @throws +* if versionString does not specify a valid HTTP version number +*/ +function nsHttpVersion(versionString) +{ + var matches = /^(\d+)\.(\d+)$/.exec(versionString); + if (!matches) + throw "Not a valid HTTP version!"; + + /** The major version number of this, as a number. */ + this.major = parseInt(matches[1], 10); + + /** The minor version number of this, as a number. */ + this.minor = parseInt(matches[2], 10); + + if (isNaN(this.major) || isNaN(this.minor) || + this.major < 0 || this.minor < 0) + throw "Not a valid HTTP version!"; +} +nsHttpVersion.prototype = +{ + /** +* Returns the standard string representation of the HTTP version represented +* by this (e.g., "1.1"). +*/ + toString: function () + { + return this.major + "." + this.minor; + }, + + /** +* Returns true if this represents the same HTTP version as otherVersion, +* false otherwise. +* +* @param otherVersion : nsHttpVersion +* the version to compare against this +*/ + equals: function (otherVersion) + { + return this.major == otherVersion.major && + this.minor == otherVersion.minor; + }, + + /** True if this >= otherVersion, false otherwise. */ + atLeast: function(otherVersion) + { + return this.major > otherVersion.major || + (this.major == otherVersion.major && + this.minor >= otherVersion.minor); + } +}; + +nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0"); +nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1"); + + +/** +* An object which stores HTTP headers for a request or response. +* +* Note that since headers are case-insensitive, this object converts headers to +* lowercase before storing them. This allows the getHeader and hasHeader +* methods to work correctly for any case of a header, but it means that the +* values returned by .enumerator may not be equal case-sensitively to the +* values passed to setHeader when adding headers to this. +*/ +function nsHttpHeaders() +{ + /** +* A hash of headers, with header field names as the keys and header field +* values as the values. Header field names are case-insensitive, but upon +* insertion here they are converted to lowercase. Header field values are +* normalized upon insertion to contain no leading or trailing whitespace. +* +* Note also that per RFC 2616, section 4.2, two headers with the same name in +* a message may be treated as one header with the same field name and a field +* value consisting of the separate field values joined together with a "," in +* their original order. This hash stores multiple headers with the same name +* in this manner. +*/ + this._headers = {}; +} +nsHttpHeaders.prototype = +{ + /** +* Sets the header represented by name and value in this. +* +* @param name : string +* the header name +* @param value : string +* the header value +* @throws NS_ERROR_INVALID_ARG +* if name or value is not a valid header component +*/ + setHeader: function(fieldName, fieldValue, merge) + { + var name = headerUtils.normalizeFieldName(fieldName); + var value = headerUtils.normalizeFieldValue(fieldValue); + + // The following three headers are stored as arrays because their real-world + // syntax prevents joining individual headers into a single header using + // ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77> + if (merge && name in this._headers) + { + if (name === "www-authenticate" || + name === "proxy-authenticate" || + name === "set-cookie") + { + this._headers[name].push(value); + } + else + { + this._headers[name][0] += "," + value; + NS_ASSERT(this._headers[name].length === 1, + "how'd a non-special header have multiple values?") + } + } + else + { + this._headers[name] = [value]; + } + }, + + /** +* Returns the value for the header specified by this. +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @throws NS_ERROR_NOT_AVAILABLE +* if the given header does not exist in this +* @returns string +* the field value for the given header, possibly with non-semantic changes +* (i.e., leading/trailing whitespace stripped, whitespace runs replaced +* with spaces, etc.) at the option of the implementation; multiple +* instances of the header will be combined with a comma, except for +* the three headers noted in the description of getHeaderValues +*/ + getHeader: function(fieldName) + { + return this.getHeaderValues(fieldName).join("\n"); + }, + + /** +* Returns the value for the header specified by fieldName as an array. +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @throws NS_ERROR_NOT_AVAILABLE +* if the given header does not exist in this +* @returns [string] +* an array of all the header values in this for the given +* header name. Header values will generally be collapsed +* into a single header by joining all header values together +* with commas, but certain headers (Proxy-Authenticate, +* WWW-Authenticate, and Set-Cookie) violate the HTTP spec +* and cannot be collapsed in this manner. For these headers +* only, the returned array may contain multiple elements if +* that header has been added more than once. +*/ + getHeaderValues: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + + if (name in this._headers) + return this._headers[name]; + else + throw Cr.NS_ERROR_NOT_AVAILABLE; + }, + + /** +* Returns true if a header with the given field name exists in this, false +* otherwise. +* +* @param fieldName : string +* the field name whose existence is to be determined in this +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @returns boolean +* true if the header's present, false otherwise +*/ + hasHeader: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + return (name in this._headers); + }, + + /** +* Returns a new enumerator over the field names of the headers in this, as +* nsISupportsStrings. The names returned will be in lowercase, regardless of +* how they were input using setHeader (header names are case-insensitive per +* RFC 2616). +*/ + get enumerator() + { + var headers = []; + for (var i in this._headers) + { + var supports = new SupportsString(); + supports.data = i; + headers.push(supports); + } + + return new nsSimpleEnumerator(headers); + } +}; + + +/** +* Constructs an nsISimpleEnumerator for the given array of items. +* +* @param items : Array +* the items, which must all implement nsISupports +*/ +function nsSimpleEnumerator(items) +{ + this._items = items; + this._nextIndex = 0; +} +nsSimpleEnumerator.prototype = +{ + hasMoreElements: function() + { + return this._nextIndex < this._items.length; + }, + getNext: function() + { + if (!this.hasMoreElements()) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + return this._items[this._nextIndex++]; + }, + QueryInterface: function(aIID) + { + if (Ci.nsISimpleEnumerator.equals(aIID) || + Ci.nsISupports.equals(aIID)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + + +/** +* A representation of the data in an HTTP request. +* +* @param port : uint +* the port on which the server receiving this request runs +*/ +function Request(port) +{ + /** Method of this request, e.g. GET or POST. */ + this._method = ""; + + /** Path of the requested resource; empty paths are converted to '/'. */ + this._path = ""; + + /** Query string, if any, associated with this request (not including '?'). */ + this._queryString = ""; + + /** Scheme of requested resource, usually http, always lowercase. */ + this._scheme = "http"; + + /** Hostname on which the requested resource resides. */ + this._host = undefined; + + /** Port number over which the request was received. */ + this._port = port; + + var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null); + + /** Stream from which data in this request's body may be read. */ + this._bodyInputStream = bodyPipe.inputStream; + + /** Stream to which data in this request's body is written. */ + this._bodyOutputStream = bodyPipe.outputStream; + + /** +* The headers in this request. +*/ + this._headers = new nsHttpHeaders(); + + /** +* For the addition of ad-hoc properties and new functionality without having +* to change nsIHttpRequest every time; currently lazily created, as its only +* use is in directory listings. +*/ + this._bag = null; +} +Request.prototype = +{ + // SERVER METADATA + + // + // see nsIHttpRequest.scheme + // + get scheme() + { + return this._scheme; + }, + + // + // see nsIHttpRequest.host + // + get host() + { + return this._host; + }, + + // + // see nsIHttpRequest.port + // + get port() + { + return this._port; + }, + + // REQUEST LINE + + // + // see nsIHttpRequest.method + // + get method() + { + return this._method; + }, + + // + // see nsIHttpRequest.httpVersion + // + get httpVersion() + { + return this._httpVersion.toString(); + }, + + // + // see nsIHttpRequest.path + // + get path() + { + return this._path; + }, + + // + // see nsIHttpRequest.queryString + // + get queryString() + { + return this._queryString; + }, + + // HEADERS + + // + // see nsIHttpRequest.getHeader + // + getHeader: function(name) + { + return this._headers.getHeader(name); + }, + + // + // see nsIHttpRequest.hasHeader + // + hasHeader: function(name) + { + return this._headers.hasHeader(name); + }, + + // + // see nsIHttpRequest.headers + // + get headers() + { + return this._headers.enumerator; + }, + + // + // see nsIPropertyBag.enumerator + // + get enumerator() + { + this._ensurePropertyBag(); + return this._bag.enumerator; + }, + + // + // see nsIHttpRequest.headers + // + get bodyInputStream() + { + return this._bodyInputStream; + }, + + // + // see nsIPropertyBag.getProperty + // + getProperty: function(name) + { + this._ensurePropertyBag(); + return this._bag.getProperty(name); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** Ensures a property bag has been created for ad-hoc behaviors. */ + _ensurePropertyBag: function() + { + if (!this._bag) + this._bag = new WritablePropertyBag(); + } +}; + + +// XPCOM trappings +if ("XPCOMUtils" in this && // Firefox 3.6 doesn't load XPCOMUtils in this scope for some reason... + "generateNSGetFactory" in XPCOMUtils) { + var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]); +} + + + +/** +* Creates a new HTTP server listening for loopback traffic on the given port, +* starts it, and runs the server until the server processes a shutdown request, +* spinning an event loop so that events posted by the server's socket are +* processed. +* +* This method is primarily intended for use in running this script from within +* xpcshell and running a functional HTTP server without having to deal with +* non-essential details. +* +* Note that running multiple servers using variants of this method probably +* doesn't work, simply due to how the internal event loop is spun and stopped. +* +* @note +* This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code); +* you should use this server as a component in Mozilla 1.8. +* @param port +* the port on which the server will run, or -1 if there exists no preference +* for a specific port; note that attempting to use some values for this +* parameter (particularly those below 1024) may cause this method to throw or +* may result in the server being prematurely shut down +* @param basePath +* a local directory from which requests will be served (i.e., if this is +* "/home/jwalden/" then a request to /index.html will load +* /home/jwalden/index.html); if this is omitted, only the default URLs in +* this server implementation will be functional +*/ +function server(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + // if you're running this, you probably want to see debugging info + DEBUG = true; + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.registerContentType("sjs", SJS_TYPE); + srv.identity.setPrimary("http", "localhost", port); + srv.start(port); + + var thread = gThreadManager.currentThread; + while (!srv.isStopped()) + thread.processNextEvent(true); + + // get rid of any pending requests + while (thread.hasPendingEvents()) + thread.processNextEvent(true); + + DEBUG = false; +} + +function startServerAsync(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.registerContentType("sjs", "sjs"); + srv.identity.setPrimary("http", "localhost", port); + srv.start(port); + return srv; +} + +exports.nsHttpServer = nsHttpServer; +exports.ScriptableInputStream = ScriptableInputStream; +exports.server = server; +exports.startServerAsync = startServerAsync; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/keyboard/hotkeys.js b/tools/addon-sdk-1.7/packages/api-utils/lib/keyboard/hotkeys.js new file mode 100644 index 0000000..bb296ed --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/keyboard/hotkeys.js @@ -0,0 +1,107 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { observer: keyboardObserver } = require("./observer"); +const { getKeyForCode, normalize, isFunctionKey, + MODIFIERS } = require("./utils"); + +/** + * Register a global `hotkey` that executes `listener` when the key combination + * in `hotkey` is pressed. If more then one `listener` is registered on the same + * key combination only last one will be executed. + * + * @param {string} hotkey + * Key combination in the format of 'modifier key'. + * + * Examples: + * + * "accel s" + * "meta shift i" + * "control alt d" + * + * Modifier keynames: + * + * - **shift**: The Shift key. + * - **alt**: The Alt key. On the Macintosh, this is the Option key. On + * Macintosh this can only be used in conjunction with another modifier, + * since `Alt+Letter` combinations are reserved for entering special + * characters in text. + * - **meta**: The Meta key. On the Macintosh, this is the Command key. + * - **control**: The Control key. + * - **accel**: The key used for keyboard shortcuts on the user's platform, + * which is Control on Windows and Linux, and Command on Mac. Usually, this + * would be the value you would use. + * + * @param {function} listener + * Function to execute when the `hotkey` is executed. + */ +exports.register = function register(hotkey, listener) { + hotkey = normalize(hotkey); + hotkeys[hotkey] = listener; +}; + +/** + * Unregister a global `hotkey`. If passed `listener` is not the one registered + * for the given `hotkey`, the call to this function will be ignored. + * + * @param {string} hotkey + * Key combination in the format of 'modifier key'. + * @param {function} listener + * Function that will be invoked when the `hotkey` is pressed. + */ +exports.unregister = function unregister(hotkey, listener) { + hotkey = normalize(hotkey); + if (hotkeys[hotkey] === listener) + delete hotkeys[hotkey]; +}; + +/** + * Map of hotkeys and associated functions. + */ +const hotkeys = exports.hotkeys = {}; + +keyboardObserver.on("keydown", function onKeypress(event, window) { + let key, modifiers = []; + let isChar = "isChar" in event && event.isChar; + let which = "which" in event ? event.which : null; + let keyCode = "keyCode" in event ? event.keyCode : null; + + if ("shiftKey" in event && event.shiftKey) + modifiers.push("shift"); + if ("altKey" in event && event.altKey) + modifiers.push("alt"); + if ("ctrlKey" in event && event.ctrlKey) + modifiers.push("control"); + if ("metaKey" in event && event.metaKey) + modifiers.push("meta"); + + // If it's not a printable character then we fall back to a human readable + // equivalent of one of the following constants. + // http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/events/nsIDOMKeyEvent.idl + key = getKeyForCode(keyCode); + + // If only non-function (f1 - f24) key or only modifiers are pressed we don't + // have a valid combination so we return immediately (Also, sometimes + // `keyCode` may be one for the modifier which means we do not have a + // modifier). + if (!key || (!isFunctionKey(key) && !modifiers.length) || key in MODIFIERS) + return; + + let combination = normalize({ key: key, modifiers: modifiers }); + let hotkey = hotkeys[combination]; + + if (hotkey) { + try { + hotkey(); + } catch (exception) { + console.exception(exception); + } finally { + // Work around bug 582052 by preventing the (nonexistent) default action. + event.preventDefault(); + } + } +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/keyboard/observer.js b/tools/addon-sdk-1.7/packages/api-utils/lib/keyboard/observer.js new file mode 100644 index 0000000..dd24865 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/keyboard/observer.js @@ -0,0 +1,53 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { Trait } = require("../light-traits"); +const { EventEmitterTrait: EventEmitter } = require("../events"); +const { DOMEventAssembler } = require("../events/assembler"); +const { browserWindowIterator, isBrowser } = require('../window-utils'); +const { observer: windowObserver } = require("../windows/observer"); + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ + /** + * Method is implemented by `EventEmitter` and is used just for emitting + * events on registered listeners. + */ + _emit: Trait.required, + /** + * Events that are supported and emitted by the module. + */ + supportedEventsTypes: [ "keydown", "keyup", "keypress" ], + /** + * Function handles all the supported events on all the windows that are + * observed. Method is used to proxy events to the listeners registered on + * this event emitter. + * @param {Event} event + * Keyboard event being emitted. + */ + handleEvent: function handleEvent(event) { + this._emit(event.type, event, event.target.ownerDocument.defaultView); + } +}); + +// Adding each opened window to a list of observed windows. +windowObserver.on("open", function onOpen(window) { + if (isBrowser(window)) + observer.observe(window); +}); +// Removing each closed window form the list of observed windows. +windowObserver.on("close", function onClose(window) { + if (isBrowser(window)) + observer.ignore(window); +}); + +// Making observer aware of already opened windows. +for each (let window in browserWindowIterator()) + observer.observe(window); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/keyboard/utils.js b/tools/addon-sdk-1.7/packages/api-utils/lib/keyboard/utils.js new file mode 100644 index 0000000..4693a12 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/keyboard/utils.js @@ -0,0 +1,186 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 runtime = require("../runtime"); +const { isString } = require("../type"); +const array = require("../array"); + + +const SWP = "{{SEPARATOR}}"; +const SEPARATOR = "-" +const INVALID_COMBINATION = "Hotkey key combination must contain one or more " + + "modifiers and only one key"; + +// Map of modifier key mappings. +const MODIFIERS = exports.MODIFIERS = { + 'accel': runtime.OS === "Darwin" ? 'meta' : 'control', + 'meta': 'meta', + 'control': 'control', + 'ctrl': 'control', + 'option': 'alt', + 'command': 'meta', + 'alt': 'alt', + 'shift': 'shift' +}; + +// Hash of key:code pairs for all the chars supported by `nsIDOMKeyEvent`. +// This is just a copy of the `nsIDOMKeyEvent` hash with normalized names. +// @See: http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/events/nsIDOMKeyEvent.idl +const CODES = exports.CODES = new function Codes() { + let nsIDOMKeyEvent = Ci.nsIDOMKeyEvent; + // Names that will be substituted with a shorter analogs. + let aliases = { + 'subtract': '-', + 'add': '+', + 'equals': '=', + 'slash': '/', + 'backslash': '\\', + 'openbracket': '[', + 'closebracket': ']', + 'quote': '\'', + 'backquote': '`', + 'period': '.', + 'semicolon': ';', + 'comma': ',' + }; + + // Normalizing keys and copying values to `this` object. + Object.keys(nsIDOMKeyEvent).filter(function(key) { + // Filter out only key codes. + return key.indexOf('DOM_VK') === 0; + }).map(function(key) { + // Map to key:values + return [ key, nsIDOMKeyEvent[key] ]; + }).map(function([key, value]) { + return [ key.replace('DOM_VK_', '').replace('_', '').toLowerCase(), value ]; + }).forEach(function ([ key, value ]) { + this[aliases[key] || key] = value; + }, this); +}; + +// Inverted `CODES` hash of `code:key`. +const KEYS = exports.KEYS = new function Keys() { + Object.keys(CODES).forEach(function(key) { + this[CODES[key]] = key; + }, this) +} + +exports.getKeyForCode = function getKeyForCode(code) { + return (code in KEYS) && KEYS[code]; +}; +exports.getCodeForKey = function getCodeForKey(key) { + return (key in CODES) && CODES[key]; +}; + +/** + * Utility function that takes string or JSON that defines a `hotkey` and + * returns normalized string version of it. + * @param {JSON|String} hotkey + * @param {String} [separator=" "] + * Optional string that represents separator used to concatenate keys in the + * given `hotkey`. + * @returns {String} + * @examples + * + * require("keyboard/hotkeys").normalize("b Shift accel"); + * // 'control shift b' -> on windows & linux + * // 'meta shift b' -> on mac + * require("keyboard/hotkeys").normalize("alt-d-shift", "-"); + * // 'alt shift d' + */ +var normalize = exports.normalize = function normalize(hotkey, separator) { + if (!isString(hotkey)) + hotkey = toString(hotkey, separator); + return toString(toJSON(hotkey, separator), separator); +}; + +/* + * Utility function that splits a string of characters that defines a `hotkey` + * into modifier keys and the defining key. + * @param {String} hotkey + * @param {String} [separator=" "] + * Optional string that represents separator used to concatenate keys in the + * given `hotkey`. + * @returns {JSON} + * @examples + * + * require("keyboard/hotkeys").toJSON("accel shift b"); + * // { key: 'b', modifiers: [ 'control', 'shift' ] } -> on windows & linux + * // { key: 'b', modifiers: [ 'meta', 'shift' ] } -> on mac + * + * require("keyboard/hotkeys").normalize("alt-d-shift", "-"); + * // { key: 'd', modifiers: [ 'alt', 'shift' ] } + */ +var toJSON = exports.toJSON = function toJSON(hotkey, separator) { + separator = separator || SEPARATOR; + // Since default separator is `-`, combination may take form of `alt--`. To + // avoid misbehavior we replace `--` with `-{{SEPARATOR}}` where + // `{{SEPARATOR}}` can be swapped later. + hotkey = hotkey.toLowerCase().replace(separator + separator, separator + SWP); + + let value = {}; + let modifiers = []; + let keys = hotkey.split(separator); + keys.forEach(function(name) { + // If name is `SEPARATOR` than we swap it back. + if (name === SWP) + name = separator; + if (name in MODIFIERS) { + array.add(modifiers, MODIFIERS[name]); + } else { + if (!value.key) + value.key = name; + else + throw new TypeError(INVALID_COMBINATION); + } + }); + + if (!value.key) + throw new TypeError(INVALID_COMBINATION); + + value.modifiers = modifiers.sort(); + return value; +}; + +/** + * Utility function that takes object that defines a `hotkey` and returns + * string representation of it. + * + * _Please note that this function does not validates data neither it normalizes + * it, if you are unsure that data is well formed use `normalize` function + * instead. + * + * @param {JSON} hotkey + * @param {String} [separator=" "] + * Optional string that represents separator used to concatenate keys in the + * given `hotkey`. + * @returns {String} + * @examples + * + * require("keyboard/hotkeys").toString({ + * key: 'b', + * modifiers: [ 'control', 'shift' ] + * }, '+'); + * // 'control+shift+b + * + */ +var toString = exports.toString = function toString(hotkey, separator) { + let keys = hotkey.modifiers.slice(); + keys.push(hotkey.key); + return keys.join(separator || SEPARATOR); +}; + +/** + * Utility function takes `key` name and returns `true` if it's function key + * (F1, ..., F24) and `false` if it's not. + */ +var isFunctionKey = exports.isFunctionKey = function isFunctionKey(key) { + var $ + return key[0].toLowerCase() === 'f' && + ($ = parseInt(key.substr(1)), 0 < $ && $ < 25); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/l10n/locale.js b/tools/addon-sdk-1.7/packages/api-utils/lib/l10n/locale.js new file mode 100644 index 0000000..ed82789 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/l10n/locale.js @@ -0,0 +1,122 @@ +/* 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 prefs = require("preferences-service"); +const { Cu, Cc, Ci } = require("chrome"); +const { Services } = Cu.import("resource://gre/modules/Services.jsm"); + + +/** + * Gets the currently selected locale for display. + * Gets all usable locale that we can use sorted by priority of relevance + * @return Array of locales, begins with highest priority + */ +const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; +const PREF_SELECTED_LOCALE = "general.useragent.locale"; +const PREF_ACCEPT_LANGUAGES = "intl.accept_languages"; +exports.getPreferedLocales = function getPreferedLocales() { + let locales = []; + + function addLocale(locale) { + locale = locale.toLowerCase(); + if (locales.indexOf(locale) === -1) + locales.push(locale); + } + + // Most important locale is OS one. But we use it, only if + // "intl.locale.matchOS" pref is set to `true`. + // Currently only used for multi-locales mobile builds. + // http://mxr.mozilla.org/mozilla-central/source/mobile/android/installer/Makefile.in#46 + if (prefs.get(PREF_MATCH_OS_LOCALE, false)) { + let localeService = Cc["@mozilla.org/intl/nslocaleservice;1"]. + getService(Ci.nsILocaleService); + let osLocale = localeService.getLocaleComponentForUserAgent(); + addLocale(osLocale); + } + + // In some cases, mainly on Fennec and on Linux version, + // `general.useragent.locale` is a special 'localized' value, like: + // "chrome://global/locale/intl.properties" + let browserUiLocale = prefs.getLocalized(PREF_SELECTED_LOCALE, "") || + prefs.get(PREF_SELECTED_LOCALE, ""); + if (browserUiLocale) + addLocale(browserUiLocale); + + + // Third priority is the list of locales used for web content + let contentLocales = prefs.get(PREF_ACCEPT_LANGUAGES, ""); + if (contentLocales) { + // This list is a string of locales seperated by commas. + // There is spaces after commas, so strip each item + for each(let locale in contentLocales.split(",")) + addLocale(locale.replace(/(^\s+)|(\s+$)/g, "")); + } + + // Finally, we ensure that en-US is the final fallback if it wasn't added + addLocale("en-US"); + + return locales; +} + +/** + * Selects the closest matching locale from a list of locales. + * + * @param aLocales + * An array of available locales + * @param aMatchLocales + * An array of prefered locales, ordered by priority. Most wanted first. + * Locales have to be in lowercase. + * If null, uses getPreferedLocales() results + * @return the best match for the currently selected locale + * + * Stolen from http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/XPIProvider.jsm + */ +exports.findClosestLocale = function findClosestLocale(aLocales, aMatchLocales) { + + aMatchLocales = aMatchLocales || exports.getPreferedLocales(); + + // Holds the best matching localized resource + let bestmatch = null; + // The number of locale parts it matched with + let bestmatchcount = 0; + // The number of locale parts in the match + let bestpartcount = 0; + + for each (let locale in aMatchLocales) { + let lparts = locale.split("-"); + for each (let localized in aLocales) { + let found = localized.toLowerCase(); + // Exact match is returned immediately + if (locale == found) + return localized; + + let fparts = found.split("-"); + /* If we have found a possible match and this one isn't any longer + then we dont need to check further. */ + if (bestmatch && fparts.length < bestmatchcount) + continue; + + // Count the number of parts that match + let maxmatchcount = Math.min(fparts.length, lparts.length); + let matchcount = 0; + while (matchcount < maxmatchcount && + fparts[matchcount] == lparts[matchcount]) + matchcount++; + + /* If we matched more than the last best match or matched the same and + this locale is less specific than the last best match. */ + if (matchcount > bestmatchcount || + (matchcount == bestmatchcount && fparts.length < bestpartcount)) { + bestmatch = localized; + bestmatchcount = matchcount; + bestpartcount = fparts.length; + } + } + // If we found a valid match for this locale return it + if (bestmatch) + return bestmatch; + } + return null; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/l10n/plural-rules.js b/tools/addon-sdk-1.7/packages/api-utils/lib/l10n/plural-rules.js new file mode 100644 index 0000000..c891870 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/l10n/plural-rules.js @@ -0,0 +1,387 @@ +/* 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/. */ + +// This file is automatically generated with /python-lib/plural-rules-generator.py +// Fetching data from: http://unicode.org/repos/cldr/trunk/common/supplemental/plurals.xml + +// Mapping of short locale name == to == > rule index in following list +const LOCALES_TO_RULES = { + "af": 2, + "ak": 3, + "am": 3, + "ar": 1, + "asa": 2, + "az": 0, + "be": 10, + "bem": 2, + "bez": 2, + "bg": 2, + "bh": 3, + "bm": 0, + "bn": 2, + "bo": 0, + "br": 19, + "brx": 2, + "bs": 10, + "ca": 2, + "cgg": 2, + "chr": 2, + "cs": 11, + "cy": 16, + "da": 2, + "de": 2, + "dv": 2, + "dz": 0, + "ee": 2, + "el": 2, + "en": 2, + "eo": 2, + "es": 2, + "et": 2, + "eu": 2, + "fa": 0, + "ff": 4, + "fi": 2, + "fil": 3, + "fo": 2, + "fr": 4, + "fur": 2, + "fy": 2, + "ga": 7, + "gd": 23, + "gl": 2, + "gsw": 2, + "gu": 2, + "guw": 3, + "gv": 22, + "ha": 2, + "haw": 2, + "he": 2, + "hi": 3, + "hr": 10, + "hu": 0, + "id": 0, + "ig": 0, + "ii": 0, + "is": 2, + "it": 2, + "iu": 6, + "ja": 0, + "jmc": 2, + "jv": 0, + "ka": 0, + "kab": 4, + "kaj": 2, + "kcg": 2, + "kde": 0, + "kea": 0, + "kk": 2, + "kl": 2, + "km": 0, + "kn": 0, + "ko": 0, + "ksb": 2, + "ksh": 20, + "ku": 2, + "kw": 6, + "lag": 17, + "lb": 2, + "lg": 2, + "ln": 3, + "lo": 0, + "lt": 9, + "lv": 5, + "mas": 2, + "mg": 3, + "mk": 15, + "ml": 2, + "mn": 2, + "mo": 8, + "mr": 2, + "ms": 0, + "mt": 14, + "my": 0, + "nah": 2, + "naq": 6, + "nb": 2, + "nd": 2, + "ne": 2, + "nl": 2, + "nn": 2, + "no": 2, + "nr": 2, + "nso": 3, + "ny": 2, + "nyn": 2, + "om": 2, + "or": 2, + "pa": 2, + "pap": 2, + "pl": 12, + "ps": 2, + "pt": 2, + "rm": 2, + "ro": 8, + "rof": 2, + "ru": 10, + "rwk": 2, + "sah": 0, + "saq": 2, + "se": 6, + "seh": 2, + "ses": 0, + "sg": 0, + "sh": 10, + "shi": 18, + "sk": 11, + "sl": 13, + "sma": 6, + "smi": 6, + "smj": 6, + "smn": 6, + "sms": 6, + "sn": 2, + "so": 2, + "sq": 2, + "sr": 10, + "ss": 2, + "ssy": 2, + "st": 2, + "sv": 2, + "sw": 2, + "syr": 2, + "ta": 2, + "te": 2, + "teo": 2, + "th": 0, + "ti": 3, + "tig": 2, + "tk": 2, + "tl": 3, + "tn": 2, + "to": 0, + "tr": 0, + "ts": 2, + "tzm": 21, + "uk": 10, + "ur": 2, + "ve": 2, + "vi": 0, + "vun": 2, + "wa": 3, + "wae": 2, + "wo": 0, + "xh": 2, + "xog": 2, + "yo": 0, + "zh": 0, + "zu": 2 +}; + +// Utility functions for plural rules methods +function isIn(n, list) list.indexOf(n) !== -1; +function isBetween(n, start, end) start <= n && n <= end; + +// List of all plural rules methods, that maps an integer to the plural form name to use +const RULES = { + "0": function (n) { + + return "other" + }, + "1": function (n) { + if ((isBetween((n % 100), 3, 10))) + return "few"; + if (n == 0) + return "zero"; + if ((isBetween((n % 100), 11, 99))) + return "many"; + if (n == 2) + return "two"; + if (n == 1) + return "one"; + return "other" + }, + "2": function (n) { + if (n == 1) + return "one"; + return "other" + }, + "3": function (n) { + if ((isBetween(n, 0, 1))) + return "one"; + return "other" + }, + "4": function (n) { + if ((isBetween(n, 0, 2)) && n != 2) + return "one"; + return "other" + }, + "5": function (n) { + if (n == 0) + return "zero"; + if ((n % 10) == 1 && (n % 100) != 11) + return "one"; + return "other" + }, + "6": function (n) { + if (n == 2) + return "two"; + if (n == 1) + return "one"; + return "other" + }, + "7": function (n) { + if ((isBetween(n, 3, 6))) + return "few"; + if ((isBetween(n, 7, 10))) + return "many"; + if (n == 2) + return "two"; + if (n == 1) + return "one"; + return "other" + }, + "8": function (n) { + if (n == 0 || n != 1 && (isBetween((n % 100), 1, 19))) + return "few"; + if (n == 1) + return "one"; + return "other" + }, + "9": function (n) { + if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) + return "few"; + if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19))) + return "one"; + return "other" + }, + "10": function (n) { + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) + return "few"; + if ((n % 10) == 0 || (isBetween((n % 10), 5, 9)) || (isBetween((n % 100), 11, 14))) + return "many"; + if ((n % 10) == 1 && (n % 100) != 11) + return "one"; + return "other" + }, + "11": function (n) { + if ((isBetween(n, 2, 4))) + return "few"; + if (n == 1) + return "one"; + return "other" + }, + "12": function (n) { + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) + return "few"; + if (n != 1 && (isBetween((n % 10), 0, 1)) || (isBetween((n % 10), 5, 9)) || (isBetween((n % 100), 12, 14))) + return "many"; + if (n == 1) + return "one"; + return "other" + }, + "13": function (n) { + if ((isBetween((n % 100), 3, 4))) + return "few"; + if ((n % 100) == 2) + return "two"; + if ((n % 100) == 1) + return "one"; + return "other" + }, + "14": function (n) { + if (n == 0 || (isBetween((n % 100), 2, 10))) + return "few"; + if ((isBetween((n % 100), 11, 19))) + return "many"; + if (n == 1) + return "one"; + return "other" + }, + "15": function (n) { + if ((n % 10) == 1 && n != 11) + return "one"; + return "other" + }, + "16": function (n) { + if (n == 3) + return "few"; + if (n == 0) + return "zero"; + if (n == 6) + return "many"; + if (n == 2) + return "two"; + if (n == 1) + return "one"; + return "other" + }, + "17": function (n) { + if (n == 0) + return "zero"; + if ((isBetween(n, 0, 2)) && n != 0 && n != 2) + return "one"; + return "other" + }, + "18": function (n) { + if ((isBetween(n, 2, 10))) + return "few"; + if ((isBetween(n, 0, 1))) + return "one"; + return "other" + }, + "19": function (n) { + if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(isBetween((n % 100), 10, 19) || isBetween((n % 100), 70, 79) || isBetween((n % 100), 90, 99))) + return "few"; + if ((n % 1000000) == 0 && n != 0) + return "many"; + if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92])) + return "two"; + if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91])) + return "one"; + return "other" + }, + "20": function (n) { + if (n == 0) + return "zero"; + if (n == 1) + return "one"; + return "other" + }, + "21": function (n) { + if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) + return "one"; + return "other" + }, + "22": function (n) { + if ((isBetween((n % 10), 1, 2)) || (n % 20) == 0) + return "one"; + return "other" + }, + "23": function (n) { + if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) + return "few"; + if (isIn(n, [2, 12])) + return "two"; + if (isIn(n, [1, 11])) + return "one"; + return "other" + }, +}; + +/** + * Return a function that gives the plural form name for a given integer + * for the specified `locale` + * let fun = getRulesForLocale('en'); + * fun(1) -> 'one' + * fun(0) -> 'other' + * fun(1000) -> 'other' + */ +exports.getRulesForLocale = function getRulesForLocale(locale) { + let index = LOCALES_TO_RULES[locale]; + if (!(index in RULES)) + throw new Error('Plural form unknown for locale "' + locale + '"'); + return RULES[index]; +} + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/light-traits.js b/tools/addon-sdk-1.7/packages/api-utils/lib/light-traits.js new file mode 100644 index 0000000..954d093 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/light-traits.js @@ -0,0 +1,596 @@ +/* vim:ts=2:sts=2:sw=2: + * 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"; + +// `var` is being used in the module in order to make it reusable in +// environments in which `let` is not yet supported. + +// Shortcut to `Object.prototype.hasOwnProperty.call`. +// owns(object, name) would be the same as +// Object.prototype.hasOwnProperty.call(object, name); +var owns = Function.prototype.call.bind(Object.prototype.hasOwnProperty); + +/** + * Whether or not given property descriptors are equivalent. They are + * equivalent either if both are marked as 'conflict' or 'required' property + * or if all the properties of descriptors are equal. + * @param {Object} actual + * @param {Object} expected + */ +function equivalentDescriptors(actual, expected) { + return (actual.conflict && expected.conflict) || + (actual.required && expected.required) || + equalDescriptors(actual, expected); +} +/** + * Whether or not given property descriptors define equal properties. + */ +function equalDescriptors(actual, expected) { + return actual.get === expected.get && + actual.set === expected.set && + actual.value === expected.value && + !!actual.enumerable === !!expected.enumerable && + !!actual.configurable === !!expected.configurable && + !!actual.writable === !!expected.writable; +} + +// Utilities that throwing exceptions for a properties that are marked +// as "required" or "conflict" properties. +function throwConflictPropertyError(name) { + throw new Error("Remaining conflicting property: `" + name + "`"); +} +function throwRequiredPropertyError(name) { + throw new Error("Missing required property: `" + name + "`"); +} + +/** + * Generates custom **required** property descriptor. Descriptor contains + * non-standard property `required` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function RequiredPropertyDescriptor(name) { + // Creating function by binding first argument to a property `name` on the + // `throwConflictPropertyError` function. Created function is used as a + // getter & setter of the created property descriptor. This way we ensure + // that we throw exception late (on property access) if object with + // `required` property was instantiated using built-in `Object.create`. + var accessor = throwRequiredPropertyError.bind(null, name); + return { get: accessor, set: accessor, required: true }; +} + +/** + * Generates custom **conflicting** property descriptor. Descriptor contains + * non-standard property `conflict` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function ConflictPropertyDescriptor(name) { + // For details see `RequiredPropertyDescriptor` since idea is same. + var accessor = throwConflictPropertyError.bind(null, name); + return { get: accessor, set: accessor, conflict: true }; +} + +/** + * Tests if property is marked as `required` property. + */ +function isRequiredProperty(object, name) { + return !!object[name].required; +} + +/** + * Tests if property is marked as `conflict` property. + */ +function isConflictProperty(object, name) { + return !!object[name].conflict; +} + +/** + * Function tests whether or not method of the `source` object with a given + * `name` is inherited from `Object.prototype`. + */ +function isBuiltInMethod(name, source) { + var target = Object.prototype[name]; + + // If methods are equal then we know it's `true`. + return target == source || + // If `source` object comes form a different sandbox `==` will evaluate + // to `false`, in that case we check if functions names and sources match. + (String(target) === String(source) && target.name === source.name); +} + +/** + * Function overrides `toString` and `constructor` methods of a given `target` + * object with a same-named methods of a given `source` if methods of `target` + * object are inherited / copied from `Object.prototype`. + * @see create + */ +function overrideBuiltInMethods(target, source) { + if (isBuiltInMethod("toString", target.toString)) { + Object.defineProperty(target, "toString", { + value: source.toString, + configurable: true, + enumerable: false + }); + } + + if (isBuiltInMethod("constructor", target.constructor)) { + Object.defineProperty(target, "constructor", { + value: source.constructor, + configurable: true, + enumerable: false + }); + } +} + +/** + * Composes new trait with the same own properties as the original trait, + * except that all property names appearing in the first argument are replaced + * by "required" property descriptors. + * @param {String[]} keys + * Array of strings property names. + * @param {Object} trait + * A trait some properties of which should be excluded. + * @returns {Object} + * @example + * var newTrait = exclude(["name", ...], trait) + */ +function exclude(names, trait) { + var map = {}; + + Object.keys(trait).forEach(function(name) { + + // If property is not excluded (the array of names does not contain it), + // or it is a "required" property, copy it to the property descriptor `map` + // that will be used for creation of resulting trait. + if (!~names.indexOf(name) || isRequiredProperty(trait, name)) + map[name] = { value: trait[name], enumerable: true }; + + // For all the `names` in the exclude name array we create required + // property descriptors and copy them to the `map`. + else + map[name] = { value: RequiredPropertyDescriptor(name), enumerable: true }; + }); + + return Object.create(Trait.prototype, map); +} + +/** + * Composes new instance of `Trait` with a properties of a given `trait`, + * except that all properties whose name is an own property of `renames` will + * be renamed to `renames[name]` and a `"required"` property for name will be + * added instead. + * + * For each renamed property, a required property is generated. If + * the `renames` map two properties to the same name, a conflict is generated. + * If the `renames` map a property to an existing unrenamed property, a + * conflict is generated. + * + * @param {Object} renames + * An object whose own properties serve as a mapping from old names to new + * names. + * @param {Object} trait + * A new trait with renamed properties. + * @returns {Object} + * @example + * + * // Return trait with `bar` property equal to `trait.foo` and with + * // `foo` and `baz` "required" properties. + * var renamedTrait = rename({ foo: "bar", baz: null }), trait); + * + * // t1 and t2 are equivalent traits + * var t1 = rename({a: "b"}, t); + * var t2 = compose(exclude(["a"], t), { a: { required: true }, b: t[a] }); + */ +function rename(renames, trait) { + var map = {}; + + // Loop over all the properties of the given `trait` and copy them to a + // property descriptor `map` that will be used for the creation of the + // resulting trait. Also, rename properties in the `map` as specified by + // `renames`. + Object.keys(trait).forEach(function(name) { + var alias; + + // If the property is in the `renames` map, and it isn't a "required" + // property (which should never need to be aliased because "required" + // properties never conflict), then we must try to rename it. + if (owns(renames, name) && !isRequiredProperty(trait, name)) { + alias = renames[name]; + + // If the `map` already has the `alias`, and it isn't a "required" + // property, that means the `alias` conflicts with an existing name for a + // provided trait (that can happen if >=2 properties are aliased to the + // same name). In this case we mark it as a conflicting property. + // Otherwise, everything is fine, and we copy property with an `alias` + // name. + if (owns(map, alias) && !map[alias].value.required) { + map[alias] = { + value: ConflictPropertyDescriptor(alias), + enumerable: true + }; + } + else { + map[alias] = { + value: trait[name], + enumerable: true + }; + } + + // Regardless of whether or not the rename was successful, we check to + // see if the original `name` exists in the map (such a property + // could exist if previous another property was aliased to this `name`). + // If it isn't, we mark it as "required", to make sure the caller + // provides another value for the old name, which methods of the trait + // might continue to reference. + if (!owns(map, name)) { + map[name] = { + value: RequiredPropertyDescriptor(name), + enumerable: true + }; + } + } + + // Otherwise, either the property isn't in the `renames` map (thus the + // caller is not trying to rename it) or it is a "required" property. + // Either way, we don't have to alias the property, we just have to copy it + // to the map. + else { + // The property isn't in the map yet, so we copy it over. + if (!owns(map, name)) { + map[name] = { value: trait[name], enumerable: true }; + } + + // The property is already in the map (that means another property was + // aliased with this `name`, which creates a conflict if the property is + // not marked as "required"), so we have to mark it as a "conflict" + // property. + else if (!isRequiredProperty(trait, name)) { + map[name] = { + value: ConflictPropertyDescriptor(name), + enumerable: true + }; + } + } + }); + return Object.create(Trait.prototype, map); +} + +/** + * Composes new resolved trait, with all the same properties as the original + * `trait`, except that all properties whose name is an own property of + * `resolutions` will be renamed to `resolutions[name]`. + * + * If `resolutions[name]` is `null`, the value is mapped to a property + * descriptor that is marked as a "required" property. + */ +function resolve(resolutions, trait) { + var renames = {}; + var exclusions = []; + + // Go through each mapping in `resolutions` object and distribute it either + // to `renames` or `exclusions`. + Object.keys(resolutions).forEach(function(name) { + + // If `resolutions[name]` is a truthy value then it's a mapping old -> new + // so we copy it to `renames` map. + if (resolutions[name]) + renames[name] = resolutions[name]; + + // Otherwise it's not a mapping but an exclusion instead in which case we + // add it to the `exclusions` array. + else + exclusions.push(name); + }); + + // First `exclude` **then** `rename` and order is important since + // `exclude` and `rename` are not associative. + return rename(renames, exclude(exclusions, trait)); +} + +/** + * Create a Trait (a custom property descriptor map) that represents the given + * `object`'s own properties. Property descriptor map is a "custom", because it + * inherits from `Trait.prototype` and it's property descriptors may contain + * two attributes that is not part of the ES5 specification: + * + * - "required" (this property must be provided by another trait + * before an instance of this trait can be created) + * - "conflict" (when the trait is composed with another trait, + * a unique value for this property is provided by two or more traits) + * + * Data properties bound to the `Trait.required` singleton exported by + * this module will be marked as "required" properties. + * + * @param {Object} object + * Map of properties to compose trait from. + * @returns {Trait} + * Trait / Property descriptor map containing all the own properties of the + * given argument. + */ +function trait(object) { + var map; + var trait = object; + + if (!(object instanceof Trait)) { + // If the passed `object` is not already an instance of `Trait`, we create + // a property descriptor `map` containing descriptors for the own properties + // of the given `object`. `map` is then used to create a `Trait` instance + // after all properties are mapped. Note that we can't create a trait and + // then just copy properties into it since that will fail for inherited + // read-only properties. + map = {}; + + // Each own property of the given `object` is mapped to a data property + // whose value is a property descriptor. + Object.keys(object).forEach(function (name) { + + // If property of an `object` is equal to a `Trait.required`, it means + // that it was marked as "required" property, in which case we map it + // to "required" property. + if (Trait.required == + Object.getOwnPropertyDescriptor(object, name).value) { + map[name] = { + value: RequiredPropertyDescriptor(name), + enumerable: true + }; + } + // Otherwise property is mapped to it's property descriptor. + else { + map[name] = { + value: Object.getOwnPropertyDescriptor(object, name), + enumerable: true + }; + } + }); + + trait = Object.create(Trait.prototype, map); + } + return trait; +} + +/** + * Compose a property descriptor map that inherits from `Trait.prototype` and + * contains property descriptors for all the own properties of the passed + * traits. + * + * If two or more traits have own properties with the same name, the returned + * trait will contain a "conflict" property for that name. Composition is a + * commutative and associative operation, and the order of its arguments is + * irrelevant. + */ +function compose(trait1, trait2/*, ...*/) { + // Create a new property descriptor `map` to which all the own properties + // of the passed traits are copied. This map will be used to create a `Trait` + // instance that will be the result of this composition. + var map = {}; + + // Properties of each passed trait are copied to the composition. + Array.prototype.forEach.call(arguments, function(trait) { + // Copying each property of the given trait. + Object.keys(trait).forEach(function(name) { + + // If `map` already owns a property with the `name` and it is not + // marked "required". + if (owns(map, name) && !map[name].value.required) { + + // If the source trait's property with the `name` is marked as + // "required", we do nothing, as the requirement was already resolved + // by a property in the `map` (because it already contains a + // non-required property with that `name`). But if properties are just + // different, we have a name clash and we substitute it with a property + // that is marked "conflict". + if (!isRequiredProperty(trait, name) && + !equivalentDescriptors(map[name].value, trait[name]) + ) { + map[name] = { + value: ConflictPropertyDescriptor(name), + enumerable: true + }; + } + } + + // Otherwise, the `map` does not have an own property with the `name`, or + // it is marked "required". Either way, the trait's property is copied to + // the map (if the property of the `map` is marked "required", it is going + // to be resolved by the property that is being copied). + else { + map[name] = { value: trait[name], enumerable: true }; + } + }); + }); + + return Object.create(Trait.prototype, map); +} + +/** + * `defineProperties` is like `Object.defineProperties`, except that it + * ensures that: + * - An exception is thrown if any property in a given `properties` map + * is marked as "required" property and same named property is not + * found in a given `prototype`. + * - An exception is thrown if any property in a given `properties` map + * is marked as "conflict" property. + * @param {Object} object + * Object to define properties on. + * @param {Object} properties + * Properties descriptor map. + * @returns {Object} + * `object` that was passed as a first argument. + */ +function defineProperties(object, properties) { + + // Create a map into which we will copy each verified property from the given + // `properties` description map. We use it to verify that none of the + // provided properties is marked as a "conflict" property and that all + // "required" properties are resolved by a property of an `object`, so we + // can throw an exception before mutating object if that isn't the case. + var verifiedProperties = {}; + + // Coping each property from a given `properties` descriptor map to a + // verified map of property descriptors. + Object.keys(properties).forEach(function(name) { + + // If property is marked as "required" property and we don't have a same + // named property in a given `object` we throw an exception. If `object` + // has same named property just skip this property since required property + // is was inherited and there for requirement was satisfied. + if (isRequiredProperty(properties, name)) { + if (!(name in object)) + throwRequiredPropertyError(name); + } + + // If property is marked as "conflict" property we throw an exception. + else if (isConflictProperty(properties, name)) { + throwConflictPropertyError(name); + } + + // If property is not marked neither as "required" nor "conflict" property + // we copy it to verified properties map. + else { + verifiedProperties[name] = properties[name]; + } + }); + + // If no exceptions were thrown yet, we know that our verified property + // descriptor map has no properties marked as "conflict" or "required", + // so we just delegate to the built-in `Object.defineProperties`. + return Object.defineProperties(object, verifiedProperties); +} + +/** + * `create` is like `Object.create`, except that it ensures that: + * - An exception is thrown if any property in a given `properties` map + * is marked as "required" property and same named property is not + * found in a given `prototype`. + * - An exception is thrown if any property in a given `properties` map + * is marked as "conflict" property. + * @param {Object} prototype + * prototype of the composed object + * @param {Object} properties + * Properties descriptor map. + * @returns {Object} + * An object that inherits form a given `prototype` and implements all the + * properties defined by a given `properties` descriptor map. + */ +function create(prototype, properties) { + + // Creating an instance of the given `prototype`. + var object = Object.create(prototype); + + // Overriding `toString`, `constructor` methods if they are just inherited + // from `Object.prototype` with a same named methods of the `Trait.prototype` + // that will have more relevant behavior. + overrideBuiltInMethods(object, Trait.prototype); + + // Trying to define given `properties` on the `object`. We use our custom + // `defineProperties` function instead of build-in `Object.defineProperties` + // that behaves exactly the same, except that it will throw if any + // property in the given `properties` descriptor is marked as "required" or + // "conflict" property. + return defineProperties(object, properties); +} + +/** + * Composes new trait. If two or more traits have own properties with the + * same name, the new trait will contain a "conflict" property for that name. + * "compose" is a commutative and associative operation, and the order of its + * arguments is not significant. + * + * **Note:** Use `Trait.compose` instead of calling this function with more + * than one argument. The multiple-argument functionality is strictly for + * backward compatibility. + * + * @params {Object} trait + * Takes traits as an arguments + * @returns {Object} + * New trait containing the combined own properties of all the traits. + * @example + * var newTrait = compose(trait_1, trait_2, ..., trait_N) + */ +function Trait(trait1, trait2) { + + // If the function was called with one argument, the argument should be + // an object whose properties are mapped to property descriptors on a new + // instance of Trait, so we delegate to the trait function. + // If the function was called with more than one argument, those arguments + // should be instances of Trait or plain property descriptor maps + // whose properties should be mixed into a new instance of Trait, + // so we delegate to the compose function. + + return trait2 === undefined ? trait(trait1) : compose.apply(null, arguments); +} + +Object.freeze(Object.defineProperties(Trait.prototype, { + toString: { + value: function toString() { + return "[object " + this.constructor.name + "]"; + } + }, + + /** + * `create` is like `Object.create`, except that it ensures that: + * - An exception is thrown if this trait defines a property that is + * marked as required property and same named property is not + * found in a given `prototype`. + * - An exception is thrown if this trait contains property that is + * marked as "conflict" property. + * @param {Object} + * prototype of the compared object + * @returns {Object} + * An object with all of the properties described by the trait. + */ + create: { + value: function createTrait(prototype) { + return create(undefined === prototype ? Object.prototype : prototype, + this); + }, + enumerable: true + }, + + /** + * Composes a new resolved trait, with all the same properties as the original + * trait, except that all properties whose name is an own property of + * `resolutions` will be renamed to the value of `resolutions[name]`. If + * `resolutions[name]` is `null`, the property is marked as "required". + * @param {Object} resolutions + * An object whose own properties serve as a mapping from old names to new + * names, or to `null` if the property should be excluded. + * @returns {Object} + * New trait with the same own properties as the original trait but renamed. + */ + resolve: { + value: function resolveTrait(resolutions) { + return resolve(resolutions, this); + }, + enumerable: true + } +})); + +/** + * @see compose + */ +Trait.compose = Object.freeze(compose); +Object.freeze(compose.prototype); + +/** + * Constant singleton, representing placeholder for required properties. + * @type {Object} + */ +Trait.required = Object.freeze(Object.create(Object.prototype, { + toString: { + value: Object.freeze(function toString() { + return "<Trait.required>"; + }) + } +})); +Object.freeze(Trait.required.toString.prototype); + +exports.Trait = Object.freeze(Trait); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/list.js b/tools/addon-sdk-1.7/packages/api-utils/lib/list.js new file mode 100644 index 0000000..4c6c126 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/list.js @@ -0,0 +1,114 @@ +/* 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 { Trait } = require('./traits'); + +/** + * @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/list + */ +const Iterable = Trait.compose({ + /** + * Hash map of key-values to iterate over. + * Note: That this property can be a getter if you need dynamic behavior. + * @type {Object} + */ + _keyValueMap: Trait.required, + /** + * Custom iterator providing `Iterable`s enumeration behavior. + * @param {Boolean} onKeys + */ + __iterator__: function __iterator__(onKeys, onKeyValue) { + let map = this._keyValueMap; + for (let key in map) + yield onKeyValue ? [key, map[key]] : onKeys ? key : map[key]; + } +}); +exports.Iterable = Iterable; + +/** + * An ordered collection (also known as a sequence) disallowing duplicate + * elements. List is composed out of `Iterable` there for it provides custom + * enumeration behavior that is similar to array (enumerates only on the + * elements of the list). List is a base trait and is meant to be a part of + * composition, since all of it's API is private except length property. + */ +const List = Trait.resolve({ toString: null }).compose({ + _keyValueMap: null, + /** + * List constructor can take any number of element to populate itself. + * @params {Object|String|Number} element + * @example + * List(1,2,3).length == 3 // true + */ + constructor: function List() { + this._keyValueMap = []; + for (let i = 0, ii = arguments.length; i < ii; i++) + this._add(arguments[i]); + }, + /** + * Number of elements in this list. + * @type {Number} + */ + get length() this._keyValueMap.length, + /** + * Returns a string representing this list. + * @returns {String} + */ + toString: function toString() 'List(' + this._keyValueMap + ')', + /** + * Returns `true` if this list contains the specified `element`. + * @param {Object|Number|String} element + * @returns {Boolean} + */ + _has: function _has(element) 0 <= this._keyValueMap.indexOf(element), + /** + * Appends the specified `element` to the end of this list, if it doesn't + * contains it. Ignores the call if `element` is already contained. + * @param {Object|Number|String} element + */ + _add: function _add(element) { + let list = this._keyValueMap, + index = list.indexOf(element); + if (0 > index) + list.push(this._public[list.length] = element); + }, + /** + * Removes specified `element` from this list, if it contains it. + * Ignores the call if `element` is not contained. + * @param {Object|Number|String} element + */ + _remove: function _remove(element) { + let list = this._keyValueMap, + index = list.indexOf(element); + if (0 <= index) { + delete this._public[list.length - 1]; + list.splice(index, 1); + for (let length = list.length; index < length; index++) + this._public[index] = list[index]; + } + }, + /** + * Removes all of the elements from this list. + */ + _clear: function _clear() { + for (let i = 0, ii = this._keyValueMap.length; i < ii; i ++) + delete this._public[i]; + this._keyValueMap.splice(0); + }, + /** + * Custom iterator providing `List`s enumeration behavior. + * We cant reuse `_iterator` that is defined by `Iterable` since it provides + * iteration in an arbitrary order. + * @see https://developer.mozilla.org/en/JavaScript/Reference/Statements/for...in + * @param {Boolean} onKeys + */ + __iterator__: function __iterator__(onKeys, onKeyValue) { + let array = this._keyValueMap.slice(0), + i = -1; + for each(let element in array) + yield onKeyValue ? [++i, element] : onKeys ? ++i : element; + } +}); +exports.List = List; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/match-pattern.js b/tools/addon-sdk-1.7/packages/api-utils/lib/match-pattern.js new file mode 100644 index 0000000..60753d2 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/match-pattern.js @@ -0,0 +1,103 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { URL } = require("./url"); + +exports.MatchPattern = MatchPattern; + +function MatchPattern(pattern) { + if (typeof pattern.test == "function") { + + // For compatibility with -moz-document rules, we require the RegExp's + // global, ignoreCase, and multiline flags to be set to false. + if (pattern.global) { + throw new Error("A RegExp match pattern cannot be set to `global` " + + "(i.e. //g)."); + } + if (pattern.ignoreCase) { + throw new Error("A RegExp match pattern cannot be set to `ignoreCase` " + + "(i.e. //i)."); + } + if (pattern.multiline) { + throw new Error("A RegExp match pattern cannot be set to `multiline` " + + "(i.e. //m)."); + } + + this.regexp = pattern; + } + else { + let firstWildcardPosition = pattern.indexOf("*"); + let lastWildcardPosition = pattern.lastIndexOf("*"); + if (firstWildcardPosition != lastWildcardPosition) + throw new Error("There can be at most one '*' character in a wildcard."); + + if (firstWildcardPosition == 0) { + if (pattern.length == 1) + this.anyWebPage = true; + else if (pattern[1] != ".") + throw new Error("Expected a *.<domain name> string, got: " + pattern); + else + this.domain = pattern.substr(2); + } + else { + if (pattern.indexOf(":") == -1) { + throw new Error("When not using *.example.org wildcard, the string " + + "supplied is expected to be either an exact URL to " + + "match or a URL prefix. The provided string ('" + + pattern + "') is unlikely to match any pages."); + } + + if (firstWildcardPosition == -1) + this.exactURL = pattern; + else if (firstWildcardPosition == pattern.length - 1) + this.urlPrefix = pattern.substr(0, pattern.length - 1); + else { + throw new Error("The provided wildcard ('" + pattern + "') has a '*' " + + "in an unexpected position. It is expected to be the " + + "first or the last character in the wildcard."); + } + } + } +} + +MatchPattern.prototype = { + + test: function MatchPattern_test(urlStr) { + try { + var url = URL(urlStr); + } + catch (err) { + return false; + } + + // Test the URL against a RegExp pattern. For compatibility with + // -moz-document rules, we require the RegExp to match the entire URL, + // so we not only test for a match, we also make sure the matched string + // is the entire URL string. + // + // Assuming most URLs don't match most match patterns, we call `test` for + // speed when determining whether or not the URL matches, then call `exec` + // for the small subset that match to make sure the entire URL matches. + // + if (this.regexp && this.regexp.test(urlStr) && + this.regexp.exec(urlStr)[0] == urlStr) + return true; + + if (this.anyWebPage && /^(https?|ftp)$/.test(url.scheme)) + return true; + if (this.exactURL && this.exactURL == urlStr) + return true; + if (this.domain && url.host && + url.host.slice(-this.domain.length) == this.domain) + return true; + if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix)) + return true; + + return false; + } + +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/memory.js b/tools/addon-sdk-1.7/packages/api-utils/lib/memory.js new file mode 100644 index 0000000..be84969 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/memory.js @@ -0,0 +1,114 @@ +/* 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,Cu,components} = require("chrome"); +var trackedObjects = {}; + +var Compacter = { + INTERVAL: 5000, + notify: function(timer) { + var newTrackedObjects = {}; + for (let name in trackedObjects) { + var oldBin = trackedObjects[name]; + var newBin = []; + var strongRefs = []; + for (var i = 0; i < oldBin.length; i++) { + var strongRef = oldBin[i].weakref.get(); + if (strongRef && strongRefs.indexOf(strongRef) == -1) { + strongRefs.push(strongRef); + newBin.push(oldBin[i]); + } + } + if (newBin.length) + newTrackedObjects[name] = newBin; + } + trackedObjects = newTrackedObjects; + } +}; + +var timer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + +timer.initWithCallback(Compacter, + Compacter.INTERVAL, + Ci.nsITimer.TYPE_REPEATING_SLACK); + +var track = exports.track = function track(object, bin, stackFrameNumber) { + var frame = components.stack.caller; + var weakref = Cu.getWeakReference(object); + if (!bin && 'constructor' in object) + bin = object.constructor.name; + if (bin == "Object") + bin = frame.name; + if (!bin) + bin = "generic"; + if (!(bin in trackedObjects)) + trackedObjects[bin] = []; + + if (stackFrameNumber > 0) + for (var i = 0; i < stackFrameNumber; i++) + frame = frame.caller; + + trackedObjects[bin].push({weakref: weakref, + created: new Date(), + filename: frame.filename, + lineNo: frame.lineNumber, + bin: bin}); +}; + +var getBins = exports.getBins = function getBins() { + var names = []; + for (let name in trackedObjects) + names.push(name); + return names; +}; + +var getObjects = exports.getObjects = function getObjects(bin) { + function getLiveObjectsInBin(bin, array) { + for (var i = 0; i < bin.length; i++) { + var object = bin[i].weakref.get(); + if (object) + array.push(bin[i]); + } + } + + var results = []; + if (bin) { + if (bin in trackedObjects) + getLiveObjectsInBin(trackedObjects[bin], results); + } else + for (let name in trackedObjects) + getLiveObjectsInBin(trackedObjects[name], results); + return results; +}; + +var gc = exports.gc = function gc() { + // Components.utils.forceGC() doesn't currently perform + // cycle collection, which means that e.g. DOM elements + // won't be collected by it. Fortunately, there are + // other ways... + + var window = Cc["@mozilla.org/appshell/appShellService;1"] + .getService(Ci.nsIAppShellService) + .hiddenDOMWindow; + var test_utils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + test_utils.garbageCollect(); + Compacter.notify(); + + // Not sure why, but sometimes it appears that we don't get + // them all with just one CC, so let's do it again. + test_utils.garbageCollect(); +}; + +require("./unload").when( + function() { + trackedObjects = {}; + if (timer) { + timer.cancel(); + timer = null; + } + }); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/message-manager.js b/tools/addon-sdk-1.7/packages/api-utils/lib/message-manager.js new file mode 100644 index 0000000..235586f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/message-manager.js @@ -0,0 +1,203 @@ +/* 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 BAD_LISTENER = "The event listener must be a function."; + +const { Cc, Ci, Cu, CC } = require("chrome"); +const { setTimeout } = require("./timer"); + +const { ns } = require("./namespace"); + +const { curry, invoke } = require("./functional"); + +const Sandbox = require("./sandbox"); + +// JSON.stringify is buggy with cross-sandbox values, +// it may return "{}" on functions. Use a replacer to match them correctly. +const jsonFixer = function (k, v) typeof v === "function" ? undefined : v; + +/** + * Defers invoking the function until the current call stack has cleared. + * + * @param {Function} fn + * The function to defer. + * + * @returns {Function} + * The deferred function + */ +const defer = function(fn) function() { + setTimeout(invoke, 0, fn, arguments, this) +}; + +/** + * Adds a message listener. + * This listener will receive messages sent from the remote frame. + * + * @param {String} name + * The name of the message for which to add a listener. + * @param {Function} listener + * The listener function called when the message is received. + */ +function addMessageListener(name, listener) { + if (typeof listener !== "function") + throw new Error(BAD_LISTENER); + + let listeners = frame(this).listeners; + + if (name in listeners) { + if (~listeners[name].indexOf(listener)) + return; + } else { + listeners[name] = []; + } + + listeners[name].push(listener); +} + +/** + * Removes a message listener previously added by calling addMessageListener. + * + * @param {String} name + * The name of the message for which to remove a listener. + * @param {Function} listener + * The listener function has to be removed. + */ +function removeMessageListener(name, listener) { + if (typeof listener !== "function") + throw new Error(BAD_LISTENER); + + let listeners = frame(this).listeners; + + if (!(name in listeners)) + return; + + let index = listeners[name].indexOf(listener); + + if (~index) { + listeners[name].splice(index, 1); + } +} + +/** + * Sends a message to the listeners. + * + * @param {Boolean} sync + * Indicates if the call is synchronous or asynchronous + * @param {String} name + * The name of the message to send to the listeners. + * @param {Object} [data=null] + * A JSON object containing data to be delivered to the listeners. + * + * @returns {Array|undefined} + * An array with the return values of the listeners if `sync` is `true`, + * otherwise `undefined`. + */ +function sendMessage(sync, name, data) { + typeof data === "undefined" && (data = null); + + let listeners = frame(frame(this).receiver).listeners; + + let responses = []; + + let returnValue = sync ? responses : undefined; + + if (!(name in listeners)) + return returnValue; + + let json = JSON.parse(JSON.stringify(data, jsonFixer)); + + for each(let listener in listeners[name]) { + try { + let response = listener.call(null, { + sync : sync, + name : name, + json : json, + target : null + }); + + if (sync) { + if (typeof response === "undefined") + responses.push(response); + else + responses.push(JSON.parse(JSON.stringify(response, jsonFixer))); + } + + } catch (e) { + console.exception(e); + } + } + return returnValue; +}; + +let sendSyncMessage = curry(sendMessage, true); +let sendAsyncMessage = curry(defer(sendMessage), false); + +let frame = ns({receiver: null, listeners: null}); + +/** + * The MessageManager object emulates the Message Manager API, without creating + * new processes. It useful in mono process context, like Fennec. + * + * @see + * https://developer.mozilla.org/en/The_message_manager + */ +function MessageManager() { + + let sandbox = Sandbox.sandbox(null, { wantXrays : false }); + + Object.defineProperties(sandbox, { + addMessageListener: {value: addMessageListener.bind(sandbox)}, + + removeMessageListener: { value: removeMessageListener.bind(sandbox)}, + + sendAsyncMessage: {value: sendAsyncMessage.bind(sandbox)}, + + sendSyncMessage: { value: sendSyncMessage.bind(sandbox) } + }); + + frame(this).receiver = sandbox; + frame(sandbox).receiver = this; + + frame(this).listeners = {}; + frame(sandbox).listeners = {}; +} + +MessageManager.prototype = { + constructor: MessageManager, + + addMessageListener : addMessageListener, + + removeMessageListener : removeMessageListener, + + sendAsyncMessage : sendAsyncMessage, + + /** + * Loads a script into the remote frame. + * + * @param {String} uri + * The URL of the script to load into the frame; this must be an absolute + * local URL, but data: URLs are supported. + * @param {Boolean} allowDelayedLoad + * Not used. + */ + loadFrameScript: function loadFrameScript(uri, async) { + if (arguments.length < loadFrameScript.length) + throw new Error("Not enough arguments"); + + let sandbox = frame(this).receiver; + + try { + Sandbox.load(sandbox, uri); + } catch (e) { + console.exception(e) + } + } +} + +Object.freeze(MessageManager); +Object.freeze(MessageManager.prototype); + +exports.MessageManager = MessageManager; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/namespace.js b/tools/addon-sdk-1.7/packages/api-utils/lib/namespace.js new file mode 100644 index 0000000..612910d --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/namespace.js @@ -0,0 +1,43 @@ +/* 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"; + +// This is a temporary workaround until bug 673468 is fixed, which causes +// entries associated with `XPCWrappedNative` wrapped keys to be GC-ed. To +// workaround that we create a cross reference with an object from the same +// compartment as `WeakMap` and use that as a key. Cross reference prevents +// wrapper to be GC-ed until reference to it's value is kept. +function handle(target) { + return target[handle.key] || Object.defineProperty(target, handle.key, { + value: { '::': target }, + enumerable: false, + configurable: false, + writable: false + })[handle.key]; +} +handle.key = '::ns::' + Math.round(Math.random() * 100000000000000000); + +/** + * Function creates a new namespace. Optionally `prototype` object may be + * passed, in which case namespace objects will inherit from it. Returned value + * is a function that can be used to get access to the namespaced properties + * for the passed object. + * @examples + * const ns = Namespace(); + * ns(myObject).secret = secret; + */ +exports.Namespace = function Namespace(prototype) { + prototype = prototype || Object.prototype; + const map = new WeakMap(); + return function namespace(target) { + let key = handle(target); + return map.get(key) || + map.set(key, Object.create(prototype)), map.get(key); + }; +}; + +// `Namespace` is a e4x function in the scope, so we export the function also as +// `ns` as alias to avoid clashing. +exports.ns = exports.Namespace; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/observer-service.js b/tools/addon-sdk-1.7/packages/api-utils/lib/observer-service.js new file mode 100644 index 0000000..04222c7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/observer-service.js @@ -0,0 +1,175 @@ +/* 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 { Unknown } = require("./xpcom"); +const { when: unload } = require("./unload"); +const memory = require("./memory"); + + +/** + * A service for adding, removing and notifying observers of notifications. + * Wraps the nsIObserverService interface. + * + * @version 0.2 + */ + +var service = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + +/** + * A cache of observers that have been added. + * + * We use this to remove observers when a caller calls |Observers.remove|. + */ +var cache = []; + +/** + * Topics specifically available to Jetpack-generated extensions. + * + * Using these predefined consts instead of the platform strings is good: + * - allows us to scope topics specifically for Jetpacks + * - addons aren't dependent on strings nor behavior of core platform topics + * - the core platform topics are not clearly named + * + */ +exports.topics = { + /** + * A topic indicating that the application is in a state usable + * by add-ons. + */ + get APPLICATION_READY() packaging.jetpackID + "_APPLICATION_READY" +}; + +/** + * Register the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic to observe + * + * @param callback {Object} + * the callback; an Object that implements nsIObserver or a Function + * that gets called when the notification occurs + * + * @param target {Object} [optional] + * the object to use as |this| when calling a Function callback + * + * @returns the observer + */ +var add = exports.add = function add(topic, callback, target) { + var observer = Observer.new(topic, callback, target); + service.addObserver(observer, topic, true); + cache.push(observer); + + return observer; +}; + +/** + * Unregister the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic being observed + * + * @param callback {Object} + * the callback doing the observing + * + * @param target {Object} [optional] + * the object being used as |this| when calling a Function callback + */ +var remove = exports.remove = function remove(topic, callback, target) { + // This seems fairly inefficient, but I'm not sure how much better + // we can make it. We could index by topic, but we can't index by callback + // or target, as far as I know, since the keys to JavaScript hashes + // (a.k.a. objects) can apparently only be primitive values. + let observers = cache.filter(function(v) { + return (v.topic == topic && + v.callback == callback && + v.target == target); + }); + + if (observers.length) { + service.removeObserver(observers[0], topic); + cache.splice(cache.indexOf(observers[0]), 1); + } +}; + +/** + * Notify observers about something. + * + * @param topic {String} + * the topic to notify observers about + * + * @param subject {Object} [optional] + * some information about the topic; can be any JS object or primitive + * + * @param data {String} [optional] [deprecated] + * some more information about the topic; deprecated as the subject + * is sufficient to pass all needed information to the JS observers + * that this module targets; if you have multiple values to pass to + * the observer, wrap them in an object and pass them via the subject + * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) + */ +var notify = exports.notify = function notify(topic, subject, data) { + subject = (typeof subject == "undefined") ? null : Subject.new(subject); + data = (typeof data == "undefined") ? null : data; + service.notifyObservers(subject, topic, data); +}; + +const Observer = Unknown.extend({ + initialize: function initialize(topic, callback, target) { + memory.track(this); + this.topic = topic; + this.callback = callback; + this.target = target; + }, + interfaces: [ 'nsIObserver', 'nsISupportsWeakReference' ], + observe: function(subject, topic, data) { + // Extract the wrapped object for subjects that are one of our + // wrappers around a JS object. This way we support both wrapped + // subjects created using this module and those that are real + // XPCOM components. + if (subject && typeof subject == "object" && + ("wrappedJSObject" in subject) && + ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) + subject = subject.wrappedJSObject.object; + + try { + if (typeof this.callback == "function") { + if (this.target) + this.callback.call(this.target, subject, data); + else + this.callback(subject, data); + } else // typeof this.callback == "object" (nsIObserver) + this.callback.observe(subject, topic, data); + } catch (e) { + console.exception(e); + } + } +}); + +const Subject = Unknown.extend({ + initialize: function initialize(object) { + // Double-wrap the object and set a property identifying the + // wrappedJSObject as one of our wrappers to distinguish between + // subjects that are one of our wrappers (which we should unwrap + // when notifying our observers) and those that are real JS XPCOM + // components (which we should pass through unaltered). + this.wrappedJSObject = { + observersModuleSubjectWrapper: true, + object: object + }; + }, + getHelperForLanguage: function() {}, + getInterfaces: function() {} +}); + +unload(function() { + // Make a copy of cache first, since cache will be changing as we + // iterate through it. + cache.slice().forEach(function({ topic, callback, target }) { + remove(topic, callback, target); + }); +}) diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/passwords/utils.js b/tools/addon-sdk-1.7/packages/api-utils/lib/passwords/utils.js new file mode 100644 index 0000000..5dc6098 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/passwords/utils.js @@ -0,0 +1,101 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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, components: { Constructor: CConstructor } } = require("chrome"); +const { uri: ADDON_URI } = require("self"); +const loginManager = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); +const { URL: parseURL } = require("../url"); +const LoginInfo = CConstructor("@mozilla.org/login-manager/loginInfo;1", + "nsILoginInfo", "init"); + +function filterMatchingLogins(loginInfo) + Object.keys(this).every(function(key) loginInfo[key] === this[key], this); + +/** + * Removes `user`, `password` and `path` fields from the given `url` if it's + * 'http', 'https' or 'ftp'. All other URLs are returned unchanged. + * @example + * http://user:pass@www.site.com/foo/?bar=baz#bang -> http://www.site.com + */ +function normalizeURL(url) { + let { scheme, host, port } = parseURL(url); + // We normalize URL only if it's `http`, `https` or `ftp`. All other types of + // URLs (`resource`, `chrome`, etc..) should not be normalized as they are + // used with add-on associated credentials path. + return scheme === "http" || scheme === "https" || scheme === "ftp" ? + scheme + "://" + (host || "") + (port ? ":" + port : "") : + url +} + +function Login(options) { + let login = Object.create(Login.prototype); + Object.keys(options || {}).forEach(function(key) { + if (key === 'url') + login.hostname = normalizeURL(options.url); + else if (key === 'formSubmitURL') + login.formSubmitURL = options.formSubmitURL ? + normalizeURL(options.formSubmitURL) : null; + else if (key === 'realm') + login.httpRealm = options.realm; + else + login[key] = options[key]; + }); + + return login; +} +Login.prototype.toJSON = function toJSON() { + return { + url: this.hostname || ADDON_URI, + realm: this.httpRealm || null, + formSubmitURL: this.formSubmitURL || null, + username: this.username || null, + password: this.password || null, + usernameField: this.usernameField || '', + passwordField: this.passwordField || '', + } +}; +Login.prototype.toLoginInfo = function toLoginInfo() { + let { url, realm, formSubmitURL, username, password, usernameField, + passwordField } = this.toJSON(); + + return new LoginInfo(url, formSubmitURL, realm, username, password, + usernameField, passwordField); +}; + +function loginToJSON(value) Login(value).toJSON() + +/** + * Returns array of `nsILoginInfo` objects that are stored in the login manager + * and have all the properties with matching values as a given `options` object. + * @param {Object} options + * @returns {nsILoginInfo[]} + */ +exports.search = function search(options) { + return loginManager.getAllLogins() + .filter(filterMatchingLogins, Login(options)) + .map(loginToJSON); +}; + +/** + * Stores login info created from the given `options` to the applications + * built-in login management system. + * @param {Object} options. + */ +exports.store = function store(options) { + loginManager.addLogin(Login(options).toLoginInfo()); +}; + +/** + * Removes login info from the applications built-in login management system. + * _Please note: When removing a login info the specified properties must + * exactly match to the one that is already stored or exception will be thrown._ + * @param {Object} options. + */ +exports.remove = function remove(options) { + loginManager.removeLogin(Login(options).toLoginInfo()); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/plain-text-console.js b/tools/addon-sdk-1.7/packages/api-utils/lib/plain-text-console.js new file mode 100644 index 0000000..d76fddb --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/plain-text-console.js @@ -0,0 +1,82 @@ +/* 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"); + +function stringify(arg) { + try { + return String(arg); + } + catch(ex) { + return "<toString() error>"; + } +} + +function stringifyArgs(args) { + return Array.map(args, stringify).join(" "); +} + +function message(print, level, args) { + print(level + ": " + stringifyArgs(args) + "\n", level); +} + +var Console = exports.PlainTextConsole = function PlainTextConsole(print) { + if (!print) + print = dump; + if (print === dump) { + // If we're just using dump(), auto-enable preferences so + // that the developer actually sees the console output. + var prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + prefs.setBoolPref("browser.dom.window.dump.enabled", true); + } + this.print = print; + + // Binding all the public methods to an instance so that they can be used + // as callback / listener functions straightaway. + this.log = this.log.bind(this); + this.info = this.info.bind(this); + this.warn = this.warn.bind(this); + this.error = this.error.bind(this); + this.debug = this.debug.bind(this); + this.exception = this.exception.bind(this); + this.trace = this.trace.bind(this); +}; + +Console.prototype = { + log: function log() { + message(this.print, "info", arguments); + }, + + info: function info() { + message(this.print, "info", arguments); + }, + + warn: function warn() { + message(this.print, "warning", arguments); + }, + + error: function error() { + message(this.print, "error", arguments); + }, + + debug: function debug() { + message(this.print, "debug", arguments); + }, + + exception: function exception(e) { + var fullString = ("An exception occurred.\n" + + require("./traceback").format(e) + "\n" + e); + this.error(fullString); + }, + + trace: function trace() { + var traceback = require("./traceback"); + var stack = traceback.get(); + stack.splice(-1, 1); + message(this.print, "info", [traceback.format(stack)]); + } +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/preferences-service.js b/tools/addon-sdk-1.7/packages/api-utils/lib/preferences-service.js new file mode 100644 index 0000000..c5ffb2a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/preferences-service.js @@ -0,0 +1,124 @@ +/* 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"; + +// The minimum and maximum integers that can be set as preferences. +// The range of valid values is narrower than the range of valid JS values +// because the native preferences code treats integers as NSPR PRInt32s, +// which are 32-bit signed integers on all platforms. +const MAX_INT = 0x7FFFFFFF; +const MIN_INT = -0x80000000; + +const {Cc,Ci,Cr} = require("chrome"); + +const prefService = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); +const prefSvc = prefService.getBranch(null); +const defaultBranch = prefService.getDefaultBranch(null); + +var get = exports.get = function get(name, defaultValue) { + switch (prefSvc.getPrefType(name)) { + case Ci.nsIPrefBranch.PREF_STRING: + return prefSvc.getComplexValue(name, Ci.nsISupportsString).data; + + case Ci.nsIPrefBranch.PREF_INT: + return prefSvc.getIntPref(name); + + case Ci.nsIPrefBranch.PREF_BOOL: + return prefSvc.getBoolPref(name); + + case Ci.nsIPrefBranch.PREF_INVALID: + return defaultValue; + + default: + // This should never happen. + throw new Error("Error getting pref " + name + + "; its value's type is " + + prefSvc.getPrefType(name) + + ", which I don't know " + + "how to handle."); + } +}; + +var set = exports.set = function set(name, value) { + var prefType; + if (typeof value != "undefined" && value != null) + prefType = value.constructor.name; + + switch (prefType) { + case "String": + { + var string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = value; + prefSvc.setComplexValue(name, Ci.nsISupportsString, string); + } + break; + + case "Number": + // We throw if the number is outside the range or not an integer, since + // the result will not be what the consumer wanted to store. + if (value > MAX_INT || value < MIN_INT) + throw new Error("you cannot set the " + name + + " pref to the number " + value + + ", as number pref values must be in the signed " + + "32-bit integer range -(2^31) to 2^31-1. " + + "To store numbers outside that range, store " + + "them as strings."); + if (value % 1 != 0) + throw new Error("cannot store non-integer number: " + value); + prefSvc.setIntPref(name, value); + break; + + case "Boolean": + prefSvc.setBoolPref(name, value); + break; + + default: + throw new Error("can't set pref " + name + " to value '" + value + + "'; it isn't a string, integer, or boolean"); + } +}; + +var has = exports.has = function has(name) { + return (prefSvc.getPrefType(name) != Ci.nsIPrefBranch.PREF_INVALID); +}; + +var isSet = exports.isSet = function isSet(name) { + return (has(name) && prefSvc.prefHasUserValue(name)); +}; + +var reset = exports.reset = function reset(name) { + try { + prefSvc.clearUserPref(name); + } catch (e if e.result == Cr.NS_ERROR_UNEXPECTED) { + // The pref service throws NS_ERROR_UNEXPECTED when the caller tries + // to reset a pref that doesn't exist or is already set to its default + // value. This interface fails silently in those cases, so callers + // can unconditionally reset a pref without having to check if it needs + // resetting first or trap exceptions after the fact. It passes through + // other exceptions, however, so callers know about them, since we don't + // know what other exceptions might be thrown and what they might mean. + } +}; + +exports.getLocalized = function getLocalized(name, defaultValue) { + let value = null; + try { + value = prefSvc.getComplexValue(name, Ci.nsIPrefLocalizedString).data; + } + finally { + return value || defaultValue; + } +} + +exports.setLocalized = function setLocalized(name, value) { + // We can't use `prefs.set` here as we have to use `getDefaultBranch` + // (instead of `getBranch`) in order to have `mIsDefault` set to true, here: + // http://mxr.mozilla.org/mozilla-central/source/modules/libpref/src/nsPrefBranch.cpp#233 + // Otherwise, we do not enter into this expected condition: + // http://mxr.mozilla.org/mozilla-central/source/modules/libpref/src/nsPrefBranch.cpp#244 + defaultBranch.setCharPref(name, value); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/process.js b/tools/addon-sdk-1.7/packages/api-utils/lib/process.js new file mode 100644 index 0000000..68ef2f9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/process.js @@ -0,0 +1,76 @@ +/* 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 { createRemoteBrowser } = require("api-utils/window-utils"); +const { channel } = require("./channel"); +const packaging = require('@packaging'); +const { when } = require('./unload'); +const { MessageManager } = require('./message-manager'); + +const addonService = '@mozilla.org/addon/service;1' in Cc ? + Cc['@mozilla.org/addon/service;1'].getService(Ci.nsIAddonService) : null + +const ENABLE_E10S = packaging.enable_e10s; + +const isFennec = require("./xul-app").is("Fennec"); + +function loadScript(target, uri, sync) { + return 'loadScript' in target ? target.loadScript(uri, sync) + : target.loadFrameScript(uri, sync) +} + +function process(target, id, path, scope) { + // Please note that even though `loadScript`, is executed before channel is + // returned, users still are able to subscribe for messages before any message + // will be sent. That's because `loadScript` queues script execution on the + // other process, which means they will execute async (on the next turn of + // event loop), while the channel for messages is returned immediately (in + // the same turn of event loop). + + let load = loadScript.bind(null, target); + + load(packaging.uriPrefix + packaging.loader, false); + load('data:,' + encodeURIComponent( + 'let loader = Loader.new(' + JSON.stringify(packaging) + ');\n' + + 'loader.main("' + id + '", "' + path + '");'), false); + + when(function (reason) { + // Please note that it's important to unload remote loader + // synchronously (using synchronous frame script), to make sure that we + // don't stop during unload. + // Bug 724433: Take care to nullify all globals set by `cuddlefish.js` + // otherwise, we will leak any still defined global. + // `dump` is set in Loader.new method, `dump = globals.dump;` + load('data:,loader.unload("' + reason + '");' + + 'loader = null; Loader = null; dump = null;', true); + }); + + return { + channel: channel.bind(null, scope, target), + loadScript: load + }; +} + +exports.spawn = function spawn(id, path) { + return function promise(deliver) { + // If `nsIAddonService` is available we use it to create an add-on process, + // otherwise we fallback to the remote browser's message manager. + if (ENABLE_E10S && addonService) { + console.log('!!!!!!!!!!!!!!!!!!!! Using addon process !!!!!!!!!!!!!!!!!!'); + deliver(process(addonService.createAddon(), id, path)); + } else if (isFennec) { + deliver(process(new MessageManager(), id, path)); + } else { + createRemoteBrowser(ENABLE_E10S)(function(browser) { + let messageManager = browser.QueryInterface(Ci.nsIFrameLoaderOwner). + frameLoader.messageManager + let window = browser.ownerDocument.defaultView; + deliver(process(messageManager, id, path, window)); + }); + } + }; +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/promise.js b/tools/addon-sdk-1.7/packages/api-utils/lib/promise.js new file mode 100644 index 0000000..7c8d7a5 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/promise.js @@ -0,0 +1,219 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/*jshint asi: true undef: true es5: true node: true devel: true + forin: true latedef: false supernew: true */ +/*global define: true */ +!function(factory) { + if (typeof(define) === 'function') { // RequireJS + define(factory) + } else if (typeof(exports) === 'object') { // CommonJS + factory(require, exports) + } else if (~String(this).indexOf('BackstagePass')) { // JSM + factory(undefined, this) + this.EXPORTED_SYMBOLS = Object.keys(this) + } else { + factory(undefined, (this.promise = {})) + } +}.call(this, function(require, exports) { + +'use strict'; + +function resolution(value) { + /** + Returns non-standard compliant (`then` does not returns a promise) promise + that resolves to a given `value`. Used just internally only. + **/ + return { then: function then(resolve) { resolve(value) } } +} + +function rejection(reason) { + /** + Returns non-standard compliant promise (`then` does not returns a promise) + that rejects with a given `reason`. This is used internally only. + **/ + return { then: function then(resolve, reject) { reject(reason) } } +} + +function attempt(f) { + /** + Returns wrapper function that delegates to `f`. If `f` throws then captures + error and returns promise that rejects with a thrown error. Otherwise returns + return value. (Internal utility) + **/ + return function effort(options) { + try { return f(options) } + catch(error) { return rejection(error) } + } +} + +function isPromise(value) { + /** + Returns true if given `value` is promise. Value is assumed to be promise if + it implements `then` method. + **/ + return value && typeof(value.then) === 'function' +} + +function defer(prototype) { + /** + Returns object containing following properties: + - `promise` Eventual value representation implementing CommonJS [Promises/A] + (http://wiki.commonjs.org/wiki/Promises/A) API. + - `resolve` Single shot function that resolves returned `promise` with a given + `value` argument. + - `reject` Single shot function that rejects returned `promise` with a given + `reason` argument. + + Given `prototype` argument is used as a prototype of the returned `promise` + allowing one to implement additional API. If prototype is not passed then + it falls back to `Object.prototype`. + + ## Examples + + // Simple usage. + var deferred = defer() + deferred.promise.then(console.log, console.error) + deferred.resolve(value) + + // Advanced usage + var prototype = { + get: function get(name) { + return this.then(function(value) { + return value[name]; + }) + } + } + + var foo = defer(prototype) + deferred.promise.get('name').then(console.log) + deferred.resolve({ name: 'Foo' }) + //=> 'Foo' + */ + var pending = [], result + prototype = (prototype || prototype === null) ? prototype : Object.prototype + + // Create an object implementing promise API. + var promise = Object.create(prototype, { + then: { value: function then(resolve, reject) { + // create a new deferred using a same `prototype`. + var deferred = defer(prototype) + // If `resolve / reject` callbacks are not provided. + resolve = resolve ? attempt(resolve) : resolution + reject = reject ? attempt(reject) : rejection + + // Create a listeners for a enclosed promise resolution / rejection that + // delegate to an actual callbacks and resolve / reject returned promise. + function resolved(value) { deferred.resolve(resolve(value)) } + function rejected(reason) { deferred.resolve(reject(reason)) } + + // If promise is pending register listeners. Otherwise forward them to + // resulting resolution. + if (pending) pending.push([ resolved, rejected ]) + else result.then(resolved, rejected) + + return deferred.promise + }} + }) + + var deferred = { + promise: promise, + resolve: function resolve(value) { + /** + Resolves associated `promise` to a given `value`, unless it's already + resolved or rejected. + **/ + if (pending) { + // store resolution `value` as a promise (`value` itself may be a + // promise), so that all subsequent listeners can be forwarded to it, + // which either resolves immediately or forwards if `value` is + // a promise. + result = isPromise(value) ? value : resolution(value) + // forward all pending observers. + while (pending.length) result.then.apply(result, pending.shift()) + // mark promise as resolved. + pending = null + } + }, + reject: function reject(reason) { + /** + Rejects associated `promise` with a given `reason`, unless it's already + resolved / rejected. + **/ + deferred.resolve(rejection(reason)) + } + } + + return deferred +} +exports.defer = defer + +function resolve(value, prototype) { + /** + Returns a promise resolved to a given `value`. Optionally second `prototype` + arguments my be provided to be used as a prototype for a returned promise. + **/ + var deferred = defer(prototype) + deferred.resolve(value) + return deferred.promise +} +exports.resolve = resolve + +function reject(reason, prototype) { + /** + Returns a promise that is rejected with a given `reason`. Optionally second + `prototype` arguments my be provided to be used as a prototype for a returned + promise. + **/ + var deferred = defer(prototype) + deferred.reject(reason) + return deferred.promise +} +exports.reject = reject + +var promised = (function() { + // Note: Define shortcuts and utility functions here in order to avoid + // slower property accesses and unnecessary closure creations on each + // call of this popular function. + + var call = Function.call + var concat = Array.prototype.concat + + // Utility function that does following: + // execute([ f, self, args...]) => f.apply(self, args) + function execute(args) { return call.apply(call, args) } + + // Utility function that takes promise of `a` array and maybe promise `b` + // as arguments and returns promise for `a.concat(b)`. + function promisedConcat(promises, unknown) { + return promises.then(function(values) { + return resolve(unknown).then(function(value) { + return values.concat(value) + }) + }) + } + + return function promised(f, prototype) { + /** + Returns a wrapped `f`, which when called returns a promise that resolves to + `f(...)` passing all the given arguments to it, which by the way may be + promises. Optionally second `prototype` argument may be provided to be used + a prototype for a returned promise. + + ## Example + + var promise = promised(Array)(1, promise(2), promise(3)) + promise.then(console.log) // => [ 1, 2, 3 ] + **/ + + return function promised() { + // create array of [ f, this, args... ] + return concat.apply([ f, this ], arguments). + // reduce it via `promisedConcat` to get promised array of fulfillments + reduce(promisedConcat, resolve([], prototype)). + // finally map that to promise of `f.apply(this, args...)` + then(execute) + } + } +})() +exports.promised = promised + +}) diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/querystring.js b/tools/addon-sdk-1.7/packages/api-utils/lib/querystring.js new file mode 100644 index 0000000..9e29515 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/querystring.js @@ -0,0 +1,117 @@ +/* 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'; + +let unescape = decodeURIComponent; +exports.unescape = unescape; + +// encodes a string safely for application/x-www-form-urlencoded +// adheres to RFC 3986 +// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURIComponent +function escape(query) { + return encodeURIComponent(query). + replace(/%20/g, '+'). + replace(/!/g, '%21'). + replace(/'/g, '%27'). + replace(/\(/g, '%28'). + replace(/\)/g, '%29'). + replace(/\*/g, '%2A'); +} +exports.escape = escape; + +// Converts an object of unordered key-vals to a string that can be passed +// as part of a request +function stringify(options, separator, assigner) { + separator = separator || '&'; + assigner = assigner || '='; + // Explicitly return null if we have null, and empty string, or empty object. + if (!options) + return ''; + + // If content is already a string, just return it as is. + if (typeof(options) == 'string') + return options; + + // At this point we have a k:v object. Iterate over it and encode each value. + // Arrays and nested objects will get encoded as needed. For example... + // + // { foo: [1, 2, { omg: 'bbq', 'all your base!': 'are belong to us' }], bar: 'baz' } + // + // will be encoded as + // + // foo[0]=1&foo[1]=2&foo[2][omg]=bbq&foo[2][all+your+base!]=are+belong+to+us&bar=baz + // + // Keys (including '[' and ']') and values will be encoded with + // `escape` before returning. + // + // Execution was inspired by jQuery, but some details have changed and numeric + // array keys are included (whereas they are not in jQuery). + + let encodedContent = []; + function add(key, val) { + encodedContent.push(escape(key) + assigner + escape(val)); + } + + function make(key, value) { + if (value && typeof(value) === 'object') + Object.keys(value).forEach(function(name) { + make(key + '[' + name + ']', value[name]); + }); + else + add(key, value); + } + + Object.keys(options).forEach(function(name) { make(name, options[name]); }); + return encodedContent.join(separator); + + //XXXzpao In theory, we can just use a FormData object on 1.9.3, but I had + // trouble getting that working. It would also be nice to stay + // backwards-compat as long as possible. Keeping this in for now... + // let formData = Cc['@mozilla.org/files/formdata;1']. + // createInstance(Ci.nsIDOMFormData); + // for ([k, v] in Iterator(content)) { + // formData.append(k, v); + // } + // return formData; +} +exports.stringify = stringify; + +// Exporting aliases that nodejs implements just for the sake of +// interoperability. +exports.encode = stringify; +exports.serialize = stringify; + +// Note: That `stringify` and `parse` aren't bijective as we use `stringify` +// as it was implement in request module, but implement `parse` to match nodejs +// behavior. +// TODO: Make `stringify` implement API as in nodejs and figure out backwards +// compatibility. +function parse(query, separator, assigner) { + separator = separator || '&'; + assigner = assigner || '='; + let result = {}; + + if (typeof query !== 'string' || query.length === 0) + return result; + + query.split(separator).forEach(function(chunk) { + let pair = chunk.split(assigner); + let key = unescape(pair[0]); + let value = unescape(pair.slice(1).join(assigner)); + + if (!(key in result)) + result[key] = value; + else if (Array.isArray(result[key])) + result[key].push(value); + else + result[key] = [result[key], value]; + }); + + return result; +}; +exports.parse = parse; +// Exporting aliases that nodejs implements just for the sake of +// interoperability. +exports.decode = parse; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/runtime.js b/tools/addon-sdk-1.7/packages/api-utils/lib/runtime.js new file mode 100644 index 0000000..845d146 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/runtime.js @@ -0,0 +1,15 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); + +exports.inSafeMode = runtime.inSafeMode; +exports.OS = runtime.OS; +exports.processType = runtime.processType; +exports.widgetToolkit = runtime.widgetToolkit; +exports.XPCOMABI = runtime.XPCOMABI; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/sandbox.js b/tools/addon-sdk-1.7/packages/api-utils/lib/sandbox.js new file mode 100644 index 0000000..6130637 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/sandbox.js @@ -0,0 +1,46 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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, CC, Cu } = require('chrome'); +const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); +const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. + getService(Ci.mozIJSSubScriptLoader); + +/** + * Make a new sandbox that inherits given `source`'s principals. Source can be + * URI string, DOMWindow or `null` for system principals. + */ +function sandbox(target, options) { + return Cu.Sandbox(target || systemPrincipal, options || {}); +} +exports.sandbox = sandbox + +/** + * Evaluates given `source` in a given `sandbox` and returns result. + */ +function evaluate(sandbox, code, uri, line, version) { + return Cu.evalInSandbox(code, sandbox, version || '1.8', uri || '', line || 1); +} +exports.evaluate = evaluate; + +/** + * Evaluates code under the given `uri` in the given `sandbox`. + * + * @param {String} uri + * The URL pointing to the script to load. + * It must be a local chrome:, resource:, file: or data: URL. + */ +function load(sandbox, uri) { + if (uri.indexOf('data:') === 0) { + let source = uri.substr(uri.indexOf(',') + 1); + + return evaluate(sandbox, decodeURIComponent(source), '1.8', uri, 0); + } else { + return scriptLoader.loadSubScript(uri, sandbox, 'UTF-8'); + } +} +exports.load = load;
\ No newline at end of file diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/self!.js b/tools/addon-sdk-1.7/packages/api-utils/lib/self!.js new file mode 100644 index 0000000..fd69f9a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/self!.js @@ -0,0 +1,48 @@ +/* vim:st=2:sts=2:sw=2: + * 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 } = require('chrome'); +const { jetpackID, name, manifest, metadata, uriPrefix } = require('@packaging'); + +const XMLHttpRequest = CC('@mozilla.org/xmlextras/xmlhttprequest;1', + 'nsIXMLHttpRequest'); + +// Utility function that synchronously reads local resource from the given +// `uri` and returns content string. +function readURI(uri) { + let request = XMLHttpRequest(); + request.open('GET', uri, false); + request.overrideMimeType('text/plain'); + request.send(); + return request.responseText; +} + +// Some XPCOM APIs require valid URIs as an argument for certain operations (see +// `nsILoginManager` for example). This property represents add-on associated +// unique URI string that can be used for that. +const uri = 'addon:' + jetpackID + +function url(root, path) root + (path || "") +function read(root, path) readURI(url(root, path)) + +exports.create = function create(base) { + let moduleData = manifest[base] && manifest[base].requirements['self']; + let root = uriPrefix + moduleData.dataURIPrefix; + return Object.freeze({ + id: 'self', + exports: Object.freeze({ + id: jetpackID, + uri: uri, + name: name, + version: metadata[name].version, + data: { + url: url.bind(null, root), + load: read.bind(null, root) + } + }) + }); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/system.js b/tools/addon-sdk-1.7/packages/api-utils/lib/system.js new file mode 100644 index 0000000..3986645 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/system.js @@ -0,0 +1,120 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/* 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, CC } = require('chrome'); +const options = require('@packaging'); +const file = require('./file'); +const runtime = require("./runtime.js"); + +const appStartup = Cc['@mozilla.org/toolkit/app-startup;1']. + getService(Ci.nsIAppStartup); +const appInfo = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULAppInfo); +const directoryService = Cc['@mozilla.org/file/directory_service;1']. + getService(Ci.nsIProperties); + + +const { eAttemptQuit: E_ATTEMPT, eForceQuit: E_FORCE } = appStartup; + +/** + * Parsed JSON object that was passed via `cfx --static-args "{ foo: 'bar' }"` + */ +exports.staticArgs = options.staticArgs; + +/** + * Environment variables. Environment variables are non-enumerable properties + * of this object (key is name and value is value). + */ +exports.env = require('./environment').env; + +/** + * Ends the process with the specified `code`. If omitted, exit uses the + * 'success' code 0. To exit with failure use `1`. + * TODO: Improve platform to actually quit with an exit code. + */ +exports.exit = function exit(code) { + // This is used by 'cfx' to find out exit code. + if ('resultFile' in options) { + let stream = file.open(options.resultFile, 'w'); + stream.write(code ? 'FAIL' : 'OK'); + stream.close(); + } + + appStartup.quit(code ? E_ATTEMPT : E_FORCE); +}; + +/** + * Returns a path of the system's or application's special directory / file + * associated with a given `id`. For list of possible `id`s please see: + * https://developer.mozilla.org/en/Code_snippets/File_I%2F%2FO#Getting_special_files + * http://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsAppDirectoryServiceDefs.h + * @example + * + * // get firefox profile path + * let profilePath = require('system').pathFor('ProfD'); + * // get OS temp files directory (/tmp) + * let temps = require('system').pathFor('TmpD'); + * // get OS desktop path for an active user (~/Desktop on linux + * // or C:\Documents and Settings\username\Desktop on windows). + * let desktopPath = require('system').pathFor('Desk'); + */ +exports.pathFor = function pathFor(id) { + return directoryService.get(id, Ci.nsIFile).path; +}; + +/** + * What platform you're running on (all lower case string). + * For possible values see: + * https://developer.mozilla.org/en/OS_TARGET + */ +exports.platform = runtime.OS.toLowerCase(); + +/** + * What processor architecture you're running on: + * `'arm', 'ia32', or 'x64'`. + */ +exports.architecture = runtime.XPCOMABI.split('_')[0]; + +/** + * What compiler used for build: + * `'msvc', 'n32', 'gcc2', 'gcc3', 'sunc', 'ibmc'...` + */ +exports.compiler = runtime.XPCOMABI.split('_')[1]; + +/** + * The application's build ID/date, for example "2004051604". + */ +exports.build = appInfo.appBuildID; + +/** + * The XUL application's UUID. + * This has traditionally been in the form + * `{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}` but for some applications it may + * be: "appname@vendor.tld". + */ +exports.id = appInfo.ID; + +/** + * The name of the application. + */ +exports.name = appInfo.name; + +/** + * The XUL application's version, for example "0.8.0+" or "3.7a1pre". + */ +exports.version = appInfo.version; + +/** + * XULRunner version. + */ +exports.platformVersion = appInfo.platformVersion; + + +/** + * The name of the application vendor, for example "Mozilla". + */ +exports.vendor = appInfo.vendor; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/tab-browser.js b/tools/addon-sdk-1.7/packages/api-utils/lib/tab-browser.js new file mode 100644 index 0000000..13d365a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/tab-browser.js @@ -0,0 +1,727 @@ +/* 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,Cu} = require("chrome"); +var NetUtil = {}; +Cu.import("resource://gre/modules/NetUtil.jsm", NetUtil); +NetUtil = NetUtil.NetUtil; +const errors = require("./errors"); +const windowUtils = require("./window-utils"); +const apiUtils = require("./api-utils"); +const collection = require("./collection"); + +// TODO: The hard-coding of app-specific info here isn't very nice; +// ideally such app-specific info should be more decoupled, and the +// module should be extensible, allowing for support of new apps at +// runtime, perhaps by inspecting supported packages (e.g. via +// dynamically-named modules or package-defined extension points). + +if (!require("./xul-app").is("Firefox")) { + throw new Error([ + "The tab-browser module currently supports only Firefox. In the future ", + "it will support other applications. Please see ", + "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information." + ].join("")); +} + +function onBrowserLoad(callback, event) { + if (event.target && event.target.defaultView == this) { + this.removeEventListener("load", onBrowserLoad, true); + try { + require("timer").setTimeout(function () { + callback(event); + }, 10); + } catch (e) { console.exception(e); } + } +} + +// Utility function to open a new browser window. +function openBrowserWindow(callback, url) { + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + let urlString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + urlString.data = url; + let window = ww.openWindow(null, "chrome://browser/content/browser.xul", + "_blank", "chrome,all,dialog=no", urlString); + if (callback) + window.addEventListener("load", onBrowserLoad.bind(window, callback), true); + + return window; +} + +// Open a URL in a new tab +exports.addTab = function addTab(url, options) { + if (!options) + options = {}; + options.url = url; + + options = apiUtils.validateOptions(options, { + // TODO: take URL object instead of string (bug 564524) + url: { + is: ["string"], + ok: function (v) !!v, + msg: "The url parameter must have be a non-empty string." + }, + inNewWindow: { + is: ["undefined", "null", "boolean"] + }, + inBackground: { + is: ["undefined", "null", "boolean"] + }, + onLoad: { + is: ["undefined", "null", "function"] + }, + isPinned: { + is: ["undefined", "boolean"] + } + }); + + var wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + var win = wm.getMostRecentWindow("navigator:browser"); + if (!win || options.inNewWindow) { + openBrowserWindow(function(e) { + if(options.isPinned) { + //get the active tab in the recently created window + let mainWindow = e.target.defaultView; + mainWindow.gBrowser.pinTab(mainWindow.gBrowser.selectedTab); + } + require("./errors").catchAndLog(function(e) options.onLoad(e))(e); + }, options.url); + } else { + let tab = win.gBrowser.addTab(options.url); + if (!options.inBackground) + win.gBrowser.selectedTab = tab; + if (options.onLoad) { + let tabBrowser = win.gBrowser.getBrowserForTab(tab); + tabBrowser.addEventListener("load", function onLoad(e) { + if (e.target.defaultView.content.location == "about:blank") + return; + // remove event handler from addTab - don't want notified + // for subsequent loads in same tab. + tabBrowser.removeEventListener("load", onLoad, true); + require("./errors").catchAndLog(function(e) options.onLoad(e))(e); + }, true); + } + } +} + +// Iterate over a window's tabbrowsers +function tabBrowserIterator(window) { + var browsers = window.document.querySelectorAll("tabbrowser"); + for (var i = 0; i < browsers.length; i++) + yield browsers[i]; +} + +// Iterate over a tabbrowser's tabs +function tabIterator(tabbrowser) { + var tabs = tabbrowser.tabContainer; + for (var i = 0; i < tabs.children.length; i++) { + yield tabs.children[i]; + } +} + +// Tracker for all tabbrowsers across all windows, +// or a single tabbrowser if the window is given. +function Tracker(delegate, window) { + this._delegate = delegate; + this._browsers = []; + this._window = window; + this._windowTracker = windowUtils.WindowTracker(this); + + require("./unload").ensure(this); +} +Tracker.prototype = { + __iterator__: function __iterator__() { + for (var i = 0; i < this._browsers.length; i++) + yield this._browsers[i]; + }, + get: function get(index) { + return this._browsers[index]; + }, + onTrack: function onTrack(window) { + if (this._window && window != this._window) + return; + + for (let browser in tabBrowserIterator(window)) + this._browsers.push(browser); + if (this._delegate) + for (let browser in tabBrowserIterator(window)) + this._delegate.onTrack(browser); + }, + onUntrack: function onUntrack(window) { + if (this._window && window != this._window) + return; + + for (let browser in tabBrowserIterator(window)) { + let index = this._browsers.indexOf(browser); + if (index != -1) + this._browsers.splice(index, 1); + else + console.error("internal error: browser tab not found"); + } + if (this._delegate) + for (let browser in tabBrowserIterator(window)) + this._delegate.onUntrack(browser); + }, + get length() { + return this._browsers.length; + }, + unload: function unload() { + this._windowTracker.unload(); + } +}; +exports.Tracker = apiUtils.publicConstructor(Tracker); + +// Tracker for all tabs across all windows, +// or a single window if it's given. +function TabTracker(delegate, window) { + this._delegate = delegate; + this._tabs = []; + this._tracker = new Tracker(this, window); + require("./unload").ensure(this); +} +TabTracker.prototype = { + _TAB_EVENTS: ["TabOpen", "TabClose"], + _safeTrackTab: function safeTrackTab(tab) { + this._tabs.push(tab); + try { + this._delegate.onTrack(tab); + } catch (e) { + console.exception(e); + } + }, + _safeUntrackTab: function safeUntrackTab(tab) { + var index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + this._tabs.splice(index, 1); + try { + this._delegate.onUntrack(tab); + } catch (e) { + console.exception(e); + } + }, + handleEvent: function handleEvent(event) { + switch (event.type) { + case "TabOpen": + this._safeTrackTab(event.target); + break; + case "TabClose": + this._safeUntrackTab(event.target); + break; + default: + throw new Error("internal error: unknown event type: " + + event.type); + } + }, + onTrack: function onTrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeTrackTab(tab); + var self = this; + this._TAB_EVENTS.forEach( + function(eventName) { + tabbrowser.tabContainer.addEventListener(eventName, self, true); + }); + }, + onUntrack: function onUntrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeUntrackTab(tab); + var self = this; + this._TAB_EVENTS.forEach( + function(eventName) { + tabbrowser.tabContainer.removeEventListener(eventName, self, true); + }); + }, + unload: function unload() { + this._tracker.unload(); + } +}; +exports.TabTracker = apiUtils.publicConstructor(TabTracker); + +exports.whenContentLoaded = function whenContentLoaded(callback) { + var cb = require("./errors").catchAndLog(function eventHandler(event) { + if (event.target && event.target.defaultView) + callback(event.target.defaultView); + }); + + var tracker = new Tracker({ + onTrack: function(tabBrowser) { + tabBrowser.addEventListener("DOMContentLoaded", cb, false); + }, + onUntrack: function(tabBrowser) { + tabBrowser.removeEventListener("DOMContentLoaded", cb, false); + } + }); + + return tracker; +}; + +exports.__defineGetter__("activeTab", function() { + const wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + let mainWindow = wm.getMostRecentWindow("navigator:browser"); + return mainWindow.gBrowser.selectedTab; +}); + +/******************* TabModule *********************/ + +// Supported tab events +const events = [ + "onActivate", + "onDeactivate", + "onOpen", + "onClose", + "onReady", + "onLoad", + "onPaint" +]; +exports.tabEvents = events; + +/** + * TabModule + * + * Constructor for a module that implements the tabs API + */ +let TabModule = exports.TabModule = function TabModule(window) { + let self = this; + /** + * Tab + * + * Safe object representing a tab. + */ + let tabConstructor = apiUtils.publicConstructor(function(element) { + if (!element) + throw new Error("no tab element."); + let win = element.ownerDocument.defaultView; + if (!win) + throw new Error("element has no window."); + if (window && win != window) + throw new Error("module's window and element's window don't match."); + let browser = win.gBrowser.getBrowserForTab(element); + + this.__defineGetter__("title", function() browser.contentDocument.title); + this.__defineGetter__("location", function() browser.contentDocument.location); + this.__defineSetter__("location", function(val) browser.contentDocument.location = val); + this.__defineGetter__("contentWindow", function() browser.contentWindow); + this.__defineGetter__("contentDocument", function() browser.contentDocument); + this.__defineGetter__("favicon", function() { + let pageURI = NetUtil.newURI(browser.contentDocument.location); + let fs = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + let faviconURL; + try { + let faviconURI = fs.getFaviconForPage(pageURI); + faviconURL = fs.getFaviconDataAsDataURL(faviconURI); + } catch(ex) { + let data = getChromeURLContents("chrome://mozapps/skin/places/defaultFavicon.png"); + let encoded = browser.contentWindow.btoa(data); + faviconURL = "data:image/png;base64," + encoded; + } + return faviconURL; + }); + this.__defineGetter__("style", function() null); // TODO + this.__defineGetter__("index", function() win.gBrowser.getBrowserIndexForDocument(browser.contentDocument)); + this.__defineGetter__("thumbnail", function() getThumbnailCanvasForTab(element, browser.contentWindow)); + + this.close = function() win.gBrowser.removeTab(element); + this.move = function(index) { + win.gBrowser.moveTabTo(element, index); + }; + + this.__defineGetter__("isPinned", function() element.pinned); + this.pin = function() win.gBrowser.pinTab(element); + this.unpin = function() win.gBrowser.unpinTab(element); + + // Set up the event handlers + let tab = this; + events.filter(function(e) e != "onOpen").forEach(function(e) { + // create a collection for each event + collection.addCollectionProperty(tab, e); + // make tabs setter for each event, for adding via property assignment + tab.__defineSetter__(e, function(val) tab[e].add(val)); + }); + + // listen for events, filtered on this tab + eventsTabDelegate.addTabDelegate(this); + }); + + /** + * tabs.activeTab + */ + this.__defineGetter__("activeTab", function() { + try { + return window ? tabConstructor(window.gBrowser.selectedTab) + : tabConstructor(exports.activeTab); + } + catch (e) { } + return null; + }); + this.__defineSetter__("activeTab", function(tab) { + let [tabElement, win] = getElementAndWindowForTab(tab, window); + if (tabElement) { + // set as active tab + win.gBrowser.selectedTab = tabElement; + // focus the window + win.focus(); + } + }); + + this.open = function TM_open(options) { + open(options, tabConstructor, window); + } + + // Set up the event handlers + events.forEach(function(eventHandler) { + // create a collection for each event + collection.addCollectionProperty(self, eventHandler); + // make tabs setter for each event, for adding via property assignment + self.__defineSetter__(eventHandler, function(val) self[eventHandler].add(val)); + }); + + // Tracker that listens for tab events, and proxies + // them to registered event listeners. + let eventsTabDelegate = { + selectedTab: null, + tabs: [], + addTabDelegate: function TETT_addTabDelegate(tabObj) { + this.tabs.push(tabObj); + }, + pushTabEvent: function TETT_pushTabEvent(event, tab) { + for (let callback in self[event]) { + require("./errors").catchAndLog(function(tab) { + callback(new tabConstructor(tab)); + })(tab); + } + + if (event != "onOpen") { + this.tabs.forEach(function(tabObj) { + if (tabObj[event].length) { + let [tabEl,] = getElementAndWindowForTab(tabObj, window); + if (tabEl == tab) { + for (let callback in tabObj[event]) + require("./errors").catchAndLog(function() callback())(); + } + } + // if being closed, remove the tab object from the cache + // of tabs to notify about events. + if (event == "onClose") + this.tabs.splice(this.tabs.indexOf(tabObj), 1); + }, this); + } + }, + unload: function() { + this.selectedTab = null; + this.tabs.splice(0); + } + }; + require("./unload").ensure(eventsTabDelegate); + + let eventsTabTracker = new ModuleTabTracker({ + onTrack: function TETT_onTrack(tab) { + eventsTabDelegate.pushTabEvent("onOpen", tab); + }, + onUntrack: function TETT_onUntrack(tab) { + eventsTabDelegate.pushTabEvent("onClose", tab); + }, + onSelect: function TETT_onSelect(tab) { + if (eventsTabDelegate.selectedTab) + eventsTabDelegate.pushTabEvent("onDeactivate", tab); + + eventsTabDelegate.selectedTab = new tabConstructor(tab); + + eventsTabDelegate.pushTabEvent("onActivate", tab); + }, + onReady: function TETT_onReady(tab) { + eventsTabDelegate.pushTabEvent("onReady", tab); + }, + onLoad: function TETT_onLoad(tab) { + eventsTabDelegate.pushTabEvent("onLoad", tab); + }, + onPaint: function TETT_onPaint(tab) { + eventsTabDelegate.pushTabEvent("onPaint", tab); + } + }, window); + require("./unload").ensure(eventsTabTracker); + + // Iterator for all tabs + this.__iterator__ = function tabsIterator() { + for (let i = 0; i < eventsTabTracker._tabs.length; i++) + yield tabConstructor(eventsTabTracker._tabs[i]); + } + + this.__defineGetter__("length", function() eventsTabTracker._tabs.length); + + // Cleanup when unloaded + this.unload = function TM_unload() { + // Unregister tabs event listeners + events.forEach(function(e) self[e] = []); + } + require("./unload").ensure(this); + +} // End of TabModule constructor + +/** + * tabs.open - open a URL in a new tab + */ +function open(options, tabConstructor, window) { + if (typeof options === "string") + options = { url: options }; + + options = apiUtils.validateOptions(options, { + url: { + is: ["string"] + }, + inNewWindow: { + is: ["undefined", "boolean"] + }, + inBackground: { + is: ["undefined", "boolean"] + }, + isPinned: { + is: ["undefined", "boolean"] + }, + onOpen: { + is: ["undefined", "function"] + } + }); + + if (window) + options.inNewWindow = false; + + let win = window || require("./window-utils").activeBrowserWindow; + + if (!win || options.inNewWindow) + openURLInNewWindow(options, tabConstructor); + else + openURLInNewTab(options, win, tabConstructor); +} + +function openURLInNewWindow(options, tabConstructor) { + let addTabOptions = { + inNewWindow: true + }; + if (options.onOpen) { + addTabOptions.onLoad = function(e) { + let win = e.target.defaultView; + let tabEl = win.gBrowser.tabContainer.childNodes[0]; + let tabBrowser = win.gBrowser.getBrowserForTab(tabEl); + tabBrowser.addEventListener("load", function onLoad(e) { + tabBrowser.removeEventListener("load", onLoad, true); + let tab = tabConstructor(tabEl); + require("./errors").catchAndLog(function(e) options.onOpen(e))(tab); + }, true); + }; + } + if (options.isPinned) { + addTabOptions.isPinned = true; + } + exports.addTab(options.url.toString(), addTabOptions); +} + +function openURLInNewTab(options, window, tabConstructor) { + window.focus(); + let tabEl = window.gBrowser.addTab(options.url.toString()); + if (!options.inBackground) + window.gBrowser.selectedTab = tabEl; + if (options.isPinned) + window.gBrowser.pinTab(tabEl); + if (options.onOpen) { + let tabBrowser = window.gBrowser.getBrowserForTab(tabEl); + tabBrowser.addEventListener("load", function onLoad(e) { + // remove event handler from addTab - don't want to be notified + // for subsequent loads in same tab. + tabBrowser.removeEventListener("load", onLoad, true); + let tab = tabConstructor(tabEl); + require("./timer").setTimeout(function() { + require("./errors").catchAndLog(function(tab) options.onOpen(tab))(tab); + }, 10); + }, true); + } +} + +function getElementAndWindowForTab(tabObj, window) { + // iterate over open windows, or use single window if provided + let windowIterator = window ? function() { yield window; } + : require("./window-utils").windowIterator; + for (let win in windowIterator()) { + if (win.gBrowser) { + // find the tab element at tab.index + let index = win.gBrowser.getBrowserIndexForDocument(tabObj.contentDocument); + if (index > -1) + return [win.gBrowser.tabContainer.getItemAtIndex(index), win]; + } + } + return [null, null]; +} + +// Tracker for all tabs across all windows +// This is tab-browser.TabTracker, but with +// support for additional events added. +function ModuleTabTracker(delegate, window) { + this._delegate = delegate; + this._tabs = []; + this._tracker = new Tracker(this, window); + require("./unload").ensure(this); +} +ModuleTabTracker.prototype = { + _TAB_EVENTS: ["TabOpen", "TabClose", "TabSelect", "DOMContentLoaded", + "load", "MozAfterPaint"], + _safeTrackTab: function safeTrackTab(tab) { + tab.addEventListener("load", this, false); + tab.linkedBrowser.addEventListener("MozAfterPaint", this, false); + this._tabs.push(tab); + try { + this._delegate.onTrack(tab); + } catch (e) { + console.exception(e); + } + }, + _safeUntrackTab: function safeUntrackTab(tab) { + tab.removeEventListener("load", this, false); + tab.linkedBrowser.removeEventListener("MozAfterPaint", this, false); + var index = this._tabs.indexOf(tab); + if (index == -1) + throw new Error("internal error: tab not found"); + this._tabs.splice(index, 1); + try { + this._delegate.onUntrack(tab); + } catch (e) { + console.exception(e); + } + }, + _safeSelectTab: function safeSelectTab(tab) { + var index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onSelect) + this._delegate.onSelect(tab); + } catch (e) { + console.exception(e); + } + }, + _safeDOMContentLoaded: function safeDOMContentLoaded(event) { + let tabBrowser = event.currentTarget; + let tabBrowserIndex = tabBrowser.getBrowserIndexForDocument(event.target); + // TODO: I'm seeing this when loading data url images + if (tabBrowserIndex == -1) + return; + let tab = tabBrowser.tabContainer.getItemAtIndex(tabBrowserIndex); + let index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onReady) + this._delegate.onReady(tab); + } catch (e) { + console.exception(e); + } + }, + _safeLoad: function safeLoad(event) { + let tab = event.target; + let index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onLoad) + this._delegate.onLoad(tab); + } catch (e) { + console.exception(e); + } + }, + _safeMozAfterPaint: function safeMozAfterPaint(event) { + let win = event.currentTarget.ownerDocument.defaultView; + let tabIndex = win.gBrowser.getBrowserIndexForDocument(event.target.document); + if (tabIndex == -1) + return; + let tab = win.gBrowser.tabContainer.getItemAtIndex(tabIndex); + let index = this._tabs.indexOf(tab); + if (index == -1) + console.error("internal error: tab not found"); + try { + if (this._delegate.onPaint) + this._delegate.onPaint(tab); + } catch (e) { + console.exception(e); + } + }, + handleEvent: function handleEvent(event) { + switch (event.type) { + case "TabOpen": + this._safeTrackTab(event.target); + break; + case "TabClose": + this._safeUntrackTab(event.target); + break; + case "TabSelect": + this._safeSelectTab(event.target); + break; + case "DOMContentLoaded": + this._safeDOMContentLoaded(event); + break; + case "load": + this._safeLoad(event); + break; + case "MozAfterPaint": + this._safeMozAfterPaint(event); + break; + default: + throw new Error("internal error: unknown event type: " + + event.type); + } + }, + onTrack: function onTrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeTrackTab(tab); + tabbrowser.tabContainer.addEventListener("TabOpen", this, false); + tabbrowser.tabContainer.addEventListener("TabClose", this, false); + tabbrowser.tabContainer.addEventListener("TabSelect", this, false); + tabbrowser.ownerDocument.defaultView.gBrowser.addEventListener("DOMContentLoaded", this, false); + }, + onUntrack: function onUntrack(tabbrowser) { + for (let tab in tabIterator(tabbrowser)) + this._safeUntrackTab(tab); + tabbrowser.tabContainer.removeEventListener("TabOpen", this, false); + tabbrowser.tabContainer.removeEventListener("TabClose", this, false); + tabbrowser.tabContainer.removeEventListener("TabSelect", this, false); + tabbrowser.ownerDocument.defaultView.gBrowser.removeEventListener("DOMContentLoaded", this, false); + }, + unload: function unload() { + this._tracker.unload(); + } +}; + +// Utility to get a thumbnail canvas from a tab object +function getThumbnailCanvasForTab(tabEl, window) { + var thumbnail = window.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + thumbnail.mozOpaque = true; + window = tabEl.linkedBrowser.contentWindow; + thumbnail.width = Math.ceil(window.screen.availWidth / 5.75); + var aspectRatio = 0.5625; // 16:9 + thumbnail.height = Math.round(thumbnail.width * aspectRatio); + var ctx = thumbnail.getContext("2d"); + var snippetWidth = window.innerWidth * .6; + var scale = thumbnail.width / snippetWidth; + ctx.scale(scale, scale); + ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth, snippetWidth * aspectRatio, "rgb(255,255,255)"); + return thumbnail; +} + +// Utility to return the contents of the target of a chrome URL +function getChromeURLContents(chromeURL) { + let io = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let channel = io.newChannel(chromeURL, null, null); + let input = channel.open(); + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(input); + let str = stream.readBytes(input.available()); + stream.close(); + input.close(); + return str; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/events.js b/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/events.js new file mode 100644 index 0000000..efb8b45 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/events.js @@ -0,0 +1,26 @@ +/* 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 ON_PREFIX = "on"; +const TAB_PREFIX = "Tab"; + +const EVENTS = { + ready: "DOMContentLoaded", + open: "TabOpen", + close: "TabClose", + activate: "TabSelect", + deactivate: null, + pinned: "TabPinned", + unpinned: "TabUnpinned" +} +exports.EVENTS = EVENTS; + +Object.keys(EVENTS).forEach(function(name) { + EVENTS[name] = { + name: name, + listener: ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1), + dom: EVENTS[name] + } +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/observer.js b/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/observer.js new file mode 100644 index 0000000..e3854cb --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/observer.js @@ -0,0 +1,93 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { EventEmitterTrait: EventEmitter } = require("../events"); +const { DOMEventAssembler } = require("../events/assembler"); +const { Trait } = require("../light-traits"); +const { getActiveTab, getTabs, getTabContainers } = require("./utils"); +const { browserWindowIterator, isBrowser } = require("../window-utils"); +const { observer: windowObserver } = require("../windows/observer"); + +const EVENTS = { + "TabOpen": "open", + "TabClose": "close", + "TabSelect": "select", + "TabMove": "move", + "TabPinned": "pinned", + "TabUnpinned": "unpinned" +}; + + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ + /** + * Method is implemented by `EventEmitter` and is used just for emitting + * events on registered listeners. + */ + _emit: Trait.required, + /** + * Events that are supported and emitted by the module. + */ + supportedEventsTypes: Object.keys(EVENTS), + /** + * Function handles all the supported events on all the windows that are + * observed. Method is used to proxy events to the listeners registered on + * this event emitter. + * @param {Event} event + * Keyboard event being emitted. + */ + handleEvent: function handleEvent(event) { + this._emit(EVENTS[event.type], event.target, event); + } +}); + +// Currently gecko does not dispatches any event on the previously selected +// tab before / after "TabSelect" is dispatched. In order to work around this +// limitation we keep track of selected tab and emit "deactivate" event with +// that before emitting "activate" on selected tab. +var selectedTab = null; +function onTabSelect(tab) { + if (selectedTab !== tab) { + if (selectedTab) observer._emit("deactivate", selectedTab); + if (tab) observer._emit("activate", selectedTab = tab); + } +}; +observer.on("select", onTabSelect); + +// We also observe opening / closing windows in order to add / remove it's +// containers to the observed list. +function onWindowOpen(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + getTabContainers(chromeWindow).forEach(function (container) { + observer.observe(container); + }); +} +windowObserver.on("open", onWindowOpen); + +function onWindowClose(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + getTabContainers(chromeWindow).forEach(function (container) { + observer.ignore(container); + }); +} +windowObserver.on("close", onWindowClose); + + +// Currently gecko does not dispatches "TabSelect" events when different +// window gets activated. To work around this limitation we emulate "select" +// event for this case. +windowObserver.on("activate", function onWindowActivate(chromeWindow) { + if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. + observer._emit("select", getActiveTab(chromeWindow)); +}); + +// We should synchronize state, since probably we already have at least one +// window open. +for each (let window in browserWindowIterator()) onWindowOpen(window); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/tab.js b/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/tab.js new file mode 100644 index 0000000..246834f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/tab.js @@ -0,0 +1,264 @@ +/* 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 { Ci } = require('chrome'); +const { Trait } = require("../traits"); +const { EventEmitter } = require("../events"); +const { validateOptions } = require("../api-utils"); +const { defer } = require("../functional"); +const { EVENTS } = require("./events"); +const { getThumbnailURIForWindow } = require("../utils/thumbnail"); +const { getFaviconURIForLocation } = require("../utils/data"); + + + +// Array of the inner instances of all the wrapped tabs. +const TABS = []; + +/** + * Trait used to create tab wrappers. + */ +const TabTrait = Trait.compose(EventEmitter, { + on: Trait.required, + _emit: Trait.required, + /** + * Tab DOM element that is being wrapped. + */ + _tab: null, + /** + * Window wrapper whose tab this object represents. + */ + window: null, + constructor: function Tab(options) { + this._onReady = this._onReady.bind(this); + this._tab = options.tab; + let window = this.window = options.window; + // Setting event listener if was passed. + for each (let type in EVENTS) { + let listener = options[type.listener]; + if (listener) + this.on(type.name, options[type.listener]); + if ('ready' != type.name) // window spreads this event. + window.tabs.on(type.name, this._onEvent.bind(this, type.name)); + } + + this.on(EVENTS.close.name, this.destroy.bind(this)); + this._browser.addEventListener(EVENTS.ready.dom, this._onReady, true); + + if (options.isPinned) + this.pin(); + + // Since we will have to identify tabs by a DOM elements facade function + // is used as constructor that collects all the instances and makes sure + // that they more then one wrapper is not created per tab. + return this; + }, + destroy: function destroy() { + this._removeAllListeners(); + this._browser.removeEventListener(EVENTS.ready.dom, this._onReady, + true); + }, + + /** + * Internal listener that emits public event 'ready' when the page of this + * tab is loaded. + */ + _onReady: function _onReady(event) { + // IFrames events will bubble so we need to ignore those. + if (event.target == this._contentDocument) + this._emit(EVENTS.ready.name, this._public); + }, + /** + * Internal tab event router. Window will emit tab related events for all it's + * tabs, this listener will propagate all the events for this tab to it's + * listeners. + */ + _onEvent: function _onEvent(type, tab) { + if (tab == this._public) + this._emit(type, tab); + }, + /** + * Browser DOM element where page of this tab is currently loaded. + */ + get _browser() this._window.gBrowser.getBrowserForTab(this._tab), + /** + * Window DOM element containing this tab. + */ + get _window() this._tab.ownerDocument.defaultView, + /** + * Document object of the page that is currently loaded in this tab. + */ + get _contentDocument() this._browser.contentDocument, + /** + * Window object of the page that is currently loaded in this tab. + */ + get _contentWindow() this._browser.contentWindow, + + /** + * The title of the page currently loaded in the tab. + * Changing this property changes an actual title. + * @type {String} + */ + get title() this._contentDocument.title, + set title(value) this._contentDocument.title = String(value), + /** + * Location of the page currently loaded in this tab. + * Changing this property will loads page under under the specified location. + * @type {String} + */ + get url() String(this._browser.currentURI.spec), + set url(value) this._changeLocation(String(value)), + // "TabOpen" event is fired when it's still "about:blank" is loaded in the + // changing `location` property of the `contentDocument` has no effect since + // seems to be either ignored or overridden by internal listener, there for + // location change is enqueued for the next turn of event loop. + _changeLocation: defer(function(url) this._browser.loadURI(url)), + /** + * URI of the favicon for the page currently loaded in this tab. + * @type {String} + */ + get favicon() getFaviconURIForLocation(this.url), + /** + * The CSS style for the tab + */ + get style() null, // TODO + /** + * The index of the tab relative to other tabs in the application window. + * Changing this property will change order of the actual position of the tab. + * @type {Number} + */ + get index() + this._window.gBrowser.getBrowserIndexForDocument(this._contentDocument), + set index(value) this._window.gBrowser.moveTabTo(this._tab, value), + /** + * Thumbnail data URI of the page currently loaded in this tab. + * @type {String} + */ + getThumbnail: function getThumbnail() + getThumbnailURIForWindow(this._contentWindow), + /** + * Whether or not tab is pinned (Is an app-tab). + * @type {Boolean} + */ + get isPinned() this._tab.pinned, + pin: function pin() { + this._window.gBrowser.pinTab(this._tab); + }, + unpin: function unpin() { + this._window.gBrowser.unpinTab(this._tab); + }, + + /** + * Create a worker for this tab, first argument is options given to Worker. + * @type {Worker} + */ + attach: function attach(options) { + let { Worker } = require("../content/worker"); + options.window = this._contentWindow; + let worker = Worker(options); + worker.once("detach", function detach() { + worker.destroy(); + }); + return worker; + }, + + /** + * Make this tab active. + * Please note: That this function is called synchronous since in E10S that + * will be the case. Besides this function is called from a constructor where + * we would like to return instance before firing a 'TabActivated' event. + */ + activate: defer(function activate() { + if (this._window) // Ignore if window is closed by the time this is invoked. + this._window.gBrowser.selectedTab = this._tab; + }), + /** + * Close the tab + */ + close: function close(callback) { + if (callback) + this.once(EVENTS.close.name, callback); + this._window.gBrowser.removeTab(this._tab); + }, + /** + * Reload the tab + */ + reload: function reload() { + this._window.gBrowser.reloadTab(this._tab); + } +}); + +function Tab(options) { + let chromeTab = options.tab; + for each (let tab in TABS) { + if (chromeTab == tab._tab) + return tab._public; + } + let tab = TabTrait(options); + TABS.push(tab); + return tab._public; +} +Tab.prototype = TabTrait.prototype; +exports.Tab = Tab; + +function Options(options) { + if ("string" === typeof options) + options = { url: options }; + + return validateOptions(options, { + url: { is: ["string"] }, + inBackground: { is: ["undefined", "boolean"] }, + isPinned: { is: ["undefined", "boolean"] }, + onOpen: { is: ["undefined", "function"] }, + onClose: { is: ["undefined", "function"] }, + onReady: { is: ["undefined", "function"] }, + onActivate: { is: ["undefined", "function"] }, + onDeactivate: { is: ["undefined", "function"] } + }); +} +exports.Options = Options; + + +exports.getTabForWindow = function (win) { + // Get browser window + let topWindow = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + if (!topWindow.gBrowser) return null; + + // Get top window object, in case we are in a content iframe + let topContentWindow; + try { + topContentWindow = win.top; + } catch(e) { + // It may throw if win is not a valid content window + return null; + } + + function getWindowID(obj) { + return obj.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + } + + // Search for related Tab + let topWindowId = getWindowID(topContentWindow); + for (let i = 0; i < topWindow.gBrowser.browsers.length; i++) { + let w = topWindow.gBrowser.browsers[i].contentWindow; + if (getWindowID(w) == topWindowId) { + return Tab({ + // TODO: api-utils should not depend on addon-kit! + window: require("addon-kit/windows").BrowserWindow({ window: topWindow }), + tab: topWindow.gBrowser.tabs[i] + }); + } + } + + // We were unable to find the related tab! + return null; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/utils.js b/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/utils.js new file mode 100644 index 0000000..1c411dc --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/tabs/utils.js @@ -0,0 +1,59 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +function getTabContainer(tabBrowser) { + return tabBrowser.tabContainer; +} +exports.getTabContainer = getTabContainer; + +function getTabBrowsers(window) { + return Array.slice(window.document.getElementsByTagName("tabbrowser")); +} +exports.getTabBrowsers = getTabBrowsers; + +function getTabContainers(window) { + return getTabBrowsers(window).map(getTabContainer); +} +exports.getTabContainers = getTabContainers; + +function getTabs(window) { + return getTabContainers(window).reduce(function (tabs, container) { + tabs.push.apply(tabs, container.children); + return tabs; + }, []); +} +exports.getTabs = getTabs; + +function getActiveTab(window) { + return window.gBrowser.selectedTab; +} +exports.getActiveTab = getActiveTab; + +function getOwnerWindow(tab) { + return tab.ownerDocument.defaultView; +} +exports.getOwnerWindow = getOwnerWindow; + +function openTab(window, url) { + return window.gBrowser.addTab(url); +} +exports.openTab = openTab; + +function isTabOpen(tab) { + return !!tab.linkedBrowser; +} +exports.isTabOpen = isTabOpen; + +function closeTab(tab) { + return getOwnerWindow(tab).gBrowser.removeTab(tab); +} +exports.closeTab = closeTab; + +function activateTab(tab) { + getOwnerWindow(tab).gBrowser.selectedTab = tab; +} +exports.activateTab = activateTab; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/test.js b/tools/addon-sdk-1.7/packages/api-utils/lib/test.js new file mode 100644 index 0000000..e69a2f4 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/test.js @@ -0,0 +1,108 @@ +/* vim:ts=2:sts=2:sw=2: + * 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 BaseAssert = require("./test/assert").Assert; +const { isFunction, isObject } = require("./type"); + +function extend(target) { + let descriptor = {} + Array.slice(arguments, 1).forEach(function(source) { + Object.getOwnPropertyNames(source).forEach(function onEach(name) { + descriptor[name] = Object.getOwnPropertyDescriptor(source, name); + }); + }); + return Object.create(target, descriptor); +} + +/** + * Function takes test `suite` object in CommonJS format and defines all of the + * tests from that suite and nested suites in a jetpack format on a given + * `target` object. Optionally third argument `prefix` can be passed to prefix + * all the test names. + */ +function defineTestSuite(target, suite, prefix) { + prefix = prefix || ""; + // If suite defines `Assert` that's what `assert` object have to be created + // from and passed to a test function (This allows custom assertion functions) + // See for details: http://wiki.commonjs.org/wiki/Unit_Testing/1.1 + let Assert = suite.Assert || BaseAssert; + // Going through each item in the test suite and wrapping it into a + // Jetpack test format. + Object.keys(suite).forEach(function(key) { + // If name starts with test then it's a test function or suite. + if (key.indexOf("test") === 0) { + let test = suite[key]; + + // For each test function so we create a wrapper test function in a + // jetpack format and copy that to a `target` exports. + if (isFunction(test)) { + + // Since names of the test may match across suites we use full object + // path as a name to avoid overriding same function. + target[prefix + key] = function(options) { + + // Creating `assert` functions for this test. + let assert = Assert(options); + + // If CommonJS test function expects more than one argument + // it means that test is async and second argument is a callback + // to notify that test is finished. + if (1 < test.length) { + + // Letting test runner know that test is executed async and + // creating a callback function that CommonJS tests will call + // once it's done. + options.waitUntilDone(); + test(assert, function() { + options.done(); + }); + } + + // Otherwise CommonJS test is synchronous so we call it only with + // one argument. + else { + test(assert); + } + } + } + + // If it's an object then it's a test suite containing test function + // and / or nested test suites. In that case we just extend prefix used + // and call this function to copy and wrap tests from nested suite. + else if (isObject(test)) { + // We need to clone `tests` instead of modifying it, since it's very + // likely that it is frozen (usually test suites imported modules). + test = extend(Object.prototype, test, { + Assert: test.Assert || Assert + }); + defineTestSuite(target, test, prefix + key + "."); + } + } + }); +} + +/** + * This function is a CommonJS test runner function, but since Jetpack test + * runner and test format is different from CommonJS this function shims given + * `exports` with all its tests into a Jetpack test format so that the built-in + * test runner will be able to run CommonJS test without manual changes. + */ +exports.run = function run(exports) { + + // We can't leave old properties on exports since those are test in a CommonJS + // format that why we move everything to a new `suite` object. + let suite = {}; + Object.keys(exports).forEach(function(key) { + suite[key] = exports[key]; + delete exports[key]; + }); + + // Now we wrap all the CommonJS tests to a Jetpack format and define + // those to a given `exports` object since that where jetpack test runner + // will look for them. + defineTestSuite(exports, suite); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/test/assert.js b/tools/addon-sdk-1.7/packages/api-utils/lib/test/assert.js new file mode 100644 index 0000000..fa40807 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/test/assert.js @@ -0,0 +1,331 @@ +/* vim:ts=2:sts=2:sw=2: + * 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 { isFunction, isNull, isObject, isString, isRegExp, isArray, isDate, + isPrimitive, isUndefined, instanceOf, source } = require("../type"); + +/** + * The `AssertionError` is defined in assert. + * @extends Error + * @example + * new assert.AssertionError({ + * message: message, + * actual: actual, + * expected: expected + * }) + */ +function AssertionError(options) { + let assertionError = Object.create(AssertionError.prototype); + + if (isString(options)) + options = { message: options }; + if ("actual" in options) + assertionError.actual = options.actual; + if ("expected" in options) + assertionError.expected = options.expected; + if ("operator" in options) + assertionError.operator = options.operator; + + assertionError.message = options.message; + assertionError.stack = new Error().stack; + return assertionError; +} +AssertionError.prototype = Object.create(Error.prototype, { + constructor: { value: AssertionError }, + name: { value: "AssertionError", enumerable: true }, + toString: { value: function toString() { + let value; + if (this.message) { + value = this.name + " : " + this.message; + } + else { + value = [ + this.name + " : ", + source(this.expected), + this.operator, + source(this.actual) + ].join(" "); + } + return value; + }} +}); +exports.AssertionError = AssertionError; + +function Assert(logger) { + return Object.create(Assert.prototype, { _log: { value: logger }}); +} +Assert.prototype = { + fail: function fail(e) { + this._log.fail(e.message); + }, + pass: function pass(message) { + this._log.pass(message); + }, + error: function error(e) { + this._log.exception(e); + }, + ok: function ok(value, message) { + if (!!!value) { + this.fail({ + actual: value, + expected: true, + message: message, + operator: "==" + }); + } + else { + this.pass(message); + } + }, + + /** + * The equality assertion tests shallow, coercive equality with `==`. + * @example + * assert.equal(1, 1, "one is one"); + */ + equal: function equal(actual, expected, message) { + if (actual == expected) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "==" + }); + } + }, + + /** + * The non-equality assertion tests for whether two objects are not equal + * with `!=`. + * @example + * assert.notEqual(1, 2, "one is not two"); + */ + notEqual: function notEqual(actual, expected, message) { + if (actual != expected) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!=", + }); + } + }, + + /** + * The equivalence assertion tests a deep (with `===`) equality relation. + * @example + * assert.deepEqual({ a: "foo" }, { a: "foo" }, "equivalent objects") + */ + deepEqual: function deepEqual(actual, expected, message) { + if (isDeepEqual(actual, expected)) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "deepEqual" + }); + } + }, + + /** + * The non-equivalence assertion tests for any deep (with `===`) inequality. + * @example + * assert.notDeepEqual({ a: "foo" }, Object.create({ a: "foo" }), + * "object's inherit from different prototypes"); + */ + notDeepEqual: function notDeepEqual(actual, expected, message) { + if (!isDeepEqual(actual, expected)) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "notDeepEqual" + }); + } + }, + + /** + * The strict equality assertion tests strict equality, as determined by + * `===`. + * @example + * assert.strictEqual(null, null, "`null` is `null`") + */ + strictEqual: function strictEqual(actual, expected, message) { + if (actual === expected) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "===" + }); + } + }, + + /** + * The strict non-equality assertion tests for strict inequality, as + * determined by `!==`. + * @example + * assert.notStrictEqual(null, undefined, "`null` is not `undefined`"); + */ + notStrictEqual: function notStrictEqual(actual, expected, message) { + if (actual !== expected) { + this.pass(message); + } + else { + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!==" + }) + } + }, + + /** + * The assertion whether or not given `block` throws an exception. If optional + * `Error` argument is provided and it's type of function thrown error is + * asserted to be an instance of it, if type of `Error` is string then message + * of throw exception is asserted to contain it. + * @param {Function} block + * Function that is expected to throw. + * @param {Error|RegExp} [Error] + * Error constructor that is expected to be thrown or a string that + * must be contained by a message of the thrown exception, or a RegExp + * matching a message of the thrown exception. + * @param {String} message + * Description message + * + * @examples + * + * assert.throws(function block() { + * doSomething(4) + * }, "Object is expected", "Incorrect argument is passed"); + * + * assert.throws(function block() { + * Object.create(5) + * }, TypeError, "TypeError is thrown"); + */ + throws: function throws(block, Error, message) { + let threw = false; + let exception = null; + + // If third argument is not provided and second argument is a string it + // means that optional `Error` argument was not passed, so we shift + // arguments. + if (isString(Error) && isUndefined(message)) { + message = Error; + Error = undefined; + } + + // Executing given `block`. + try { + block(); + } + catch (e) { + threw = true; + exception = e; + } + + // If exception was thrown and `Error` argument was not passed assert is + // passed. + if (threw && (isUndefined(Error) || + // If passed `Error` is RegExp using it's test method to + // assert thrown exception message. + (isRegExp(Error) && Error.test(exception.message)) || + // If passed `Error` is a constructor function testing if + // thrown exception is an instance of it. + (isFunction(Error) && instanceOf(exception, Error)))) + { + this.pass(message); + } + + // Otherwise we report assertion failure. + else { + let failure = { + message: message, + operator: "throws" + }; + + if (exception) + failure.actual = exception; + + if (Error) + failure.expected = Error; + + this.fail(failure); + } + } +}; +exports.Assert = Assert; + +function isDeepEqual(actual, expected) { + + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + } + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + else if (isDate(actual) && isDate(expected)) { + return actual.getTime() === expected.getTime(); + } + + // XXX specification bug: this should be specified + else if (isPrimitive(actual) || isPrimitive(expected)) { + return expected === actual; + } + + // 7.3. Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + else if (!isObject(actual) && !isObject(expected)) { + return actual == expected; + } + + // 7.4. For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical "prototype" property. Note: this + // accounts for both named and indexed properties on Arrays. + else { + return actual.prototype === expected.prototype && + isEquivalent(actual, expected); + } +} + +function isEquivalent(a, b, stack) { + let aKeys = Object.keys(a); + let bKeys = Object.keys(b); + + return aKeys.length === bKeys.length && + isArrayEquivalent(aKeys.sort(), bKeys.sort()) && + aKeys.every(function(key) { + return isDeepEqual(a[key], b[key], stack) + }); +} + +function isArrayEquivalent(a, b, stack) { + return isArray(a) && isArray(b) && + a.every(function(value, index) { + return isDeepEqual(value, b[index]); + }); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/text-streams.js b/tools/addon-sdk-1.7/packages/api-utils/lib/text-streams.js new file mode 100644 index 0000000..96217eb --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/text-streams.js @@ -0,0 +1,240 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * 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,Cu,components} = require("chrome"); +var NetUtil = {}; +Cu.import("resource://gre/modules/NetUtil.jsm", NetUtil); +NetUtil = NetUtil.NetUtil; + +// NetUtil.asyncCopy() uses this buffer length, and since we call it, for best +// performance we use it, too. +const BUFFER_BYTE_LEN = 0x8000; +const PR_UINT32_MAX = 0xffffffff; +const DEFAULT_CHARSET = "UTF-8"; + +exports.TextReader = TextReader; +exports.TextWriter = TextWriter; + +/** + * An input stream that reads text from a backing stream using a given text + * encoding. + * + * @param inputStream + * The stream is backed by this nsIInputStream. It must already be + * opened. + * @param charset + * Text in inputStream is expected to be in this character encoding. If + * not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl for + * documentation on how to determine other valid values for this. + */ +function TextReader(inputStream, charset) { + const self = this; + charset = checkCharset(charset); + + let stream = Cc["@mozilla.org/intl/converter-input-stream;1"]. + createInstance(Ci.nsIConverterInputStream); + stream.init(inputStream, charset, BUFFER_BYTE_LEN, + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + + let manager = new StreamManager(this, stream); + + /** + * Reads a string from the stream. If the stream is closed, an exception is + * thrown. + * + * @param numChars + * The number of characters to read. If not given, the remainder of + * the stream is read. + * @return The string read. If the stream is already at EOS, returns the + * empty string. + */ + this.read = function TextReader_read(numChars) { + manager.ensureOpened(); + + let readAll = false; + if (typeof(numChars) === "number") + numChars = Math.max(numChars, 0); + else + readAll = true; + + let str = ""; + let totalRead = 0; + let chunkRead = 1; + + // Read in numChars or until EOS, whichever comes first. Note that the + // units here are characters, not bytes. + while (true) { + let chunk = {}; + let toRead = readAll ? + PR_UINT32_MAX : + Math.min(numChars - totalRead, PR_UINT32_MAX); + if (toRead <= 0 || chunkRead <= 0) + break; + + // The converter stream reads in at most BUFFER_BYTE_LEN bytes in a call + // to readString, enough to fill its byte buffer. chunkRead will be the + // number of characters encoded by the bytes in that buffer. + chunkRead = stream.readString(toRead, chunk); + str += chunk.value; + totalRead += chunkRead; + } + + return str; + }; +} + +/** + * A buffered output stream that writes text to a backing stream using a given + * text encoding. + * + * @param outputStream + * The stream is backed by this nsIOutputStream. It must already be + * opened. + * @param charset + * Text will be written to outputStream using this character encoding. + * If not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl + * for documentation on how to determine other valid values for this. + */ +function TextWriter(outputStream, charset) { + const self = this; + charset = checkCharset(charset); + + let stream = outputStream; + + // Buffer outputStream if it's not already. + let ioUtils = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil); + if (!ioUtils.outputStreamIsBuffered(outputStream)) { + stream = Cc["@mozilla.org/network/buffered-output-stream;1"]. + createInstance(Ci.nsIBufferedOutputStream); + stream.init(outputStream, BUFFER_BYTE_LEN); + } + + // I'd like to use nsIConverterOutputStream. But NetUtil.asyncCopy(), which + // we use below in writeAsync(), naturally expects its sink to be an instance + // of nsIOutputStream, which nsIConverterOutputStream's only implementation is + // not. So we use uconv and manually convert all strings before writing to + // outputStream. + let uconv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + uconv.charset = charset; + + let manager = new StreamManager(this, stream); + + /** + * Flushes the backing stream's buffer. + */ + this.flush = function TextWriter_flush() { + manager.ensureOpened(); + stream.flush(); + }; + + /** + * Writes a string to the stream. If the stream is closed, an exception is + * thrown. + * + * @param str + * The string to write. + */ + this.write = function TextWriter_write(str) { + manager.ensureOpened(); + let istream = uconv.convertToInputStream(str); + let len = istream.available(); + while (len > 0) { + stream.writeFrom(istream, len); + len = istream.available(); + } + istream.close(); + }; + + /** + * Writes a string on a background thread. After the write completes, the + * backing stream's buffer is flushed, and both the stream and the backing + * stream are closed, also on the background thread. If the stream is already + * closed, an exception is thrown immediately. + * + * @param str + * The string to write. + * @param callback + * An optional function. If given, it's called as callback(error) when + * the write completes. error is an Error object or undefined if there + * was no error. Inside callback, |this| is the stream object. + */ + this.writeAsync = function TextWriter_writeAsync(str, callback) { + manager.ensureOpened(); + let istream = uconv.convertToInputStream(str); + NetUtil.asyncCopy(istream, stream, function (result) { + let err = components.isSuccessCode(result) ? undefined : + new Error("An error occured while writing to the stream: " + result); + if (err) + console.error(err); + + // asyncCopy() closes its output (and input) stream. + manager.opened = false; + + if (typeof(callback) === "function") { + try { + callback.call(self, err); + } + catch (exc) { + console.exception(exc); + } + } + }); + }; +} + +// This manages the lifetime of stream, a TextReader or TextWriter. It defines +// closed and close() on stream and registers an unload listener that closes +// rawStream if it's still opened. It also provides ensureOpened(), which +// throws an exception if the stream is closed. +function StreamManager(stream, rawStream) { + const self = this; + this.rawStream = rawStream; + this.opened = true; + + /** + * True iff the stream is closed. + */ + stream.__defineGetter__("closed", function stream_closed() { + return !self.opened; + }); + + /** + * Closes both the stream and its backing stream. If the stream is already + * closed, an exception is thrown. For TextWriters, this first flushes the + * backing stream's buffer. + */ + stream.close = function stream_close() { + self.ensureOpened(); + self.unload(); + }; + + require("./unload").ensure(this); +} + +StreamManager.prototype = { + ensureOpened: function StreamManager_ensureOpened() { + if (!this.opened) + throw new Error("The stream is closed and cannot be used."); + }, + unload: function StreamManager_unload() { + // TextWriter.writeAsync() causes rawStream to close and therefore sets + // opened to false, so check that we're still opened. + if (this.opened) { + // Calling close() on both an nsIUnicharInputStream and + // nsIBufferedOutputStream closes their backing streams. It also forces + // nsIOutputStreams to flush first. + this.rawStream.close(); + this.opened = false; + } + } +}; + +function checkCharset(charset) { + return typeof(charset) === "string" ? charset : DEFAULT_CHARSET; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/timer.js b/tools/addon-sdk-1.7/packages/api-utils/lib/timer.js new file mode 100644 index 0000000..55654a8 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/timer.js @@ -0,0 +1,78 @@ +/* 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 } = require("chrome"); +const { Unknown } = require("./xpcom"); +const { when: unload } = require("./unload"); + +const Timer = CC('@mozilla.org/timer;1', 'nsITimer'); + +// Registry for all timers. +const timers = (function(map, id) { + return Object.defineProperties(function registry(key, fallback) { + return key in map ? map[key] : fallback; + }, { + register: { value: function(value) (map[++id] = value, id) }, + unregister: { value: function(key) delete map[key] }, + forEach: { value: function(callback) Object.keys(map).forEach(callback) } + }); +})(Object.create(null), 0); + +const TimerCallback = Unknown.extend({ + interfaces: [ 'nsITimerCallback' ], + initialize: function initialize(id, callback, rest) { + this.id = id; + this.callback = callback; + this.arguments = rest; + } +}); + +const TimeoutCallback = TimerCallback.extend({ + type: 0, // nsITimer.TYPE_ONE_SHOT + notify: function notify() { + try { + timers.unregister(this.id); + this.callback.apply(null, this.arguments); + } + catch (error) { + console.exception(error); + } + } +}); + +const IntervalCallback = TimerCallback.extend({ + type: 1, // nsITimer.TYPE_REPEATING_SLACK + notify: function notify() { + try { + this.callback.apply(null, this.arguments); + } + catch (error) { + console.exception(error); + } + } +}); + +function setTimer(TimerCallback, listener, delay) { + let timer = Timer(); + let id = timers.register(timer); + let callback = TimerCallback.new(id, listener, Array.slice(arguments, 3)); + timer.initWithCallback(callback, delay || 0, TimerCallback.type); + return id; +} + +function unsetTimer(id) { + let timer = timers(id); + timers.unregister(id); + if (timer) + timer.cancel(); +} + +exports.setTimeout = setTimer.bind(null, TimeoutCallback); +exports.setInterval = setTimer.bind(null, IntervalCallback); +exports.clearTimeout = unsetTimer; +exports.clearInterval = unsetTimer; + +unload(function() { timers.forEach(unsetTimer); }); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/traceback.js b/tools/addon-sdk-1.7/packages/api-utils/lib/traceback.js new file mode 100644 index 0000000..c55fd9d --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/traceback.js @@ -0,0 +1,123 @@ +/* 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,components} = require("chrome"); + +// Undo the auto-parentification of URLs done in bug 418356. +function deParentifyURL(url) { + return url ? url.split(" -> ").slice(-1)[0] : url; +} + +// TODO: We might want to move this function to url or some similar +// module. +function getLocalFile(path) { + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + var channel = ios.newChannel(path, null, null); + var iStream = channel.open(); + var siStream = Cc['@mozilla.org/scriptableinputstream;1'] + .createInstance(Ci.nsIScriptableInputStream); + siStream.init(iStream); + var data = new String(); + data += siStream.read(-1); + siStream.close(); + iStream.close(); + return data; +} + +function safeGetFileLine(path, line) { + try { + var scheme = require("./url").URL(path).scheme; + // TODO: There should be an easier, more accurate way to figure out + // what's the case here. + if (!(scheme == "http" || scheme == "https")) + return getLocalFile(path).split("\n")[line - 1]; + } catch (e) {} + return null; +} + +function errorStackToJSON(stack) { + var lines = stack.split("\n"); + + var frames = []; + lines.forEach( + function(line) { + if (!line) + return; + var atIndex = line.indexOf("@"); + var colonIndex = line.lastIndexOf(":"); + var filename = deParentifyURL(line.slice(atIndex + 1, colonIndex)); + var lineNo = parseInt(line.slice(colonIndex + 1)); + var funcSig = line.slice(0, atIndex); + var funcName = funcSig.slice(0, funcSig.indexOf("(")); + frames.unshift({filename: filename, + funcName: funcName, + lineNo: lineNo}); + }); + + return frames; +}; + +function nsIStackFramesToJSON(frame) { + var stack = []; + + while (frame) { + if (frame.filename) { + var filename = deParentifyURL(frame.filename); + stack.splice(0, 0, {filename: filename, + lineNo: frame.lineNumber, + funcName: frame.name}); + } + frame = frame.caller; + } + + return stack; +}; + +var fromException = exports.fromException = function fromException(e) { + if (e instanceof Ci.nsIException) + return nsIStackFramesToJSON(e.location); + if (e.stack && e.stack.length) + return errorStackToJSON(e.stack); + if (e.fileName && typeof(e.lineNumber == "number")) + return [{filename: deParentifyURL(e.fileName), + lineNo: e.lineNumber, + funcName: null}]; + return []; +}; + +var get = exports.get = function get() { + return nsIStackFramesToJSON(components.stack.caller); +}; + +var format = exports.format = function format(tbOrException) { + if (tbOrException === undefined) { + tbOrException = get(); + tbOrException.splice(-1, 1); + } + + var tb; + if (typeof(tbOrException) == "object" && + tbOrException.constructor.name == "Array") + tb = tbOrException; + else + tb = fromException(tbOrException); + + var lines = ["Traceback (most recent call last):"]; + + tb.forEach( + function(frame) { + if (!(frame.filename || frame.lineNo || frame.funcName)) + return; + lines.push(' File "' + frame.filename + '", line ' + + frame.lineNo + ', in ' + frame.funcName); + var sourceLine = safeGetFileLine(frame.filename, frame.lineNo); + if (sourceLine) + lines.push(' ' + sourceLine.trim()); + }); + + return lines.join("\n"); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/traits.js b/tools/addon-sdk-1.7/packages/api-utils/lib/traits.js new file mode 100644 index 0000000..7c1c4ec --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/traits.js @@ -0,0 +1,183 @@ +/* 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 { + compose: _compose, + override: _override, + resolve: _resolve, + trait: _trait, + //create: _create, + required, +} = require('./traits/core'); + +const defineProperties = Object.defineProperties, + freeze = Object.freeze, + create = Object.create; + +/** + * Work around bug 608959 by defining the _create function here instead of + * importing it from traits/core. For docs on this function, see the create + * function in that module. + * + * FIXME: remove this workaround in favor of importing the function once that + * bug has been fixed. + */ +function _create(proto, trait) { + let properties = {}, + keys = Object.getOwnPropertyNames(trait); + for each(let key in keys) { + let descriptor = trait[key]; + if (descriptor.required && + !Object.prototype.hasOwnProperty.call(proto, key)) + throw new Error('Missing required property: ' + key); + else if (descriptor.conflict) + throw new Error('Remaining conflicting property: ' + key); + else + properties[key] = descriptor; + } + return Object.create(proto, properties); +} + +/** + * Placeholder for `Trait.prototype` + */ +let TraitProto = Object.prototype; + +function Get(key) this[key] +function Set(key, value) this[key] = value + +/** + * Creates anonymous trait descriptor from the passed argument, unless argument + * is a trait constructor. In later case trait's already existing properties + * descriptor is returned. + * This is module's internal function and is used as a gateway to a trait's + * internal properties descriptor. + * @param {Function} $ + * Composed trait's constructor. + * @returns {Object} + * Private trait property of the composition. + */ +function TraitDescriptor(object) + ( + 'function' == typeof object && + (object.prototype == TraitProto || object.prototype instanceof Trait) + ) ? object._trait(TraitDescriptor) : _trait(object) + +function Public(instance, trait) { + let result = {}, + keys = Object.getOwnPropertyNames(trait); + for each (let key in keys) { + if ('_' === key.charAt(0) && '__iterator__' !== key ) + continue; + let property = trait[key], + descriptor = { + configurable: property.configurable, + enumerable: property.enumerable + }; + if (property.get) + descriptor.get = property.get.bind(instance); + if (property.set) + descriptor.set = property.set.bind(instance); + if ('value' in property) { + let value = property.value; + if ('function' === typeof value) { + descriptor.value = property.value.bind(instance); + descriptor.writable = property.writable; + } else { + descriptor.get = Get.bind(instance, key); + descriptor.set = Set.bind(instance, key); + } + } + result[key] = descriptor; + } + return result; +} + +/** + * This is private function that composes new trait with privates. + */ +function Composition(trait) { + function Trait() { + let self = _create({}, trait); + self._public = create(Trait.prototype, Public(self, trait)); + delete self._public.constructor; + if (Object === self.constructor) + self.constructor = Trait; + else + return self.constructor.apply(self, arguments) || self._public; + return self._public; + } + defineProperties(Trait, { + prototype: { value: freeze(create(TraitProto, { + constructor: { value: constructor, writable: true } + }))}, // writable is `true` to avoid getters in custom ES5 + displayName: { value: (trait.constructor || constructor).name }, + compose: { value: compose, enumerable: true }, + override: { value: override, enumerable: true }, + resolve: { value: resolve, enumerable: true }, + required: { value: required, enumerable: true }, + _trait: { value: function _trait(caller) + caller === TraitDescriptor ? trait : undefined + } + }); + return freeze(Trait); +} + +/** + * Composes new trait out of itself and traits / property maps passed as an + * arguments. If two or more traits / property maps have properties with the + * same name, the new trait will contain a "conflict" property for that name. + * This is a commutative and associative operation, and the order of its + * arguments is not significant. + * @params {Object|Function} + * List of Traits or property maps to create traits from. + * @returns {Function} + * New trait containing the combined properties of all the traits. + */ +function compose() { + let traits = Array.slice(arguments, 0); + traits.push(this); + return Composition(_compose.apply(null, traits.map(TraitDescriptor))); +} + +/** + * Composes a new trait with all of the combined properties of `this` and the + * argument traits. In contrast to `compose`, `override` immediately resolves + * all conflicts resulting from this composition by overriding the properties of + * later traits. Trait priority is from left to right. I.e. the properties of + * the leftmost trait are never overridden. + * @params {Object} trait + * @returns {Object} + */ +function override() { + let traits = Array.slice(arguments, 0); + traits.push(this); + return Composition(_override.apply(null, traits.map(TraitDescriptor))); +} + +/** + * Composes new resolved trait, with all the same properties as this + * trait, except that all properties whose name is an own property of + * `resolutions` will be renamed to `resolutions[name]`. If it is + * `resolutions[name]` is `null` value is changed into a required property + * descriptor. + */ +function resolve(resolutions) + Composition(_resolve(resolutions, TraitDescriptor(this))) + +/** + * Base Trait, that all the traits are composed of. + */ +const Trait = Composition({ + /** + * Internal property holding public API of this instance. + */ + _public: { value: null, configurable: true, writable: true }, + toString: { value: function() '[object ' + this.constructor.name + ']' } +}); +TraitProto = Trait.prototype; +exports.Trait = Trait; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/traits/core.js b/tools/addon-sdk-1.7/packages/api-utils/lib/traits/core.js new file mode 100644 index 0000000..6e802c7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/traits/core.js @@ -0,0 +1,317 @@ +/* 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"; +// Design inspired by: http://www.traitsjs.org/ + +// shortcuts +const getOwnPropertyNames = Object.getOwnPropertyNames, + getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor, + hasOwn = Object.prototype.hasOwnProperty, + _create = Object.create; + +function doPropertiesMatch(object1, object2, name) { + // If `object1` has property with the given `name` + return name in object1 ? + // then `object2` should have it with the same value. + name in object2 && object1[name] === object2[name] : + // otherwise `object2` should not have property with the given `name`. + !(name in object2); +} + +/** + * Compares two trait custom property descriptors if they are the same. If + * both are `conflict` or all the properties of descriptor are equal returned + * value will be `true`, otherwise it will be `false`. + * @param {Object} desc1 + * @param {Object} desc2 + */ +function areSame(desc1, desc2) { + return ('conflict' in desc1 && desc1.conflict && + 'conflict' in desc2 && desc2.conflict) || + (doPropertiesMatch(desc1, desc2, 'get') && + doPropertiesMatch(desc1, desc2, 'set') && + doPropertiesMatch(desc1, desc2, 'value') && + doPropertiesMatch(desc1, desc2, 'enumerable') && + doPropertiesMatch(desc1, desc2, 'required') && + doPropertiesMatch(desc1, desc2, 'conflict')); +} + +/** + * Converts array to an object whose own property names represent + * values of array. + * @param {String[]} names + * @returns {Object} + * @example + * Map(['foo', ...]) => { foo: true, ...} + */ +function Map(names) { + let map = {}; + for each (let name in names) + map[name] = true; + return map; +} + + +const ERR_CONFLICT = 'Remaining conflicting property: ', + ERR_REQUIRED = 'Missing required property: '; +/** + * Constant singleton, representing placeholder for required properties. + * @type {Object} + */ +const required = { toString: function()'<Trait.required>' }; +exports.required = required; + +/** + * Generates custom **required** property descriptor. Descriptor contains + * non-standard property `required` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function Required(name) { + function required() { throw new Error(ERR_REQUIRED + name) } + return { + get: required, + set: required, + required: true + }; +} + +/** + * Generates custom **conflicting** property descriptor. Descriptor contains + * non-standard property `conflict` that is equal to `true`. + * @param {String} name + * property name to generate descriptor for. + * @returns {Object} + * custom property descriptor + */ +function Conflict(name) { + function conflict() { throw new Error(ERR_CONFLICT + name) } + return { + get: conflict, + set: conflict, + conflict: true + }; +} + +/** + * Function generates custom properties descriptor of the `object`s own + * properties. All the inherited properties are going to be ignored. + * Properties with values matching `required` singleton will be marked as + * 'required' properties. + * @param {Object} object + * Set of properties to generate trait from. + * @returns {Object} + * Properties descriptor of all of the `object`'s own properties. + */ +function trait(properties) { + let result = {}, + keys = getOwnPropertyNames(properties); + for each (let key in keys) { + let descriptor = getOwnPropertyDescriptor(properties, key); + result[key] = (required === descriptor.value) ? Required(key) : descriptor; + } + return result; +} +exports.Trait = exports.trait = trait; + +/** + * Composes new trait. If two or more traits have own properties with the + * same name, the new trait will contain a 'conflict' property for that name. + * 'compose' is a commutative and associative operation, and the order of its + * arguments is not significant. + * + * @params {Object} trait + * Takes traits as an arguments + * @returns {Object} + * New trait containing the combined own properties of all the traits. + * @example + * var newTrait = compose(trait_1, trait_2, ..., trait_N); + */ +function compose(trait1, trait2) { + let traits = Array.slice(arguments, 0), + result = {}; + for each (let trait in traits) { + let keys = getOwnPropertyNames(trait); + for each (let key in keys) { + let descriptor = trait[key]; + // if property already exists and it's not a requirement + if (hasOwn.call(result, key) && !result[key].required) { + if (descriptor.required) + continue; + if (!areSame(descriptor, result[key])) + result[key] = Conflict(key); + } else { + result[key] = descriptor; + } + } + } + return result; +} +exports.compose = compose; + +/** + * Composes new trait with the same own properties as the original trait, + * except that all property names appearing in the first argument are replaced + * by 'required' property descriptors. + * @param {String[]} keys + * Array of strings property names. + * @param {Object} trait + * A trait some properties of which should be excluded. + * @returns {Object} + * @example + * var newTrait = exclude(['name', ...], trait) + */ +function exclude(keys, trait) { + let exclusions = Map(keys), + result = {}; + + keys = getOwnPropertyNames(trait); + + for each (let key in keys) { + if (!hasOwn.call(exclusions, key) || trait[key].required) + result[key] = trait[key]; + else + result[key] = Required(key); + } + return result; +} + +/** + * Composes a new trait with all of the combined properties of the argument + * traits. In contrast to `compose`, `override` immediately resolves all + * conflicts resulting from this composition by overriding the properties of + * later traits. Trait priority is from left to right. I.e. the properties of + * the leftmost trait are never overridden. + * @params {Object} trait + * @returns {Object} + * @examples + * // override is associative: + * override(t1,t2,t3) + * // is equivalent to + * override(t1, override(t2, t3)) + * // or + * to override(override(t1, t2), t3) + * + * // override is not commutative: + * override(t1,t2) + * // is not equivalent to + * override(t2,t1) + */ +function override() { + let traits = Array.slice(arguments, 0), + result = {}; + for each (let trait in traits) { + let keys = getOwnPropertyNames(trait); + for each(let key in keys) { + let descriptor = trait[key]; + if (!hasOwn.call(result, key) || result[key].required) + result[key] = descriptor; + } + } + return result; +} +exports.override = override; + +/** + * Composes a new trait with the same properties as the original trait, except + * that all properties whose name is an own property of map will be renamed to + * map[name], and a 'required' property for name will be added instead. + * @param {Object} map + * An object whose own properties serve as a mapping from old names to new + * names. + * @param {Object} trait + * A trait object + * @returns {Object} + * @example + * var newTrait = rename(map, trait); + */ +function rename(map, trait) { + let result = {}, + keys = getOwnPropertyNames(trait); + for each(let key in keys) { + // must be renamed & it's not requirement + if (hasOwn.call(map, key) && !trait[key].required) { + let alias = map[key]; + if (hasOwn.call(result, alias) && !result[alias].required) + result[alias] = Conflict(alias); + else + result[alias] = trait[key]; + if (!hasOwn.call(result, key)) + result[key] = Required(key); + } else { // must not be renamed or its a requirement + // property is not in result trait yet + if (!hasOwn.call(result, key)) + result[key] = trait[key]; + // property is already in resulted trait & it's not requirement + else if (!trait[key].required) + result[key] = Conflict(key); + } + } + return result; +} + +/** +* Composes new resolved trait, with all the same properties as the original +* trait, except that all properties whose name is an own property of +* resolutions will be renamed to `resolutions[name]`. If it is +* `resolutions[name]` is `null` value is changed into a required property +* descriptor. +* function can be implemented as `rename(map,exclude(exclusions, trait))` +* where map is the subset of mappings from oldName to newName and exclusions +* is an array of all the keys that map to `null`. +* Note: it's important to **first** `exclude`, **then** `rename`, since +* `exclude` and rename are not associative. +* @param {Object} resolutions +* An object whose own properties serve as a mapping from old names to new +* names, or to `null` if the property should be excluded. +* @param {Object} trait +* A trait object +* @returns {Object} +* Resolved trait with the same own properties as the original trait. +*/ +function resolve(resolutions, trait) { + let renames = {}, + exclusions = [], + keys = getOwnPropertyNames(resolutions); + for each (let key in keys) { // pre-process renamed and excluded properties + if (resolutions[key]) // old name -> new name + renames[key] = resolutions[key]; + else // name -> undefined + exclusions.push(key); + } + return rename(renames, exclude(exclusions, trait)); +} +exports.resolve = resolve; + +/** + * `create` is like `Object.create`, except that it ensures that: + * - an exception is thrown if 'trait' still contains required properties + * - an exception is thrown if 'trait' still contains conflicting + * properties + * @param {Object} + * prototype of the completed object + * @param {Object} trait + * trait object to be turned into a complete object + * @returns {Object} + * An object with all of the properties described by the trait. + */ +function create(proto, trait) { + let properties = {}, + keys = getOwnPropertyNames(trait); + for each(let key in keys) { + let descriptor = trait[key]; + if (descriptor.required && !hasOwn.call(proto, key)) + throw new Error(ERR_REQUIRED + key); + else if (descriptor.conflict) + throw new Error(ERR_CONFLICT + key); + else + properties[key] = descriptor; + } + return _create(proto, properties); +} +exports.create = create; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/type.js b/tools/addon-sdk-1.7/packages/api-utils/lib/type.js new file mode 100644 index 0000000..56df14e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/type.js @@ -0,0 +1,340 @@ +/* vim:ts=2:sts=2:sw=2: + * 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"; + +/** + * Returns `true` if `value` is `undefined`. + * @examples + * var foo; isUndefined(foo); // true + * isUndefined(0); // false + */ +function isUndefined(value) { + return value === undefined; +} +exports.isUndefined = isUndefined; + +/** + * Returns `true` if value is `null`. + * @examples + * isNull(null); // true + * isNull(undefined); // false + */ +function isNull(value) { + return value === null; +} +exports.isNull = isNull; + +/** + * Returns `true` if value is a string. + * @examples + * isString("moe"); // true + */ +function isString(value) { + return typeof value === "string"; +} +exports.isString = isString; + +/** + * Returns `true` if `value` is a number. + * @examples + * isNumber(8.4 * 5); // true + */ +function isNumber(value) { + return typeof value === "number"; +} +exports.isNumber = isNumber; + +/** + * Returns `true` if `value` is a `RegExp`. + * @examples + * isRegExp(/moe/); // true + */ +function isRegExp(value) { + return isObject(value) && instanceOf(value, RegExp); +} +exports.isRegExp = isRegExp; + +/** + * Returns true if `value` is a `Date`. + * @examples + * isDate(new Date()); // true + */ +function isDate(value) { + return isObject(value) && instanceOf(value, Date); +} +exports.isDate = isDate; + +/** + * Returns true if object is a Function. + * @examples + * isFunction(function foo(){}) // true + */ +function isFunction(value) { + return typeof value === "function"; +} +exports.isFunction = isFunction; + +/** + * Returns `true` if `value` is an object (please note that `null` is considered + * to be an atom and not an object). + * @examples + * isObject({}) // true + * isObject(null) // false + */ +function isObject(value) { + return typeof value === "object" && value !== null; +} +exports.isObject = isObject; + +/** + * Returns true if `value` is an Array. + * @examples + * isArray([1, 2, 3]) // true + * isArray({ 0: 'foo', length: 1 }) // false + */ +var isArray = Array.isArray || function isArray(value) { + Object.prototype.toString.call(value) === "[object Array]"; +} +exports.isArray = isArray; + +/** + * Returns `true` if `value` is an Arguments object. + * @examples + * (function(){ return isArguments(arguments); })(1, 2, 3); // true + * isArguments([1,2,3]); // false + */ +function isArguments(value) { + Object.prototype.toString.call(value) === "[object Arguments]"; +} +exports.isArguments = isArguments; + +/** + * Returns true if it is a primitive `value`. (null, undefined, number, + * boolean, string) + * @examples + * isPrimitive(3) // true + * isPrimitive('foo') // true + * isPrimitive({ bar: 3 }) // false + */ +function isPrimitive(value) { + return !isFunction(value) && !isObject(value); +} +exports.isPrimitive = isPrimitive; + +/** + * Returns `true` if given `object` is flat (it is direct decedent of + * `Object.prototype` or `null`). + * @examples + * isFlat({}) // true + * isFlat(new Type()) // false + */ +function isFlat(object) { + return isObject(object) && (isNull(Object.getPrototypeOf(object)) || + isNull(Object.getPrototypeOf( + Object.getPrototypeOf(object)))); +} +exports.isFlat = isFlat; + +/** + * Returns `true` if object contains no values. + */ +function isEmpty(object) { + if (isObject(object)) { + for (var key in object) + return false; + return true; + } + return false; +} +exports.isEmpty = isEmpty; + +/** + * Returns `true` if `value` is an array / flat object containing only atomic + * values and other flat objects. + */ +function isJSON(value, visited) { + // Adding value to array of visited values. + (visited || (visited = [])).push(value); + // If `value` is an atom return `true` cause it's valid JSON. + return isPrimitive(value) || + // If `value` is an array of JSON values that has not been visited + // yet. + (isArray(value) && value.every(function(element) { + return isJSON(element, visited); + })) || + // If `value` is a plain object containing properties with a JSON + // values it's a valid JSON. + (isFlat(value) && Object.keys(value).every(function(key) { + var $ = Object.getOwnPropertyDescriptor(value, key); + // Check every proprety of a plain object to verify that + // it's neither getter nor setter, but a JSON value, that + // has not been visited yet. + return ((!isObject($.value) || !~visited.indexOf($.value)) && + !('get' in $) && !('set' in $) && + isJSON($.value, visited)); + })); +} +exports.isJSON = function (value) { + return isJSON(value); +}; + +/** + * Returns if `value` is an instance of a given `Type`. This is exactly same as + * `value instanceof Type` with a difference that `Type` can be from a scope + * that has a different top level object. (Like in case where `Type` is a + * function from different iframe / jetpack module / sandbox). + */ +function instanceOf(value, Type) { + var isConstructorNameSame; + var isConstructorSourceSame; + + // If `instanceof` returned `true` we know result right away. + var isInstanceOf = value instanceof Type; + + // If `instanceof` returned `false` we do ducktype check since `Type` may be + // from a different sandbox. If a constructor of the `value` or a constructor + // of the value's prototype has same name and source we assume that it's an + // instance of the Type. + if (!isInstanceOf && value) { + isConstructorNameSame = value.constructor.name === Type.name; + isConstructorSourceSame = String(value.constructor) == String(Type); + isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || + instanceOf(Object.getPrototypeOf(value), Type); + } + return isInstanceOf; +} +exports.instanceOf = instanceOf; + +/** + * Function returns textual representation of a value passed to it. Function + * takes additional `indent` argument that is used for indentation. Also + * optional `limit` argument may be passed to limit amount of detail returned. + * @param {Object} value + * @param {String} [indent=" "] + * @param {Number} [limit] + */ +function source(value, indent, limit, offset, visited) { + var result; + var names; + var nestingIndex; + var isCompact = !isUndefined(limit); + + indent = indent || " "; + offset = (offset || ""); + result = ""; + visited = visited || []; + + if (isUndefined(value)) { + result += "undefined"; + } + else if (isNull(value)) { + result += "null"; + } + else if (isString(value)) { + result += '"' + value + '"'; + } + else if (isFunction(value)) { + value = String(value).split("\n"); + if (isCompact && value.length > 2) { + value = value.splice(0, 2); + value.push("...}"); + } + result += value.join("\n" + offset); + } + else if (isArray(value)) { + if ((nestingIndex = (visited.indexOf(value) + 1))) { + result = "#" + nestingIndex + "#"; + } + else { + visited.push(value); + + if (isCompact) + value = value.slice(0, limit); + + result += "[\n"; + result += value.map(function(value) { + return offset + indent + source(value, indent, limit, offset + indent, + visited); + }).join(",\n"); + result += isCompact && value.length > limit ? + ",\n" + offset + "...]" : "\n" + offset + "]"; + } + } + else if (isObject(value)) { + if ((nestingIndex = (visited.indexOf(value) + 1))) { + result = "#" + nestingIndex + "#" + } + else { + visited.push(value) + + names = Object.keys(value); + + result += "{ // " + value + "\n"; + result += (isCompact ? names.slice(0, limit) : names).map(function(name) { + var _limit = isCompact ? limit - 1 : limit; + var descriptor = Object.getOwnPropertyDescriptor(value, name); + var result = offset + indent + "// "; + var accessor; + if (0 <= name.indexOf(" ")) + name = '"' + name + '"'; + + if (descriptor.writable) + result += "writable "; + if (descriptor.configurable) + result += "configurable "; + if (descriptor.enumerable) + result += "enumerable "; + + result += "\n"; + if ("value" in descriptor) { + result += offset + indent + name + ": "; + result += source(descriptor.value, indent, _limit, indent + offset, + visited); + } + else { + + if (descriptor.get) { + result += offset + indent + "get " + name + " "; + accessor = source(descriptor.get, indent, _limit, indent + offset, + visited); + result += accessor.substr(accessor.indexOf("{")); + } + + if (descriptor.set) { + result += offset + indent + "set " + name + " "; + accessor = source(descriptor.set, indent, _limit, indent + offset, + visited); + result += accessor.substr(accessor.indexOf("{")); + } + } + return result; + }).join(",\n"); + + if (isCompact) { + if (names.length > limit && limit > 0) { + result += ",\n" + offset + indent + "//..."; + } + } + else { + if (names.length) + result += ","; + + result += "\n" + offset + indent + '"__proto__": '; + result += source(Object.getPrototypeOf(value), indent, 0, + offset + indent); + } + + result += "\n" + offset + "}"; + } + } + else { + result += String(value); + } + return result; +} +exports.source = function (value, indentation, limit) { + return source(value, indentation, limit); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/unit-test-finder.js b/tools/addon-sdk-1.7/packages/api-utils/lib/unit-test-finder.js new file mode 100644 index 0000000..993749b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/unit-test-finder.js @@ -0,0 +1,76 @@ +/* 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 file = require("./file"); +const packaging = require('@packaging'); +const memory = require('api-utils/memory'); +const suites = packaging.allTestModules; + + +const NOT_TESTS = ['setup', 'teardown']; + +var TestFinder = exports.TestFinder = function TestFinder(options) { + memory.track(this); + this.filter = options.filter; + this.testInProcess = options.testInProcess === false ? false : true; + this.testOutOfProcess = options.testOutOfProcess === true ? true : false; +}; + +TestFinder.prototype = { + _makeTest: function _makeTest(suite, name, test) { + function runTest(runner) { + console.info("executing '" + suite + "." + name + "'"); + test(runner); + } + return runTest; + }, + + findTests: function findTests(cb) { + var self = this; + var tests = []; + var filter; + // A filter string is {fileNameRegex}[:{testNameRegex}] - ie, a colon + // optionally separates a regex for the test fileName from a regex for the + // testName. + if (this.filter) { + var colonPos = this.filter.indexOf(':'); + var filterFileRegex, filterNameRegex; + if (colonPos === -1) { + filterFileRegex = new RegExp(self.filter); + } else { + filterFileRegex = new RegExp(self.filter.substr(0, colonPos)); + filterNameRegex = new RegExp(self.filter.substr(colonPos + 1)); + } + // This function will first be called with just the filename; if + // it returns true the module will be loaded then the function + // called again with both the filename and the testname. + filter = function(filename, testname) { + return filterFileRegex.test(filename) && + ((testname && filterNameRegex) ? filterNameRegex.test(testname) + : true); + }; + } else + filter = function() {return true}; + + suites.forEach( + function(suite) { + var module = require(suite); + if (self.testInProcess) + for each (let name in Object.keys(module).sort()) { + if(NOT_TESTS.indexOf(name) === -1 && filter(suite, name)) { + tests.push({ + setup: module.setup, + teardown: module.teardown, + testFunction: self._makeTest(suite, name, module[name]), + name: suite + "." + name + }); + } + } + }); + + cb(tests); + } +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/unit-test.js b/tools/addon-sdk-1.7/packages/api-utils/lib/unit-test.js new file mode 100644 index 0000000..4018487 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/unit-test.js @@ -0,0 +1,440 @@ +/* vim:st=2:sts=2:sw=2: + * 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 memory = require('api-utils/memory'); +var timer = require("./timer"); + +exports.findAndRunTests = function findAndRunTests(options) { + var TestFinder = require("./unit-test-finder").TestFinder; + var finder = new TestFinder({ + filter: options.filter, + testInProcess: options.testInProcess, + testOutOfProcess: options.testOutOfProcess + }); + var runner = new TestRunner({fs: options.fs}); + finder.findTests( + function (tests) { + runner.startMany({tests: tests, + stopOnError: options.stopOnError, + onDone: options.onDone}); + }); +}; + +var TestRunner = exports.TestRunner = function TestRunner(options) { + if (options) { + this.fs = options.fs; + } + this.console = (options && "console" in options) ? options.console : console; + memory.track(this); + this.passed = 0; + this.failed = 0; + this.testRunSummary = []; + this.expectFailNesting = 0; +}; + +TestRunner.prototype = { + toString: function toString() "[object TestRunner]", + + DEFAULT_PAUSE_TIMEOUT: 10000, + PAUSE_DELAY: 500, + + _logTestFailed: function _logTestFailed(why) { + this.test.errors[why]++; + if (!this.testFailureLogged) { + this.console.error("TEST FAILED: " + this.test.name + " (" + why + ")"); + this.testFailureLogged = true; + } + }, + + pass: function pass(message) { + if(!this.expectFailure) { + this.console.info("pass:", message); + this.passed++; + this.test.passed++; + } + else { + this.expectFailure = false; + this.fail('Failure Expected: ' + message); + } + }, + + fail: function fail(message) { + if(!this.expectFailure) { + this._logTestFailed("failure"); + this.console.error("fail:", message); + this.console.trace(); + this.failed++; + this.test.failed++; + } + else { + this.expectFailure = false; + this.pass(message); + } + }, + + expectFail: function(callback) { + this.expectFailure = true; + callback(); + this.expectFailure = false; + }, + + exception: function exception(e) { + this._logTestFailed("exception"); + this.console.exception(e); + this.failed++; + this.test.failed++; + }, + + assertMatches: function assertMatches(string, regexp, message) { + if (regexp.test(string)) { + if (!message) + message = uneval(string) + " matches " + uneval(regexp); + this.pass(message); + } else { + var no = uneval(string) + " doesn't match " + uneval(regexp); + if (!message) + message = no; + else + message = message + " (" + no + ")"; + this.fail(message); + } + }, + + assertRaises: function assertRaises(func, predicate, message) { + try { + func(); + if (message) + this.fail(message + " (no exception thrown)"); + else + this.fail("function failed to throw exception"); + } catch (e) { + var errorMessage; + if (typeof(e) == "string") + errorMessage = e; + else + errorMessage = e.message; + if (typeof(predicate) == "string") + this.assertEqual(errorMessage, predicate, message); + else + this.assertMatches(errorMessage, predicate, message); + } + }, + + assert: function assert(a, message) { + if (!a) { + if (!message) + message = "assertion failed, value is " + a; + this.fail(message); + } else + this.pass(message || "assertion successful"); + }, + + assertNotEqual: function assertNotEqual(a, b, message) { + if (a != b) { + if (!message) + message = "a != b != " + uneval(a); + this.pass(message); + } else { + var equality = uneval(a) + " == " + uneval(b); + if (!message) + message = equality; + else + message += " (" + equality + ")"; + this.fail(message); + } + }, + + assertEqual: function assertEqual(a, b, message) { + if (a == b) { + if (!message) + message = "a == b == " + uneval(a); + this.pass(message); + } else { + var inequality = uneval(a) + " != " + uneval(b); + if (!message) + message = inequality; + else + message += " (" + inequality + ")"; + this.fail(message); + } + }, + + assertNotStrictEqual: function assertNotStrictEqual(a, b, message) { + if (a !== b) { + if (!message) + message = "a !== b !== " + uneval(a); + this.pass(message); + } else { + var equality = uneval(a) + " === " + uneval(b); + if (!message) + message = equality; + else + message += " (" + equality + ")"; + this.fail(message); + } + }, + + assertStrictEqual: function assertStrictEqual(a, b, message) { + if (a === b) { + if (!message) + message = "a === b === " + uneval(a); + this.pass(message); + } else { + var inequality = uneval(a) + " !== " + uneval(b); + if (!message) + message = inequality; + else + message += " (" + inequality + ")"; + this.fail(message); + } + }, + + assertFunction: function assertFunction(a, message) { + this.assertStrictEqual('function', typeof a, message); + }, + + assertUndefined: function(a, message) { + this.assertStrictEqual('undefined', typeof a, message); + }, + + assertNotUndefined: function(a, message) { + this.assertNotStrictEqual('undefined', typeof a, message); + }, + + assertNull: function(a, message) { + this.assertStrictEqual(null, a, message); + }, + + assertNotNull: function(a, message) { + this.assertNotStrictEqual(null, a, message); + }, + + assertObject: function(a, message) { + this.assertStrictEqual('[object Object]', Object.prototype.toString.apply(a), message); + }, + + assertString: function(a, message) { + this.assertStrictEqual('[object String]', Object.prototype.toString.apply(a), message); + }, + + assertArray: function(a, message) { + this.assertStrictEqual('[object Array]', Object.prototype.toString.apply(a), message); + }, + + assertNumber: function(a, message) { + this.assertStrictEqual('[object Number]', Object.prototype.toString.apply(a), message); + }, + + done: function done() { + if (!this.isDone) { + this.isDone = true; + if(this.test.teardown) { + this.test.teardown(this); + } + if (this.waitTimeout !== null) { + timer.clearTimeout(this.waitTimeout); + this.waitTimeout = null; + } + if (this.test.passed == 0 && this.test.failed == 0) { + this._logTestFailed("empty test"); + this.failed++; + this.test.failed++; + } + + this.testRunSummary.push({ + name: this.test.name, + passed: this.test.passed, + failed: this.test.failed, + errors: [error for (error in this.test.errors)].join(", ") + }); + + if (this.onDone !== null) { + var onDone = this.onDone; + var self = this; + this.onDone = null; + timer.setTimeout(function() { onDone(self); }, 0); + } + } + }, + + // Set of assertion functions to wait for an assertion to become true + // These functions take the same arguments as the TestRunner.assert* methods. + waitUntil: function waitUntil() { + return this._waitUntil(this.assert, arguments); + }, + + waitUntilNotEqual: function waitUntilNotEqual() { + return this._waitUntil(this.assertNotEqual, arguments); + }, + + waitUntilEqual: function waitUntilEqual() { + return this._waitUntil(this.assertEqual, arguments); + }, + + waitUntilMatches: function waitUntilMatches() { + return this._waitUntil(this.assertMatches, arguments); + }, + + /** + * Internal function that waits for an assertion to become true. + * @param {Function} assertionMethod + * Reference to a TestRunner assertion method like test.assert, + * test.assertEqual, ... + * @param {Array} args + * List of arguments to give to the previous assertion method. + * All functions in this list are going to be called to retrieve current + * assertion values. + */ + _waitUntil: function waitUntil(assertionMethod, args) { + let count = 0; + let maxCount = this.DEFAULT_PAUSE_TIMEOUT / this.PAUSE_DELAY; + + // We need to ensure that test is asynchronous + if (!this.waitTimeout) + this.waitUntilDone(this.DEFAULT_PAUSE_TIMEOUT); + + let callback = null; + let finished = false; + + let test = this; + + // capture a traceback before we go async. + let traceback = require("./traceback"); + let stack = traceback.get(); + stack.splice(-2, 2); + let currentWaitStack = traceback.format(stack); + let timeout = null; + + function loop(stopIt) { + timeout = null; + + // Build a mockup object to fake TestRunner API and intercept calls to + // pass and fail methods, in order to retrieve nice error messages + // and assertion result + let mock = { + pass: function (msg) { + test.pass(msg); + test.waitUntilCallback = null; + if (callback && !stopIt) + callback(); + finished = true; + }, + fail: function (msg) { + // If we are called on test timeout, we stop the loop + // and print which test keeps failing: + if (stopIt) { + test.console.error("test assertion never became true:\n", + msg + "\n", + currentWaitStack); + if (timeout) + timer.clearTimeout(timeout); + return; + } + timeout = timer.setTimeout(loop, test.PAUSE_DELAY); + } + }; + + // Automatically call args closures in order to build arguments for + // assertion function + let appliedArgs = []; + for (let i = 0, l = args.length; i < l; i++) { + let a = args[i]; + if (typeof a == "function") { + try { + a = a(); + } + catch(e) { + test.fail("Exception when calling asynchronous assertion: " + e); + finished = true; + return; + } + } + appliedArgs.push(a); + } + + // Finally call assertion function with current assertion values + assertionMethod.apply(mock, appliedArgs); + } + loop(); + this.waitUntilCallback = loop; + + // Return an object with `then` method, to offer a way to execute + // some code when the assertion passed or failed + return { + then: function (c) { + callback = c; + + // In case of immediate positive result, we need to execute callback + // immediately here: + if (finished) + callback(); + } + }; + }, + + waitUntilDone: function waitUntilDone(ms) { + if (ms === undefined) + ms = this.DEFAULT_PAUSE_TIMEOUT; + + var self = this; + + function tiredOfWaiting() { + self._logTestFailed("timed out"); + if (self.waitUntilCallback) { + self.waitUntilCallback(true); + self.waitUntilCallback = null; + } + self.failed++; + self.test.failed++; + self.done(); + } + + // We may already have registered a timeout callback + if (this.waitTimeout) + timer.clearTimeout(this.waitTimeout); + + this.waitTimeout = timer.setTimeout(tiredOfWaiting, ms); + }, + + startMany: function startMany(options) { + function runNextTest(self) { + var test = options.tests.shift(); + if (options.stopOnError && self.test && self.test.failed) { + self.console.error("aborted: test failed and --stop-on-error was specified"); + options.onDone(self); + } else if (test) { + self.start({test: test, onDone: runNextTest}); + } else { + options.onDone(self); + } + } + runNextTest(this); + }, + + start: function start(options) { + this.test = options.test; + this.test.passed = 0; + this.test.failed = 0; + this.test.errors = {}; + + this.isDone = false; + this.onDone = options.onDone; + this.waitTimeout = null; + this.testFailureLogged = false; + + try { + if(this.test.setup) { + this.test.setup(this); + } + this.test.testFunction(this); + } catch (e) { + this.exception(e); + } + if (this.waitTimeout === null) + this.done(); + } +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/unload.js b/tools/addon-sdk-1.7/packages/api-utils/lib/unload.js new file mode 100644 index 0000000..aba5923 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/unload.js @@ -0,0 +1,63 @@ +/* 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/. */ + +// Parts of this module were taken from narwhal: +// +// http://narwhaljs.org + +var observers = []; +var unloaders = []; + +var when = exports.when = function when(observer) { + if (observers.indexOf(observer) != -1) + return; + observers.unshift(observer); +}; + +var send = exports.send = function send(reason, onError) { + onError = onError || console.exception; + observers.forEach(function (observer) { + try { + observer(reason); + } catch (e) { + onError(e); + } + }); +}; + +var ensure = exports.ensure = function ensure(obj, destructorName) { + if (!destructorName) + destructorName = "unload"; + if (!(destructorName in obj)) + throw new Error("object has no '" + destructorName + "' property"); + + let called = false; + let originalDestructor = obj[destructorName]; + + function unloadWrapper(reason) { + if (!called) { + called = true; + let index = unloaders.indexOf(unloadWrapper); + if (index == -1) + throw new Error("internal error: unloader not found"); + unloaders.splice(index, 1); + originalDestructor.call(obj, reason); + originalDestructor = null; + destructorName = null; + obj = null; + } + }; + + unloaders.push(unloadWrapper); + + obj[destructorName] = unloadWrapper; +}; + +when( + function(reason) { + unloaders.slice().forEach( + function(unloadWrapper) { + unloadWrapper(reason); + }); + }); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/url.js b/tools/addon-sdk-1.7/packages/api-utils/lib/url.js new file mode 100644 index 0000000..29fc12c --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/url.js @@ -0,0 +1,113 @@ +/* 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,Cr} = require("chrome"); + +var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + +var resProt = ios.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + +function newURI(uriStr, base) { + try { + let baseURI = base ? ios.newURI(base, null, null) : null; + return ios.newURI(uriStr, null, baseURI); + } + catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) { + throw new Error("malformed URI: " + uriStr); + } + catch (e if (e.result == Cr.NS_ERROR_FAILURE || + e.result == Cr.NS_ERROR_ILLEGAL_VALUE)) { + throw new Error("invalid URI: " + uriStr); + } +} + +function resolveResourceURI(uri) { + var resolved; + try { + resolved = resProt.resolveURI(uri); + } catch (e if e.result == Cr.NS_ERROR_NOT_AVAILABLE) { + throw new Error("resource does not exist: " + uri.spec); + }; + return resolved; +} + +let fromFilename = exports.fromFilename = function fromFilename(path) { + var file = Cc['@mozilla.org/file/local;1'] + .createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return ios.newFileURI(file).spec; +}; + +let toFilename = exports.toFilename = function toFilename(url) { + var uri = newURI(url); + if (uri.scheme == "resource") + uri = newURI(resolveResourceURI(uri)); + if (uri.scheme == "chrome") { + var channel = ios.newChannelFromURI(uri); + try { + channel = channel.QueryInterface(Ci.nsIFileChannel); + return channel.file.path; + } catch (e if e.result == Cr.NS_NOINTERFACE) { + throw new Error("chrome url isn't on filesystem: " + url); + } + } + if (uri.scheme == "file") { + var file = uri.QueryInterface(Ci.nsIFileURL).file; + return file.path; + } + throw new Error("cannot map to filename: " + url); +}; + +function URL(url, base) { + if (!(this instanceof URL)) { + return new URL(url, base); + } + + var uri = newURI(url, base); + + var userPass = null; + try { + userPass = uri.userPass ? uri.userPass : null; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + var host = null; + try { + host = uri.host; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + var port = null; + try { + port = uri.port == -1 ? null : uri.port; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + this.__defineGetter__("scheme", function() uri.scheme); + this.__defineGetter__("userPass", function() userPass); + this.__defineGetter__("host", function() host); + this.__defineGetter__("port", function() port); + this.__defineGetter__("path", function() uri.path); + + Object.defineProperties(this, { + toString: { + value: function URL_toString() new String(uri.spec).toString(), + enumerable: false + }, + valueOf: { + value: function() new String(uri.spec).valueOf(), + enumerable: false + }, + toSource: { + value: function() new String(uri.spec).toSource(), + enumerable: false + } + }); + + return this; +}; + +URL.prototype = Object.create(String.prototype); +exports.URL = URL; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/utils/data.js b/tools/addon-sdk-1.7/packages/api-utils/lib/utils/data.js new file mode 100644 index 0000000..33356cd --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/utils/data.js @@ -0,0 +1,71 @@ +/* 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, Cu } = require("chrome"); +const IOService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); +const AppShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); + +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm"); +const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + +const PNG_B64 = "data:image/png;base64,"; +const DEF_FAVICON_URI = "chrome://mozapps/skin/places/defaultFavicon.png"; +let DEF_FAVICON = null; + +/** + * Takes URI of the page and returns associated favicon URI. + * If page under passed uri has no favicon then base64 encoded data URI of + * default faveicon is returned. + * @param {String} uri + * @returns {String} + */ +exports.getFaviconURIForLocation = function getFaviconURIForLocation(uri) { + let pageURI = NetUtil.newURI(uri); + try { + return FaviconService.getFaviconDataAsDataURL( + FaviconService.getFaviconForPage(pageURI)); + } + catch(e) { + if (!DEF_FAVICON) { + DEF_FAVICON = PNG_B64 + + base64Encode(getChromeURIContent(DEF_FAVICON_URI)); + } + return DEF_FAVICON; + } +} + +/** + * Takes chrome URI and returns content under that URI. + * @param {String} chromeURI + * @returns {String} + */ +function getChromeURIContent(chromeURI) { + let channel = IOService.newChannel(chromeURI, null, null); + let input = channel.open(); + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(input); + let content = stream.readBytes(input.available()); + stream.close(); + input.close(); + return content; +} +exports.getChromeURIContent = getChromeURIContent; + +/** + * Creates a base-64 encoded ASCII string from a string of binary data. + */ +function base64Encode(data) AppShellService.hiddenDOMWindow.btoa(String(data)); +exports.base64Encode = base64Encode; + +/** + * Decodes a string of data which has been encoded using base-64 encoding. + */ +function base64Decode(data) AppShellService.hiddenDOMWindow.atob(String(data)); +exports.base64Decode = base64Decode; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/utils/object.js b/tools/addon-sdk-1.7/packages/api-utils/lib/utils/object.js new file mode 100644 index 0000000..165aa0b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/utils/object.js @@ -0,0 +1,46 @@ +/* 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"; + +/** + * Merges all the properties of all arguments into first argument. If two or + * more argument objects have own properties with the same name, the property + * is overridden, with precedence from right to left, implying, that properties + * of the object on the left are overridden by a same named property of the + * object on the right. + * @examples + * var a = { bar: 0, a: 'a' } + * var b = merge(a, { foo: 'foo', bar: 1 }, { foo: 'bar', name: 'b' }); + * b === a // true + * b.a // 'a' + * b.foo // 'bar' + * b.bar // 1 + * b.name // 'b' + */ +function merge(source) { + let descriptor = {}; + Array.slice(arguments, 1).forEach(function onEach(properties) { + Object.getOwnPropertyNames(properties).forEach(function(name) { + descriptor[name] = Object.getOwnPropertyDescriptor(properties, name); + }); + }); + return Object.defineProperties(source, descriptor); +} +exports.merge = merge; + +/** + * Returns an object that inherits from the first argument and contains all the + * properties from all following arguments. + * `extend(source1, source2, source3)` is equivalent of + * `merge(Object.create(source1), source2, source3)`. + */ +function extend(source) { + let rest = Array.slice(arguments, 1); + rest.unshift(Object.create(source)); + return merge.apply(null, rest); +} +exports.extend = extend; + + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/utils/registry.js b/tools/addon-sdk-1.7/packages/api-utils/lib/utils/registry.js new file mode 100644 index 0000000..2ce6a6e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/utils/registry.js @@ -0,0 +1,57 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { EventEmitter } = require('../events'); +const unload = require('../unload'); + +const Registry = EventEmitter.compose({ + _registry: null, + _constructor: null, + constructor: function Registry(constructor) { + this._registry = []; + this._constructor = constructor; + this.on('error', this._onError = this._onError.bind(this)); + unload.ensure(this, "_destructor"); + }, + _destructor: function _destructor() { + let _registry = this._registry.slice(0); + for each (let instance in _registry) + this._emit('remove', instance); + this._registry.splice(0); + }, + _onError: function _onError(e) { + if (!this._listeners('error').length) + console.error(e); + }, + has: function has(instance) { + let _registry = this._registry; + return ( + (0 <= _registry.indexOf(instance)) || + (instance && instance._public && 0 <= _registry.indexOf(instance._public)) + ); + }, + add: function add(instance) { + let { _constructor, _registry } = this; + if (!(instance instanceof _constructor)) + instance = new _constructor(instance); + if (0 > _registry.indexOf(instance)) { + _registry.push(instance); + this._emit('add', instance); + } + return instance; + }, + remove: function remove(instance) { + let _registry = this._registry; + let index = _registry.indexOf(instance) + if (0 <= index) { + this._emit('remove', instance); + _registry.splice(index, 1); + } + } +}); +exports.Registry = Registry; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/utils/thumbnail.js b/tools/addon-sdk-1.7/packages/api-utils/lib/utils/thumbnail.js new file mode 100644 index 0000000..dfd03dd --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/utils/thumbnail.js @@ -0,0 +1,43 @@ +/* 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, Cu } = require("chrome"); +const AppShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); + +const NS = "http://www.w3.org/1999/xhtml"; +const COLOR = "rgb(255,255,255)"; + +/** + * Creates canvas element with a thumbnail of the passed window. + * @param {Window} window + * @returns {Element} + */ +function getThumbnailCanvasForWindow(window) { + let aspectRatio = 0.5625; // 16:9 + let thumbnail = AppShellService.hiddenDOMWindow.document + .createElementNS(NS, "canvas"); + thumbnail.mozOpaque = true; + thumbnail.width = Math.ceil(window.screen.availWidth / 5.75); + thumbnail.height = Math.round(thumbnail.width * aspectRatio); + let ctx = thumbnail.getContext("2d"); + let snippetWidth = window.innerWidth * .6; + let scale = thumbnail.width / snippetWidth; + ctx.scale(scale, scale); + ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth, + snippetWidth * aspectRatio, COLOR); + return thumbnail; +} +exports.getThumbnailCanvasForWindow = getThumbnailCanvasForWindow; + +/** + * Creates Base64 encoded data URI of the thumbnail for the passed window. + * @param {Window} window + * @returns {String} + */ +exports.getThumbnailURIForWindow = function getThumbnailURIForWindow(window) { + return getThumbnailCanvasForWindow(window).toDataURL() +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/uuid.js b/tools/addon-sdk-1.7/packages/api-utils/lib/uuid.js new file mode 100644 index 0000000..cd667ae --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/uuid.js @@ -0,0 +1,13 @@ +/* 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, components: { ID: parseUUID } } = require('chrome'); +const { generateUUID } = Cc['@mozilla.org/uuid-generator;1']. + getService(Ci.nsIUUIDGenerator); + +// Returns `uuid`. If `id` is passed then it's parsed to `uuid` and returned +// if not then new one is generated. +exports.uuid = function uuid(id) id ? parseUUID(id) : generateUUID() diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/window-utils.js b/tools/addon-sdk-1.7/packages/api-utils/lib/window-utils.js new file mode 100644 index 0000000..bd2f2d5 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/window-utils.js @@ -0,0 +1,278 @@ +/* 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 { EventEmitter } = require('./events'), + { Trait } = require('./traits'); +const { when } = require('./unload'); +const { getInnerId, getOuterId } = require('./window-utils'); +const errors = require("./errors"); + +const windowWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); +const appShellService = Cc["@mozilla.org/appshell/appShellService;1"]. + getService(Ci.nsIAppShellService); +const observers = require('api-utils/observer-service'); + + +const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + +/** + * An iterator for XUL windows currently in the application. + * + * @return A generator that yields XUL windows exposing the + * nsIDOMWindow interface. + */ +function windowIterator() { + let winEnum = windowWatcher.getWindowEnumerator(); + while (winEnum.hasMoreElements()) + yield winEnum.getNext().QueryInterface(Ci.nsIDOMWindow); +}; +exports.windowIterator = windowIterator; + +/** + * An iterator for browser windows currently open in the application. + * @returns {Function} + * A generator that yields browser windows exposing the `nsIDOMWindow` + * interface. + */ +function browserWindowIterator() { + for each (let window in windowIterator()) { + if (isBrowser(window)) + yield window; + } +} +exports.browserWindowIterator = browserWindowIterator; + +function WindowTracker(delegate) { + if (!(this instanceof WindowTracker)) { + return new WindowTracker(delegate); + } + + this._delegate = delegate; + this._loadingWindows = []; + + for (let window in windowIterator()) + this._regWindow(window); + windowWatcher.registerNotification(this); + + require("./unload").ensure(this); + + return this; +}; + +WindowTracker.prototype = { + _regLoadingWindow: function _regLoadingWindow(window) { + this._loadingWindows.push(window); + window.addEventListener("load", this, true); + }, + + _unregLoadingWindow: function _unregLoadingWindow(window) { + var index = this._loadingWindows.indexOf(window); + + if (index != -1) { + this._loadingWindows.splice(index, 1); + window.removeEventListener("load", this, true); + } + }, + + _regWindow: function _regWindow(window) { + if (window.document.readyState == "complete") { + this._unregLoadingWindow(window); + this._delegate.onTrack(window); + } else + this._regLoadingWindow(window); + }, + + _unregWindow: function _unregWindow(window) { + if (window.document.readyState == "complete") { + if (this._delegate.onUntrack) + this._delegate.onUntrack(window); + } else { + this._unregLoadingWindow(window); + } + }, + + unload: function unload() { + windowWatcher.unregisterNotification(this); + for (let window in windowIterator()) + this._unregWindow(window); + }, + + handleEvent: errors.catchAndLog(function handleEvent(event) { + if (event.type == "load" && event.target) { + var window = event.target.defaultView; + if (window) + this._regWindow(window); + } + }), + + observe: errors.catchAndLog(function observe(subject, topic, data) { + var window = subject.QueryInterface(Ci.nsIDOMWindow); + if (topic == "domwindowopened") + this._regWindow(window); + else + this._unregWindow(window); + }) +}; +exports.WindowTracker = WindowTracker; + +const WindowTrackerTrait = Trait.compose({ + _onTrack: Trait.required, + _onUntrack: Trait.required, + constructor: function WindowTrackerTrait() { + WindowTracker({ + onTrack: this._onTrack.bind(this), + onUntrack: this._onUntrack.bind(this) + }); + } +}); +exports.WindowTrackerTrait = WindowTrackerTrait; + +var gDocsToClose = []; + +function onDocUnload(event) { + var index = gDocsToClose.indexOf(event.target); + if (index == -1) + throw new Error("internal error: unloading document not found"); + var document = gDocsToClose.splice(index, 1)[0]; + // Just in case, let's remove the event listener too. + document.defaultView.removeEventListener("unload", onDocUnload, false); +} + +onDocUnload = require("./errors").catchAndLog(onDocUnload); + +exports.closeOnUnload = function closeOnUnload(window) { + window.addEventListener("unload", onDocUnload, false); + gDocsToClose.push(window.document); +}; + +Object.defineProperties(exports, { + activeWindow: { + enumerable: true, + get: function() { + return Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow(null); + }, + set: function(window) { + try { window.focus(); } catch (e) { } + } + }, + activeBrowserWindow: { + enumerable: true, + get: function() { + return Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); + } + } +}); + + +/** + * Returns the ID of the window's current inner window. + */ +exports.getInnerId = function(window) { + console.warn('require("window-utils").getInnerId is deprecated, ' + + 'please use require("window/utils").getInnerId instead'); + return getInnerId(window); +}; + +exports.getOuterId = function(window) { + console.warn('require("window-utils").getOuterId is deprecated, ' + + 'please use require("window/utils").getOuterId instead'); + return getOuterId(window); +}; + +function isBrowser(window) { + return window.document.documentElement.getAttribute("windowtype") === + "navigator:browser"; +}; +exports.isBrowser = isBrowser; + +exports.hiddenWindow = appShellService.hiddenDOMWindow; + +function createHiddenXULFrame() { + return function promise(deliver) { + let window = appShellService.hiddenDOMWindow; + + // Ensuring waiting for hidden window end of loading + // (The hidden window is still loading on windows/thunderbird) + if (window.document.readyState != "complete") { + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, false); + // We recurse with same arguments, when the window is ready + promise(deliver); + }, false); + return; + } + + let document = window.document; + let isXMLDoc = (document.contentType == "application/xhtml+xml" || + document.contentType == "application/vnd.mozilla.xul+xml") + + if (isXMLDoc) { + deliver(window) + } + else { + let frame = document.createElement('iframe'); + // This is ugly but we need window for XUL document in order to create + // browser elements. + + // See bug 725323: hiddenWindow URL is different on each mozilla product + let prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + let hiddenWindowURL = prefs.getCharPref("browser.hiddenWindowChromeURL", ""); + + frame.src = hiddenWindowURL; + frame.setAttribute('src', hiddenWindowURL); + frame.addEventListener('DOMContentLoaded', function onLoad(event) { + frame.removeEventListener('DOMContentLoaded', onLoad, false); + deliver(frame.contentWindow); + }, false); + document.documentElement.appendChild(frame); + } + } +}; +exports.createHiddenXULFrame = createHiddenXULFrame; + +function createRemoteBrowser(remote) { + return function promise(deliver) { + createHiddenXULFrame()(function(hiddenWindow) { + let document = hiddenWindow.document; + let browser = document.createElementNS(XUL, "browser"); + // Remote="true" enable everything here: + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1347 + if (remote !== false) + browser.setAttribute("remote","true"); + // Type="content" is mandatory to enable stuff here: + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1776 + browser.setAttribute("type","content"); + // We remove XBL binding to avoid execution of code that is not going to work + // because browser has no docShell attribute in remote mode (for example) + browser.setAttribute("style","-moz-binding: none;"); + // Flex it in order to be visible (optional, for debug purpose) + browser.setAttribute("flex", "1"); + document.documentElement.appendChild(browser); + + // Bug 724433: do not leak this <browser> DOM node + when(function () { + document.documentElement.removeChild(browser); + }); + + // Return browser + deliver(browser); + }); + }; +}; +exports.createRemoteBrowser = createRemoteBrowser; + +require("./unload").when( + function() { + gDocsToClose.slice().forEach( + function(doc) { doc.defaultView.close(); }); + }); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/window/utils.js b/tools/addon-sdk-1.7/packages/api-utils/lib/window/utils.js new file mode 100644 index 0000000..b4a8493 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/window/utils.js @@ -0,0 +1,110 @@ +/* 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 windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1']. + getService(Ci.nsIWindowWatcher); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. + getService(Ci.nsIAppShellService); +const observers = require('api-utils/observer-service'); + +/** + * Returns the ID of the window's current inner window. + */ +function getInnerId(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; +}; +exports.getInnerId = getInnerId; + +/** + * Returns the ID of the window's outer window. + */ +function getOuterId(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).outerWindowID; +}; +exports.getOuterId = getOuterId; + +/** + * Returns `nsIXULWindow` for the given `nsIDOMWindow`. + */ +function getXULWindow(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShellTreeItem). + treeOwner.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIXULWindow); +}; +exports.getXULWindow = getXULWindow; + +/** + * Returns `nsIBaseWindow` for the given `nsIDOMWindow`. + */ +function getBaseWindow(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShell). + QueryInterface(Ci.nsIDocShellTreeItem). + treeOwner. + QueryInterface(Ci.nsIBaseWindow); +} +exports.getBaseWindow = getBaseWindow; + +/** + * Removes given window from the application's window registry. Unless + * `options.close` is `false` window is automatically closed on application + * quit. + * @params {nsIDOMWindow} window + * @params {Boolean} options.close + */ +function backgroundify(window, options) { + let base = getBaseWindow(window); + base.visibility = false; + base.enabled = false; + appShellService.unregisterTopLevelWindow(getXULWindow(window)); + if (!options || options.close !== false) + observers.add('quit-application-granted', window.close.bind(window)); + + return window; +} +exports.backgroundify = backgroundify; + +/** + * Takes hash of options and serializes it to a features string that + * can be used passed to `window.open`. For more details on features string see: + * https://developer.mozilla.org/en/DOM/window.open#Position_and_size_features + */ +function serializeFeatures(options) { + return Object.keys(options).reduce(function(result, name) { + let value = options[name]; + return result + ',' + name + '=' + + (value === true ? 'yes' : value === false ? 'no' : value); + }, '').substr(1); +} + +/** + * Opens a top level window and returns it's `nsIDOMWindow` representation. + * @params {String} uri + * URI of the document to be loaded into window. + * @params {nsIDOMWindow} options.parent + * Used as parent for the created window. + * @params {String} options.name + * Optional name that is assigned to the window. + * @params {Object} options.features + * Map of key, values like: `{ width: 10, height: 15, chrome: true }`. + */ +function open(uri, options) { + options = options || {}; + return windowWatcher. + openWindow(options.parent || null, + uri, + options.name || null, + serializeFeatures(options.features || {}), + null); +} +exports.open = open; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/windows/dom.js b/tools/addon-sdk-1.7/packages/api-utils/lib/windows/dom.js new file mode 100644 index 0000000..1ff966a --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/windows/dom.js @@ -0,0 +1,27 @@ +/* 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 { Trait } = require('../traits'); + +const WindowDom = Trait.compose({ + _window: Trait.required, + get title() { + let window = this._window; + return window && window.document ? window.document.title : null + }, + close: function close() { + let window = this._window; + if (window) window.close(); + return this._public; + }, + activate: function activate() { + let window = this._window; + if (window) window.focus(); + return this._public; + } +}); +exports.WindowDom = WindowDom; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/windows/loader.js b/tools/addon-sdk-1.7/packages/api-utils/lib/windows/loader.js new file mode 100644 index 0000000..b596e73 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/windows/loader.js @@ -0,0 +1,120 @@ +/* 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'), + { setTimeout } = require("../timer"), + { Trait } = require('../traits'), + + WM = Cc['@mozilla.org/appshell/window-mediator;1']. + getService(Ci.nsIWindowMediator), + + URI_BROWSER = 'chrome://browser/content/browser.xul', + NAME = '_blank', + FEATURES = 'chrome,all,dialog=no', + PARAMS = [ URI_BROWSER, NAME, FEATURES ], + ON_LOAD = 'load', + ON_UNLOAD = 'unload', + STATE_LOADED = 'complete', + BROWSER = 'navigator:browser'; + +/** + * Trait provides private `_window` property and requires `_onLoad` property + * that will be called when `_window` is loaded. If `_window` property value + * is changed with already loaded window `_onLoad` still will be called. + */ +const WindowLoader = Trait.compose({ + /** + * Internal listener that is called when window is loaded. + * Please keep in mind that this trait will not handle exceptions that may + * be thrown by this method so method itself should take care of + * handling them. + * @param {nsIWindow} window + */ + _onLoad: Trait.required, + _tabOptions: Trait.required, + /** + * Internal listener that is called when `_window`'s DOM 'unload' event + * is dispatched. Please note that this trait will not handle exceptions that + * may be thrown by this method so method itself should take care of + * handling them. + */ + _onUnload: Trait.required, + _load: function _load() { + if (this.__window) return; + let params = PARAMS.slice() + params.push(this._tabOptions.map(function(options) options.url).join("|")) + let browser = WM.getMostRecentWindow(BROWSER); + this._window = browser.openDialog.apply(browser, params); + }, + /** + * Private window who's load event is being tracked. Once window is loaded + * `_onLoad` is called. + * @type {nsIWindow} + */ + get _window() this.__window, + set _window(window) { + let _window = this.__window; + if (!window) window = null; + if (window !== _window) { + if (_window) { + _window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); + _window.removeEventListener(ON_LOAD, this.__loadListener, false); + } + if (window) { + window.addEventListener( + ON_UNLOAD, + this.__unloadListener || + (this.__unloadListener = this._unloadListener.bind(this)) + , + false + ); + this.__window = window; + // If window is not loaded yet setting up a listener. + if (STATE_LOADED != window.document.readyState) { + window.addEventListener( + ON_LOAD, + this.__loadListener || + (this.__loadListener = this._loadListener.bind(this)) + , + false + ); + } + else { // If window is loaded calling listener next turn of event loop. + this._onLoad(window) + } + } + } + }, + __window: null, + /** + * Internal method used for listening 'load' event on the `_window`. + * Method takes care of removing itself from 'load' event listeners once + * event is being handled. + */ + _loadListener: function _loadListener(event) { + let window = this._window; + if (!event.target || event.target.defaultView != window) return; + window.removeEventListener(ON_LOAD, this.__loadListener, false); + this._onLoad(window); + }, + __loadListener: null, + /** + * Internal method used for listening 'unload' event on the `_window`. + * Method takes care of removing itself from 'unload' event listeners once + * event is being handled. + */ + _unloadListener: function _unloadListener(event) { + let window = this._window; + if (!event.target + || event.target.defaultView != window + || STATE_LOADED != window.document.readyState + ) return; + window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); + this._onUnload(window); + }, + __unloadListener: null +}); +exports.WindowLoader = WindowLoader; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/windows/observer.js b/tools/addon-sdk-1.7/packages/api-utils/lib/windows/observer.js new file mode 100644 index 0000000..45191b7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/windows/observer.js @@ -0,0 +1,53 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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 { EventEmitterTrait: EventEmitter } = require("../events"); +const { WindowTracker, windowIterator } = require("../window-utils"); +const { DOMEventAssembler } = require("../events/assembler"); +const { Trait } = require("../light-traits"); + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ + /** + * Method is implemented by `EventEmitter` and is used just for emitting + * events on registered listeners. + */ + _emit: Trait.required, + /** + * Events that are supported and emitted by the module. + */ + supportedEventsTypes: [ "activate", "deactivate" ], + /** + * Function handles all the supported events on all the windows that are + * observed. Method is used to proxy events to the listeners registered on + * this event emitter. + * @param {Event} event + * Keyboard event being emitted. + */ + handleEvent: function handleEvent(event) { + this._emit(event.type, event.target, event); + } +}); + +// Using `WindowTracker` to track window events. +WindowTracker({ + onTrack: function onTrack(chromeWindow) { + observer._emit("open", chromeWindow); + observer.observe(chromeWindow); + }, + onUntrack: function onUntrack(chromeWindow) { + observer._emit("close", chromeWindow); + observer.ignore(chromeWindow); + } +}); + +// Making observer aware of already opened windows. +for each (let window in windowIterator()) + observer.observe(window); + +exports.observer = observer; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/windows/tabs.js b/tools/addon-sdk-1.7/packages/api-utils/lib/windows/tabs.js new file mode 100644 index 0000000..231a22e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/windows/tabs.js @@ -0,0 +1,178 @@ +/* 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 { Trait } = require("../traits"); +const { List } = require("../list"); +const { Tab, Options } = require("../tabs/tab"); +const { EventEmitter } = require("../events"); +const { EVENTS } = require("../tabs/events"); +const { getOwnerWindow, getActiveTab, getTabs, + openTab, activateTab } = require("../tabs/utils"); +const { observer: tabsObserver } = require("../tabs/observer"); + +const TAB_BROWSER = "tabbrowser"; + +/** + * This is a trait that is used in composition of window wrapper. Trait tracks + * tab related events of the wrapped window in order to keep track of open + * tabs and maintain their wrappers. Every new tab gets wrapped and jetpack + * type event is emitted. + */ +const WindowTabTracker = Trait.compose({ + /** + * Chrome window whose tabs are tracked. + */ + _window: Trait.required, + /** + * Function used to emit events. + */ + _emit: EventEmitter.required, + _tabOptions: Trait.required, + /** + * Function to add event listeners. + */ + on: EventEmitter.required, + removeListener: EventEmitter.required, + /** + * Initializes tab tracker for a browser window. + */ + _initWindowTabTracker: function _initWindowTabTracker() { + // Ugly hack that we have to remove at some point (see Bug 658059). At this + // point it is necessary to invoke lazy `tabs` getter on the windows object + // which creates a `TabList` instance. + this.tabs; + // Binding all methods used as event listeners to the instance. + this._onTabReady = this._emitEvent.bind(this, "ready"); + this._onTabOpen = this._onTabEvent.bind(this, "open"); + this._onTabClose = this._onTabEvent.bind(this, "close"); + this._onTabActivate = this._onTabEvent.bind(this, "activate"); + this._onTabDeactivate = this._onTabEvent.bind(this, "deactivate"); + this._onTabPinned = this._onTabEvent.bind(this, "pinned"); + this._onTabUnpinned = this._onTabEvent.bind(this, "unpinned"); + + for each (let tab in getTabs(this._window)) { + // We emulate "open" events for all open tabs since gecko does not emits + // them on the tabs that new windows are open with. Also this is + // necessary to synchronize tabs lists with an actual state. + this._onTabOpen(tab); + } + // We also emulate "activate" event so that it's picked up by a tab list. + this._onTabActivate(getActiveTab(this._window)); + + // Setting up event listeners + tabsObserver.on("open", this._onTabOpen); + tabsObserver.on("close", this._onTabClose); + tabsObserver.on("activate", this._onTabActivate); + tabsObserver.on("deactivate", this._onTabDeactivate); + tabsObserver.on("pinned", this._onTabPinned); + tabsObserver.on("unpinned", this._onTabUnpinned); + }, + _destroyWindowTabTracker: function _destroyWindowTabTracker() { + // We emulate close events on all tabs, since gecko does not emits such + // events by itself. + for each (let tab in this.tabs) + this._emitEvent("close", tab); + + this._tabs._clear(); + + tabsObserver.removeListener("open", this._onTabOpen); + tabsObserver.removeListener("close", this._onTabClose); + tabsObserver.removeListener("activate", this._onTabActivate); + tabsObserver.removeListener("deactivate", this._onTabDeactivate); + }, + _onTabEvent: function _onTabEvent(type, tab) { + if (this._window === getOwnerWindow(tab)) { + let options = this._tabOptions.shift() || {}; + options.tab = tab; + options.window = this._public; + // creating tab wrapper and adding listener to "ready" events. + let wrappedTab = Tab(options); + + // Setting up an event listener for ready events. + if (type === "open") + wrappedTab.on("ready", this._onTabReady); + + this._emitEvent(type, wrappedTab); + } + }, + _emitEvent: function _emitEvent(type, tab) { + // Notifies combined tab list that tab was added / removed. + tabs._emit(type, tab); + // Notifies contained tab list that window was added / removed. + this._tabs._emit(type, tab); + } +}); +exports.WindowTabTracker = WindowTabTracker; + +/** + * This trait is used to create live representation of open tab lists. Each + * window wrapper's tab list is represented by an object created from this + * trait. It is also used to represent list of all the open windows. Trait is + * composed out of `EventEmitter` in order to emit 'TabOpen', 'TabClose' events. + * **Please note** that objects created by this trait can't be exposed outside + * instead you should expose it's `_public` property, see comments in + * constructor for details. + */ +const TabList = List.resolve({ constructor: "_init" }).compose( + // This is ugly, but necessary. Will be removed by #596248 + EventEmitter.resolve({ toString: null }), + Trait.compose({ + on: Trait.required, + _emit: Trait.required, + constructor: function TabList(options) { + this._window = options.window; + // Add new items to the list + this.on(EVENTS.open.name, this._add.bind(this)); + // Remove closed items from the list + this.on(EVENTS.close.name, this._remove.bind(this)); + + // Set value whenever new tab becomes active. + this.on("activate", function onTabActivate(tab) { + this._activeTab = tab; + }.bind(this)); + // Initialize list. + this._init(); + // This list is not going to emit any events, object holding this list + // will do it instead, to make that possible we return a private API. + return this; + }, + get activeTab() this._activeTab, + _activeTab: null, + + open: function open(options) { + options = Options(options); + this._window._tabOptions.push(options); + let tab = openTab(this._window._window, options.url); + if (!options.inBackground) + activateTab(tab); + } + // This is ugly, but necessary. Will be removed by #596248 + }).resolve({ toString: null }) +); + +/** + * Combined list of all open tabs on all the windows. + * type {TabList} + */ +var tabs = TabList({ window: null }); +exports.tabs = tabs._public; + +/** + * Trait is a part of composition that represents window wrapper. This trait is + * composed out of `WindowTabTracker` that allows it to keep track of open tabs + * on the window being wrapped. + */ +const WindowTabs = Trait.compose( + WindowTabTracker, + Trait.compose({ + _window: Trait.required, + /** + * List of tabs + */ + get tabs() (this._tabs || (this._tabs = TabList({ window: this })))._public, + _tabs: null, + }) +); +exports.WindowTabs = WindowTabs; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/xhr.js b/tools/addon-sdk-1.7/packages/api-utils/lib/xhr.js new file mode 100644 index 0000000..b7808dc --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/xhr.js @@ -0,0 +1,150 @@ +/* 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 memory = require('api-utils/memory'); + +// ## Implementation Notes ## +// +// Making `XMLHttpRequest` objects available to Jetpack code involves a +// few key principles universal to all low-level module implementations: +// +// * **Unloadability**. A Jetpack-based extension using this module can be +// asked to unload itself at any time, e.g. because the user decides to +// uninstall or disable the extension. This means we need to keep track of +// all in-progress reqests and abort them on unload. +// +// * **Developer-Ergonomic Tracebacks**. Whenever an exception is raised +// by a Jetpack-based extension, we want it to be logged in a +// place that is specific to that extension--so that a developer +// can distinguish it from an error on a web page or in another +// extension, for instance. We also want it to be logged with a +// full stack traceback, which the Mozilla platform doesn't usually +// do. +// +// Because of this, we don't actually want to give the Mozilla +// platform's "real" XHR implementation to clients, but instead provide +// a simple wrapper that trivially delegates to the implementation in +// all cases except where callbacks are involved: whenever Mozilla +// platform code calls into the extension, such as during the XHR's +// `onreadystatechange` callback, we want to wrap the client's callback +// in a try-catch clause that traps any exceptions raised by the +// callback and logs them via console.exception() instead of allowing +// them to propagate back into Mozilla platform code. + +// This is a private list of all active requests, so we know what to +// abort if we're asked to unload. +var requests = []; + +// Events on XHRs that we should listen for, so we know when to remove +// a request from our private list. +const TERMINATE_EVENTS = ["load", "error", "abort"]; + +// Read-only properties of XMLHttpRequest objects that we want to +// directly delegate to. +const READ_ONLY_PROPS = ["readyState", "responseText", "responseXML", + "status", "statusText"]; + +// Methods of XMLHttpRequest that we want to directly delegate to. +const DELEGATED_METHODS = ["abort", "getAllResponseHeaders", + "getResponseHeader", "overrideMimeType", + "send", "sendAsBinary", "setRequestHeader", + "open"]; + +var getRequestCount = exports.getRequestCount = function getRequestCount() { + return requests.length; +}; + +var XMLHttpRequest = exports.XMLHttpRequest = function XMLHttpRequest() { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + // For the sake of simplicity, don't tie this request to any UI. + req.mozBackgroundRequest = true; + + memory.track(req, "XMLHttpRequest"); + + this._req = req; + this._orsc = null; + + requests.push(this); + + var self = this; + + this._boundCleanup = function _boundCleanup() { + self._cleanup(); + }; + + TERMINATE_EVENTS.forEach( + function(name) { + self._req.addEventListener(name, self._boundCleanup, false); + }); +}; + +XMLHttpRequest.prototype = { + _cleanup: function _cleanup() { + this.onreadystatechange = null; + var index = requests.indexOf(this); + if (index != -1) { + var self = this; + TERMINATE_EVENTS.forEach( + function(name) { + self._req.removeEventListener(name, self._boundCleanup, false); + }); + requests.splice(index, 1); + } + }, + _unload: function _unload() { + this._req.abort(); + this._cleanup(); + }, + addEventListener: function addEventListener() { + throw new Error("not implemented"); + }, + removeEventListener: function removeEventListener() { + throw new Error("not implemented"); + }, + set upload(newValue) { + throw new Error("not implemented"); + }, + get onreadystatechange() { + return this._orsc; + }, + set onreadystatechange(cb) { + this._orsc = cb; + if (cb) { + var self = this; + this._req.onreadystatechange = function() { + try { + self._orsc.apply(self, arguments); + } catch (e) { + console.exception(e); + } + }; + } else + this._req.onreadystatechange = null; + } +}; + +READ_ONLY_PROPS.forEach( + function(name) { + XMLHttpRequest.prototype.__defineGetter__( + name, + function() { + return this._req[name]; + }); + }); + +DELEGATED_METHODS.forEach( + function(name) { + XMLHttpRequest.prototype[name] = function() { + return this._req[name].apply(this._req, arguments); + }; + }); + +require("./unload").when( + function() { + requests.slice().forEach(function(request) { request._unload(); }); + }); diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/xpcom.js b/tools/addon-sdk-1.7/packages/api-utils/lib/xpcom.js new file mode 100644 index 0000000..efdf873 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/xpcom.js @@ -0,0 +1,207 @@ +/* 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, Cr, Cm, components: { classesByID } } = require('chrome'); +const { registerFactory, unregisterFactory, isCIDRegistered } = + Cm.QueryInterface(Ci.nsIComponentRegistrar); + +const { when: unload } = require('./unload'); +const { Base } = require('./base'); +const { uuid } = require('./uuid'); + +// This is a base prototype, that provides bare bones of XPCOM. JS based +// components can be easily implement by extending it. +const Unknown = Base.extend({ + // Method `extend` is overridden so that resulting object will contain + // `interfaces` array property, containing elements from ancestor and all + // provided sources. + extend: function extend() { + let args = Array.slice(arguments); + return Base.extend.apply(this, args.concat([{ + interfaces: args.reduce(function(interfaces, source) { + // If given source has `interfaces` property concatenate it's elements + // them resulting `interfaces` array, otherwise return resulting + // `interfaces` array. + return 'interfaces' in source ? source.interfaces.concat(interfaces) + : interfaces; + }, this.interfaces) + }])); + }, + /** + * The `QueryInterface` method provides runtime type discovery used by XPCOM. + * This method return quired instance of `this` if given `iid` is listed in + * the `interfaces` property. + */ + QueryInterface: function QueryInterface(iid) { + // For some reason there are cases when `iid` is `null`. In such cases we + // just return `this`. Otherwise we verify that component implements given + // `iid` interface. + if (iid && !this.interfaces.some(function(id) iid.equals(Ci[id]))) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, + /** + * Array of `XPCOM` interfaces (as strings) implemented by this component. All + * components implement `nsISupports` by default which is default value here. + * Provide array of interfaces implemented by an object when extending, to + * append them to this list (Please note that there is no need to repeat + * interfaces implemented by super as they will be added automatically). + */ + interfaces: [ 'nsISupports' ] +}); +exports.Unknown = Unknown; + +// Base exemplar for creating instances implementing `nsIFactory` interface, +// that maybe registered into runtime via `register` function. Instances of +// this factory create instances of enclosed component on `createInstance`. +const Factory = Unknown.extend({ + interfaces: [ 'nsIFactory' ], + /** + * All the descendants will get auto generated `id` (also known as `classID` + * in XPCOM world) unless one is manually provided. + */ + get id() { throw Error('Factory must implement `id` property') }, + /** + * XPCOM `contractID` may optionally be provided to associate this factory + * with it. `contract` is a unique string that has a following format: + * '@vendor.com/unique/id;1'. + */ + contract: null, + /** + * Class description that is being registered. This value is intended as a + * human-readable description for the given class and does not needs to be + * globally unique. + */ + description: 'Jetpack generated factory', + /** + * This method is required by `nsIFactory` interfaces, but as in most + * implementations it does nothing interesting. + */ + lockFactory: function lockFactory(lock) undefined, + /** + * If property is `true` XPCOM service / factory will be registered + * automatically on creation. + */ + register: true, + /** + * If property is `true` XPCOM factory will be unregistered prior to add-on + * unload. + */ + unregister: true, + /** + * Method is called on `Service.new(options)` passing given `options` to + * it. Options is expected to have `component` property holding XPCOM + * component implementation typically decedent of `Unknown` or any custom + * implementation with a `new` method and optional `register`, `unregister` + * flags. Unless `register` is `false` Service / Factory will be + * automatically registered. Unless `unregister` is `false` component will + * be automatically unregistered on add-on unload. + */ + initialize: function initialize(options) { + options = options || {} + this.merge(options, { id: 'id' in options && options.id || uuid() }); + + // If service / factory has auto registration enabled then register. + if (this.register) + register(this); + }, + /** + * Creates an instance of the class associated with this factory. + */ + createInstance: function createInstance(outer, iid) { + try { + if (outer) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.create().QueryInterface(iid); + } + catch (error) { + throw error instanceof Ci.nsIException ? error : Cr.NS_ERROR_FAILURE; + } + }, + create: function create() this.component.new() +}); +exports.Factory = Factory; + +// Exemplar for creating services that implement `nsIFactory` interface, that +// can be registered into runtime via call to `register`. This services return +// enclosed `component` on `getService`. +const Service = Factory.extend({ + description: 'Jetpack generated service', + /** + * Creates an instance of the class associated with this factory. + */ + create: function create() this.component +}); +exports.Service = Service; + +function isRegistered({ id }) isCIDRegistered(id) +exports.isRegistered = isRegistered; + +/** + * Registers given `component` object to be used to instantiate a particular + * class identified by `component.id`, and creates an association of class + * name and `component.contract` with the class. + */ +function register(factory) { + registerFactory(factory.id, factory.description, factory.contract, factory); + + if (factory.unregister) + unload(unregister.bind(null, factory)); +} +exports.register = register; + +/** + * Unregister a factory associated with a particular class identified by + * `factory.classID`. + */ +function unregister(factory) { + if (isRegistered(factory)) + unregisterFactory(factory.id, factory); +} +exports.unregister = unregister; + +function autoRegister(path) { + // TODO: This assumes that the url points to a directory + // that contains subdirectories corresponding to OS/ABI and then + // further subdirectories corresponding to Gecko platform version. + // we should probably either behave intelligently here or allow + // the caller to pass-in more options if e.g. there aren't + // Gecko-specific binaries for a component (which will be the case + // if only frozen interfaces are used). + + var runtime = require("./runtime"); + var osDirName = runtime.OS + "_" + runtime.XPCOMABI; + var platformVersion = require("./xul-app").platformVersion.substring(0, 5); + + var file = Cc['@mozilla.org/file/local;1'] + .createInstance(Ci.nsILocalFile); + file.initWithPath(path); + file.append(osDirName); + file.append(platformVersion); + + if (!(file.exists() && file.isDirectory())) + throw new Error("component not available for OS/ABI " + + osDirName + " and platform " + platformVersion); + + Cm.QueryInterface(Ci.nsIComponentRegistrar); + Cm.autoRegister(file); +} +exports.autoRegister = autoRegister; + +/** + * Returns registered factory that has a given `id` or `null` if not found. + */ +function factoryByID(id) classesByID[id] || null +exports.factoryByID = factoryByID; + +/** + * Returns factory registered with a given `contract` or `null` if not found. + * In contrast to `Cc[contract]` that does ignores new factory registration + * with a given `contract` this will return a factory currently associated + * with a `contract`. + */ +function factoryByContract(contract) factoryByID(Cm.contractIDToCID(contract)) +exports.factoryByContract = factoryByContract; diff --git a/tools/addon-sdk-1.7/packages/api-utils/lib/xul-app.js b/tools/addon-sdk-1.7/packages/api-utils/lib/xul-app.js new file mode 100644 index 0000000..9272d88 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/lib/xul-app.js @@ -0,0 +1,63 @@ +/* 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"); + +var appInfo = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULAppInfo); + +var ID = exports.ID = appInfo.ID; +var name = exports.name = appInfo.name; +var version = exports.version = appInfo.version; +var platformVersion = exports.platformVersion = appInfo.platformVersion; + +// The following mapping of application names to GUIDs was taken from: +// +// https://addons.mozilla.org/en-US/firefox/pages/appversions +// +// Using the GUID instead of the app's name is preferable because sometimes +// re-branded versions of a product have different names: for instance, +// Firefox, Minefield, Iceweasel, and Shiretoko all have the same +// GUID. +// This mapping is duplicated in `app-extensions/bootstrap.js`. They should keep +// in sync, so if you change one, change the other too! + +var ids = exports.ids = { + Firefox: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + Mozilla: "{86c18b42-e466-45a9-ae7a-9b95ba6f5640}", + Sunbird: "{718e30fb-e89b-41dd-9da7-e25a45638b28}", + SeaMonkey: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + Fennec: "{aa3c5121-dab2-40e2-81ca-7ea25febc110}", + Thunderbird: "{3550f703-e582-4d05-9a08-453d09bdfdc6}" +}; + +var is = exports.is = function is(name) { + if (!(name in ids)) + throw new Error("Unkown Mozilla Application: " + name); + return ID == ids[name]; +}; + +var isOneOf = exports.isOneOf = function isOneOf(names) { + for (var i = 0; i < names.length; i++) + if (is(names[i])) + return true; + return false; +}; + +/** + * Use this to check whether the given version (e.g. xulApp.platformVersion) + * is in the given range. Versions must be in version comparator-compatible + * format. See MDC for details: + * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIVersionComparator + */ +var versionInRange = exports.versionInRange = +function versionInRange(version, lowInclusive, highExclusive) { + var vc = Cc["@mozilla.org/xpcom/version-comparator;1"] + .getService(Ci.nsIVersionComparator); + return (vc.compare(version, lowInclusive) >= 0) && + (vc.compare(version, highExclusive) < 0); +} + diff --git a/tools/addon-sdk-1.7/packages/api-utils/package.json b/tools/addon-sdk-1.7/packages/api-utils/package.json new file mode 100644 index 0000000..1e777fe --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/package.json @@ -0,0 +1,14 @@ +{ + "name": "api-utils", + "description": "Foundational infrastructure and utilities.", + "keywords": ["javascript", "engine", "platform", "xulrunner", + "jetpack-low-level"], + "author": "Atul Varma (http://toolness.com/) <atul@mozilla.com>", + "contributors": [ + "Myk Melez (http://melez.com/) <myk@mozilla.org>", + "Daniel Aquino <mr.danielaquino@gmail.com>" + ], + "license": "MPL 2.0", + "dependencies": ["addon-kit"], + "loader": "lib/cuddlefish.js" +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/commonjs-test-adapter/asserts.js b/tools/addon-sdk-1.7/packages/api-utils/tests/commonjs-test-adapter/asserts.js new file mode 100644 index 0000000..8618ace --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/commonjs-test-adapter/asserts.js @@ -0,0 +1,54 @@ +/* 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 AssertBase = require("test/assert").Assert; + +/** + * Generates custom assertion constructors that may be bundled with a test + * suite. + * @params {String} + * names of assertion function to be added to the generated Assert. + */ +function Assert() { + let assertDescriptor = {}; + Array.forEach(arguments, function(name) { + assertDescriptor[name] = { value: function(message) { + this.pass(message); + }} + }); + + return function Assert() { + return Object.create(AssertBase.apply(null, arguments), assertDescriptor); + }; +} + +exports["test suite"] = { + Assert: Assert("foo"), + "test that custom assertor is passed to test function": function(assert) { + assert.ok("foo" in assert, "custom assertion function `foo` is defined"); + assert.foo("custom assertion function `foo` is called"); + }, + "test sub suite": { + "test that `Assert` is inherited by sub suits": function(assert) { + assert.ok("foo" in assert, "assertion function `foo` is not defined"); + }, + "test sub sub suite": { + Assert: Assert("bar"), + "test that custom assertor is passed to test function": function(assert) { + assert.ok("bar" in assert, + "custom assertion function `bar` is defined"); + assert.bar("custom assertion function `bar` is called"); + }, + "test that `Assert` is not inherited by sub sub suits": function(assert) { + assert.ok(!("foo" in assert), + "assertion function `foo` is not defined"); + } + } + } +}; + +if (module == require.main) + require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/fixtures/es5.js b/tools/addon-sdk-1.7/packages/api-utils/tests/fixtures/es5.js new file mode 100644 index 0000000..746cae3 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/fixtures/es5.js @@ -0,0 +1,8 @@ +/* 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"; +exports.frozen = Object.freeze({}); +exports.sealed = Object.seal({}); +exports.inextensible = Object.preventExtensions({}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/fixtures/sandbox-complex-character.js b/tools/addon-sdk-1.7/packages/api-utils/tests/fixtures/sandbox-complex-character.js new file mode 100644 index 0000000..6ae6769 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/fixtures/sandbox-complex-character.js @@ -0,0 +1,5 @@ +/* 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/. */ + +var chars = 'გამარჯობა'; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/fixtures/sandbox-normal.js b/tools/addon-sdk-1.7/packages/api-utils/tests/fixtures/sandbox-normal.js new file mode 100644 index 0000000..5425298 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/fixtures/sandbox-normal.js @@ -0,0 +1,7 @@ +/* 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/. */ + +var a = 1; +this.b = 2; +function f() { return 4; } diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/helpers.js b/tools/addon-sdk-1.7/packages/api-utils/tests/helpers.js new file mode 100644 index 0000000..c639849 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/helpers.js @@ -0,0 +1,24 @@ +/* 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 { Loader } = require("@loader"); + +exports.Loader = function(module, globals, packaging) { + var options = packaging || JSON.parse(JSON.stringify(require("@packaging"))); + options.globals = globals; + + let loader = Loader.new(options); + return Object.create(loader, { + require: { value: Loader.require.bind(loader, module.path) }, + sandbox: { value: function sandbox(id) { + let path = options.manifest[module.path].requirements[id].path; + return loader.sandboxes[path].sandbox; + }}, + unload: { value: function unload(reason, callback) { + loader.unload(reason, callback); + }} + }) +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/loader/fixture.js b/tools/addon-sdk-1.7/packages/api-utils/tests/loader/fixture.js new file mode 100644 index 0000000..c2dc0f1 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/loader/fixture.js @@ -0,0 +1,6 @@ +/* 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/. */ + +exports.foo = foo; +console.log('testing', 1, [2, 3, 4]); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/add.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/add.js new file mode 100644 index 0000000..5472934 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/add.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define('modules/add', function () { + return function (a, b) { + return a + b; + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/async1.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/async1.js new file mode 100644 index 0000000..b7b60fd --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/async1.js @@ -0,0 +1,14 @@ +/* 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/. */ + +define(['./traditional2', './async2'], function () { + var traditional2 = require('./traditional2'); + return { + name: 'async1', + traditional1Name: traditional2.traditional1Name, + traditional2Name: traditional2.name, + async2Name: require('./async2').name, + async2Traditional2Name: require('./async2').traditional2Name + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/async2.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/async2.js new file mode 100644 index 0000000..802fb25 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/async2.js @@ -0,0 +1,8 @@ +/* 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/. */ + +define(['./traditional2', 'exports'], function (traditional2, exports) { + exports.name = 'async2'; + exports.traditional2Name = traditional2.name; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/badExportAndReturn.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/badExportAndReturn.js new file mode 100644 index 0000000..35a5359 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/badExportAndReturn.js @@ -0,0 +1,10 @@ +/* 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/. */ + +// This is a bad module, it asks for exports but also returns a value from +// the define defintion function. +define(['exports'], function (exports) { + return 'badExportAndReturn'; +}); + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/badFirst.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/badFirst.js new file mode 100644 index 0000000..fdb03bd --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/badFirst.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define(['./badSecond'], function (badSecond) { + return { + name: 'badFirst' + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/badSecond.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/badSecond.js new file mode 100644 index 0000000..8321a85 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/badSecond.js @@ -0,0 +1,8 @@ +/* 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/. */ + +var first = require('./badFirst'); + +exports.name = 'badSecond'; +exports.badFirstName = first.name; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/blue.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/blue.js new file mode 100644 index 0000000..14ab149 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/blue.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define(function () { + return { + name: 'blue' + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/castor.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/castor.js new file mode 100644 index 0000000..9209623 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/castor.js @@ -0,0 +1,10 @@ +/* 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/. */ + +define(['exports', './pollux'], function(exports, pollux) { + exports.name = 'castor'; + exports.getPolluxName = function () { + return pollux.name; + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/cheetah.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/cheetah.js new file mode 100644 index 0000000..37d12bf --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/cheetah.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define(function () { + return function () { + return 'cheetah'; + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/color.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/color.js new file mode 100644 index 0000000..0d946bd --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/color.js @@ -0,0 +1,7 @@ +/* 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/. */ + +define({ + type: 'color' +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/dupe.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/dupe.js new file mode 100644 index 0000000..f87fa55 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/dupe.js @@ -0,0 +1,15 @@ +/* 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/. */ + +define({ + name: 'dupe' +}); + +// This is wrong and should not be allowed. Only one call to +// define per file. +define([], function () { + return { + name: 'dupe2' + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/dupeNested.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/dupeNested.js new file mode 100644 index 0000000..703948c --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/dupeNested.js @@ -0,0 +1,15 @@ +/* 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/. */ + + +define(function () { + // This is wrong and should not be allowed. + define('dupeNested2', { + name: 'dupeNested2' + }); + + return { + name: 'dupeNested' + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/dupeSetExports.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/dupeSetExports.js new file mode 100644 index 0000000..12a1be2 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/dupeSetExports.js @@ -0,0 +1,8 @@ +/* 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/. */ + +define({name: "dupeSetExports"}); + +// so this should cause a failure +module.setExports("no no no"); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/exportsEquals.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/exportsEquals.js new file mode 100644 index 0000000..e176316 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/exportsEquals.js @@ -0,0 +1,5 @@ +/* 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/. */ + +module.exports = 4; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/green.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/green.js new file mode 100644 index 0000000..ce2d134 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/green.js @@ -0,0 +1,10 @@ +/* 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/. */ + +define('modules/green', ['./color'], function (color) { + return { + name: 'green', + parentType: color.type + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/lion.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/lion.js new file mode 100644 index 0000000..9c3ac3b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/lion.js @@ -0,0 +1,7 @@ +/* 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/. */ + +define(function(require) { + return 'lion'; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/orange.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/orange.js new file mode 100644 index 0000000..900a32b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/orange.js @@ -0,0 +1,10 @@ +/* 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/. */ + +define(['./color'], function (color) { + return { + name: 'orange', + parentType: color.type + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/pollux.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/pollux.js new file mode 100644 index 0000000..61cf66f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/pollux.js @@ -0,0 +1,10 @@ +/* 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/. */ + +define(['exports', './castor'], function(exports, castor) { + exports.name = 'pollux'; + exports.getCastorName = function () { + return castor.name; + }; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/red.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/red.js new file mode 100644 index 0000000..c47b8e9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/red.js @@ -0,0 +1,16 @@ +/* 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/. */ + +define(function (require) { + // comment fake-outs for require finding. + // require('bad1'); + return { + name: 'red', + parentType: require('./color').type + }; + + /* + require('bad2'); + */ +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/setExports.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/setExports.js new file mode 100644 index 0000000..495c301 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/setExports.js @@ -0,0 +1,5 @@ +/* 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/. */ + +module.setExports(5); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/subtract.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/subtract.js new file mode 100644 index 0000000..06e1053 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/subtract.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define(function () { + return function (a, b) { + return a - b; + } +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/tiger.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/tiger.js new file mode 100644 index 0000000..e332deb --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/tiger.js @@ -0,0 +1,8 @@ +/* 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/. */ + +define(function (require, exports) { + exports.name = 'tiger'; + exports.type = require('modules/types/cat').type; +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/traditional1.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/traditional1.js new file mode 100644 index 0000000..28af8ef --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/traditional1.js @@ -0,0 +1,12 @@ +/* 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/. */ + +exports.name = 'traditional1' + +var async1 = require('./async1'); + +exports.traditional2Name = async1.traditional2Name; +exports.traditional1Name = async1.traditional1Name; +exports.async2Name = async1.async2Name; +exports.async2Traditional2Name = async1.async2Traditional2Name; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/traditional2.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/traditional2.js new file mode 100644 index 0000000..67b64ee --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/traditional2.js @@ -0,0 +1,6 @@ +/* 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/. */ + +exports.name = 'traditional2'; +exports.traditional1Name = require('./traditional1').name; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/modules/types/cat.js b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/types/cat.js new file mode 100644 index 0000000..41513d6 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/modules/types/cat.js @@ -0,0 +1,5 @@ +/* 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/. */ + +exports.type = 'cat'; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-api-utils.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-api-utils.js new file mode 100644 index 0000000..15f1f3e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-api-utils.js @@ -0,0 +1,241 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +const apiUtils = require("api-utils/api-utils"); + +exports.testPublicConstructor = function (test) { + function PrivateCtor() {} + PrivateCtor.prototype = {}; + + let PublicCtor = apiUtils.publicConstructor(PrivateCtor); + test.assert( + PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype), + "PrivateCtor.prototype should be prototype of PublicCtor.prototype" + ); + + function testObj(useNew) { + let obj = useNew ? new PublicCtor() : PublicCtor(); + test.assert(obj instanceof PublicCtor, + "Object should be instance of PublicCtor"); + test.assert(obj instanceof PrivateCtor, + "Object should be instance of PrivateCtor"); + test.assert(PublicCtor.prototype.isPrototypeOf(obj), + "PublicCtor's prototype should be prototype of object"); + test.assertEqual(obj.constructor, PublicCtor, + "Object constructor should be PublicCtor"); + } + testObj(true); + testObj(false); +}; + +exports.testValidateOptionsEmpty = function (test) { + let val = apiUtils.validateOptions(null, {}); + assertObjsEqual(test, val, {}); + + val = apiUtils.validateOptions(null, { foo: {} }); + assertObjsEqual(test, val, {}); + + val = apiUtils.validateOptions({}, {}); + assertObjsEqual(test, val, {}); + + val = apiUtils.validateOptions({}, { foo: {} }); + assertObjsEqual(test, val, {}); +}; + +exports.testValidateOptionsNonempty = function (test) { + let val = apiUtils.validateOptions({ foo: 123 }, {}); + assertObjsEqual(test, val, {}); + + val = apiUtils.validateOptions({ foo: 123, bar: 456 }, + { foo: {}, bar: {}, baz: {} }); + assertObjsEqual(test, val, { foo: 123, bar: 456 }); +}; + +exports.testValidateOptionsMap = function (test) { + let val = apiUtils.validateOptions({ foo: 3, bar: 2 }, { + foo: { map: function (v) v * v }, + bar: { map: function (v) undefined } + }); + assertObjsEqual(test, val, { foo: 9, bar: undefined }); +}; + +exports.testValidateOptionsMapException = function (test) { + let val = apiUtils.validateOptions({ foo: 3 }, { + foo: { map: function () { throw new Error(); }} + }); + assertObjsEqual(test, val, { foo: 3 }); +}; + +exports.testValidateOptionsOk = function (test) { + let val = apiUtils.validateOptions({ foo: 3, bar: 2, baz: 1 }, { + foo: { ok: function (v) v }, + bar: { ok: function (v) v } + }); + assertObjsEqual(test, val, { foo: 3, bar: 2 }); + + test.assertRaises( + function () apiUtils.validateOptions({ foo: 2, bar: 2 }, { + bar: { ok: function (v) v > 2 } + }), + 'The option "bar" is invalid.', + "ok should raise exception on invalid option" + ); + + test.assertRaises( + function () apiUtils.validateOptions(null, { foo: { ok: function (v) v }}), + 'The option "foo" is invalid.', + "ok should raise exception on invalid option" + ); +}; + +exports.testValidateOptionsIs = function (test) { + let opts = { + array: [], + boolean: true, + func: function () {}, + nul: null, + number: 1337, + object: {}, + string: "foo", + undef1: undefined + }; + let requirements = { + array: { is: ["array"] }, + boolean: { is: ["boolean"] }, + func: { is: ["function"] }, + nul: { is: ["null"] }, + number: { is: ["number"] }, + object: { is: ["object"] }, + string: { is: ["string"] }, + undef1: { is: ["undefined"] }, + undef2: { is: ["undefined"] } + }; + let val = apiUtils.validateOptions(opts, requirements); + assertObjsEqual(test, val, opts); + + test.assertRaises( + function () apiUtils.validateOptions(null, { + foo: { is: ["object", "number"] } + }), + 'The option "foo" must be one of the following types: object, number', + "Invalid type should raise exception" + ); +}; + +exports.testValidateOptionsMapIsOk = function (test) { + let [map, is, ok] = [false, false, false]; + let val = apiUtils.validateOptions({ foo: 1337 }, { + foo: { + map: function (v) v.toString(), + is: ["string"], + ok: function (v) v.length > 0 + } + }); + assertObjsEqual(test, val, { foo: "1337" }); + + let requirements = { + foo: { + is: ["object"], + ok: function () test.fail("is should have caused us to throw by now") + } + }; + test.assertRaises( + function () apiUtils.validateOptions(null, requirements), + 'The option "foo" must be one of the following types: object', + "is should be used before ok is called" + ); +}; + +exports.testValidateOptionsErrorMsg = function (test) { + test.assertRaises( + function () apiUtils.validateOptions(null, { + foo: { ok: function (v) v, msg: "foo!" } + }), + "foo!", + "ok should raise exception with customized message" + ); +}; + +exports.testValidateMapWithMissingKey = function (test) { + let val = apiUtils.validateOptions({ }, { + foo: { + map: function (v) v || "bar" + } + }); + assertObjsEqual(test, val, { foo: "bar" }); + + val = apiUtils.validateOptions({ }, { + foo: { + map: function (v) { throw "bar" } + } + }); + assertObjsEqual(test, val, { }); +}; + +exports.testAddIterator = function testAddIterator(test) { + let obj = {}; + let keys = ["foo", "bar", "baz"]; + let vals = [1, 2, 3]; + let keysVals = [["foo", 1], ["bar", 2], ["baz", 3]]; + apiUtils.addIterator( + obj, + function keysValsGen() { + for each (let keyVal in keysVals) + yield keyVal; + } + ); + + let keysItr = []; + for (let key in obj) + keysItr.push(key); + test.assertEqual(keysItr.length, keys.length, + "the keys iterator returns the correct number of items"); + for (let i = 0; i < keys.length; i++) + test.assertEqual(keysItr[i], keys[i], "the key is correct"); + + let valsItr = []; + for each (let val in obj) + valsItr.push(val); + test.assertEqual(valsItr.length, vals.length, + "the vals iterator returns the correct number of items"); + for (let i = 0; i < vals.length; i++) + test.assertEqual(valsItr[i], vals[i], "the val is correct"); + + let keysValsItr = []; + for (let keyVal in Iterator(obj)) + keysValsItr.push(keyVal); + test.assertEqual(keysValsItr.length, keysVals.length, "the keys/vals " + + "iterator returns the correct number of items"); + for (let i = 0; i < keysVals.length; i++) { + test.assertEqual(keysValsItr[i][0], keysVals[i][0], "the key is correct"); + test.assertEqual(keysValsItr[i][1], keysVals[i][1], "the val is correct"); + } + + let keysOnlyItr = []; + for (let key in Iterator(obj, true /* keysonly */)) + keysOnlyItr.push(key); + test.assertEqual(keysOnlyItr.length, keysVals.length, "the keys only " + + "iterator returns the correct number of items"); + for (let i = 0; i < keysVals.length; i++) + test.assertEqual(keysOnlyItr[i], keysVals[i][0], "the key is correct"); +}; + +function assertObjsEqual(test, obj1, obj2) { + var items = 0; + for (let [key, val] in Iterator(obj1)) { + items++; + test.assert(key in obj2, "obj1 key should be present in obj2"); + test.assertEqual(obj2[key], val, "obj1 value should match obj2 value"); + } + for (let [key, val] in Iterator(obj2)) { + items++; + test.assert(key in obj1, "obj2 key should be present in obj1"); + test.assertEqual(obj1[key], val, "obj2 value should match obj1 value"); + } + if (!items) + test.assertEqual(JSON.stringify(obj1), JSON.stringify(obj2), + "obj1 should have same JSON representation as obj2"); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-app-strings.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-app-strings.js new file mode 100644 index 0000000..902c31f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-app-strings.js @@ -0,0 +1,62 @@ +/* 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/. */ + +const { Cc,Ci } = require("chrome"); + +let StringBundle = require("app-strings").StringBundle; +exports.testStringBundle = function(test) { + let url = "chrome://global/locale/security/caps.properties"; + + let strings = StringBundle(url); + + test.assertEqual(strings.url, url, + "'url' property contains correct URL of string bundle"); + + let appLocale = Cc["@mozilla.org/intl/nslocaleservice;1"]. + getService(Ci.nsILocaleService). + getApplicationLocale(); + + let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(url, appLocale); + + let (name = "Yes") { + test.assertEqual(strings.get(name), stringBundle.GetStringFromName(name), + "getting a string returns the string"); + } + + let (name = "ExtensionCapability", args = ["foo"]) { + test.assertEqual(strings.get(name, args), + stringBundle.formatStringFromName(name, args, args.length), + "getting a formatted string returns the formatted string"); + } + + test.assertRaises(function () strings.get("nonexistentString"), + "String 'nonexistentString' could not be retrieved from " + + "the bundle due to an unknown error (it doesn't exist?).", + "retrieving a nonexistent string throws an exception"); + + let a = [], b = []; + let enumerator = stringBundle.getSimpleEnumeration(); + while (enumerator.hasMoreElements()) { + let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + a.push([elem.key, elem.value]); + } + for (let keyVal in Iterator(strings)) + b.push(keyVal); + + // Sort the arrays, because we don't assume enumeration has a set order. + // Sort compares [key, val] as string "key,val", which sorts the way we want + // it to, so there is no need to provide a custom compare function. + a.sort(); + b.sort(); + + test.assertEqual(a.length, b.length, + "the iterator returns the correct number of items"); + for (let i = 0; i < a.length; i++) { + test.assertEqual(a[i][0], b[i][0], "the iterated string's name is correct"); + test.assertEqual(a[i][1], b[i][1], + "the iterated string's value is correct"); + } +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-array.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-array.js new file mode 100644 index 0000000..878a6ce --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-array.js @@ -0,0 +1,40 @@ +/* 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/. */ + +var array = require("array"); + +exports.testHas = function(test) { + var testAry = [1, 2, 3]; + test.assertEqual(array.has([1, 2, 3], 1), true); + test.assertEqual(testAry.length, 3); + test.assertEqual(testAry[0], 1); + test.assertEqual(testAry[1], 2); + test.assertEqual(testAry[2], 3); + test.assertEqual(array.has(testAry, 2), true); + test.assertEqual(array.has(testAry, 3), true); + test.assertEqual(array.has(testAry, 4), false); + test.assertEqual(array.has(testAry, "1"), false); +}; + +exports.testAdd = function(test) { + var testAry = [1]; + test.assertEqual(array.add(testAry, 1), false); + test.assertEqual(testAry.length, 1); + test.assertEqual(testAry[0], 1); + test.assertEqual(array.add(testAry, 2), true); + test.assertEqual(testAry.length, 2); + test.assertEqual(testAry[0], 1); + test.assertEqual(testAry[1], 2); +}; + +exports.testRemove = function(test) { + var testAry = [1, 2]; + test.assertEqual(array.remove(testAry, 3), false); + test.assertEqual(testAry.length, 2); + test.assertEqual(testAry[0], 1); + test.assertEqual(testAry[1], 2); + test.assertEqual(array.remove(testAry, 2), true); + test.assertEqual(testAry.length, 1); + test.assertEqual(testAry[0], 1); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-base.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-base.js new file mode 100644 index 0000000..e97b8b0 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-base.js @@ -0,0 +1,240 @@ +/* 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"; + +var { Base, Class } = require("api-utils/base"); + +exports["test .isPrototypeOf"] = function(assert) { + assert.ok(Base.isPrototypeOf(Base.new()), + "Base is a prototype of Base.new()"); + assert.ok(Base.isPrototypeOf(Base.extend()), + "Base is a prototype of Base.extned()"); + assert.ok(Base.isPrototypeOf(Base.extend().new()), + "Base is a prototoype of Base.extend().new()"); + assert.ok(!Base.extend().isPrototypeOf(Base.extend()), + "Base.extend() in not prototype of Base.extend()"); + assert.ok(!Base.extend().isPrototypeOf(Base.new()), + "Base.extend() is not prototype of Base.new()"); + assert.ok(!Base.new().isPrototypeOf(Base.extend()), + "Base.new() is not prototype of Base.extend()"); + assert.ok(!Base.new().isPrototypeOf(Base.new()), + "Base.new() is not prototype of Base.new()"); +}; + +exports["test inheritance"] = function(assert) { + var Parent = Base.extend({ + name: "parent", + method: function () { + return "hello " + this.name; + } + }); + + assert.equal(Parent.name, "parent", "Parent name is parent"); + assert.equal(Parent.method(), "hello parent", "method works on prototype"); + assert.equal(Parent.new().name, Parent.name, "Parent instance inherits name"); + assert.equal(Parent.new().method(), Parent.method(), + "method behaves same on the prototype"); + assert.equal(Parent.extend({}).name, Parent.name, + "Parent decedent inherits name"); + + var Child = Parent.extend({ name: "child" }); + assert.notEqual(Child.name, Parent.name, "Child overides name"); + assert.equal(Child.new().name, Child.name, "Child intsances inherit name"); + assert.equal(Child.extend().name, Child.name, + "Child decedents inherit name"); + + assert.equal(Child.method, Parent.method, "Child inherits method"); + assert.equal(Child.extend().method, Parent.method, + "Child decedent inherit method"); + assert.equal(Child.new().method, Parent.method, + "Child instances inherit method"); + + assert.equal(Child.method(), "hello child", + "method refers to instance proprety"); + assert.equal(Child.extend({ name: "decedent" }).new().method(), + "hello decedent", "method may be overrided"); +}; + +exports["test prototype immutability"] = function(assert) { + + assert.throws(function() { + var override = function() {}; + Base.extend = override; + if (Base.extend !== override) + throw Error("Property was not set"); + }, "Base prototype is imutable"); + + assert.throws(function() { + Base.foo = "bar"; + if (Base.foo !== "bar") + throw Error("Property was not set"); + }, "Base prototype is non-configurabel"); + + assert.throws(function() { + delete Base.new; + if ('new' in Base) + throw Error('Property was not deleted'); + }, "Can't delete properties on prototype"); + + var Foo = Base.extend({ + name: 'hello', + rename: function rename(name) { + this.name = name; + } + }); + + assert.throws(function() { + var override = function() {}; + Foo.extend = override; + if (Foo.extend !== override) + throw Error("Property was not set"); + }, "Can't change prototype properties"); + + assert.throws(function() { + Foo.foo = "bar"; + if (Foo.foo !== "bar") + throw Error("Property was not set"); + }, "Can't add prototype properties"); + + assert.throws(function() { + delete Foo.name; + if ('new' in Foo) + throw Error('Property was not deleted'); + }, "Can't remove prototype properties"); + + assert.throws(function() { + Foo.rename("new name"); + if (Foo.name !== "new name") + throw Error("Property was not modified"); + }, "Method's can't mutate prototypes"); + + var Bar = Foo.extend({ + rename: function rename() { + return this.name; + } + }); + + assert.equal(Bar.rename(), Foo.name, + "properties may be overided on decedents"); +}; + +exports['test instance mutability'] = function(assert) { + var Foo = Base.extend({ + name: "foo", + init: function init(number) { + this.number = number; + } + }); + var f1 = Foo.new(); + /* V8 does not supports this yet! + assert.throws(function() { + f1.name = "f1"; + }, "can't change prototype properties"); + */ + f1.alias = "f1"; + assert.equal(f1.alias, "f1", "instance is mutable"); + delete f1.alias; + assert.ok(!('alias' in f1), "own properties are deletable"); + f1.init(1); + assert.equal(f1.number, 1, "method can mutate instance's own properties"); +}; + +exports['test super'] = function(assert) { + var Foo = Base.extend({ + initialize: function Foo(options) { + this.name = options.name; + } + }); + + var Bar = Foo.extend({ + initialize: function Bar(options) { + Foo.initialize.call(this, options); + this.type = 'bar'; + } + }); + + var bar = Bar.new({ name: 'test' }); + + assert.ok(Bar.isPrototypeOf(bar), 'Bar is prototype of Bar.new'); + assert.ok(Foo.isPrototypeOf(bar), 'Foo is prototype of Bar.new'); + assert.ok(Base.isPrototypeOf(bar), 'Base is prototype of Bar.new'); + assert.equal(bar.type, 'bar', 'bar initializer was called'); + assert.equal(bar.name, 'test', 'bar initializer called Foo initializer'); +}; + +exports['test class'] = function(assert) { + var Foo = Base.extend({ + type: 'Foo', + initialize: function(options) { + this.name = options.name; + }, + serialize: function serialize() { + return '<' + this.name + ':' + this.type + '>'; + } + }); + var CFoo = Class(Foo); + var f1 = CFoo({ name: 'f1' }); + var f2 = new CFoo({ name: 'f2' }); + var f3 = CFoo.new({ name: 'f3' }); + var f4 = Foo.new({ name: 'f4' }); + + assert.ok(f1 instanceof CFoo, 'correct instanceof'); + assert.equal(f1.name, 'f1', 'property initialized'); + assert.equal(f1.serialize(), '<f1:Foo>', 'method works'); + + assert.ok(f2 instanceof CFoo, 'correct instanceof when created with new') + assert.equal(f2.name, 'f2', 'property initialized'); + assert.equal(f2.serialize(), '<f2:Foo>', 'method works'); + + assert.ok(f3 instanceof CFoo, 'correct instanceof when created with .new') + assert.equal(f3.name, 'f3', 'property initialized'); + assert.equal(f3.serialize(), '<f3:Foo>', 'method works'); + + assert.ok(f4 instanceof CFoo, 'correct instanceof when created from prototype') + assert.equal(f4.name, 'f4', 'property initialized'); + assert.equal(f4.serialize(), '<f4:Foo>', 'method works'); + + var Bar = Foo.extend({ + type: 'Bar', + initialize: function(options) { + this.size = options.size; + Foo.initialize.call(this, options); + } + }); + var CBar = Class(Bar); + + + var b1 = CBar({ name: 'b1', size: 1 }); + var b2 = new CBar({ name: 'b2', size: 2 }); + var b3 = CBar.new({ name: 'b3', size: 3 }); + var b4 = Bar.new({ name: 'b4', size: 4 }); + + assert.ok(b1 instanceof CFoo, 'correct instanceof'); + assert.ok(b1 instanceof CBar, 'correct instanceof'); + assert.equal(b1.name, 'b1', 'property initialized'); + assert.equal(b1.size, 1, 'property initialized'); + assert.equal(b1.serialize(), '<b1:Bar>', 'method works'); + + assert.ok(b2 instanceof CFoo, 'correct instanceof when created with new'); + assert.ok(b2 instanceof CBar, 'correct instanceof when created with new'); + assert.equal(b2.name, 'b2', 'property initialized'); + assert.equal(b2.size, 2, 'property initialized'); + assert.equal(b2.serialize(), '<b2:Bar>', 'method works'); + + assert.ok(b3 instanceof CFoo, 'correct instanceof when created with .new'); + assert.ok(b3 instanceof CBar, 'correct instanceof when created with .new'); + assert.equal(b3.name, 'b3', 'property initialized'); + assert.equal(b3.size, 3, 'property initialized'); + assert.equal(b3.serialize(), '<b3:Bar>', 'method works'); + + assert.ok(b4 instanceof CFoo, 'correct instanceof when created from prototype'); + assert.ok(b4 instanceof CBar, 'correct instanceof when created from prototype'); + assert.equal(b4.name, 'b4', 'property initialized'); + assert.equal(b4.size, 4, 'property initialized'); + assert.equal(b4.serialize(), '<b4:Bar>', 'method works'); +}; + +require("test").run(exports); + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-byte-streams.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-byte-streams.js new file mode 100644 index 0000000..5bd1e42 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-byte-streams.js @@ -0,0 +1,169 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * 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/. */ + +const byteStreams = require("byte-streams"); +const file = require("file"); +const { pathFor } = require("api-utils/system"); +const { Loader } = require("./helpers"); + +const STREAM_CLOSED_ERROR = "The stream is closed and cannot be used."; + +// This should match the constant of the same name in byte-streams.js. +const BUFFER_BYTE_LEN = 0x8000; + +exports.testWriteRead = function (test) { + let fname = dataFileFilename(); + + // Write a small string less than the stream's buffer size... + let str = "exports.testWriteRead data!"; + let stream = open(test, fname, true); + test.assert(!stream.closed, "stream.closed after open should be false"); + stream.write(str); + stream.close(); + test.assert(stream.closed, "Stream should be closed after stream.close"); + test.assertRaises(function () stream.write("This shouldn't be written!"), + STREAM_CLOSED_ERROR, + "stream.write after close should raise error"); + + // ... and read it. + stream = open(test, fname); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + test.assertEqual(stream.read(), "", + "stream.read at EOS should return empty string"); + stream.close(); + test.assert(stream.closed, "Stream should be closed after stream.close"); + test.assertRaises(function () stream.read(), + STREAM_CLOSED_ERROR, + "stream.read after close should raise error"); + + file.remove(fname); +}; + +// Write a big string many times the size of the stream's buffer and read it. +exports.testWriteReadBig = function (test) { + let str = ""; + let bufLen = BUFFER_BYTE_LEN; + let fileSize = bufLen * 10; + for (let i = 0; i < fileSize; i++) + str += i % 10; + let fname = dataFileFilename(); + let stream = open(test, fname, true); + stream.write(str); + stream.close(); + stream = open(test, fname); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + stream.close(); + file.remove(fname); +}; + +// The same, but write and read in chunks. +exports.testWriteReadChunks = function (test) { + let str = ""; + let bufLen = BUFFER_BYTE_LEN; + let fileSize = bufLen * 10; + for (let i = 0; i < fileSize; i++) + str += i % 10; + let fname = dataFileFilename(); + let stream = open(test, fname, true); + let i = 0; + while (i < str.length) { + // Use a chunk length that spans buffers. + let chunk = str.substr(i, bufLen + 1); + stream.write(chunk); + i += bufLen + 1; + } + stream.close(); + stream = open(test, fname); + let readStr = ""; + bufLen = BUFFER_BYTE_LEN; + let readLen = bufLen + 1; + do { + var frag = stream.read(readLen); + readStr += frag; + } while (frag); + stream.close(); + test.assertEqual(readStr, str, + "stream.write and read in chunks should work as expected"); + file.remove(fname); +}; + +exports.testReadLengths = function (test) { + let fname = dataFileFilename(); + let str = "exports.testReadLengths data!"; + let stream = open(test, fname, true); + stream.write(str); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(str.length * 1000), str, + "stream.read with big byte length should return string " + + "written"); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(0), "", + "string.read with zero byte length should return empty " + + "string"); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(-1), "", + "string.read with negative byte length should return " + + "empty string"); + stream.close(); + + file.remove(fname); +}; + +exports.testTruncate = function (test) { + let fname = dataFileFilename(); + let str = "exports.testReadLengths data!"; + let stream = open(test, fname, true); + stream.write(str); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + stream.close(); + + stream = open(test, fname, true); + stream.close(); + + stream = open(test, fname); + test.assertEqual(stream.read(), "", + "stream.read after truncate should be empty"); + stream.close(); + + file.remove(fname); +}; + +exports.testUnload = function (test) { + let loader = Loader(module); + let file = loader.require("file"); + + let filename = dataFileFilename("temp-b"); + let stream = file.open(filename, "wb"); + + loader.unload(); + test.assert(stream.closed, "Stream should be closed after module unload"); +}; + +// Returns the name of a file that should be used to test writing and reading. +function dataFileFilename() { + return file.join(pathFor("ProfD"), "test-byte-streams-data"); +} + +// Opens and returns the given file and ensures it's of the correct class. +function open(test, filename, forWriting) { + let stream = file.open(filename, forWriting ? "wb" : "b"); + let klass = forWriting ? "ByteWriter" : "ByteReader"; + test.assert(stream instanceof byteStreams[klass], + "Opened stream should be a " + klass); + return stream; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-collection.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-collection.js new file mode 100644 index 0000000..f3e5476 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-collection.js @@ -0,0 +1,127 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +const collection = require("collection"); + +exports.testAddRemove = function (test) { + let coll = new collection.Collection(); + compare(test, coll, []); + addRemove(test, coll, [], false); +}; + +exports.testAddRemoveBackingArray = function (test) { + let items = ["foo"]; + let coll = new collection.Collection(items); + compare(test, coll, items); + addRemove(test, coll, items, true); + + items = ["foo", "bar"]; + coll = new collection.Collection(items); + compare(test, coll, items); + addRemove(test, coll, items, true); +}; + +exports.testProperty = function (test) { + let obj = makeObjWithCollProp(); + compare(test, obj.coll, []); + addRemove(test, obj.coll, [], false); + + // Test single-value set. + let items = ["foo"]; + obj.coll = items[0]; + compare(test, obj.coll, items); + addRemove(test, obj.coll, items, false); + + // Test array set. + items = ["foo", "bar"]; + obj.coll = items; + compare(test, obj.coll, items); + addRemove(test, obj.coll, items, false); +}; + +exports.testPropertyBackingArray = function (test) { + let items = ["foo"]; + let obj = makeObjWithCollProp(items); + compare(test, obj.coll, items); + addRemove(test, obj.coll, items, true); + + items = ["foo", "bar"]; + obj = makeObjWithCollProp(items); + compare(test, obj.coll, items); + addRemove(test, obj.coll, items, true); +}; + +// Adds some values to coll and then removes them. initialItems is an array +// containing coll's initial items. isBacking is true if initialItems is coll's +// backing array; the point is that updates to coll should affect initialItems +// if that's the case. +function addRemove(test, coll, initialItems, isBacking) { + let items = isBacking ? initialItems : initialItems.slice(0); + let numInitialItems = items.length; + + // Test add(val). + let numInsertions = 5; + for (let i = 0; i < numInsertions; i++) { + compare(test, coll, items); + coll.add(i); + if (!isBacking) + items.push(i); + } + compare(test, coll, items); + + // Add the items we just added to make sure duplicates aren't added. + for (let i = 0; i < numInsertions; i++) + coll.add(i); + compare(test, coll, items); + + // Test remove(val). Do a kind of shuffled remove. Remove item 1, then + // item 0, 3, 2, 5, 4, ... + for (let i = 0; i < numInsertions; i++) { + let val = i % 2 ? i - 1 : + i === numInsertions - 1 ? i : i + 1; + coll.remove(val); + if (!isBacking) + items.splice(items.indexOf(val), 1); + compare(test, coll, items); + } + test.assertEqual(coll.length, numInitialItems, + "All inserted items should be removed"); + + // Remove the items we just removed. coll should be unchanged. + for (let i = 0; i < numInsertions; i++) + coll.remove(i); + compare(test, coll, items); + + // Test add and remove([val1, val2]). + let newItems = [0, 1]; + coll.add(newItems); + compare(test, coll, isBacking ? items : items.concat(newItems)); + coll.remove(newItems); + compare(test, coll, items); + test.assertEqual(coll.length, numInitialItems, + "All inserted items should be removed"); +} + +// Asserts that the items in coll are the items of array. +function compare(test, coll, array) { + test.assertEqual(coll.length, array.length, + "Collection length should be correct"); + let numItems = 0; + for (let item in coll) { + test.assertEqual(item, array[numItems], "Items should be equal"); + numItems++; + } + test.assertEqual(numItems, array.length, + "Number of items in iteration should be correct"); +} + +// Returns a new object with a collection property named "coll". backingArray, +// if defined, will create the collection with that backing array. +function makeObjWithCollProp(backingArray) { + let obj = {}; + collection.addCollectionProperty(obj, "coll", backingArray); + return obj; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-commonjs-test-adapter.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-commonjs-test-adapter.js new file mode 100644 index 0000000..936bea9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-commonjs-test-adapter.js @@ -0,0 +1,11 @@ +/* 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"; + +exports["test custom `Assert`'s"] = require("./commonjs-test-adapter/asserts"); + +// Disabling this check since it is not yet supported by jetpack. +// if (module == require.main) + require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-loader.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-loader.js new file mode 100644 index 0000000..e19e316 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-loader.js @@ -0,0 +1,226 @@ +/* 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 { Loader } = require('content/loader'); +const self = require("self"); + +exports['test:contentURL'] = function(test) { + let loader = Loader(), + value, emitted = 0, changes = 0; + + test.assertRaises( + function() loader.contentURL = undefined, + 'The `contentURL` option must be a valid URL.', + 'Must throw an exception if `contentURL` is not URL.' + ); + test.assertRaises( + function() loader.contentURL = null, + 'The `contentURL` option must be a valid URL.', + 'Must throw an exception if `contentURL` is not URL.' + ); + test.assertRaises( + function() loader.contentURL = 4, + 'The `contentURL` option must be a valid URL.', + 'Must throw an exception if `contentURL` is not URL.' + ); + test.assertRaises( + function() loader.contentURL = { toString: function() 'Oops' }, + 'The `contentURL` option must be a valid URL.', + 'Must throw an exception if `contentURL` is not URL.' + ); + + function listener(e) { + emitted ++; + test.assert( + 'contentURL' in e, + 'emitted event must contain "content" property' + ); + test.assert( + value, + '' + e.contentURL, + 'content property of an event must match value' + ); + } + loader.on('propertyChange', listener); + + test.assertEqual( + null, + loader.contentURL, + 'default value is `null`' + ); + loader.contentURL = value = 'data:text/html,<html><body>Hi</body><html>'; + test.assertEqual( + value, + '' + loader.contentURL, + 'data uri is ok' + ); + test.assertEqual( + ++changes, + emitted, + 'had to emit `propertyChange`' + ); + loader.contentURL = value; + test.assertEqual( + changes, + emitted, + 'must not emit `propertyChange` if same value is set' + ); + + loader.contentURL = value = 'http://google.com/'; + test.assertEqual( + value, + '' + loader.contentURL, + 'value must be set' + ); + test.assertEqual( + ++ changes, + emitted, + 'had to emit `propertyChange`' + ); + loader.contentURL = value; + test.assertEqual( + changes, + emitted, + 'must not emit `propertyChange` if same value is set' + ); + + loader.removeListener('propertyChange', listener); + loader.contentURL = value = 'about:blank'; + test.assertEqual( + value, + '' + loader.contentURL, + 'contentURL must be an actual value' + ); + test.assertEqual( + changes, + emitted, + 'listener had to be romeved' + ); +}; + +exports['test:contentScriptWhen'] = function(test) { + let loader = Loader(); + test.assertEqual( + 'end', + loader.contentScriptWhen, + '`contentScriptWhen` defaults to "end"' + ); + loader.contentScriptWhen = "end"; + test.assertEqual( + "end", + loader.contentScriptWhen + ); + try { + loader.contentScriptWhen = 'boom'; + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The `contentScriptWhen` option must be either "start", "ready" or "end".', + e.message + ); + } + loader.contentScriptWhen = null; + test.assertEqual( + 'end', + loader.contentScriptWhen, + '`contentScriptWhen` defaults to "end"' + ); + loader.contentScriptWhen = "ready"; + test.assertEqual( + "ready", + loader.contentScriptWhen + ); + loader.contentScriptWhen = "start"; + test.assertEqual( + 'start', + loader.contentScriptWhen + ); +}; + +exports['test:contentScript'] = function(test) { + let loader = Loader(), value; + test.assertEqual( + null, + loader.contentScript, + '`contentScript` defaults to `null`' + ); + loader.contentScript = value = 'let test = {};'; + test.assertEqual( + value, + loader.contentScript + ); + try { + loader.contentScript = { 1: value } + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The `contentScript` option must be a string or an array of strings.', + e.message + ); + } + try { + loader.contentScript = ['oue', 2] + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The `contentScript` option must be a string or an array of strings.', + e.message + ); + } + loader.contentScript = undefined; + test.assertEqual( + null, + loader.contentScript + ); + loader.contentScript = value = ["1;", "2;"]; + test.assertEqual( + value, + loader.contentScript + ); +}; + +exports['test:contentScriptFile'] = function(test) { + let loader = Loader(), value, uri = self.data.url("test-content-loader.js"); + test.assertEqual( + null, + loader.contentScriptFile, + '`contentScriptFile` defaults to `null`' + ); + loader.contentScriptFile = value = uri; + test.assertEqual( + value, + loader.contentScriptFile + ); + try { + loader.contentScriptFile = { 1: uri } + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The `contentScriptFile` option must be a local URL or an array of URLs.', + e.message + ); + } + + try { + loader.contentScriptFile = [ 'oue', uri ] + test.fail('must throw when wrong value is set'); + } catch(e) { + test.assertEqual( + 'The `contentScriptFile` option must be a local URL or an array of URLs.', + e.message + ); + } + + loader.contentScriptFile = undefined; + test.assertEqual( + null, + loader.contentScriptFile + ); + loader.contentScriptFile = value = [uri]; + test.assertEqual( + value, + loader.contentScriptFile + ); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-proxy.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-proxy.js new file mode 100644 index 0000000..f7f1235 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-proxy.js @@ -0,0 +1,807 @@ +/* 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/. */ + +const hiddenFrames = require("hidden-frame"); +const xulApp = require("xul-app"); + +const { Loader } = require('./helpers'); + +/* + * Utility function that allow to easily run a proxy test with a clean + * new HTML document. See first unit test for usage. + */ +function createProxyTest(html, callback) { + return function (test) { + test.waitUntilDone(); + + let url = 'data:text/html,' + encodeURI(html); + + let hiddenFrame = hiddenFrames.add(hiddenFrames.HiddenFrame({ + onReady: function () { + + function onDOMReady() { + hiddenFrame.element.removeEventListener("DOMContentLoaded", onDOMReady, + false); + + let xrayWindow = hiddenFrame.element.contentWindow; + let rawWindow = xrayWindow.wrappedJSObject; + + let done = false; + let helper = { + xrayWindow: xrayWindow, + rawWindow: rawWindow, + createWorker: function (contentScript) { + return createWorker(test, xrayWindow, contentScript, helper.done); + }, + done: function () { + if (done) + return; + done = true; + hiddenFrames.remove(hiddenFrame); + test.done(); + } + } + callback(helper, test); + } + + hiddenFrame.element.addEventListener("DOMContentLoaded", onDOMReady, false); + hiddenFrame.element.setAttribute("src", url); + + } + })); + }; +} + +function createWorker(test, xrayWindow, contentScript, done) { + // We have to use Sandboxed loader in order to get access to the private + // unlock key `PRIVATE_KEY`. This key should not be used anywhere else. + // See `PRIVATE_KEY` definition in worker.js + let loader = Loader(module); + let Worker = loader.require("api-utils/content/worker").Worker; + let key = loader.sandbox("api-utils/content/worker").PRIVATE_KEY; + let worker = Worker({ + exposeUnlockKey : key, + window: xrayWindow, + contentScript: [ + 'new ' + function () { + assert = function assert(v, msg) { + self.port.emit("assert", {assertion:v, msg:msg}); + } + done = function done() { + self.port.emit("done"); + } + }, + contentScript + ] + }); + + worker.port.on("done", done); + worker.port.on("assert", function (data) { + test.assert(data.assertion, data.msg); + }); + + return worker; +} + +/* Examples for the `createProxyTest` uses */ + +let html = "<script>var documentGlobal = true</script>"; +exports.testCreateProxyTest = createProxyTest(html, function (helper, test) { + // You can get access to regular `test` object in second argument of + // `createProxyTest` method: + test.assert(helper.rawWindow.documentGlobal, + "You have access to a raw window reference via `helper.rawWindow`"); + test.assert(!("documentGlobal" in helper.xrayWindow), + "You have access to an XrayWrapper reference via `helper.xrayWindow`"); + + // If you do not create a Worker, you have to call helper.done(), + // in order to say when your test is finished + helper.done(); +}); + +exports.testCreateProxyTestWithWorker = createProxyTest("", function (helper) { + + helper.createWorker( + "new " + function WorkerScope() { + assert(true, "You can do assertions in your content script"); + // And if you create a worker, you either have to call `done` + // from content script or helper.done() + done(); + } + ); + +}); + +exports.testCreateProxyTestWithEvents = createProxyTest("", function (helper, test) { + + let worker = helper.createWorker( + "new " + function WorkerScope() { + self.port.emit("foo"); + } + ); + + worker.port.on("foo", function () { + test.pass("You can use events"); + // And terminate your test with helper.done: + helper.done(); + }); + +}); + +// Verify that the attribute `exposeUnlockKey`, that allow this test +// to identify proxies, works correctly. +// See `PRIVATE_KEY` definition in worker.js +exports.testKeyAccess = createProxyTest("", function(helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + assert("UNWRAP_ACCESS_KEY" in window, "have access to `UNWRAP_ACCESS_KEY`"); + done(); + } + ); + +}); + + +// Bug 714778: There was some issue around `toString` functions +// that ended up being shared between content scripts +exports.testSharedToStringProxies = createProxyTest("", function(helper) { + + let worker = helper.createWorker( + 'new ' + function ContentScriptScope() { + // We ensure that `toString` can't be modified so that nothing could + // leak to/from the document and between content scripts + //document.location.toString = function foo() {}; + document.location.toString.foo = "bar"; + assert(!("foo" in document.location.toString), "document.location.toString can't be modified"); + assert(document.location.toString() == "data:text/html,", + "First document.location.toString()"); + self.postMessage("next"); + } + ); + worker.on("message", function () { + helper.createWorker( + 'new ' + function ContentScriptScope2() { + assert(!("foo" in document.location.toString), + "document.location.toString is different for each content script"); + assert(document.location.toString() == "data:text/html,", + "Second document.location.toString()"); + done(); + } + ); + }); +}); + + +// Ensure that postMessage is working correctly across documents with an iframe +let html = '<iframe id="iframe" name="test" src="data:text/html," />'; +exports.testPostMessage = createProxyTest(html, function (helper, test) { + let ifWindow = helper.xrayWindow.document.getElementById("iframe").contentWindow; + // Listen without proxies, to check that it will work in regular case + // simulate listening from a web document. + ifWindow.addEventListener("message", function listener(event) { + //if (event.source.wrappedJSObject == helper.rawWindow) return; + ifWindow.removeEventListener("message", listener, false); + // As we are in system principal, event is an XrayWrapper + test.assertEqual(event.source, ifWindow, + "event.source is the iframe window"); + test.assertEqual(event.origin, "null", "origin is null"); + + test.assertEqual(event.data, "{\"foo\":\"bar\\n \\\"escaped\\\".\"}", + "message data is correct"); + + helper.done(); + }, false); + + helper.createWorker( + 'new ' + function ContentScriptScope() { + assert(postMessage === postMessage, + "verify that we doesn't generate multiple functions for the same method"); + + var json = JSON.stringify({foo : "bar\n \"escaped\"."}); + + document.getElementById("iframe").contentWindow.postMessage(json, "*"); + } + ); +}); + +let html = '<input id="input2" type="checkbox" />'; +exports.testObjectListener = createProxyTest(html, function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Test objects being given as event listener + let input = document.getElementById("input2"); + let myClickListener = { + called: false, + handleEvent: function(event) { + assert(this === myClickListener, "`this` is the original object"); + assert(!this.called, "called only once"); + this.called = true; + assert(event.valueOf() !== event.valueOf(UNWRAP_ACCESS_KEY), "event is wrapped"); + assert(event.target, input, "event.target is the wrapped window"); + done(); + } + }; + + window.addEventListener("click", myClickListener, true); + input.click(); + window.removeEventListener("click", myClickListener, true); + } + ); + +}); + +exports.testObjectListener2 = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Verify object as DOM event listener + let myMessageListener = { + called: false, + handleEvent: function(event) { + window.removeEventListener("message", myMessageListener, true); + + assert(this == myMessageListener, "`this` is the original object"); + assert(!this.called, "called only once"); + this.called = true; + assert(event.valueOf() !== event.valueOf(UNWRAP_ACCESS_KEY), "event is wrapped"); + assert(event.target == document.defaultView, "event.target is the wrapped window"); + assert(event.source == document.defaultView, "event.source is the wrapped window"); + assert(event.origin == "null", "origin is null"); + assert(event.data == "ok", "message data is correct"); + done(); + } + }; + + window.addEventListener("message", myMessageListener, true); + document.defaultView.postMessage("ok", '*'); + } + ); + +}); + +let html = '<input id="input" type="text" /><input id="input3" type="checkbox" />' + + '<input id="input2" type="checkbox" />'; +exports.testStringOverload = createProxyTest(html, function (helper, test) { + // Proxy - toString error + let originalString = "string"; + let p = Proxy.create({ + get: function(receiver, name) { + if (name == "binded") + return originalString.toString.bind(originalString); + return originalString[name]; + } + }); + test.assertRaises(function () { + p.toString(); + }, + /String.prototype.toString called on incompatible Proxy/, + "toString can't be called with this being the proxy"); + test.assertEqual(p.binded(), "string", "but it works if we bind this to the original string"); + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // RightJS is hacking around String.prototype, and do similar thing: + // Pass `this` from a String prototype method. + // It is funny because typeof this == "object"! + // So that when we pass `this` to a native method, + // our proxy code can fail on another even more crazy thing. + // See following test to see what fails around proxies. + String.prototype.update = function () { + assert(typeof this == "object", "in update, `this` is an object"); + assert(this.toString() == "input", "in update, `this.toString works"); + return document.querySelectorAll(this); + }; + assert("input".update().length == 3, "String.prototype overload works"); + done(); + } + ); +}); + +exports.testMozMatchedSelector = createProxyTest("", function (helper) { + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Check mozMatchesSelector XrayWrappers bug: + // mozMatchesSelector returns bad results when we are not calling it from the node itself + // SEE BUG 658909: mozMatchesSelector returns incorrect results with XrayWrappers + assert(document.createElement( "div" ).mozMatchesSelector("div"), + "mozMatchesSelector works while being called from the node"); + assert(document.documentElement.mozMatchesSelector.call( + document.createElement( "div" ), + "div" + ), + "mozMatchesSelector works while being called from a " + + "function reference to " + + "document.documentElement.mozMatchesSelector.call"); + done(); + } + ); +}); + +exports.testEventsOverload = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // If we add a "____proxy" attribute on XrayWrappers in order to store + // the related proxy to create an unique proxy for each wrapper; + // we end up setting this attribute to prototype objects :x + // And so, instances created with such prototype will be considered + // as equal to the prototype ... + // // Internal method that return the proxy for a given XrayWrapper + // function proxify(obj) { + // if (obj._proxy) return obj._proxy; + // return obj._proxy = Proxy.create(...); + // } + // + // // Get a proxy of an XrayWrapper prototype object + // let proto = proxify(xpcProto); + // + // // Use this proxy as a prototype + // function Constr() {} + // Constr.proto = proto; + // + // // Try to create an instance using this prototype + // let xpcInstance = new Constr(); + // let wrapper = proxify(xpcInstance) + // + // xpcProto._proxy = proto and as xpcInstance.__proto__ = xpcProto, + // xpcInstance._proxy = proto ... and profixy(xpcInstance) = proto :( + // + let proto = window.document.createEvent('HTMLEvents').__proto__; + window.Event.prototype = proto; + let event = document.createEvent('HTMLEvents'); + assert(event !== proto, "Event should not be equal to its prototype"); + event.initEvent('dataavailable', true, true); + assert(event.type === 'dataavailable', "Events are working fine"); + done(); + } + ); + +}); + +exports.testNestedAttributes = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // XrayWrappers has a bug when you set an attribute on it, + // in some cases, it creates an unnecessary wrapper that introduces + // a different object that refers to the same original object + // Check that our wrappers don't reproduce this bug + // SEE BUG 658560: Fix identity problem with CrossOriginWrappers + let o = {sandboxObject:true}; + window.nested = o; + o.foo = true; + assert(o === window.nested, "Nested attribute to sandbox object should not be proxified"); + window.nested = document; + assert(window.nested === document, "Nested attribute to proxy should not be double proxified"); + done(); + } + ); + +}); + +exports.testFormNodeName = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + let body = document.body; + // Check form[nodeName] + let form = document.createElement("form"); + let input = document.createElement("input"); + input.setAttribute("name", "test"); + form.appendChild(input); + body.appendChild(form); + assert(form.test == input, "form[nodeName] is valid"); + body.removeChild(form); + done(); + } + ); + +}); + +exports.testLocalStorage = createProxyTest("", function (helper, test) { + + let worker = helper.createWorker( + 'new ' + function ContentScriptScope() { + // Check localStorage: + assert(window.localStorage, "has access to localStorage"); + window.localStorage.name = 1; + assert(window.localStorage.name == 1, "localStorage appears to work"); + + self.port.on("step2", function () { + window.localStorage.clear(); + assert(window.localStorage.name == undefined, "localStorage really, really works"); + done(); + }); + self.port.emit("step1"); + } + ); + + worker.port.on("step1", function () { + test.assertEqual(helper.rawWindow.localStorage.name, 1, "localStorage really works"); + worker.port.emit("step2"); + }); + +}); + +exports.testAutoUnwrapCustomAttributes = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + let body = document.body; + // Setting a custom object to a proxy attribute is not wrapped when we get it afterward + let object = {custom: true, enumerable: false}; + body.customAttribute = object; + assert(body.customAttribute.valueOf() === body.customAttribute.valueOf(UNWRAP_ACCESS_KEY), "custom JS attributes are not wrapped"); + assert(object === body.customAttribute, "custom JS attributes are not wrapped"); + done(); + } + ); + +}); + +exports.testObjectTag = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // <object>, <embed> and other tags return typeof 'function' + let flash = document.createElement("object"); + assert(typeof flash == "function", "<object> is typeof 'function'"); + assert(flash.toString().match(/\[object HTMLObjectElement.*\]/), "<object> is HTMLObjectElement"); + assert("setAttribute" in flash, "<object> has a setAttribute method"); + done(); + } + ); + +}); + +exports.testHighlightToStringBehavior = createProxyTest("", function (helper, test) { + // We do not have any workaround this particular use of toString + // applied on <object> elements. So disable this test until we found one! + //test.assertEqual(helper.rawWindow.Object.prototype.toString.call(flash), "[object HTMLObjectElement]", "<object> is HTMLObjectElement"); + function f() {}; + test.assertMatches(Object.prototype.toString.call(f), /\[object Function.*\]/, "functions are functions 1"); + // This is how jquery call toString: + test.assertMatches(helper.rawWindow.Object.prototype.toString.call(""), /\[object String.*\]/, "strings are strings"); + test.assertMatches(helper.rawWindow.Object.prototype.toString.call({}), /\[object Object.*\]/, "objects are objects"); + + // Make sure to pass a function from the same compartments + // or toString will return [object Object] on FF8+ + let f2 = helper.rawWindow.eval("(function () {})"); + test.assertMatches(helper.rawWindow.Object.prototype.toString.call(f2), /\[object Function.*\]/, "functions are functions 2"); + + helper.done(); +}); + +exports.testDocumentTagName = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + let body = document.body; + // Check document[tagName] + let div = document.createElement("div"); + div.setAttribute("name", "test"); + body.appendChild(div); + assert(!document.test, "document[divName] is undefined"); + body.removeChild(div); + + let form = document.createElement("form"); + form.setAttribute("name", "test"); + body.appendChild(form); + assert(document.test == form, "document[formName] is valid"); + body.removeChild(form); + + let img = document.createElement("img"); + img.setAttribute("name", "test"); + body.appendChild(img); + assert(document.test == img, "document[imgName] is valid"); + body.removeChild(img); + done(); + } + ); + +}); + +let html = '<iframe id="iframe" name="test" src="data:text/html," />'; +exports.testWindowFrames = createProxyTest(html, function (helper) { + + helper.createWorker( + 'let glob = this; new ' + function ContentScriptScope() { + // Check window[frameName] and window.frames[i] + let iframe = document.getElementById("iframe"); + //assert(window.frames.length == 1, "The iframe is reported in window.frames check1"); + //assert(window.frames[0] == iframe.contentWindow, "The iframe is reported in window.frames check2"); + //console.log(window.test+ "-"+iframe.contentWindow); + //console.log(window); + assert(window.test == iframe.contentWindow, "window[frameName] is valid"); + done(); + } + ); + +}); + +exports.testCollections = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Highlight XPCNativeWrapper bug with HTMLCollection + // tds[0] is only defined on first access :o + let body = document.body; + let div = document.createElement("div"); + body.appendChild(div); + div.innerHTML = "<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>"; + let tds = div.getElementsByTagName("td"); + assert(tds[0] == tds[0], "We can get array element multiple times"); + body.removeChild(div); + done(); + } + ); + +}); + +let html = '<input id="input" type="text" /><input id="input3" type="checkbox" />' + + '<input id="input2" type="checkbox" />'; +exports.testCollections2 = createProxyTest(html, function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Verify that NodeList/HTMLCollection are working fine + let body = document.body; + let inputs = body.getElementsByTagName("input"); + assert(body.childNodes.length == 3, "body.childNodes length is correct"); + assert(inputs.length == 3, "inputs.length is correct"); + assert(body.childNodes[0] == inputs[0], "body.childNodes[0] is correct"); + assert(body.childNodes[1] == inputs[1], "body.childNodes[1] is correct"); + assert(body.childNodes[2] == inputs[2], "body.childNodes[2] is correct"); + let count = 0; + for(let i in body.childNodes) { + count++; + } + assert(count == 3, "body.childNodes is iterable"); + done(); + } + ); + +}); + +exports.testValueOf = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Check internal use of valueOf() + assert(window.valueOf().toString().match(/\[object Window.*\]/), "proxy.valueOf() returns the wrapped version"); + assert(window.valueOf({}).toString().match(/\[object Window.*\]/), "proxy.valueOf({}) returns the wrapped version"); + assert(window.valueOf(UNWRAP_ACCESS_KEY).toString().match(/\[object XrayWrapper \[object Window.*\].*\]/), "proxy.valueOf(UNWRAP_ACCESS_KEY) returns the unwrapped version"); + done(); + } + ); + +}); + +exports.testXMLHttpRequest = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // XMLHttpRequest doesn't support XMLHttpRequest.apply, + // that may break our proxy code + assert(window.XMLHttpRequest(), "we are able to instantiate XMLHttpRequest object"); + done(); + } + ); + +}); + +exports.testXPathResult = createProxyTest("", function (helper, test) { + + // Check XPathResult bug with constants being undefined on + // XPCNativeWrapper + let value = helper.rawWindow.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE; + let xpcXPathResult = helper.xrayWindow.XPathResult; + test.assertEqual(xpcXPathResult.wrappedJSObject. + UNORDERED_NODE_SNAPSHOT_TYPE, + value, + "XPathResult's constants are valid on unwrapped node"); + + if (xulApp.versionInRange(xulApp.platformVersion, "10.0a1", "*")) { + test.assertEqual(xpcXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, 6, + "XPathResult's constants are defined on " + + "XPCNativeWrapper (platform bug #)"); + } + else { + test.assertEqual(xpcXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, + undefined, + "XPathResult's constants are undefined on " + + "XPCNativeWrapper (platform bug #665279)"); + } + + let worker = helper.createWorker( + 'new ' + function ContentScriptScope() { + self.port.on("value", function (value) { + // Check that our work around is working: + assert(window.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE === value, + "XPathResult works correctly on Proxies"); + done(); + }); + } + ); + worker.port.emit("value", value); + +}); + +exports.testPrototypeInheritance = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Verify that inherited prototype function like initEvent + // are handled correctly. (e2.type will return an error if it's not the case) + let event1 = document.createEvent( 'MouseEvents' ); + event1.initEvent( "click", true, true ); + let event2 = document.createEvent( 'MouseEvents' ); + event2.initEvent( "click", true, true ); + assert(event2.type == "click", "We are able to create an event"); + done(); + } + ); + +}); + +exports.testFunctions = createProxyTest("", function (helper) { + + helper.rawWindow.callFunction = function (f) f(); + helper.rawWindow.isEqual = function (a, b) a == b; + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Check basic usage of functions + let closure2 = function () {return "ok";}; + assert(window.wrappedJSObject.callFunction(closure2) == "ok", "Function references work"); + + // Ensure that functions are cached when being wrapped to native code + let closure = function () {}; + assert(window.wrappedJSObject.isEqual(closure, closure), "Function references are cached before being wrapped to native"); + done(); + } + ); + +}); + +let html = '<input id="input2" type="checkbox" />'; +exports.testListeners = createProxyTest(html, function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + // Verify listeners: + let input = document.getElementById("input2"); + assert(input, "proxy.getElementById works"); + + function onclick() {}; + input.onclick = onclick; + assert(input.onclick === onclick, "on* attributes are equal to original function set"); + + let addEventListenerCalled = false; + let expandoCalled = false; + input.addEventListener("click", function onclick(event) { + input.removeEventListener("click", onclick, true); + + assert(!addEventListenerCalled, "closure given to addEventListener is called once"); + if (addEventListenerCalled) + return; + addEventListenerCalled = true; + + assert(!event.target.ownerDocument.defaultView.documentGlobal, "event object is still wrapped and doesn't expose document globals"); + assert("__isWrappedProxy" in event.target, "event object is a proxy"); + + let input2 = document.getElementById("input2"); + + input.onclick = function (event) { + input.onclick = null; + assert(!expandoCalled, "closure set to expando is called once"); + if (expandoCalled) return; + expandoCalled = true; + + assert(!event.target.ownerDocument.defaultView.documentGlobal, "event object is still wrapped and doesn't expose document globals"); + assert("__isWrappedProxy" in event.target, "event object is a proxy"); + + setTimeout(function () { + input.click(); + done(); + }, 0); + + } + + setTimeout(function () { + input.click(); + }, 0); + + }, true); + + input.click(); + } + ); + +}); + +exports.testMozRequestAnimationFrame = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + window.mozRequestAnimationFrame(function callback() { + assert(callback == this, "callback is equal to `this`"); + done(); + }); + } + ); + +}); + +exports.testGlobalScope = createProxyTest("", function (helper) { + + helper.createWorker( + 'let toplevelScope = true;' + + 'assert(window.toplevelScope, "variables in toplevel scope are set to `window` object");' + + 'assert(this.toplevelScope, "variables in toplevel scope are set to `this` object");' + + 'done();' + ); + +}); + +// Bug 671016: Typed arrays should not be proxified +exports.testTypedArrays = createProxyTest("", function (helper) { + + helper.createWorker( + 'new ' + function ContentScriptScope() { + let canvas = document.createElement("canvas"); + let context = canvas.getContext("2d"); + let imageData = context.getImageData(0,0, 1, 1); + let unwrappedData = imageData.valueOf(UNWRAP_ACCESS_KEY).data; + let data = imageData.data; + assert(unwrappedData === data, "Typed array isn't proxified") + done(); + } + ); + +}); + +// Bug 715755: proxy code throw an exception on COW +// Create an http server in order to simulate real cross domain documents +exports.testCrossDomainIframe = createProxyTest("", function (helper) { + let serverPort = 8099; + let server = require("httpd").startServerAsync(serverPort); + server.registerPathHandler("/", function handle(request, response) { + // Returns an empty webpage + response.write(""); + }); + + let worker = helper.createWorker( + 'new ' + function ContentScriptScope() { + // Waits for the server page url + self.on("message", function (url) { + // Creates an iframe with this page + let iframe = document.createElement("iframe"); + iframe.addEventListener("load", function onload() { + iframe.removeEventListener("load", onload, true); + try { + // Try accessing iframe's content that is made of COW wrappers + // Take care of debug builds that add object address after `Window` + assert(String(iframe.contentWindow).match(/\[object Window.*\]/), + "COW works properly"); + } catch(e) { + assert(false, "COW fails : "+e.message); + } + self.port.emit("end"); + }, true); + iframe.setAttribute("src", url); + document.body.appendChild(iframe); + }); + } + ); + + worker.port.on("end", function () { + server.stop(helper.done); + }); + + worker.postMessage("http://localhost:" + serverPort + "/"); + +}); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-symbiont.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-symbiont.js new file mode 100644 index 0000000..526af05 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-symbiont.js @@ -0,0 +1,190 @@ +/* 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 { Symbiont } = require('content/symbiont'); +const self = require("self"); + +function makeWindow() { + let content = + '<?xml version="1.0"?>' + + '<window ' + + 'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">' + + '<iframe id="content" type="content"/>' + + '</window>'; + var url = "data:application/vnd.mozilla.xul+xml," + + 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); +} + +exports['test:constructing symbiont && validating API'] = function(test) { + let window = makeWindow(); + window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, false); + let frame = window.document.getElementById("content"); + // TODO: support arrays ?? + let contentScripts = ["1;", "2;"]; + let contentSymbiont = Symbiont({ + frame: frame, + contentScriptFile: self.data.url("test-content-symbiont.js"), + contentScript: contentScripts, + contentScriptWhen: "start" + }); + + test.assertEqual( + self.data.url("test-content-symbiont.js"), + contentSymbiont.contentScriptFile, + "There is one contentScriptFile, as specified in options." + ); + test.assertEqual( + contentScripts.length, + contentSymbiont.contentScript.length, + "There are two contentScripts, as specified in options." + ); + test.assertEqual( + contentScripts[0], + contentSymbiont.contentScript[0], + "There are two contentScripts, as specified in options." + ); + test.assertEqual( + contentScripts[1], + contentSymbiont.contentScript[1], + "There are two contentScripts, as specified in options." + ) + test.assertEqual( + contentSymbiont.contentScriptWhen, + "start", + "contentScriptWhen is as specified in options." + ); + + test.done(); + window.close(); + frame.setAttribute("src", "data:text/html,<html><body></body></html>"); + }, false); + test.waitUntilDone(); +}; + +exports["test:communication with worker global scope"] = function(test) { + let window = makeWindow(); + let contentSymbiont; + + function onMessage1(message) { + test.assertEqual(message, 1, "Program gets message via onMessage."); + contentSymbiont.removeListener('message', onMessage1); + contentSymbiont.on('message', onMessage2); + contentSymbiont.postMessage(2); + }; + + function onMessage2(message) { + if (5 == message) { + test.done(); + } else { + test.assertEqual(message, 3, "Program gets message via onMessage2."); + contentSymbiont.postMessage(4) + } + } + + window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, false); + let frame = window.document.getElementById("content"); + contentSymbiont = Symbiont({ + frame: frame, + contentScript: 'new ' + function() { + self.postMessage(1); + self.on("message", function onMessage(message) { + if (message === 2) + self.postMessage(3); + if (message === 4) + self.postMessage(5); + }); + } + '()', + contentScriptWhen: 'ready', + onMessage: onMessage1 + }); + + frame.setAttribute("src", "data:text/html,<html><body></body></html>"); + }, false); + test.waitUntilDone(); +}; + +exports['test:pageWorker'] = function(test) { + test.waitUntilDone(); + let worker = Symbiont({ + contentURL: 'about:buildconfig', + contentScript: 'new ' + function WorkerScope() { + self.on('message', function(data) { + if (data.valid) + self.postMessage('bye!'); + }) + self.postMessage(window.location.toString()); + }, + onMessage: function(msg) { + if (msg == 'bye!') { + test.done() + } else { + test.assertEqual( + worker.contentURL + '', + msg + ); + worker.postMessage({ valid: true }); + } + } + }); +}; + +exports["test:document element present on 'start'"] = function(test) { + test.waitUntilDone(); + let xulApp = require("xul-app"); + let worker = Symbiont({ + contentURL: "about:buildconfig", + contentScript: "self.postMessage(!!document.documentElement)", + contentScriptWhen: "start", + onMessage: function(message) { + if (xulApp.versionInRange(xulApp.platformVersion, "2.0b6", "*")) + test.assert(message, "document element present on 'start'"); + else + test.pass("document element not necessarily present on 'start'"); + test.done(); + } + }); +}; + +exports["test:direct communication with trusted document"] = function(test) { + test.waitUntilDone(); + + let worker = Symbiont({ + contentURL: require("self").data.url("test-trusted-document.html") + }); + + worker.port.on('document-to-addon', function (arg) { + test.assertEqual(arg, "ok", "Received an event from the document"); + worker.destroy(); + test.done(); + }); + worker.port.emit('addon-to-document', 'ok'); +}; + +exports["test:`addon` is not available when a content script is set"] = function(test) { + test.waitUntilDone(); + + let worker = Symbiont({ + contentURL: require("self").data.url("test-trusted-document.html"), + contentScript: "new " + function ContentScriptScope() { + self.port.emit("cs-to-addon", "addon" in unsafeWindow); + } + }); + + worker.port.on('cs-to-addon', function (hasAddon) { + test.assertEqual(hasAddon, false, + "`addon` is not available"); + worker.destroy(); + test.done(); + }); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-worker.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-worker.js new file mode 100644 index 0000000..d5e95a1 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-content-worker.js @@ -0,0 +1,465 @@ +/* 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 timer = require('timer'); +const { Loader } = require("@loader"); + +function makeWindow(contentURL) { + let content = + '<?xml version="1.0"?>' + + '<window ' + + 'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">' + + '<iframe id="content" type="content" src="' + + encodeURIComponent(contentURL) + '"/>' + + '<script>var documentValue=true;</script>' + + '</window>'; + var url = "data:application/vnd.mozilla.xul+xml," + + 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); +} + +const { Worker } = require('content/worker'); +exports['test:sample'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + // As window has just being created, its document is still loading, + // and we have about:blank document before the expected one + test.assertEqual(window.document.location.href, "about:blank", + "window starts by loading about:blank"); + + // We need to wait for the load/unload of temporary about:blank + // or our worker is going to be automatically destroyed + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + test.assertNotEqual(window.document.location.href, "about:blank", + "window is now on the right document"); + + 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.document.location.href, + "worker.url still works"); + test.done(); + } + }); + + test.assertEqual(worker.url, window.document.location.href, + "worker.url works"); + worker.postMessage('hi!'); + + }, true); + +} + +exports['test:emit'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + let worker = Worker({ + window: window, + 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"); + test.done(); + }); + worker.port.emit('addon-to-content', 'event data'); + +} + +exports['test:emit hack message'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + let worker = Worker({ + window: window, + 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"); + test.done(); + }); + worker.on('message', function (data) { + test.fail("Got an unexpected message : "+msg); + }); + worker.port.emit('message', 'event data'); + +} + +exports['test:n-arguments emit'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + let worker = Worker({ + window: window, + 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"); + test.done(); + }); + worker.port.emit('addon-to-content', 'first argument', 'second', 'third'); +} + +exports['test:post-json-values-only'] = function(test) { + let window = makeWindow("data:text/html,"); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let worker = Worker({ + window: window.document.getElementById("content").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], "about:blank", "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"); + test.done(); + }); + worker.postMessage({ fun: function () {}, w: worker, array: array }); + + }, true); + +}; + + +exports['test:emit-json-values-only'] = function(test) { + let window = makeWindow("data:text/html,"); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let win = window.document.getElementById("content").contentWindow; + let worker = Worker({ + window: win, + 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], "about:blank", "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"); + test.done(); + }); + + let obj = { + fun: function () {}, + dom: window.document.createElement("div") + }; + worker.port.emit("addon-to-content", function () {}, worker, obj, array); + + }, true); +} + +exports['test:content is wrapped'] = function(test) { + let contentURL = 'data:text/html,<script>var documentValue=true;</script>'; + let window = makeWindow(contentURL); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let worker = Worker({ + window: window.document.getElementById("content").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"); + test.done(); + } + }); + + }, true); + +} + +exports['test:chrome is unwrapped'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + 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"); + test.done(); + } + }); + + }, true); + +} + +exports['test:nothing is leaked to content script'] = function(test) { + let window = makeWindow(); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let worker = Worker({ + window: window, + 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"); + test.done(); + } + }); + + }, true); + +} + +exports['test:ensure console.xxx works in cs'] = function(test) { + test.waitUntilDone(5000); + + // Create a new module loader in order to be able to create a `console` + // module mockup: + let sandbox = Loader.new(require("@packaging")); + let sandboxRequire = Loader.require.bind(sandbox, module.path); + Object.defineProperty(sandbox.globals, 'console', { + value: { + 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 window = makeWindow(); + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let worker = sandboxRequire('content/worker').Worker({ + window: window, + 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"); + test.done(); + } + }); + }, true); + +} + + +exports['test:setTimeout can\'t be cancelled by content'] = function(test) { + let contentURL = 'data:text/html,<script>var documentValue=true;</script>'; + let window = makeWindow(contentURL); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let worker = Worker({ + window: window.document.getElementById("content").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"); + test.done(); + } + }); + + }, true); + +} + +exports['test:setTimeout are unregistered on content unload'] = function(test) { + let contentURL = 'data:text/html,foo'; + let window = makeWindow(contentURL); + test.waitUntilDone(); + + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + let iframe = window.document.getElementById("content"); + let originalDocument = iframe.contentDocument; + let worker = Worker({ + window: iframe.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. + timer.setTimeout(function () { + // Bug 689621: Wait for the new document load so that we are sure that + // previous document cancelled its intervals + iframe.addEventListener("load", function onload() { + iframe.removeEventListener("load", onload, true); + let titleAfterLoad = originalDocument.title; + // Wait additional cycles to verify that intervals are really cancelled + timer.setTimeout(function () { + test.assertEqual(iframe.contentDocument.title, "final", + "New document has not been modified"); + test.assertEqual(originalDocument.title, titleAfterLoad, + "Nor previous one"); + test.done(); + }, 100); + }, true); + iframe.setAttribute("src", "data:text/html,<title>final</title>"); + }, 100); + + }, true); + +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-cortex.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-cortex.js new file mode 100644 index 0000000..9f13f0e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-cortex.js @@ -0,0 +1,122 @@ +/* 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/. */ + +// vim:set ts=2 sw=2 sts=2 + +"use strict"; + +var Cortex = require("cortex").Cortex; + +exports["test property changes propagate"] = function (assert) { + var source = { + _foo: "secret", + get foo() { + return this._foo; + }, + set foo(value) { + this._foo = value; + }, + get getOnly() { + return this._foo; + }, + set setOnly(value) { + this._setOnly = value; + }, + bar: "public", + method: function method(a, b) { + return this._foo + a + b + } + }; + var fixture = Cortex(source); + + assert.ok(!('_foo' in fixture), + "properties that start with `_` are omitted"); + assert.equal(fixture.foo, "secret", "get accessor alias works"); + fixture.foo = "new secret"; + assert.equal(fixture.foo, "new secret", "set accessor alias works"); + assert.equal(source.foo, "new secret", "accessor delegates to the source"); + assert.equal(fixture.bar, "public", "data property alias works"); + fixture.bar = "bar"; + assert.equal(source.bar, "bar", "data property change propagates"); + source.bar = "foo" + assert.equal(fixture.bar, "foo", "data property change probagets back"); + assert.equal(fixture.method("a", "b"), "new secretab", + "public methods are callable"); + assert.equal(fixture.method.call({ _foo: "test" }, " a,", "b"), + "new secret a,b", + "`this` pseudo-variable can not be passed through call."); + assert.equal(fixture.method.apply({ _foo: "test" }, [" a,", "b"]), + "new secret a,b", + "`this` pseudo-variable can not be passed through apply."); + assert.equal(fixture.getOnly, source._foo, + "getter returned property of wrapped object"); + fixture.setOnly = 'bar' + assert.equal(source._setOnly, 'bar', "setter modified wrapped object") +}; + + +exports["test immunity of inheritance"] = function(assert) { + function Type() {} + Type.prototype = { + constructor: Type, + _bar: 2, + bar: 3, + get_Foo: function getFoo() { + return this._foo; + } + } + var source = Object.create(Type.prototype, { + _foo: { value: 'secret' }, + getBar: { value: function get_Bar() { + return this.bar + }}, + get_Bar: { value: function getBar() { + return this._bar + }} + }); + + var fixture = Cortex(source); + + assert.ok(Cortex({}, null, Type.prototype) instanceof Type, + "if custom prototype is providede cortex will inherit from it"); + assert.ok(fixture instanceof Type, + "if no prototype is given cortex inherits from object's prototype"); + + source.bar += 1; + assert.notEqual(fixture.bar, source.bar, + "chages of properties don't propagate to non-aliases"); + assert.equal(fixture.getBar(), source.bar, + "methods accessing public properties are bound to the source"); + + fixture._bar += 1; + assert.notEqual(fixture._bar, source._bar, + "changes of non aliased properties don't propagate"); + assert.equal(fixture.get_Bar(), source._bar, + "methods accessing privates are bound to the source"); + assert.notEqual(fixture.get_Foo(), source._foo, + "prototoype methods are not bound to the source"); +} + +exports["test customized public properties"] = function(assert) { + var source = { + _a: 'a', + b: 'b', + get: function get(name) { + return this[name]; + } + }; + + var fixture = Cortex(source, ['_a', 'get']); + fixture._a += "#change"; + + + assert.ok(!("b" in fixture), "non-public own property is not defined"); + assert.equal(fixture.get("b"), source.b, + "public methods preserve access to the private properties"); + assert.equal(fixture._a, source._a, + "custom public property changes propagate"); +} + +//if (require.main == module) + require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-dom.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-dom.js new file mode 100644 index 0000000..52ab1c3 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-dom.js @@ -0,0 +1,88 @@ +/* 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 events = require("dom/events"); +const { activeBrowserWindow: { document } } = require("window-utils"); +const window = document.window; + +exports["test on / emit"] = function (assert, done) { + let element = document.createElement("div"); + events.on(element, "click", function listener(event) { + assert.equal(event.target, element, "event has correct target"); + events.removeListener(element, "click", listener); + done(); + }); + + events.emit(element, "click", { + category: "MouseEvents", + settings: [ + true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null + ] + }); +}; + +exports["test remove"] = function (assert, done) { + let element = document.createElement("span"); + let l1 = 0; + let l2 = 0; + let options = { + category: "MouseEvents", + settings: [ + true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null + ] + }; + + events.on(element, "click", function listener1(event) { + l1 ++; + assert.equal(event.target, element, "event has correct target"); + events.removeListener(element, "click", listener1); + }); + + events.on(element, "click", function listener2(event) { + l2 ++; + if (l1 < l2) { + assert.equal(l1, 1, "firs listener was called and then romeved"); + events.removeListener(element, "click", listener2); + done(); + } + events.emit(element, "click", options); + }); + + events.emit(element, "click", options); +}; + +exports["test once"] = function (assert, done) { + let element = document.createElement("h1"); + let l1 = 0; + let l2 = 0; + let options = { + category: "MouseEvents", + settings: [ + true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null + ] + }; + + + events.once(element, "click", function listener(event) { + assert.equal(event.target, element, "event target is a correct element"); + l1 ++; + }); + + events.on(element, "click", function listener(event) { + l2 ++; + if (l2 > 3) { + events.removeListener(element, "click", listener); + assert.equal(event.target, element, "event has correct target"); + assert.equal(l1, 1, "once was called only once"); + done(); + } + events.emit(element, "click", options); + }); + + events.emit(element, "click", options); +} + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-environment.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-environment.js new file mode 100644 index 0000000..1d7e783 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-environment.js @@ -0,0 +1,50 @@ +/* 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 { env } = require('api-utils/environment'); +const { Cc, Ci } = require('chrome'); +const { get, set, exists } = Cc['@mozilla.org/process/environment;1']. + getService(Ci.nsIEnvironment); + +exports['test exists'] = function(assert) { + assert.equal('PATH' in env, exists('PATH'), + 'PATH environment variable is defined'); + assert.equal('FOO1' in env, exists('FOO1'), + 'FOO1 environment variable is not defined'); + set('FOO1', 'foo'); + assert.equal('FOO1' in env, true, + 'FOO1 environment variable was set'); + set('FOO1', null); + assert.equal('FOO1' in env, false, + 'FOO1 environment variable was unset'); +}; + +exports['test get'] = function(assert) { + assert.equal(env.PATH, get('PATH'), 'PATH env variable matches'); + assert.equal(env.BAR2, undefined, 'BAR2 env variable is not defined'); + set('BAR2', 'bar'); + assert.equal(env.BAR2, 'bar', 'BAR2 env variable was set'); + set('BAR2', null); + assert.equal(env.BAR2, undefined, 'BAR2 env variable was unset'); +}; + +exports['test set'] = function(assert) { + assert.equal(get('BAZ3'), '', 'BAZ3 env variable is not set'); + assert.equal(env.BAZ3, undefined, 'BAZ3 is not set'); + env.BAZ3 = 'baz'; + assert.equal(env.BAZ3, get('BAZ3'), 'BAZ3 env variable is set'); + assert.equal(get('BAZ3'), 'baz', 'BAZ3 env variable was set to "baz"'); +}; + +exports['test unset'] = function(assert) { + env.BLA4 = 'bla'; + assert.equal(env.BLA4, 'bla', 'BLA4 env varibale is set'); + delete env.BLA4; + assert.equal(env.BLA4, undefined, 'BLA4 env variable is unset'); + assert.equal('BLA4' in env, false, 'BLA4 env variable no longer exists' ); +}; + +require('test').run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-errors.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-errors.js new file mode 100644 index 0000000..64bb6c9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-errors.js @@ -0,0 +1,70 @@ +/* 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/. */ + +var errors = require("errors"); + +exports.testCatchAndLog = function(test) { + var caught = []; + function dummyLog(e) { caught.push(e); } + + var wrapped = errors.catchAndLog(function(x) { + throw Error("blah" + x + this); + }, + "boop", + dummyLog); + test.assertEqual(wrapped.call("hi", 1), "boop", + "exceptions should be trapped, def. resp. returned"); + test.assertEqual(caught.length, 1, + "logging function should be called"); + test.assertEqual(caught[0].message, "blah1hi", + "args and this should be passed to wrapped func"); +}; + +exports.testCatchAndLogProps = function(test) { + var caught = []; + function dummyLog(e) { caught.push(e); } + + var thing = { + foo: function(x) { throw Error("nowai" + x); }, + bar: function() { throw Error("blah"); }, + baz: function() { throw Error("fnarg"); } + }; + + errors.catchAndLogProps(thing, "foo", "ugh", dummyLog); + + test.assertEqual(thing.foo(1), "ugh", + "props should be wrapped"); + test.assertEqual(caught.length, 1, + "logging function should be called"); + test.assertEqual(caught[0].message, "nowai1", + "args should be passed to wrapped func"); + test.assertRaises(function() { thing.bar(); }, + "blah", + "non-wrapped props should be wrapped"); + + errors.catchAndLogProps(thing, ["bar", "baz"], "err", dummyLog); + test.assert((thing.bar() == thing.baz()) && + (thing.bar() == "err"), + "multiple props should be wrapped if array passed in"); +}; + +exports.testCatchAndReturn = function(test) { + var wrapped = errors.catchAndReturn(function(x) { + if (x == 1) + return "one"; + if (x == 2) + throw new Error("two"); + return this + x; + }); + + test.assertEqual(wrapped(1).returnValue, "one", + "arg should be passed; return value should be returned"); + test.assert(wrapped(2).exception, "exception should be returned"); + test.assertEqual(wrapped(2).exception.message, "two", "message is correct"); + test.assert(wrapped(2).exception.fileName.indexOf("test-errors.js") != -1, + "filename is present"); + test.assert(wrapped(2).exception.stack, "stack is available"); + test.assertEqual(wrapped.call("hi", 3).returnValue, "hi3", + "`this` should be set correctly"); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-event-core.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-event-core.js new file mode 100644 index 0000000..4b664ff --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-event-core.js @@ -0,0 +1,217 @@ +/* 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 { on, once, off, emit, count, amass } = require('api-utils/event/core'); +const { Loader } = require('./helpers'); + +exports['test add a listener'] = function(assert) { + let events = [ { name: 'event#1' }, 'event#2' ]; + let target = { name: 'target' }; + + on(target, 'message', function(message) { + assert.equal(this, target, 'this is a target object'); + assert.equal(message, events.shift(), 'message is emitted event'); + }); + emit(target, 'message', events[0]); + emit(target, 'message', events[0]); +}; + +exports['test that listener is unique per type'] = function(assert) { + let actual = [] + let target = {} + function listener() { actual.push(1) } + on(target, 'message', listener); + on(target, 'message', listener); + on(target, 'message', listener); + on(target, 'foo', listener); + on(target, 'foo', listener); + + emit(target, 'message'); + assert.deepEqual([ 1 ], actual, 'only one message listener added'); + emit(target, 'foo'); + assert.deepEqual([ 1, 1 ], actual, 'same listener added for other event'); +}; + +exports['test event type matters'] = function(assert) { + let target = { name: 'target' } + on(target, 'message', function() { + assert.fail('no event is expected'); + }); + on(target, 'done', function() { + assert.pass('event is emitted'); + }); + emit(target, 'foo') + emit(target, 'done'); +}; + +exports['test all arguments are pasesd'] = function(assert) { + let foo = { name: 'foo' }, bar = 'bar'; + let target = { name: 'target' }; + on(target, 'message', function(a, b) { + assert.equal(a, foo, 'first argument passed'); + assert.equal(b, bar, 'second argument passed'); + }); + emit(target, 'message', foo, bar); +}; + +exports['test no side-effects in emit'] = function(assert) { + let target = { name: 'target' }; + on(target, 'message', function() { + assert.pass('first listener is called'); + on(target, 'message', function() { + assert.fail('second listener is called'); + }); + }); + emit(target, 'message'); +}; + +exports['test order of propagation'] = function(assert) { + let actual = []; + let target = { name: 'target' }; + on(target, 'message', function() { actual.push(1); }); + on(target, 'message', function() { actual.push(2); }); + on(target, 'message', function() { actual.push(3); }); + emit(target, 'message'); + assert.deepEqual([ 1, 2, 3 ], actual, 'called in order they were added'); +}; + +exports['test remove a listener'] = function(assert) { + let target = { name: 'target' }; + let actual = []; + on(target, 'message', function listener() { + actual.push(1); + on(target, 'message', function() { + off(target, 'message', listener); + actual.push(2); + }) + }); + + emit(target, 'message'); + assert.deepEqual([ 1 ], actual, 'first listener called'); + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2 ], actual, 'second listener called'); + + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2, 2, 2 ], actual, 'first listener removed'); +}; + +exports['test remove all listeners for type'] = function(assert) { + let actual = []; + let target = { name: 'target' } + on(target, 'message', function() { actual.push(1); }); + on(target, 'message', function() { actual.push(2); }); + on(target, 'message', function() { actual.push(3); }); + on(target, 'bar', function() { actual.push('b') }); + off(target, 'message'); + + emit(target, 'message'); + emit(target, 'bar'); + + assert.deepEqual([ 'b' ], actual, 'all message listeners were removed'); +}; + +exports['test remove all listeners'] = function(assert) { + let actual = []; + let target = { name: 'target' } + on(target, 'message', function() { actual.push(1); }); + on(target, 'message', function() { actual.push(2); }); + on(target, 'message', function() { actual.push(3); }); + on(target, 'bar', function() { actual.push('b') }); + off(target); + + emit(target, 'message'); + emit(target, 'bar'); + + assert.deepEqual([], actual, 'all listeners events were removed'); +}; + +exports['test falsy arguments are fine'] = function(assert) { + let type, listener, actual = []; + let target = { name: 'target' } + on(target, 'bar', function() { actual.push(0) }); + + off(target, 'bar', listener); + emit(target, 'bar'); + assert.deepEqual([ 0 ], actual, '3rd bad ard will keep listeners'); + + off(target, type); + emit(target, 'bar'); + assert.deepEqual([ 0, 0 ], actual, '2nd bad arg will keep listener'); + + off(target, type, listener); + emit(target, 'bar'); + assert.deepEqual([ 0, 0, 0 ], actual, '2nd&3rd bad args will keep listener'); +}; + +exports['test error handling'] = function(assert) { + let target = Object.create(null); + let error = Error('boom!'); + + on(target, 'message', function() { throw error; }) + on(target, 'error', function(boom) { + assert.equal(boom, error, 'thrown exception causes error event'); + }); + emit(target, 'message'); +}; + +exports['test unhandled errors'] = function(assert) { + let exceptions = []; + let loader = Loader(module); + let { emit, on } = loader.require('api-utils/event/core'); + Object.defineProperties(loader.sandbox('api-utils/event/core'), { + console: { value: { + exception: function(e) { + exceptions.push(e) + } + }} + }); + let target = {}; + let boom = Error('Boom!'); + let drax = Error('Draax!!'); + + on(target, 'message', function() { throw boom; }); + + emit(target, 'message'); + assert.ok(~String(exceptions[0]).indexOf('Boom!'), + 'unhandled exception is logged'); + + on(target, 'error', function() { throw drax; }); + emit(target, 'message'); + assert.ok(~String(exceptions[1]).indexOf('Draax!'), + 'error in error handler is logged'); +}; + +exports['test count'] = function(assert) { + let target = {}; + + assert.equal(count(target, 'foo'), 0, 'no listeners for "foo" events'); + on(target, 'foo', function() {}); + assert.equal(count(target, 'foo'), 1, 'listener registered'); + on(target, 'foo', function() {}, 2, 'another listener registered'); + off(target) + assert.equal(count(target, 'foo'), 0, 'listeners unregistered'); +}; + +exports['test emit.lazy'] = function(assert) { + let target = {}, boom = Error('boom!'), errors = [], actual = [] + + on(target, 'error', function error(e) errors.push(e)) + + on(target, 'a', function() 1); + on(target, 'a', function() {}); + on(target, 'a', function() 2); + on(target, 'a', function() { throw boom }); + on(target, 'a', function() 3); + + for each (let value in emit.lazy(target, 'a')) + actual.push(value); + + assert.deepEqual(actual, [ 1, undefined, 2, 3 ], + 'all results were collected'); + assert.deepEqual(errors, [ boom ], 'errors reporetd'); +}; + +require('test').run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-event-target.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-event-target.js new file mode 100644 index 0000000..b8960e5 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-event-target.js @@ -0,0 +1,167 @@ +/* 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 { emit } = require('api-utils/event/core'); +const { EventTarget } = require('api-utils/event/target'); +const { Loader } = require('./helpers'); + +exports['test add a listener'] = function(assert) { + let events = [ { name: 'event#1' }, 'event#2' ]; + let target = EventTarget.new(); + + target.on('message', function(message) { + assert.equal(this, target, 'this is a target object'); + assert.equal(message, events.shift(), 'message is emitted event'); + }); + + emit(target, 'message', events[0]); + emit(target, 'message', events[0]); +}; + +exports['test pass in listeners'] = function(assert) { + let actual = [ ]; + let target = EventTarget.new({ + onMessage: function onMessage(message) { + assert.equal(this, target, 'this is an event target'); + actual.push(1); + }, + onFoo: null, + onbla: function() { + assert.fail('`onbla` is not supposed to be called'); + } + }); + target.on('message', function(message) { + assert.equal(this, target, 'this is an event target'); + actual.push(2); + }); + + emit(target, 'message'); + emit(target, 'missing'); + + assert.deepEqual([ 1, 2 ], actual, 'all listeners trigerred in right order'); +}; + +exports['test that listener is unique per type'] = function(assert) { + let actual = [] + let target = EventTarget.new(); + function listener() { actual.push(1) } + target.on('message', listener); + target.on('message', listener); + target.on('message', listener); + target.on('foo', listener); + target.on('foo', listener); + + emit(target, 'message'); + assert.deepEqual([ 1 ], actual, 'only one message listener added'); + emit(target, 'foo'); + assert.deepEqual([ 1, 1 ], actual, 'same listener added for other event'); +}; + +exports['test event type matters'] = function(assert) { + let target = EventTarget.new(); + target.on('message', function() { + assert.fail('no event is expected'); + }); + target.on('done', function() { + assert.pass('event is emitted'); + }); + + emit(target, 'foo'); + emit(target, 'done'); +}; + +exports['test all arguments are pasesd'] = function(assert) { + let foo = { name: 'foo' }, bar = 'bar'; + let target = EventTarget.new(); + target.on('message', function(a, b) { + assert.equal(a, foo, 'first argument passed'); + assert.equal(b, bar, 'second argument passed'); + }); + emit(target, 'message', foo, bar); +}; + +exports['test no side-effects in emit'] = function(assert) { + let target = EventTarget.new(); + target.on('message', function() { + assert.pass('first listener is called'); + target.on('message', function() { + assert.fail('second listener is called'); + }); + }); + emit(target, 'message'); +}; + +exports['test order of propagation'] = function(assert) { + let actual = []; + let target = EventTarget.new(); + target.on('message', function() { actual.push(1); }); + target.on('message', function() { actual.push(2); }); + target.on('message', function() { actual.push(3); }); + emit(target, 'message'); + assert.deepEqual([ 1, 2, 3 ], actual, 'called in order they were added'); +}; + +exports['test remove a listener'] = function(assert) { + let target = EventTarget.new(); + let actual = []; + target.on('message', function listener() { + actual.push(1); + target.on('message', function() { + target.removeListener('message', listener); + actual.push(2); + }) + }); + + target.removeListener('message'); // must do nothing. + emit(target, 'message'); + assert.deepEqual([ 1 ], actual, 'first listener called'); + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2 ], actual, 'second listener called'); + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2, 2, 2 ], actual, 'first listener removed'); +}; + +exports['test error handling'] = function(assert) { + let target = EventTarget.new(); + let error = Error('boom!'); + + target.on('message', function() { throw error; }) + target.on('error', function(boom) { + assert.equal(boom, error, 'thrown exception causes error event'); + }); + emit(target, 'message'); +}; + +exports['test unhandled errors'] = function(assert) { + let exceptions = []; + let loader = Loader(module); + let { emit } = loader.require('api-utils/event/core'); + let { EventTarget } = loader.require('api-utils/event/target'); + Object.defineProperties(loader.sandbox('api-utils/event/core'), { + console: { value: { + exception: function(e) { + exceptions.push(e); + } + }} + }); + let target = EventTarget.new(); + let boom = Error('Boom!'); + let drax = Error('Draax!!'); + + target.on('message', function() { throw boom; }); + + emit(target, 'message'); + assert.ok(~String(exceptions[0]).indexOf('Boom!'), + 'unhandled exception is logged'); + + target.on('error', function() { throw drax; }); + emit(target, 'message'); + assert.ok(~String(exceptions[1]).indexOf('Draax!'), + 'error in error handler is logged'); +}; + +require('test').run(exports); + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-events.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-events.js new file mode 100644 index 0000000..3fe6f03 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-events.js @@ -0,0 +1,267 @@ +/* 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'; + +// Exposing private methods as public in order to test +const EventEmitter = require('events').EventEmitter.compose({ + listeners: function(type) this._listeners(type), + emit: function() this._emit.apply(this, arguments), + emitOnObject: function() this._emitOnObject.apply(this, arguments), + removeAllListeners: function(type) this._removeAllListeners(type) +}); + +exports['test:add listeners'] = function(test) { + let e = new EventEmitter(); + + let events_new_listener_emited = []; + let times_hello_emited = 0; + + e.on("newListener", function (event, listener) { + events_new_listener_emited.push(event) + }) + + e.on("hello", function (a, b) { + times_hello_emited += 1 + test.assertEqual("a", a) + test.assertEqual("b", b) + test.assertEqual(this, e, '`this` pseudo-variable is bound to instance'); + }) + + e.emit("hello", "a", "b") +}; + +exports['test:removeListener'] = function(test) { + let count = 0; + + function listener1 () { + count++; + } + function listener2 () { + count++; + } + + // test adding and removing listener + let e1 = new EventEmitter(); + test.assertEqual(0, e1.listeners('hello').length); + e1.on("hello", listener1); + test.assertEqual(1, e1.listeners('hello').length); + test.assertEqual(listener1, e1.listeners('hello')[0]); + e1.removeListener("hello", listener1); + test.assertEqual(0, e1.listeners('hello').length); + e1.emit("hello", ""); + test.assertEqual(0, count); + + // test adding one listener and removing another which was not added + let e2 = new EventEmitter(); + test.assertEqual(0, e2.listeners('hello').length); + e2.on("hello", listener1); + test.assertEqual(1, e2.listeners('hello').length); + e2.removeListener("hello", listener2); + test.assertEqual(1, e2.listeners('hello').length); + test.assertEqual(listener1, e2.listeners('hello')[0]); + e2.emit("hello", ""); + test.assertEqual(1, count); + + // test adding 2 listeners, and removing one + let e3 = new EventEmitter(); + test.assertEqual(0, e3.listeners('hello').length); + e3.on("hello", listener1); + test.assertEqual(1, e3.listeners('hello').length); + e3.on("hello", listener2); + test.assertEqual(2, e3.listeners('hello').length); + e3.removeListener("hello", listener1); + test.assertEqual(1, e3.listeners('hello').length); + test.assertEqual(listener2, e3.listeners('hello')[0]); + e3.emit("hello", ""); + test.assertEqual(2, count); +}; + +exports['test:removeAllListeners'] = function(test) { + let count = 0; + + function listener1 () { + count++; + } + function listener2 () { + count++; + } + + // test adding a listener and removing all of that type + let e1 = new EventEmitter(); + e1.on("hello", listener1); + test.assertEqual(1, e1.listeners('hello').length); + e1.removeAllListeners("hello"); + test.assertEqual(0, e1.listeners('hello').length); + e1.emit("hello", ""); + test.assertEqual(0, count); + + // test adding a listener and removing all of another type + let e2 = new EventEmitter(); + e2.on("hello", listener1); + test.assertEqual(1, e2.listeners('hello').length); + e2.removeAllListeners('goodbye'); + test.assertEqual(1, e2.listeners('hello').length); + e2.emit("hello", ""); + test.assertEqual(1, count); + + // test adding 1+ listeners and removing all of that type + let e3 = new EventEmitter(); + e3.on("hello", listener1); + test.assertEqual(1, e3.listeners('hello').length); + e3.on("hello", listener2); + test.assertEqual(2, e3.listeners('hello').length); + e3.removeAllListeners("hello"); + test.assertEqual(0, e3.listeners('hello').length); + e3.emit("hello", ""); + test.assertEqual(1, count); + + // test adding 2 listeners for 2 types and removing all listeners + let e4 = new EventEmitter(); + e4.on("hello", listener1); + test.assertEqual(1, e4.listeners('hello').length); + e4.on('goodbye', listener2); + test.assertEqual(1, e4.listeners('goodbye').length); + e4.emit("goodbye", ""); + e4.removeAllListeners(); + test.assertEqual(0, e4.listeners('hello').length); + test.assertEqual(0, e4.listeners('goodbye').length); + e4.emit("hello", ""); + e4.emit("goodbye", ""); + test.assertEqual(2, count); +}; + +exports['test: modify in emit'] = function(test) { + let callbacks_called = [ ]; + let e = new EventEmitter(); + + function callback1() { + callbacks_called.push("callback1"); + e.on("foo", callback2); + e.on("foo", callback3); + e.removeListener("foo", callback1); + } + function callback2() { + callbacks_called.push("callback2"); + e.removeListener("foo", callback2); + } + function callback3() { + callbacks_called.push("callback3"); + e.removeListener("foo", callback3); + } + + e.on("foo", callback1); + test.assertEqual(1, e.listeners("foo").length); + + e.emit("foo"); + test.assertEqual(2, e.listeners("foo").length); + test.assertEqual(1, callbacks_called.length); + test.assertEqual('callback1', callbacks_called[0]); + + e.emit("foo"); + test.assertEqual(0, e.listeners("foo").length); + test.assertEqual(3, callbacks_called.length); + test.assertEqual('callback1', callbacks_called[0]); + test.assertEqual('callback2', callbacks_called[1]); + test.assertEqual('callback3', callbacks_called[2]); + + e.emit("foo"); + test.assertEqual(0, e.listeners("foo").length); + test.assertEqual(3, callbacks_called.length); + test.assertEqual('callback1', callbacks_called[0]); + test.assertEqual('callback2', callbacks_called[1]); + test.assertEqual('callback3', callbacks_called[2]); + + e.on("foo", callback1); + e.on("foo", callback2); + test.assertEqual(2, e.listeners("foo").length); + e.removeAllListeners("foo"); + test.assertEqual(0, e.listeners("foo").length); + + // Verify that removing callbacks while in emit allows emits to propagate to + // all listeners + callbacks_called = [ ]; + + e.on("foo", callback2); + e.on("foo", callback3); + test.assertEqual(2, e.listeners("foo").length); + e.emit("foo"); + test.assertEqual(2, callbacks_called.length); + test.assertEqual('callback2', callbacks_called[0]); + test.assertEqual('callback3', callbacks_called[1]); + test.assertEqual(0, e.listeners("foo").length); +}; + +exports['test:adding same listener'] = function(test) { + function foo() {} + let e = new EventEmitter(); + e.on("foo", foo); + e.on("foo", foo); + test.assertEqual( + 1, + e.listeners("foo").length, + "listener reregistration is ignored" + ); +} + +exports['test:errors are reported if listener throws'] = function(test) { + let e = new EventEmitter(), + reported = false; + e.on('error', function(e) reported = true) + e.on('boom', function() { throw new Error('Boom!') }); + e.emit('boom', 3); + test.assert(reported, 'error should be reported through event'); +}; + +exports['test:emitOnObject'] = function(test) { + let e = new EventEmitter(); + + e.on("foo", function() { + test.assertEqual(this, e, "`this` should be emitter"); + }); + e.emitOnObject(e, "foo"); + + e.on("bar", function() { + test.assertEqual(this, obj, "`this` should be other object"); + }); + let obj = {}; + e.emitOnObject(obj, "bar"); +}; + +exports['test:once'] = function(test) { + let e = new EventEmitter(); + let called = false; + + e.once('foo', function(value) { + test.assert(!called, "listener called only once"); + test.assertEqual(value, "bar", "correct argument was passed"); + }); + + e.emit('foo', 'bar'); + e.emit('foo', 'baz'); +}; + +exports["test:removing once"] = function(test) { + let e = require("events").EventEmitterTrait.create(); + e.once("foo", function() { test.pass("listener was called"); }); + e.once("error", function() { test.fail("error event was emitted"); }); + e._emit("foo", "bug-656684"); +}; + +// Bug 726967: Ensure that `emit` doesn't do an infinite loop when `error` +// listener throws an exception +exports['test:emitLoop'] = function(test) { + let e = new EventEmitter(); + + e.on("foo", function() { + throw new Error("foo"); + }); + + e.on("error", function() { + throw new Error("error"); + }); + e.emit("foo"); + + test.pass("emit didn't looped"); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-file.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-file.js new file mode 100644 index 0000000..de89db8 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-file.js @@ -0,0 +1,273 @@ +/* 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 { pathFor } = require('api-utils/system'); +const file = require("api-utils/file"); +const url = require("api-utils/url"); + +const byteStreams = require("api-utils/byte-streams"); +const textStreams = require("api-utils/text-streams"); + +const ERRORS = { + FILE_NOT_FOUND: /^path does not exist: .+$/, + NOT_A_DIRECTORY: /^path is not a directory: .+$/, + NOT_A_FILE: /^path is not a file: .+$/, +}; + +// Use profile directory to list / read / write files. +const profilePath = pathFor('ProfD'); +const fileNameInProfile = 'compatibility.ini'; +const dirNameInProfile = 'extensions'; +const filePathInProfile = file.join(profilePath, fileNameInProfile); +const dirPathInProfile = file.join(profilePath, dirNameInProfile); + +exports.testDirName = function(test) { + test.assertEqual(file.dirname(dirPathInProfile), profilePath, + "file.dirname() of dir should return parent dir"); + + test.assertEqual(file.dirname(filePathInProfile), profilePath, + "file.dirname() of file should return its dir"); + + let dir = profilePath; + while (dir) + dir = file.dirname(dir); + + test.assertEqual(dir, "", + "dirname should return empty string when dir has no parent"); +}; + +exports.testBasename = function(test) { + // Get the top-most path -- the path with no basename. E.g., on Unix-like + // systems this will be /. We'll use it below to build up some test paths. + // We have to go to this trouble because file.join() needs a legal path as a + // base case; join("foo", "bar") doesn't work unfortunately. + let topPath = profilePath; + let parentPath = file.dirname(topPath); + while (parentPath) { + topPath = parentPath; + parentPath = file.dirname(topPath); + } + + let path = topPath; + test.assertEqual(file.basename(path), "", + "basename should work on paths with no components"); + + path = file.join(path, "foo"); + test.assertEqual(file.basename(path), "foo", + "basename should work on paths with a single component"); + + path = file.join(path, "bar"); + test.assertEqual(file.basename(path), "bar", + "basename should work on paths with multiple components"); +}; + +exports.testList = function(test) { + let list = file.list(profilePath); + let found = [ true for each (name in list) + if (name === fileNameInProfile) ]; + + if (found.length > 1) + test.fail("a dir can't contain two files of the same name!"); + test.assertEqual(found[0], true, "file.list() should work"); + + test.assertRaises(function() { + file.list(filePathInProfile); + }, ERRORS.NOT_A_DIRECTORY, "file.list() on non-dir should raise error"); + + test.assertRaises(function() { + file.list(file.join(dirPathInProfile, "does-not-exist")); + }, ERRORS.FILE_NOT_FOUND, "file.list() on nonexistent should raise error"); +}; + +exports.testRead = function(test) { + let contents = file.read(filePathInProfile); + test.assertMatches(contents, /Compatibility/, + "file.read() should work"); + + test.assertRaises(function() { + file.read(file.join(dirPathInProfile, "does-not-exists")); + }, ERRORS.FILE_NOT_FOUND, "file.read() on nonexistent file should throw"); + + test.assertRaises(function() { + file.read(dirPathInProfile); + }, ERRORS.NOT_A_FILE, "file.read() on dir should throw"); +}; + +exports.testJoin = function(test) { + let baseDir = file.dirname(filePathInProfile); + + test.assertEqual(file.join(baseDir, fileNameInProfile), + filePathInProfile, "file.join() should work"); +}; + +exports.testOpenNonexistentForRead = function (test) { + var filename = file.join(profilePath, 'does-not-exists'); + test.assertRaises(function() { + file.open(filename); + }, ERRORS.FILE_NOT_FOUND, "file.open() on nonexistent file should throw"); + + test.assertRaises(function() { + file.open(filename, "r"); + }, ERRORS.FILE_NOT_FOUND, "file.open('r') on nonexistent file should throw"); + + test.assertRaises(function() { + file.open(filename, "zz"); + }, ERRORS.FILE_NOT_FOUND, "file.open('zz') on nonexistent file should throw"); +}; + +exports.testOpenNonexistentForWrite = function (test) { + let filename = file.join(profilePath, 'open.txt'); + + let stream = file.open(filename, "w"); + stream.close(); + + test.assert(file.exists(filename), + "file.exists() should return true after file.open('w')"); + file.remove(filename); + test.assert(!file.exists(filename), + "file.exists() should return false after file.remove()"); + + stream = file.open(filename, "rw"); + stream.close(); + + test.assert(file.exists(filename), + "file.exists() should return true after file.open('rw')"); + file.remove(filename); + test.assert(!file.exists(filename), + "file.exists() should return false after file.remove()"); +}; + +exports.testOpenDirectory = function (test) { + let dir = dirPathInProfile; + test.assertRaises(function() { + file.open(dir); + }, ERRORS.NOT_A_FILE, "file.open() on directory should throw"); + + test.assertRaises(function() { + file.open(dir, "w"); + }, ERRORS.NOT_A_FILE, "file.open('w') on directory should throw"); +}; + +exports.testOpenTypes = function (test) { + let filename = file.join(profilePath, 'open-types.txt'); + + + // Do the opens first to create the data file. + var stream = file.open(filename, "w"); + test.assert(stream instanceof textStreams.TextWriter, + "open(w) should return a TextWriter"); + stream.close(); + + stream = file.open(filename, "wb"); + test.assert(stream instanceof byteStreams.ByteWriter, + "open(wb) should return a ByteWriter"); + stream.close(); + + stream = file.open(filename); + test.assert(stream instanceof textStreams.TextReader, + "open() should return a TextReader"); + stream.close(); + + stream = file.open(filename, "r"); + test.assert(stream instanceof textStreams.TextReader, + "open(r) should return a TextReader"); + stream.close(); + + stream = file.open(filename, "b"); + test.assert(stream instanceof byteStreams.ByteReader, + "open(b) should return a ByteReader"); + stream.close(); + + stream = file.open(filename, "rb"); + test.assert(stream instanceof byteStreams.ByteReader, + "open(rb) should return a ByteReader"); + stream.close(); + + file.remove(filename); +}; + +exports.testMkpathRmdir = function (test) { + let basePath = profilePath; + let dirs = []; + for (let i = 0; i < 3; i++) + dirs.push("test-file-dir"); + + let paths = []; + for (let i = 0; i < dirs.length; i++) { + let args = [basePath].concat(dirs.slice(0, i + 1)); + paths.unshift(file.join.apply(null, args)); + } + + for (let i = 0; i < paths.length; i++) { + test.assert(!file.exists(paths[i]), + "Sanity check: path should not exist: " + paths[i]); + } + + file.mkpath(paths[0]); + test.assert(file.exists(paths[0]), "mkpath should create path: " + paths[0]); + + for (let i = 0; i < paths.length; i++) { + file.rmdir(paths[i]); + test.assert(!file.exists(paths[i]), + "rmdir should remove path: " + paths[i]); + } +}; + +exports.testMkpathTwice = function (test) { + let dir = profilePath; + let path = file.join(dir, "test-file-dir"); + test.assert(!file.exists(path), + "Sanity check: path should not exist: " + path); + file.mkpath(path); + test.assert(file.exists(path), "mkpath should create path: " + path); + file.mkpath(path); + test.assert(file.exists(path), + "After second mkpath, path should still exist: " + path); + file.rmdir(path); + test.assert(!file.exists(path), "rmdir should remove path: " + path); +}; + +exports.testMkpathExistingNondirectory = function (test) { + var fname = file.join(profilePath, 'conflict.txt'); + file.open(fname, "w").close(); + test.assert(file.exists(fname), "File should exist"); + test.assertRaises(function() file.mkpath(fname), + /^The path already exists and is not a directory: .+$/, + "mkpath on file should raise error"); + file.remove(fname); +}; + +exports.testRmdirNondirectory = function (test) { + var fname = file.join(profilePath, 'not-a-dir') + file.open(fname, "w").close(); + test.assert(file.exists(fname), "File should exist"); + test.assertRaises(function() { + file.rmdir(fname); + }, ERRORS.NOT_A_DIRECTORY, "rmdir on file should raise error"); + file.remove(fname); + test.assert(!file.exists(fname), "File should not exist"); + test.assertRaises(function () file.rmdir(fname), + ERRORS.FILE_NOT_FOUND, + "rmdir on non-existing file should raise error"); +}; + +exports.testRmdirNonempty = function (test) { + let dir = profilePath; + let path = file.join(dir, "test-file-dir"); + test.assert(!file.exists(path), + "Sanity check: path should not exist: " + path); + file.mkpath(path); + let filePath = file.join(path, "file"); + file.open(filePath, "w").close(); + test.assert(file.exists(filePath), + "Sanity check: path should exist: " + filePath); + test.assertRaises(function () file.rmdir(path), + /^The directory is not empty: .+$/, + "rmdir on non-empty directory should raise error"); + file.remove(filePath); + file.rmdir(path); + test.assert(!file.exists(path), "Path should not exist"); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-frame-utils.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-frame-utils.js new file mode 100644 index 0000000..268643c --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-frame-utils.js @@ -0,0 +1,67 @@ +/* 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 { open } = require('api-utils/window/utils'); +const { create } = require('api-utils/frame/utils'); + +exports['test frame creation'] = function(assert) { + let window = open('data:text/html,Window'); + let frame = create(window.document); + + assert.equal(frame.getAttribute('type'), 'content', + 'frame type is content'); + assert.ok(frame.contentWindow, 'frame has contentWindow'); + assert.equal(frame.contentWindow.location.href, 'about:blank', + 'by default "about:blank" is loaded'); + assert.equal(frame.docShell.allowAuth, false, 'auth disabled by default'); + assert.equal(frame.docShell.allowJavascript, false, 'js disabled by default'); + assert.equal(frame.docShell.allowPlugins, false, + 'plugins disabled by default'); + window.close(); +}; + +exports['test fram has js disabled by default'] = function(assert, done) { + let window = open('data:text/html,window'); + window.addEventListener('DOMContentLoaded', function windowReady() { + window.removeEventListener('DOMContentLoaded', windowReady, false); + let frame = create(window.document, { + uri: 'data:text/html,<script>document.documentElement.innerHTML' + + '= "J" + "S"</script>', + }); + frame.contentWindow.addEventListener('DOMContentLoaded', function ready() { + frame.contentWindow.removeEventListener('DOMContentLoaded', ready, false); + assert.ok(!~frame.contentDocument.documentElement.innerHTML.indexOf('JS'), + 'JS was executed'); + + window.close(); + done(); + }, false); + + }, false); +}; + +exports['test frame with js enabled'] = function(assert, done) { + let window = open('data:text/html,window'); + window.addEventListener('DOMContentLoaded', function windowReady() { + window.removeEventListener('DOMContentLoaded', windowReady, false); + let frame = create(window.document, { + uri: 'data:text/html,<script>document.documentElement.innerHTML' + + '= "J" + "S"</script>', + allowJavascript: true + }); + frame.contentWindow.addEventListener('DOMContentLoaded', function ready() { + frame.contentWindow.removeEventListener('DOMContentLoaded', ready, false); + assert.ok(~frame.contentDocument.documentElement.innerHTML.indexOf('JS'), + 'JS was executed'); + + window.close(); + done(); + }, false); + + }, false); +}; + +require('test').run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-functional.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-functional.js new file mode 100644 index 0000000..377a709 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-functional.js @@ -0,0 +1,170 @@ +/* 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/. */ + +const { setTimeout } = require('api-utils/timer'); +const utils = require('api-utils/functional'); +const { invoke, defer, curry, compose, memoize, once, delay, wrap } = utils; + +exports['test forwardApply'] = function(assert) { + function sum(b, c) this.a + b + c + assert.equal(invoke(sum, [2, 3], { a: 1 }), 6, + 'passed arguments and pseoude-variable are used'); + + assert.equal(invoke(sum.bind({ a: 2 }), [2, 3], { a: 1 }), 7, + 'bounded `this` pseoudo variable is used'); +} + +exports['test deferred function'] = function(assert, done) { + let nextTurn = false; + function sum(b, c) { + assert.ok(nextTurn, 'enqueued is called in next turn of event loop'); + assert.equal(this.a + b + c, 6, + 'passed arguments an pseoude-variable are used'); + done(); + } + + let fixture = { a: 1, method: defer(sum) } + fixture.method(2, 3); + nextTurn = true; +}; + +exports['test curry function'] = function(assert) { + function sum(b, c) this.a + b + c; + + let foo = { a : 5 }; + + foo.sum7 = curry(sum, 7); + foo.sum8and4 = curry(sum, 8, 4); + + assert.equal(foo.sum7(2), 14, 'curry one arguments works'); + + assert.equal(foo.sum8and4(), 17, 'curry both arguments works'); +}; + +exports['test compose'] = function(assert) { + let greet = function(name) { return 'hi: ' + name; }; + let exclaim = function(sentence) { return sentence + '!'; }; + + assert.equal(compose(exclaim, greet)('moe'), 'hi: moe!', + 'can compose a function that takes another'); + + assert.equal(compose(greet, exclaim)('moe'), 'hi: moe!', + 'in this case, the functions are also commutative'); + + let target = { + name: 'Joe', + greet: compose(function exclaim(sentence) { + return sentence + '!' + }, function(title) { + return 'hi : ' + title + ' ' + this.name; + }) + } + + assert.equal(target.greet('Mr'), 'hi : Mr Joe!', + 'this can be passed in'); + assert.equal(target.greet.call({ name: 'Alex' }, 'Dr'), 'hi : Dr Alex!', + 'this can be applied'); + + let single = compose(function(value) { + return value + ':suffix'; + }); + + assert.equal(single('text'), 'text:suffix', 'works with single function'); + + let identity = compose(); + assert.equal(identity('bla'), 'bla', 'works with zero functions'); +}; + +exports['test wrap'] = function(assert) { + let greet = function(name) { return 'hi: ' + name; }; + let backwards = wrap(greet, function(f, name) { + return f(name) + ' ' + name.split('').reverse().join(''); + }); + + assert.equal(backwards('moe'), 'hi: moe eom', + 'wrapped the saluation function'); + + let inner = function () { return 'Hello '; }; + let target = { + name: 'Matteo', + hi: wrap(inner, function(f) { return f() + this.name; }) + }; + + assert.equal(target.hi(), 'Hello Matteo', 'works with this'); + + function noop() { }; + let wrapped = wrap(noop, function(f) { + return Array.slice(arguments); + }); + + let actual = wrapped([ 'whats', 'your' ], 'vector', 'victor'); + assert.deepEqual(actual, [ noop, ['whats', 'your'], 'vector', 'victor' ], + 'works with fancy stuff'); +}; + +exports['test memoize'] = function(assert) { + function fib(n) n < 2 ? n : fib(n - 1) + fib(n - 2) + let fibnitro = memoize(fib); + + assert.equal(fib(10), 55, + 'a memoized version of fibonacci produces identical results'); + assert.equal(fibnitro(10), 55, + 'a memoized version of fibonacci produces identical results'); + + function o(key, value) { return value; }; + let oo = memoize(o), v1 = {}, v2 = {}; + + + assert.equal(oo(1, v1), v1, 'returns value back'); + assert.equal(oo(1, v2), v1, 'memoized by a first argument'); + assert.equal(oo(2, v2), v2, 'returns back value if not memoized'); + assert.equal(oo(2), v2, 'memoized new value'); + assert.notEqual(oo(1), oo(2), 'values do not override'); + assert.equal(o(3, v2), oo(2, 3), 'returns same value as un-memoized'); + + let get = memoize(function(attribute) this[attribute]) + let target = { name: 'Bob', get: get } + + assert.equal(target.get('name'), 'Bob', 'has correct `this`'); + assert.equal(target.get.call({ name: 'Jack' }, 'name'), 'Bob', + 'name is memoized') + assert.equal(get('name'), 'Bob', 'once memoized can be called without this'); +}; + +exports['test delay'] = function(assert, done) { + let delayed = false; + delay(function() { + assert.ok(delayed, 'delayed the function'); + done(); + }, 1); + delayed = true; +}; + +exports['test delay with this'] = function(assert, done) { + let context = {} + delay.call(context, function(name) { + assert.equal(this, context, 'this was passed in'); + assert.equal(name, 'Tom', 'argument was passed in'); + done(); + }, 10, 'Tom'); +} + +exports['test once'] = function(assert) { + let n = 0; + let increment = once(function() { n ++; }); + + increment(); + increment(); + + assert.equal(n, 1, 'only incremented once'); + + let target = { state: 0, update: once(function() this.state ++ ) }; + + target.update(); + target.update(); + + assert.equal(target.state, 1, 'this was passed in and called only once'); +}; + +require('test').run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-globals.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-globals.js new file mode 100644 index 0000000..1d8caf0 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-globals.js @@ -0,0 +1,22 @@ +/* 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/. */ + +Object.defineProperty(this, "global", { value: this }); + +exports.testGlobals = function(test) { + // the only globals in module scope should be: + // module, exports, require, dump, console + test.assertObject(module, "have 'module', good"); + test.assertObject(exports, "have 'exports', good"); + test.assertFunction(require, "have 'require', good"); + test.assertFunction(dump, "have 'dump', good"); + test.assertObject(console, "have 'console', good"); + + // in particular, these old globals should no longer be present + test.assert(!('packaging' in global), "no 'packaging', good"); + test.assert(!('memory' in global), "no 'memory', good"); + + test.assertMatches(module.uri, /test-globals\.js$/, + 'should contain filename'); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-hidden-frame.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-hidden-frame.js new file mode 100644 index 0000000..2441c3e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-hidden-frame.js @@ -0,0 +1,51 @@ +/* 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/. */ + +let tests = {}, hiddenFrames, HiddenFrame; + +tests.testFrame = function(test) { + let url = "data:text/html,<!DOCTYPE%20html>"; + test.waitUntilDone(); + let hiddenFrame = hiddenFrames.add(HiddenFrame({ + onReady: function () { + test.assertEqual(this.element.contentWindow.location, "about:blank", + "HiddenFrame loads about:blank by default."); + + function onDOMReady() { + hiddenFrame.element.removeEventListener("DOMContentLoaded", onDOMReady, + false); + test.assertEqual(hiddenFrame.element.contentWindow.location, url, + "HiddenFrame loads the specified content."); + test.done(); + } + this.element.addEventListener("DOMContentLoaded", onDOMReady, false); + this.element.setAttribute("src", url); + } + })); +}; + +let hiddenFrameSupported = true; + +try { + hiddenFrames = require("hidden-frame"); + HiddenFrame = hiddenFrames.HiddenFrame; +} +catch(ex if ex.message == [ + "The hidden-frame module currently supports only Firefox and Thunderbird. ", + "In the future, we would like it to support other applications, however. ", + "Please see https://bugzilla.mozilla.org/show_bug.cgi?id=546740 for more ", + "information." + ].join("")) { + hiddenFrameSupported = false; +} + +if (hiddenFrameSupported) { + for (let test in tests) + exports[test] = tests[test]; +} +else { + exports.testHiddenFrameNotSupported = function(test) { + test.pass("The hidden-frame module is not supported on this app."); + } +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-httpd.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-httpd.js new file mode 100644 index 0000000..dc1adf1 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-httpd.js @@ -0,0 +1,72 @@ +/* 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/. */ + +const port = 8099; +const file = require("api-utils/file"); +const { pathFor } = require("api-utils/system"); + +exports.testBasicHTTPServer = function(test) { + let basePath = pathFor("TmpD"); + let filePath = file.join(basePath, 'test-httpd.txt'); + let content = "This is the HTTPD test file.\n"; + let fileStream = file.open(filePath, 'w'); + fileStream.write(content); + fileStream.close(); + + let { startServerAsync } = require("httpd"); + let srv = startServerAsync(port, basePath); + + test.waitUntilDone(); + + // Request this very file. + let Request = require('request').Request; + Request({ + url: "http://localhost:" + port + "/test-httpd.txt", + onComplete: function (response) { + test.assertEqual(response.text, content); + done(); + } + }).get(); + + function done() { + srv.stop(function() { + test.done(); + }); + } +}; + +exports.testDynamicServer = function (test) { + let content = "This is the HTTPD test file.\n"; + + let { startServerAsync } = require("httpd"); + let srv = startServerAsync(port); + + // See documentation here: + //http://doxygen.db48x.net/mozilla/html/interfacensIHttpServer.html#a81fc7e7e29d82aac5ce7d56d0bedfb3a + //http://doxygen.db48x.net/mozilla/html/interfacensIHttpRequestHandler.html + srv.registerPathHandler("/test-httpd.txt", function handle(request, response) { + // Add text content type, only to avoid error in `Request` API + response.setHeader("Content-Type", "text/plain", false); + response.write(content); + }); + + test.waitUntilDone(); + + // Request this very file. + let Request = require('request').Request; + Request({ + url: "http://localhost:" + port + "/test-httpd.txt", + onComplete: function (response) { + test.assertEqual(response.text, content); + done(); + } + }).get(); + + function done() { + srv.stop(function() { + test.done(); + }); + } + +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-keyboard-observer.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-keyboard-observer.js new file mode 100644 index 0000000..584f01b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-keyboard-observer.js @@ -0,0 +1,36 @@ +/* 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 { keyPress } = require("api-utils/dom/events/keys"); +const { Loader } = require("./helpers"); + +exports["test unload keyboard observer"] = function(assert, done) { + let loader = Loader(module); + let element = loader.require("api-utils/window-utils"). + activeBrowserWindow.document.documentElement; + let observer = loader.require("api-utils/keyboard/observer"). + observer; + let called = 0; + + observer.on("keypress", function () { called++; }); + + // dispatching "keypress" event to trigger observer listeners. + keyPress(element, "accel-%"); + + // Unload the module. + loader.unload(); + + // dispatching "keypress" even once again. + keyPress(element, "accel-%"); + + // Enqueuing asserts to make sure that assertion is not performed early. + require("timer").setTimeout(function () { + assert.equal(called, 1, "observer was called before unload only."); + done(); + }, 0); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-keyboard-utils.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-keyboard-utils.js new file mode 100644 index 0000000..1146a7b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-keyboard-utils.js @@ -0,0 +1,62 @@ +/* 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 utils = require("keyboard/utils"); +const runtime = require("runtime"); + +const isMac = runtime.OS === "Darwin"; + +exports["test toString"] = function(assert) { + assert.equal(utils.toString({ + key: "B", + modifiers: [ "Shift", "Ctrl" ] + }), "Shift-Ctrl-B", "toString does not normalizes JSON"); + + assert.equal(utils.toString({ + key: "C", + modifiers: [], + }), "C", "Works with objects with empty array of modifiers"); + + assert.equal(utils.toString(Object.create((function Type() {}).prototype, { + key: { value: "d" }, + modifiers: { value: [ "alt" ] }, + method: { value: function() {} } + })), "alt-d", "Works with non-json objects"); + + assert.equal(utils.toString({ + modifiers: [ "shift", "alt" ] + }), "shift-alt-", "works with only modifiers"); +}; + +exports["test toJSON"] = function(assert) { + assert.deepEqual(utils.toJSON("Shift-Ctrl-B"), { + key: "b", + modifiers: [ "control", "shift" ] + }, "toJSON normalizes input"); + + assert.deepEqual(utils.toJSON("Meta-Alt-option-C"), { + key: "c", + modifiers: [ "alt", "meta" ] + }, "removes dublicates"); + + assert.deepEqual(utils.toJSON("AccEl+sHiFt+Z", "+"), { + key: "z", + modifiers: isMac ? [ "meta", "shift" ] : [ "control", "shift" ] + }, "normalizes OS specific keys and adjustes seperator"); +}; + +exports["test normalize"] = function assert(assert) { + assert.equal(utils.normalize("Shift Ctrl A control ctrl", " "), + "control shift a", "removes reapeted modifiers"); + assert.equal(utils.normalize("shift-ctrl-left"), "control-shift-left", + "normilizes non printed characters"); + + assert.throws(function() { + utils.normalize("shift-alt-b-z"); + }, "throws if contains more then on non-modifier key"); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-l10n-locale.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-l10n-locale.js new file mode 100644 index 0000000..db559ba --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-l10n-locale.js @@ -0,0 +1,141 @@ +/* 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/. */ + +const { getPreferedLocales, findClosestLocale } = require("api-utils/l10n/locale"); +const prefs = require("preferences-service"); +const { Cc, Ci, Cu } = require("chrome"); +const { Services } = Cu.import("resource://gre/modules/Services.jsm"); +const BundleService = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService); + +const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; +const PREF_SELECTED_LOCALE = "general.useragent.locale"; +const PREF_ACCEPT_LANGUAGES = "intl.accept_languages"; + +function assertPrefered(test, expected, msg) { + test.assertEqual(JSON.stringify(getPreferedLocales()), JSON.stringify(expected), + msg); +} + +exports.testGetPreferedLocales = function(test) { + prefs.set(PREF_MATCH_OS_LOCALE, false); + prefs.set(PREF_SELECTED_LOCALE, ""); + prefs.set(PREF_ACCEPT_LANGUAGES, ""); + assertPrefered(test, ["en-us"], + "When all preferences are empty, we only have en-us"); + + prefs.set(PREF_SELECTED_LOCALE, "fr"); + prefs.set(PREF_ACCEPT_LANGUAGES, "jp"); + assertPrefered(test, ["fr", "jp", "en-us"], + "We first have useragent locale, then web one and finally en-US"); + + prefs.set(PREF_SELECTED_LOCALE, "en-US"); + prefs.set(PREF_ACCEPT_LANGUAGES, "en-US"); + assertPrefered(test, ["en-us"], + "We do not have duplicates"); + + prefs.set(PREF_SELECTED_LOCALE, "en-US"); + prefs.set(PREF_ACCEPT_LANGUAGES, "fr"); + assertPrefered(test, ["en-us", "fr"], + "en-US can be first if specified by higher priority preference"); + + // Reset what we changed + prefs.reset(PREF_MATCH_OS_LOCALE); + prefs.reset(PREF_SELECTED_LOCALE); + prefs.reset(PREF_ACCEPT_LANGUAGES); +} + +// In some cases, mainly on Fennec and on Linux version, +// `general.useragent.locale` is a special 'localized' value, like: +// "chrome://global/locale/intl.properties" +exports.testPreferedLocalizedLocale = function(test) { + prefs.set(PREF_MATCH_OS_LOCALE, false); + let bundleURL = "chrome://global/locale/intl.properties"; + prefs.setLocalized(PREF_SELECTED_LOCALE, bundleURL); + let contentLocale = "ja"; + prefs.set(PREF_ACCEPT_LANGUAGES, contentLocale); + + // Read manually the expected locale value from the property file + let expectedLocale = BundleService.createBundle(bundleURL). + GetStringFromName(PREF_SELECTED_LOCALE). + toLowerCase(); + + // First add the useragent locale + let expectedLocaleList = [expectedLocale]; + + // Then the content locale + if (expectedLocaleList.indexOf(contentLocale) == -1) + expectedLocaleList.push(contentLocale); + + // Add default "en-us" fallback if the main language is not already en-us + if (expectedLocaleList.indexOf("en-us") == -1) + expectedLocaleList.push("en-us"); + + assertPrefered(test, expectedLocaleList, "test localized pref value"); + + // Reset what we have changed + prefs.reset(PREF_MATCH_OS_LOCALE); + prefs.reset(PREF_SELECTED_LOCALE); + prefs.reset(PREF_ACCEPT_LANGUAGES); +} + +exports.testPreferedOsLocale = function(test) { + prefs.set(PREF_MATCH_OS_LOCALE, true); + prefs.set(PREF_SELECTED_LOCALE, ""); + prefs.set(PREF_ACCEPT_LANGUAGES, ""); + + let expectedLocale = Services.locale.getLocaleComponentForUserAgent(). + toLowerCase(); + let expectedLocaleList = [expectedLocale]; + + // Add default "en-us" fallback if the main language is not already en-us + if (expectedLocale != "en-us") + expectedLocaleList.push("en-us"); + + assertPrefered(test, expectedLocaleList, "Ensure that we select OS locale when related preference is set"); + + // Reset what we have changed + prefs.reset(PREF_MATCH_OS_LOCALE); + prefs.reset(PREF_SELECTED_LOCALE); + prefs.reset(PREF_ACCEPT_LANGUAGES); +} + +exports.testFindClosestLocale = function(test) { + // Second param of findClosestLocale (aMatchLocales) have to be in lowercase + test.assertEqual(findClosestLocale([], []), null, + "When everything is empty we get null"); + + test.assertEqual(findClosestLocale(["en", "en-US"], ["en"]), + "en", "We always accept exact match first 1/5"); + test.assertEqual(findClosestLocale(["en-US", "en"], ["en"]), + "en", "We always accept exact match first 2/5"); + test.assertEqual(findClosestLocale(["en", "en-US"], ["en-us"]), + "en-US", "We always accept exact match first 3/5"); + test.assertEqual(findClosestLocale(["ja-JP-mac", "ja", "ja-JP"], ["ja-jp"]), + "ja-JP", "We always accept exact match first 4/5"); + test.assertEqual(findClosestLocale(["ja-JP-mac", "ja", "ja-JP"], ["ja-jp-mac"]), + "ja-JP-mac", "We always accept exact match first 5/5"); + + test.assertEqual(findClosestLocale(["en", "en-GB"], ["en-us"]), + "en", "We accept more generic locale, when there is no exact match 1/2"); + test.assertEqual(findClosestLocale(["en-ZA", "en"], ["en-gb"]), + "en", "We accept more generic locale, when there is no exact match 2/2"); + + test.assertEqual(findClosestLocale(["ja-JP"], ["ja"]), + "ja-JP", "We accept more specialized locale, when there is no exact match 1/2"); + // Better to select "ja" in this case but behave same as current AddonManager + test.assertEqual(findClosestLocale(["ja-JP-mac", "ja"], ["ja-jp"]), + "ja-JP-mac", "We accept more specialized locale, when there is no exact match 2/2"); + + test.assertEqual(findClosestLocale(["en-US"], ["en-us"]), + "en-US", "We keep the original one as result 1/2"); + test.assertEqual(findClosestLocale(["en-us"], ["en-us"]), + "en-us", "We keep the original one as result 2/2"); + + test.assertEqual(findClosestLocale(["ja-JP-mac"], ["ja-jp-mac"]), + "ja-JP-mac", "We accept locale with 3 parts"); + test.assertEqual(findClosestLocale(["ja-JP"], ["ja-jp-mac"]), + "ja-JP", "We accept locale with 2 parts from locale with 3 parts"); + test.assertEqual(findClosestLocale(["ja"], ["ja-jp-mac"]), + "ja", "We accept locale with 1 part from locale with 3 parts"); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-l10n-plural-rules.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-l10n-plural-rules.js new file mode 100644 index 0000000..e46ecf6 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-l10n-plural-rules.js @@ -0,0 +1,82 @@ +/* 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/. */ + +const { getRulesForLocale } = require("api-utils/l10n/plural-rules"); + +// For more information, please visit unicode website: +// http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html + +function map(test, f, n, form) { + test.assertEqual(f(n), form, n + " maps to '" + form + "'"); +} + +exports.testFrench = function(test) { + let f = getRulesForLocale("fr"); + map(test, f, -1, "other"); + map(test, f, 0, "one"); + map(test, f, 1, "one"); + map(test, f, 1.5, "one"); + map(test, f, 2, "other"); + map(test, f, 100, "other"); +} + +exports.testEnglish = function(test) { + let f = getRulesForLocale("en"); + map(test, f, -1, "other"); + map(test, f, 0, "other"); + map(test, f, 1, "one"); + map(test, f, 1.5, "other"); + map(test, f, 2, "other"); + map(test, f, 100, "other"); +} + +exports.testArabic = function(test) { + let f = getRulesForLocale("ar"); + map(test, f, -1, "other"); + map(test, f, 0, "zero"); + map(test, f, 0.5, "other"); + + map(test, f, 1, "one"); + map(test, f, 1.5, "other"); + + map(test, f, 2, "two"); + map(test, f, 2.5, "other"); + + map(test, f, 3, "few"); + map(test, f, 3.5, "few"); // I'd expect it to be 'other', but the unicode.org + // algorithm computes 'few'. + map(test, f, 5, "few"); + map(test, f, 10, "few"); + map(test, f, 103, "few"); + map(test, f, 105, "few"); + map(test, f, 110, "few"); + map(test, f, 203, "few"); + map(test, f, 205, "few"); + map(test, f, 210, "few"); + + map(test, f, 11, "many"); + map(test, f, 50, "many"); + map(test, f, 99, "many"); + map(test, f, 111, "many"); + map(test, f, 150, "many"); + map(test, f, 199, "many"); + + map(test, f, 100, "other"); + map(test, f, 101, "other"); + map(test, f, 102, "other"); + map(test, f, 200, "other"); + map(test, f, 201, "other"); + map(test, f, 202, "other"); +} + +exports.testJapanese = function(test) { + // Japanese doesn't have plural forms. + let f = getRulesForLocale("ja"); + map(test, f, -1, "other"); + map(test, f, 0, "other"); + map(test, f, 1, "other"); + map(test, f, 1.5, "other"); + map(test, f, 2, "other"); + map(test, f, 100, "other"); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-light-traits.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-light-traits.js new file mode 100644 index 0000000..7c5ca42 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-light-traits.js @@ -0,0 +1,11 @@ +/* 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"; + +exports["test traits from objects"] = require("./traits/object-tests"); +exports["test traits from descriptors"] = require("./traits/descriptor-tests"); +exports["test inheritance"] = require("./traits/inheritance-tests"); + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-list.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-list.js new file mode 100644 index 0000000..67e67aa --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-list.js @@ -0,0 +1,207 @@ +/* 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"; + +function assertList(test, array, list) { + for (let i = 0, ii = array.length; i < ii; i < ii, i++) { + test.assertEqual( + array.length, + list.length, + 'list must contain same amount of elements as array' + ); + test.assertEqual( + 'List(' + array + ')', + list + '', + 'toString must output array like result' + ); + test.assert( + i in list, + 'must contain element with index: ' + i + ); + test.assertEqual( + array[i], + list[i], + 'element with index: ' + i + ' should match' + ); + } +} + +const { List } = require('api-utils/list'); + +exports['test:test for'] = function(test) { + let fixture = List(3, 2, 1); + + test.assertEqual(3, fixture.length, 'length is 3'); + let i = 0; + for (let key in fixture) { + test.assertEqual(i++, key, 'key should match'); + } +}; + +exports['test:test for each'] = function(test) { + let fixture = new List(3, 2, 1); + + test.assertEqual(3, fixture.length, 'length is 3'); + let i = 3; + for each (let value in fixture) { + test.assertEqual(i--, value, 'value should match'); + } +}; + +exports['test: for each using Iterator'] = function(test) { + let fixture = new List(3, 2, 1); + + test.assertEqual(3, fixture.length, 'length is 3'); + let v = 3, k = 0; + for each (let [key, value] in Iterator(fixture)) { + test.assertEqual(k++, key, 'key should match'); + test.assertEqual(v--, value, 'value should match'); + } +}; + +exports['test:test toString'] = function(test) { + let fixture = List(3, 2, 1); + + test.assertEqual( + 'List(3,2,1)', + fixture + '', + 'toString must output array like result' + ) +}; + +exports['test:test constructor with apply'] = function(test) { + let array = ['a', 'b', 'c']; + let fixture = List.apply(null, array); + + test.assertEqual( + 3, + fixture.length, + 'should have applied arguments' + ); +}; + +exports['test:direct element access'] = function(test) { + let array = [1, 'foo', 2, 'bar', {}, 'bar', function a() {}, test, 1]; + let fixture = List.apply(null, array); + array.splice(5, 1); + array.splice(7, 1); + + test.assertEqual( + array.length, + fixture.length, + 'list should omit duplicate elements' + ); + + test.assertEqual( + 'List(' + array + ')', + fixture.toString(), + 'elements should not be rearranged' + ); + + for (let key in array) { + test.assert(key in fixture,'should contain key for index:' + key); + test.assertEqual( + array[key], + fixture[key], + 'values should match for: ' + key + ); + } +}; + +exports['test:removing adding elements'] = function(test) { + let array = [1, 'foo', 2, 'bar', {}, 'bar', function a() {}, test, 1]; + let fixture = List.compose({ + add: function() this._add.apply(this, arguments), + remove: function() this._remove.apply(this, arguments), + clear: function() this._clear() + }).apply(null, array); + array.splice(5, 1); + array.splice(7, 1); + + assertList(test, array, fixture); + + array.splice(array.indexOf(2), 1); + fixture.remove(2); + assertList(test, array, fixture); + + array.splice(array.indexOf('foo'), 1); + fixture.remove('foo'); + array.splice(array.indexOf(1), 1); + fixture.remove(1); + array.push('foo'); + fixture.add('foo'); + assertList(test, array, fixture); + + array.splice(0); + fixture.clear(0); + assertList(test, array, fixture); + + array.push(1, 'foo', 2, 'bar', 3); + fixture.add(1); + fixture.add('foo'); + fixture.add(2); + fixture.add('bar'); + fixture.add(3); + + assertList(test, array, fixture); +}; + +exports['test: remove does not leave invalid numerical properties'] = function(test) { + let fixture = List.compose({ + remove: function() this._remove.apply(this, arguments), + }).apply(null, [1, 2, 3]); + + fixture.remove(1); + test.assertEqual(fixture[fixture.length], undefined); +} + +exports['test:add list item from Iterator'] = function(test) { + let array = [1, 2, 3, 4], sum = 0, added = false; + + let fixture = List.compose({ + add: function() this._add.apply(this, arguments), + }).apply(null, array); + + for each (let item in fixture) { + sum += item; + + if (!added) { + fixture.add(5); + added = true; + } + } + + test.assertEqual(sum, 1 + 2 + 3 + 4); +}; + +exports['test:remove list item from Iterator'] = function(test) { + let array = [1, 2, 3, 4], sum = 0; + + let fixture = List.compose({ + remove: function() this._remove.apply(this, arguments), + }).apply(null, array); + + for each (let item in fixture) { + sum += item; + fixture.remove(item); + } + + test.assertEqual(sum, 1 + 2 + 3 + 4); +}; + +exports['test:clear list from Iterator'] = function(test) { + let array = [1, 2, 3, 4], sum = 0; + + let fixture = List.compose({ + clear: function() this._clear() + }).apply(null, array); + + for each (let item in fixture) { + sum += item; + fixture.clear(); + } + + test.assertEqual(sum, 1 + 2 + 3 + 4); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-loader.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-loader.js new file mode 100644 index 0000000..82d97ba --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-loader.js @@ -0,0 +1,35 @@ +/* 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/. */ + +const { Loader } = require("./helpers"); + +exports.testLoader = function(test) { + var prints = []; + function print(message) { + prints.push(message); + } + + var loader = Loader(module, { dump: print, foo: 1 }); + + var fixture = loader.require('./loader/fixture'); + + test.assertEqual(fixture.foo, 1, "custom globals must work."); + + test.assertEqual(prints[0], "info: testing 1 2,3,4\n", + "global console must work."); + + var unloadsCalled = ''; + + loader.require("unload").when(function() { + unloadsCalled += 'a'; + }); + loader.require("unload.js").when(function() { + unloadsCalled += 'b'; + }); + + loader.unload(); + + test.assertEqual(unloadsCalled, 'ba', + "loader.unload() must call cb's in LIFO order."); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-match-pattern.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-match-pattern.js new file mode 100644 index 0000000..9cedf66 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-match-pattern.js @@ -0,0 +1,127 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +const { MatchPattern } = require("match-pattern"); + +exports.testMatchPatternTestTrue = function(test) { + function ok(pattern, url) { + let mp = new MatchPattern(pattern); + test.assert(mp.test(url), pattern + " should match " + url); + } + + ok("*", "http://example.com"); + ok("*", "https://example.com"); + ok("*", "ftp://example.com"); + + ok("*.example.com", "http://example.com"); + ok("*.example.com", "http://hamburger.example.com"); + ok("*.example.com", "http://hotdog.hamburger.example.com"); + + ok("http://example.com*", "http://example.com"); + ok("http://example.com*", "http://example.com/"); + ok("http://example.com/*", "http://example.com/"); + ok("http://example.com/*", "http://example.com/potato-salad"); + ok("http://example.com/pickles/*", "http://example.com/pickles/"); + ok("http://example.com/pickles/*", "http://example.com/pickles/lemonade"); + + ok("http://example.com", "http://example.com"); + ok("http://example.com/ice-cream", "http://example.com/ice-cream"); + + ok(/.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok(/https:.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); +}; + +exports.testMatchPatternTestFalse = function(test) { + function ok(pattern, url) { + let mp = new MatchPattern(pattern); + test.assert(!mp.test(url), pattern + " should not match " + url); + } + + ok("*", null); + ok("*", ""); + ok("*", "bogus"); + ok("*", "chrome://browser/content/browser.xul"); + ok("*", "nttp://example.com"); + + ok("*.example.com", null); + ok("*.example.com", ""); + ok("*.example.com", "bogus"); + ok("*.example.com", "http://example.net"); + ok("*.example.com", "http://foo.com"); + ok("*.example.com", "http://example.com.foo"); + ok("*.example2.com", "http://example.com"); + + ok("http://example.com/*", null); + ok("http://example.com/*", ""); + ok("http://example.com/*", "bogus"); + ok("http://example.com/*", "http://example.com"); + ok("http://example.com/*", "http://foo.com/"); + + ok("http://example.com", null); + ok("http://example.com", ""); + ok("http://example.com", "bogus"); + ok("http://example.com", "http://example.com/"); + + ok(/zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok(/.*zilla/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok(/https:.*zilla/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); +}; + +exports.testMatchPatternErrors = function(test) { + test.assertRaises( + function() new MatchPattern("*.google.com/*"), + /There can be at most one/, + "MatchPattern throws when supplied multiple '*'" + ); + + test.assertRaises( + function() new MatchPattern("google.com"), + /expected to be either an exact URL/, + "MatchPattern throws when the wildcard doesn't use '*' and doesn't " + + "look like a URL" + ); + + test.assertRaises( + function() new MatchPattern("http://google*.com"), + /expected to be the first or the last/, + "MatchPattern throws when a '*' is in the middle of the wildcard" + ); + + test.assertRaises( + function() new MatchPattern(/ /g), + /^A RegExp match pattern cannot be set to `global` \(i\.e\. \/\/g\)\.$/, + "MatchPattern throws on a RegExp set to `global` (i.e. //g)." + ); + + test.assertRaises( + function() new MatchPattern(/ /i), + /^A RegExp match pattern cannot be set to `ignoreCase` \(i\.e\. \/\/i\)\.$/, + "MatchPattern throws on a RegExp set to `ignoreCase` (i.e. //i)." + ); + + test.assertRaises( + function() new MatchPattern( / /m ), + /^A RegExp match pattern cannot be set to `multiline` \(i\.e\. \/\/m\)\.$/, + "MatchPattern throws on a RegExp set to `multiline` (i.e. //m)." + ); +}; + +exports.testMatchPatternInternals = function(test) { + test.assertEqual( + new MatchPattern("http://google.com/test").exactURL, + "http://google.com/test" + ); + + test.assertEqual( + new MatchPattern("http://google.com/test/*").urlPrefix, + "http://google.com/test/" + ); + + test.assertEqual( + new MatchPattern("*.example.com").domain, + "example.com" + ); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-memory.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-memory.js new file mode 100644 index 0000000..99e1682 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-memory.js @@ -0,0 +1,19 @@ +/* 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/. */ + +var memory = require("api-utils/memory"); + +exports.testMemory = function(test) { + test.pass("Skipping this test until Gecko memory debugging issues " + + "are resolved (see bug 592774)."); + return; + + var obj = {}; + memory.track(obj, "testMemory.testObj"); + var objs = memory.getObjects("testMemory.testObj"); + test.assertEqual(objs[0].weakref.get(), obj); + obj = null; + memory.gc(); + test.assertEqual(objs[0].weakref.get(), null); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-message-manager.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-message-manager.js new file mode 100644 index 0000000..386d3bd --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-message-manager.js @@ -0,0 +1,600 @@ +/* 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 { Loader } = require('./helpers'); + +function createMessageManager() { + let loader = Loader(module); + let { MessageManager } = loader.require("api-utils/message-manager"); + let frame = loader.sandbox("api-utils/message-manager").frame; + + return [new MessageManager, frame]; +} + + +exports["test MessageManager addMessageListener"] = + function(assert) { + let [mm, frame] = createMessageManager(); + let remoteFrame = frame(mm).receiver; + + let listeners = frame(mm).listeners; + let remoteListeners = frame(remoteFrame).listeners; + + let topic = "message-topic"; + + let listener = function () {}; + + assert.equal(topic in listeners, false, + "No listeners for MessageManager"); + assert.equal(topic in remoteListeners, false, + "No listeners for Remote Frame"); + + mm.addMessageListener(topic, listener); + + assert.deepEqual(listeners[topic], [listener], + "Listener is added properly"); + assert.equal(topic in remoteListeners, false, + "No listeners for Remote Frame"); + } + + +exports["test MessageManager addMessageListener with duplicates"] = + function(assert) { + let [mm, frame] = createMessageManager(); + let topic = "message-topic"; + let listeners = frame(mm).listeners; + + let listener = function () {}; + + mm.addMessageListener(topic, listener); + mm.addMessageListener(topic, listener); + mm.addMessageListener(topic, listener); + + assert.deepEqual(listeners[topic], [listener], + "Same listener is added only once"); + } + + +exports["test MessageManager addMessageListener exceptions"] = + function(assert) { + const BAD_LISTENER = "The event listener must be a function."; + + let [mm, frame] = createMessageManager(); + let listeners = frame(mm).listeners; + let topic = "message-topic"; + + assert.throws( + function() mm.addMessageListener(), + BAD_LISTENER + ); + + assert.throws( + function() mm.addMessageListener(topic), + BAD_LISTENER + ); + + assert.throws( + function() mm.addMessageListener(topic, "something-else"), + BAD_LISTENER + ); + } + +exports["test MessageManager removeMessageListener"] = + function(assert) { + let [mm, frame] = createMessageManager(); + let topic = "message-topic"; + let listeners = frame(mm).listeners; + + let listenerA = function () {}; + let listenerB = function () {}; + let listenerC = function () {}; + + mm.removeMessageListener(topic, listenerA); + + assert.deepEqual(listeners, {}, + "No listeners yet"); + + mm.addMessageListener(topic, listenerA); + mm.addMessageListener(topic, listenerB); + mm.addMessageListener(topic, listenerC); + + mm.removeMessageListener(topic, listenerB); + + assert.deepEqual(listeners[topic], [listenerA, listenerC], + "Listener is removed properly"); + } + + +exports["test MessageManager loadFrameScript with data URL"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + + assert.equal("TEST_VALUE" in remoteFrame, false, + "TEST_VALUE is not defined in Remote Frame"); + + mm.loadFrameScript("data:, const TEST_VALUE = 77;", true); + + assert.equal(remoteFrame.TEST_VALUE, 77, + "TEST_VALUE is properly defined in Remote Frame"); + } + + +exports["test MessageManager loadFrameScript with a File"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + + assert.equal("TEST_VALUE" in remoteFrame, false, + "TEST_VALUE is not defined in Remote Frame"); + + mm.loadFrameScript(require("self").data.url("test-message-manager.js"), true); + + assert.equal(remoteFrame.TEST_VALUE, 11, + "TEST_VALUE is properly defined in Remote Frame"); + } + +exports["test MessageManager loadFrameScript exception"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + + + assert.throws( + function() mm.loadFrameScript("data:, const TEST_VALUE = 77;"), + "Not enough arguments" + ); + + } + +exports["test Remote Frame addMessageListener"] = + function(assert) { + let [mm, frame] = createMessageManager(); + let remoteFrame = frame(mm).receiver; + + let listeners = frame(remoteFrame).listeners; + let managerListeners = frame(mm).listeners; + + let topic = "message-topic"; + + let listener = function () {}; + + assert.equal(topic in listeners, false, + "No listeners for Remote Frame"); + assert.equal(topic in managerListeners, false, + "No listeners for MessageManager"); + + remoteFrame.addMessageListener(topic, listener); + + assert.deepEqual(listeners[topic], [listener], + "Listener is added properly"); + assert.equal(topic in managerListeners, false, + "No listeners for MessageManager"); + } + + +exports["test Remote Frame addMessageListener with duplicates"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + let listeners = frame(remoteFrame).listeners; + let topic = "message-topic"; + + let listener = function () {}; + + assert.equal(topic in listeners, false, + "No listeners in Remote Frame"); + + remoteFrame.addMessageListener(topic, listener); + remoteFrame.addMessageListener(topic, listener); + remoteFrame.addMessageListener(topic, listener); + + assert.deepEqual(listeners[topic], [listener], + "Listener is added properly"); + } + + +exports["test Remote Frame addMessageListener exceptions"] = + function(assert) { + const BAD_LISTENER = "The event listener must be a function."; + + let [mm, frame] = createMessageManager(); + let remoteFrame = frame(mm).receiver; + let listeners = frame(remoteFrame).listeners; + let topic = "message-topic"; + + assert.throws( + function() mm.addMessageListener(), + BAD_LISTENER + ); + + assert.throws( + function() mm.addMessageListener(topic), + BAD_LISTENER + ); + + assert.throws( + function() mm.addMessageListener(topic, "something-else"), + BAD_LISTENER + ); + } + + +exports["test Remote Frame removeMessageListener"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + let listeners = frame(remoteFrame).listeners; + + let topic = "message-topic"; + + let listenerA = function () {}; + let listenerB = function () {}; + let listenerC = function () {}; + + remoteFrame.removeMessageListener(topic, listenerA); + + assert.deepEqual(listeners, {}, + "No listeners yet"); + + remoteFrame.addMessageListener(topic, listenerA); + remoteFrame.addMessageListener(topic, listenerB); + remoteFrame.addMessageListener(topic, listenerC); + + remoteFrame.removeMessageListener(topic, listenerB); + + assert.deepEqual(listeners[topic], [listenerA, listenerC], + "Listener is removed properly"); + } + + +exports["test MessageManager sendAsyncMessage"] = + function(assert, done) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + let calls = 0; + + let topic = "message-topic"; + + let listener = function(data) { + calls++; + + assert.deepEqual(data, { + sync : false, + name : topic, + json : {foo : "bar"}, + target : null + }, "Data received as expected"); + + assert.equal(calls, 1, + "Listener called once"); + + done(); + } + + remoteFrame.addMessageListener(topic, listener); + + let response = mm.sendAsyncMessage(topic, {foo : "bar"}); + + assert.strictEqual(response, undefined, + "No response for async messages"); + + assert.equal(calls, 0, + "Listener not called yet"); + } + +exports["test MessageManager sendAsyncMessage without listeners"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + + let topic = "message-topic"; + + let response = mm.sendAsyncMessage(topic, {foo : "bar"}); + + assert.strictEqual(response, undefined, + "No response for async messages"); + } + + +exports["test MessageManager sendAsyncMessage without arguments"] = + function(assert, done) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + let calls = 0; + + let topic = "message-topic"; + + let listener = function(data) { + calls++; + + assert.deepEqual(data, { + sync : false, + name : topic, + json : null, + target : null + }, "Data received as expected"); + + assert.equal(calls, 1, + "Listener called once"); + + done(); + } + + remoteFrame.addMessageListener(topic, listener); + + mm.sendAsyncMessage(); + + mm.sendAsyncMessage(topic); + + assert.equal(calls, 0, + "Listener not called yet"); + } + + +exports["test Remote Frame sendAsyncMessage"] = + function(assert, done) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + let calls = 0; + + let topic = "message-topic"; + + let listener = function(data) { + calls++; + + assert.deepEqual(data, { + sync : false, + name : topic, + json : {foo : "bar"}, + target : null + }, "Data received as expected"); + + assert.equal(calls, 1, + "Listener called once"); + + done(); + } + + mm.addMessageListener(topic, listener); + + let response = remoteFrame.sendAsyncMessage(topic, {foo : "bar"}); + + assert.strictEqual(response, undefined, + "No response for async messages"); + + assert.equal(calls, 0, + "Listener not called yet"); + } + + +exports["test Remote Frame sendAsyncMessage without arguments"] = + function(assert, done) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + let calls = 0; + + let topic = "message-topic"; + + let listener = function(data) { + calls++; + + assert.deepEqual(data, { + sync : false, + name : topic, + json : null, + target : null + }, "Data received as expected"); + + assert.equal(calls, 1, + "Listener called once"); + + done(); + } + + mm.addMessageListener(topic, listener); + + remoteFrame.sendAsyncMessage(); + + remoteFrame.sendAsyncMessage(topic); + + assert.equal(calls, 0, + "Listener not called yet"); + } + +exports["test Remote Frame sendAsyncMessage without listeners"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + + let topic = "message-topic"; + + let response = remoteFrame.sendAsyncMessage(topic, {foo : "bar"}); + + assert.strictEqual(response, undefined, + "No response for async messages"); + } + +exports["test Remote Frame sendSyncMessage"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + + let topic = "message-topic"; + + let expectedData = { + sync : true, + name : topic, + json : {foo : "bar"}, + target : null + } + + let listenerA = function(data) { + assert.deepEqual(data, expectedData, + "Data received as expected"); + + return "my value"; + } + + let listenerB = function(data) { + assert.deepEqual(data, expectedData, + "Data received as expected"); + + return {complex : "object", method : function() "not allowed"}; + } + + let listenerC = function(data) { + assert.deepEqual(data, expectedData, + "Data received as expected"); + } + + mm.addMessageListener(topic, listenerA); + mm.addMessageListener(topic, listenerB); + mm.addMessageListener(topic, listenerC); + + let response = remoteFrame.sendSyncMessage(topic, {foo : "bar"}); + + assert.deepEqual(response, ["my value", {complex : "object"}, undefined], + "Response from sync messages as expected"); + } + +exports["test Remote Frame sendSyncMessage without arguments"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + + let topic = "message-topic"; + + let expectedData = { + sync : true, + name : topic, + json : null, + target : null + } + + let listener = function(data) { + assert.deepEqual(data, expectedData, + "Data received as expected"); + } + + mm.addMessageListener(topic, listener); + + let response = remoteFrame.sendSyncMessage(); + + assert.deepEqual(response, [], + "Empty response as expected"); + + let response = remoteFrame.sendSyncMessage(topic); + + assert.deepEqual(response, [undefined], + "Response from sync messages as expected"); + } + +exports["test Remote Frame sendSyncMessage without listeners"] = + function(assert) { + let [mm, frame] = createMessageManager(); + + let remoteFrame = frame(mm).receiver; + let obj = {foo : "bar"}; + + let topic = "message-topic"; + + let response = remoteFrame.sendSyncMessage(topic, obj); + + assert.deepEqual(response, [], + "Response from sync messages as expected"); + } + + +exports["test Message Manager / Remote Frame async pipeline"] = + function(assert, done) { + let [mm] = createMessageManager(); + + let expectedMessages = ["alpha:remote", "alpha", "omega:remote", "omega"]; + + mm.addMessageListener("handshake", function (data) { + let messages = data.json.concat("alpha"); + + mm.sendAsyncMessage("shutdown", messages); + }); + + mm.addMessageListener("shutdown", function (data) { + let messages = data.json.concat("omega"); + + assert.deepEqual(messages, expectedMessages, + "messages delivered in the expected order"); + + done(); + }); + + mm.loadFrameScript( + "data:, \ + addMessageListener('handshake', function (data) {\ + let messages = data.json.concat('alpha:remote');\ + sendAsyncMessage('handshake', messages);\ + });\ + addMessageListener('shutdown', function (data) {\ + let messages = data.json.concat('omega:remote');\ + sendAsyncMessage('shutdown', messages);\ + });\ + ", true); + + mm.sendAsyncMessage("handshake", []); + } + + +exports["test Message Manager / Remote Frame async / sync pipeline"] = + function(assert, done) { + let [mm] = createMessageManager(); + + let expectedMessages = ["alpha:remote", "alpha", "omega:remote", "omega"]; + + mm.addMessageListener("handshake", + function (data) data.json.concat("alpha") + ); + + mm.addMessageListener("shutdown", + function (data) data.json.concat("omega") + ); + + mm.addMessageListener("verify", function (data) { + + assert.deepEqual(data.json, expectedMessages, + "messages delivered in the expected order"); + + done(); + }); + + mm.loadFrameScript( + "data:, \ + addMessageListener('handshake', function (data) {\ + let messages = data.json.concat('alpha:remote');\ + \ + messages = sendSyncMessage('handshake', messages)[0];\ + messages.push('omega:remote');\ + messages = sendSyncMessage('shutdown', messages)[0];\ + \ + sendSyncMessage('verify', messages);\ + });\ + ", true); + + mm.sendAsyncMessage("handshake", []); + } + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-modules.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-modules.js new file mode 100644 index 0000000..2868560 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-modules.js @@ -0,0 +1,148 @@ +/* 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/. */ + +exports.testDefine = function(test) { + let tiger = require('./modules/tiger'); + test.assertEqual(tiger.name, 'tiger', 'name proprety was exported properly'); + test.assertEqual(tiger.type, 'cat', 'property form other module exported'); +}; + +exports.testDefineInoresNonFactory = function(test) { + let mod = require('./modules/async2'); + test.assertEqual(mod.name, 'async2', 'name proprety was exported properly'); + test.assertNotEqual(mod.traditional2Name, 'traditional2', '1st is ignored'); +}; +/* Disable test that require AMD specific functionality: + +// define() that exports a function as the module value, +// specifying a module name. +exports.testDefExport = function(test) { + var add = require('modules/add'); + test.assertEqual(add(1, 1), 2, 'Named define() exporting a function'); +}; + +// define() that exports function as a value, but is anonymous +exports.testAnonDefExport = function (test) { + var subtract = require('modules/subtract'); + test.assertEqual(subtract(4, 2), 2, + 'Anonymous define() exporting a function'); +} + +// using require([], function () {}) to load modules. +exports.testSimpleRequire = function (test) { + require(['modules/blue', 'modules/orange'], function (blue, orange) { + test.assertEqual(blue.name, 'blue', 'Simple require for blue'); + test.assertEqual(orange.name, 'orange', 'Simple require for orange'); + test.assertEqual(orange.parentType, 'color', + 'Simple require dependency check for orange'); + }); +} + +// using nested require([]) calls. +exports.testSimpleRequireNested = function (test) { + require(['modules/blue', 'modules/orange', 'modules/green'], + function (blue, orange, green) { + + require(['modules/orange', 'modules/red'], function (orange, red) { + test.assertEqual(red.name, 'red', 'Simple require for red'); + test.assertEqual(red.parentType, 'color', + 'Simple require dependency check for red'); + test.assertEqual(blue.name, 'blue', 'Simple require for blue'); + test.assertEqual(orange.name, 'orange', 'Simple require for orange'); + test.assertEqual(orange.parentType, 'color', + 'Simple require dependency check for orange'); + test.assertEqual(green.name, 'green', 'Simple require for green'); + test.assertEqual(green.parentType, 'color', + 'Simple require dependency check for green'); + }); + + }); +} + +// requiring a traditional module, that uses async, that use traditional and +// async, with a circular reference +exports.testMixedCircular = function (test) { + var t = require('modules/traditional1'); + test.assertEqual(t.name, 'traditional1', 'Testing name'); + test.assertEqual(t.traditional2Name, 'traditional2', + 'Testing dependent name'); + test.assertEqual(t.traditional1Name, 'traditional1', 'Testing circular name'); + test.assertEqual(t.async2Name, 'async2', 'Testing async2 name'); + test.assertEqual(t.async2Traditional2Name, 'traditional2', + 'Testing nested traditional2 name'); +} + +// Testing define()(function(require) {}) with some that use exports, +// some that use return. +exports.testAnonExportsReturn = function (test) { + var lion = require('modules/lion'); + require(['modules/tiger', 'modules/cheetah'], function (tiger, cheetah) { + test.assertEqual('lion', lion, 'Check lion name'); + test.assertEqual('tiger', tiger.name, 'Check tiger name'); + test.assertEqual('cat', tiger.type, 'Check tiger type'); + test.assertEqual('cheetah', cheetah(), 'Check cheetah name'); + }); +} + +// circular dependency +exports.testCircular = function (test) { + var pollux = require('modules/pollux'), + castor = require('modules/castor'); + + test.assertEqual(pollux.name, 'pollux', 'Pollux\'s name'); + test.assertEqual(pollux.getCastorName(), + 'castor', 'Castor\'s name from Pollux.'); + test.assertEqual(castor.name, 'castor', 'Castor\'s name'); + test.assertEqual(castor.getPolluxName(), 'pollux', + 'Pollux\'s name from Castor.'); +} + +// test a bad module that asks for exports but also does a define() return +exports.testBadExportAndReturn = function (test) { + var passed = false; + try { + var bad = require('modules/badExportAndReturn'); + } catch(e) { + passed = /cannot use exports and also return/.test(e.toString()); + } + test.assertEqual(passed, true, 'Make sure exports and return fail'); +} + +// test a bad circular dependency, where an exported value is needed, but +// the return value happens too late, a module already asked for the exported +// value. +exports.testBadExportAndReturnCircular = function (test) { + var passed = false; + try { + var bad = require('modules/badFirst'); + } catch(e) { + passed = /after another module has referenced its exported value/ + .test(e.toString()); + } + test.assertEqual(passed, true, 'Make sure return after an exported ' + + 'value is grabbed by another module fails.'); +} + +// only allow one define call per file. +exports.testOneDefine = function (test) { + var passed = false; + try { + var dupe = require('modules/dupe'); + } catch(e) { + passed = /Only one call to define/.test(e.toString()); + } + test.assertEqual(passed, true, 'Only allow one define call per module'); +} + +// only allow one define call per file, testing a bad nested define call. +exports.testOneDefineNested = function (test) { + var passed = false; + try { + var dupe = require('modules/dupeNested'); + } catch(e) { + passed = /Only one call to define/.test(e.toString()); + } + test.assertEqual(passed, true, 'Only allow one define call per module'); +} +*/ diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-namespace.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-namespace.js new file mode 100644 index 0000000..cb69bb0 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-namespace.js @@ -0,0 +1,95 @@ +/* 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"; + +let { Namespace, ns } = require("api-utils/namespace"); +let { Cc, Ci, Cu } = require("chrome"); +let { setTimeout } = require("api-utils/timer") + +exports["test post GC references"] = function (assert, done) { + // Test temporary workaround for a bug 673468. + var target = {}, local = ns() + local(target).there = true + + assert.equal(local(target).there, true, "namespaced preserved"); + + setTimeout(function() { + Cu.forceGC(); + assert.equal(local(target).there, true, "namespace is preserved post GC"); + done(); + }, 300); +}; + +exports["test namsepace basics"] = function(assert) { + var privates = Namespace(); + var object = { foo: function foo() { return "hello foo"; } }; + + assert.notEqual(privates(object), object, + "namespaced object is not the same"); + assert.ok(!('foo' in privates(object)), + "public properties are not in the namespace"); + + assert.equal(privates(object), privates(object), + "same namespaced object is returned on each call"); +}; + +exports["test namespace overlays"] = function(assert) { + var _ = new Namespace(); + var object = { foo: 'foo' }; + + _(object).foo = 'bar'; + + assert.equal(_(object).foo, "bar", + "namespaced property `foo` changed value"); + + assert.equal(object.foo, "foo", + "public property `foo` has original value"); + + object.foo = "baz"; + assert.equal(_(object).foo, "bar", + "property changes do not affect namespaced properties"); + + object.bar = "foo"; + assert.ok(!("bar" in _(object)), + "new public properties are not reflected in namespace"); +}; + +exports["test shared namespaces"] = function(assert) { + var _ = new Namespace({ hello: 'hello world' }); + + var f1 = { hello: 1 }; + var f2 = { foo: 'foo' }; + + assert.equal(_(f1).hello, _(f2).hello, "namespace can be shared"); + assert.notEqual(f1.hello, _(f1).hello, "shared namespace can overlay"); + assert.notEqual(f2.hello, _(f2).hello, "target is not affected"); + + _(f1).hello = 2; + + assert.notEqual(_(f1).hello, _(f2).hello, + "namespaced property can be overided"); + assert.equal(_(f2).hello, _({}).hello, "namespace does not change"); +}; + +exports["test multi namespace"] = function(assert) { + var n1 = new Namespace(); + var n2 = new Namespace(); + var object = { baz: 1 }; + n1(object).foo = 1; + n2(object).foo = 2; + n1(object).bar = n2(object).bar = 3; + + assert.notEqual(n1(object).foo, n2(object).foo, + "object can have multiple namespaces"); + assert.equal(n1(object).bar, n2(object).bar, + "object can have matching props in diff namespaces"); +}; + +exports["test ns alias"] = function(assert) { + assert.strictEqual(ns, Namespace, + "ns is an alias of Namespace"); +} + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-observer-service.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-observer-service.js new file mode 100644 index 0000000..222ae22 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-observer-service.js @@ -0,0 +1,77 @@ +/* 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/. */ + +var observers = require("api-utils/observer-service"); +var {Cc,Ci} = require("chrome"); +const { Loader } = require("./helpers"); + +exports.testUnloadAndErrorLogging = function(test) { + var prints = []; + var loader = Loader(module, { dump: function print(message) { + prints.push(message); + }}); + var sbobsvc = loader.require("api-utils/observer-service"); + + var timesCalled = 0; + var cb = function(subject, data) { + timesCalled++; + }; + var badCb = function(subject, data) { + throw new Error("foo"); + }; + sbobsvc.add("blarg", cb); + observers.notify("blarg", "yo yo"); + test.assertEqual(timesCalled, 1); + sbobsvc.add("narg", badCb); + observers.notify("narg", "yo yo"); + var lines = prints[0].split("\n"); + test.assertEqual(lines[0], "error: An exception occurred."); + test.assertEqual(lines[1], "Traceback (most recent call last):"); + test.assertEqual(lines.slice(-2)[0], "Error: foo"); + + loader.unload(); + observers.notify("blarg", "yo yo"); + test.assertEqual(timesCalled, 1); +}; + +exports.testObserverService = function(test) { + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + var service = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + var uri = ios.newURI("http://www.foo.com", null, null); + var timesCalled = 0; + var lastSubject = null; + var lastData = null; + + var cb = function(subject, data) { + timesCalled++; + lastSubject = subject; + lastData = data; + }; + + observers.add("blarg", cb); + service.notifyObservers(uri, "blarg", "some data"); + test.assertEqual(timesCalled, 1, + "observer-service.add() should call callback"); + test.assertEqual(lastSubject, uri, + "observer-service.add() should pass subject"); + test.assertEqual(lastData, "some data", + "observer-service.add() should pass data"); + + function customSubject() {} + function customData() {} + observers.notify("blarg", customSubject, customData); + test.assertEqual(timesCalled, 2, + "observer-service.notify() should work"); + test.assertEqual(lastSubject, customSubject, + "observer-service.notify() should pass+wrap subject"); + test.assertEqual(lastData, customData, + "observer-service.notify() should pass data"); + + observers.remove("blarg", cb); + service.notifyObservers(null, "blarg", "some data"); + test.assertEqual(timesCalled, 2, + "observer-service.remove() should work"); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-passwords-utils.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-passwords-utils.js new file mode 100644 index 0000000..ab758ed --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-passwords-utils.js @@ -0,0 +1,142 @@ +/* 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 { store, search, remove } = require("passwords/utils"); + +exports["test store requires `password` field"] = function(assert) { + assert.throws(function() { + store({ username: "foo", realm: "bar" }); + }, "`password` is required"); +}; + +exports["test store requires `username` field"] = function(assert) { + assert.throws(function() { + store({ password: "foo", realm: "bar" }); + }, "`username` is required"); +}; + +exports["test store requires `realm` field"] = function(assert) { + assert.throws(function() { + store({ username: "foo", password: "bar" }); + }, "`password` is required"); +}; + +exports["test can't store same login twice"] = function(assert) { + let options = { username: "user", password: "pass", realm: "realm" }; + store(options); + assert.throws(function() { + store(options); + }, "can't store same pass twice"); + remove(options); +}; + +exports["test remove throws if no login found"] = function(assert) { + assert.throws(function() { + remove({ username: "foo", password: "bar", realm: "baz" }); + }, "can't remove unstored credentials"); +}; + +exports["test addon associated credentials"] = function(assert) { + let options = { username: "foo", password: "bar", realm: "baz" }; + store(options); + + assert.ok(search().length, "credential was stored"); + assert.ok(search(options).length, "stored credential found"); + assert.ok(search({ username: options.username }).length, "found by username"); + assert.ok(search({ password: options.password }).length, "found by password"); + assert.ok(search({ realm: options.realm }).length, "found by realm"); + + let credential = search(options)[0]; + assert.equal(credential.url.indexOf("addon:"), 0, + "`addon:` uri is used for add-on associated credentials"); + assert.equal(credential.username, options.username, "username matches"); + assert.equal(credential.password, options.password, "password matches"); + assert.equal(credential.realm, options.realm, "realm matches"); + assert.equal(credential.formSubmitURL, null, + "`formSubmitURL` is `null` for add-on associated credentials"); + assert.equal(credential.usernameField, "", "usernameField is empty"); + assert.equal(credential.passwordField, "", "passwordField is empty"); + + remove(search(options)[0]); + assert.ok(!search(options).length, "remove worked"); +}; + +exports["test web page associated credentials"] = function(assert) { + let options = { + url: "http://www.example.com", + formSubmitURL: "http://login.example.com", + username: "user", + password: "pass", + usernameField: "user-f", + passwordField: "pass-f" + }; + store({ + url: "http://www.example.com/login", + formSubmitURL: "http://login.example.com/foo/authenticate.cgi", + username: options.username, + password: options.password, + usernameField: options.usernameField, + passwordField: options.passwordField + }); + + assert.ok(search().length, "credential was stored"); + assert.ok(search(options).length, "stored credential found"); + assert.ok(search({ username: options.username }).length, "found by username"); + assert.ok(search({ password: options.password }).length, "found by password"); + assert.ok(search({ formSubmitURL: options.formSubmitURL }).length, + "found by formSubmitURL"); + assert.ok(search({ usernameField: options.usernameField }).length, + "found by usernameField"); + assert.ok(search({ passwordField: options.passwordField }).length, + "found by passwordField"); + + let credential = search(options)[0]; + assert.equal(credential.url, options.url, "url matches"); + assert.equal(credential.username, options.username, "username matches"); + assert.equal(credential.password, options.password, "password matches"); + assert.equal(credential.realm, null, "realm is "); + assert.equal(credential.formSubmitURL, options.formSubmitURL, + "`formSubmitURL` matches"); + assert.equal(credential.usernameField, options.usernameField, + "usernameField matches"); + assert.equal(credential.passwordField, options.passwordField, + "passwordField matches"); + + remove(search(options)[0]); + assert.ok(!search(options).length, "remove worked"); +}; + +exports["test site authentication credentials"] = function(assert) { + let options = { + url: "http://test.authentication.com", + username: "u", + password: "p", + realm: "r" + }; + + store(options); + assert.ok(search().length, "credential was stored"); + assert.ok(search(options).length, "stored credential found"); + assert.ok(search({ username: options.username }).length, "found by username"); + assert.ok(search({ password: options.password }).length, "found by password"); + assert.ok(search({ realm: options.realm }).length, "found by realm"); + assert.ok(search({ url: options.url }).length, "found by url"); + + let credential = search(options)[0]; + assert.equal(credential.url, options.url, "url matches"); + assert.equal(credential.username, options.username, "username matches"); + assert.equal(credential.password, options.password, "password matches"); + assert.equal(credential.realm, options.realm, "realm matches"); + assert.equal(credential.formSubmitURL, null, + "`formSubmitURL` is `null` for site authentication credentials"); + assert.equal(credential.usernameField, "", "usernameField is empty"); + assert.equal(credential.passwordField, "", "passwordField is empty"); + + remove(search(options)[0]); + assert.ok(!search(options).length, "remove worked"); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-plain-text-console.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-plain-text-console.js new file mode 100644 index 0000000..40fb519 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-plain-text-console.js @@ -0,0 +1,68 @@ +/* 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/. */ + +exports.testPlainTextConsole = function(test) { + var prints = []; + function print(message) { + prints.push(message); + } + function lastPrint() { + var last = prints.slice(-1)[0]; + prints = []; + return last; + } + + var Console = require("plain-text-console").PlainTextConsole; + var con = new Console(print); + + test.pass("PlainTextConsole instantiates"); + + con.log('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "info: testing 1 2,3,4\n", + "PlainTextConsole.log() must work."); + + con.info('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "info: testing 1 2,3,4\n", + "PlainTextConsole.info() must work."); + + con.warn('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "warning: testing 1 2,3,4\n", + "PlainTextConsole.warn() must work."); + + con.error('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "error: testing 1 2,3,4\n", + "PlainTextConsole.error() must work."); + + con.debug('testing', 1, [2, 3, 4]); + test.assertEqual(lastPrint(), "debug: testing 1 2,3,4\n", + "PlainTextConsole.debug() must work."); + + con.log('testing', undefined); + test.assertEqual(lastPrint(), "info: testing undefined\n", + "PlainTextConsole.log() must stringify undefined."); + + con.log('testing', null); + test.assertEqual(lastPrint(), "info: testing null\n", + "PlainTextConsole.log() must stringify null."); + + con.log("testing", { toString: function() "obj.toString()" }); + test.assertEqual(lastPrint(), "info: testing obj.toString()\n", + "PlainTextConsole.log() must stringify custom toString."); + + con.log("testing", { toString: function() { throw "fail!"; } }); + test.assertEqual(lastPrint(), "info: testing <toString() error>\n", + "PlainTextConsole.log() must stringify custom bad toString."); + + con.exception(new Error("blah")); + var tbLines = prints[0].split("\n"); + test.assertEqual(tbLines[0], "error: An exception occurred."); + test.assertEqual(tbLines[1], "Traceback (most recent call last):"); + test.assertEqual(tbLines.slice(-2)[0], "Error: blah"); + + prints = []; + con.trace(); + tbLines = prints[0].split("\n"); + test.assertEqual(tbLines[0], "info: Traceback (most recent call last):"); + test.assertEqual(tbLines.slice(-2)[0].trim(), "con.trace();"); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-preferences-service.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-preferences-service.js new file mode 100644 index 0000000..857357e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-preferences-service.js @@ -0,0 +1,112 @@ +/* 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/. */ + +const prefs = require("preferences-service"); +const { Cc, Ci, Cu } = require("chrome"); +const BundleService = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService); + +exports.testReset = function(test) { + prefs.reset("test_reset_pref"); + test.assertEqual(prefs.has("test_reset_pref"), false); + test.assertEqual(prefs.isSet("test_reset_pref"), false); + prefs.set("test_reset_pref", 5); + test.assertEqual(prefs.has("test_reset_pref"), true); + test.assertEqual(prefs.isSet("test_reset_pref"), true); +}; + +exports.testGetAndSet = function(test) { + let svc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(null); + svc.setCharPref("test_get_string_pref", "a normal string"); + test.assertEqual(prefs.get("test_get_string_pref"), "a normal string", + "preferences-service should read from " + + "application-wide preferences service"); + + prefs.set("test_set_get_pref.integer", 1); + test.assertEqual(prefs.get("test_set_get_pref.integer"), 1, + "set/get integer preference should work"); + + prefs.set("test_set_get_number_pref", 42); + test.assertRaises( + function() { prefs.set("test_set_get_number_pref", 3.14159); }, + "cannot store non-integer number: 3.14159", + "setting a float preference should raise an error" + ); + test.assertEqual(prefs.get("test_set_get_number_pref"), 42, + "bad-type write attempt should not overwrite"); + + // 0x80000000 (no), 0x7fffffff (yes), -0x80000000 (yes), -0x80000001 (no) + test.assertRaises( + function() { prefs.set("test_set_get_number_pref", Math.pow(2, 31)); }, + ("you cannot set the test_set_get_number_pref pref to the number " + + "2147483648, as number pref values must be in the signed 32-bit " + + "integer range -(2^31) to 2^31-1. To store numbers outside that " + + "range, store them as strings."), + "setting an int pref outside the range -(2^31) to 2^31-1 shouldn't work" + ); + test.assertEqual(prefs.get("test_set_get_number_pref"), 42, + "out-of-range write attempt should not overwrite 1"); + prefs.set("test_set_get_number_pref", Math.pow(2, 31)-1); + test.assertEqual(prefs.get("test_set_get_number_pref"), 0x7fffffff, + "in-range write attempt should work 1"); + prefs.set("test_set_get_number_pref", -Math.pow(2, 31)); + test.assertEqual(prefs.get("test_set_get_number_pref"), -0x80000000, + "in-range write attempt should work 2"); + test.assertRaises( + function() { prefs.set("test_set_get_number_pref", -0x80000001); }, + ("you cannot set the test_set_get_number_pref pref to the number " + + "-2147483649, as number pref values must be in the signed 32-bit " + + "integer range -(2^31) to 2^31-1. To store numbers outside that " + + "range, store them as strings."), + "setting an int pref outside the range -(2^31) to 2^31-1 shouldn't work" + ); + test.assertEqual(prefs.get("test_set_get_number_pref"), -0x80000000, + "out-of-range write attempt should not overwrite 2"); + + + prefs.set("test_set_get_pref.string", "foo"); + test.assertEqual(prefs.get("test_set_get_pref.string"), "foo", + "set/get string preference should work"); + + prefs.set("test_set_get_pref.boolean", true); + test.assertEqual(prefs.get("test_set_get_pref.boolean"), true, + "set/get boolean preference should work"); + + prefs.set("test_set_get_unicode_pref", String.fromCharCode(960)); + test.assertEqual(prefs.get("test_set_get_unicode_pref"), + String.fromCharCode(960), + "set/get unicode preference should work"); + + var unsupportedValues = [null, [], undefined]; + unsupportedValues.forEach( + function(value) { + test.assertRaises( + function() { prefs.set("test_set_pref", value); }, + ("can't set pref test_set_pref to value '" + value + "'; " + + "it isn't a string, integer, or boolean"), + "Setting a pref to " + uneval(value) + " should raise error" + ); + }); +}; + +exports.testGetSetLocalized = function(test) { + let prefName = "general.useragent.locale"; + + // Ensure that "general.useragent.locale" is a 'localized' pref + let bundleURL = "chrome://global/locale/intl.properties"; + prefs.setLocalized(prefName, bundleURL); + + // Fetch the expected value directly from the property file + let expectedValue = BundleService.createBundle(bundleURL). + GetStringFromName(prefName). + toLowerCase(); + + test.assertEqual(prefs.getLocalized(prefName).toLowerCase(), + expectedValue, + "get localized preference"); + + // Undo our modification + prefs.reset(prefName); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-process.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-process.js new file mode 100644 index 0000000..43d0864 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-process.js @@ -0,0 +1,36 @@ +/* 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 { Loader } = require("./helpers"); +const { jetpackID } = require('@packaging'); + +// bug 707562: '#' char in packaging was causing loader to be undefined +exports.testBug707562 = function(test) { + test.waitUntilDone(); + + let packaging = JSON.parse(JSON.stringify(require("@packaging"))); + packaging.metadata["api-utils"].author = "###"; + + let loader = Loader(module, {}, packaging); + let process = loader.require("process"); + + // has spawn? + test.assert(process.spawn, "'process' module exports 'spawn' method."); + + let promise = process.spawn("testID", ""); + test.assertFunction(promise, "spawn makes a promise."); + + promise(function(addon) { + addon.channel("TEST:LOADED").input(function(data) { + test.assert(data, "The loader was successfully created!"); + loader.unload(); + test.done(); + }); + + addon.loadScript('data:,sendAsyncMessage("'+jetpackID+':TEST:LOADED", !!this.loader);', false); + test.pass("spawn's promise was delivered! (which means a addon process object is available))."); + }); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-promise.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-promise.js new file mode 100644 index 0000000..fc7706f --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-promise.js @@ -0,0 +1,311 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/*jshint asi: true undef: true es5: true node: true devel: true + forin: true */ +/*global define: true */ + +'use strict'; + +var core = require('api-utils/promise'), + defer = core.defer, resolve = core.resolve, reject = core.reject, + promised = core.promised + +exports['test all observers are notified'] = function(assert, done) { + var expected = 'Taram pam param!' + var deferred = defer() + var pending = 10, i = 0 + + function resolved(value) { + assert.equal(value, expected, 'value resolved as expected: #' + pending) + if (!--pending) done() + } + + while (i++ < pending) deferred.promise.then(resolved) + + deferred.resolve(expected) +} + +exports['test exceptions dont stop notifications'] = function(assert, done) { + var threw = false, boom = Error('Boom!') + var deferred = defer() + + var promise2 = deferred.promise.then(function() { + threw = true + throw boom + }) + + deferred.promise.then(function() { + assert.ok(threw, 'observer is called even though previos one threw') + promise2.then(function() { + assert.fail('should not resolve') + }, function(reason) { + assert.equal(reason, boom, 'rejects to thrown error') + done() + }) + }) + + deferred.resolve('go!') +} + +exports['test subsequent resolves are ignored'] = function(assert, done) { + var deferred = defer() + deferred.resolve(1) + deferred.resolve(2) + deferred.reject(3) + + deferred.promise.then(function(actual) { + assert.equal(actual, 1, 'resolves to first value') + }, function() { + assert.fail('must not reject') + }) + deferred.promise.then(function(actual) { + assert.equal(actual, 1, 'subsequent resolutions are ignored') + done() + }, function() { + assert.fail('must not reject') + }) +} + +exports['test subsequent rejections are ignored'] = function(assert, done) { + var deferred = defer() + deferred.reject(1) + deferred.resolve(2) + deferred.reject(3) + + deferred.promise.then(function(actual) { + assert.fail('must not resolve') + }, function(actual) { + assert.equal(actual, 1, 'must reject to first') + }) + deferred.promise.then(function(actual) { + assert.fail('must not resolve') + }, function(actual) { + assert.equal(actual, 1, 'must reject to first') + done() + }) +} + +exports['test error recovery'] = function(assert, done) { + var boom = Error('Boom!') + var deferred = defer() + + deferred.promise.then(function() { + assert.fail('rejected promise should not resolve') + }, function(reason) { + assert.equal(reason, boom, 'rejection reason delivered') + return 'recovery' + }).then(function(value) { + assert.equal(value, 'recovery', 'error handled by a handler') + done() + }) + + deferred.reject(boom) +} + + +exports['test error recovery with promise'] = function(assert, done) { + var deferred = defer() + + deferred.promise.then(function() { + assert.fail('must reject') + }, function(actual) { + assert.equal(actual, 'reason', 'rejected') + var deferred = defer() + deferred.resolve('recovery') + return deferred.promise + }).then(function(actual) { + assert.equal(actual, 'recovery', 'recorvered via promise') + var deferred = defer() + deferred.reject('error') + return deferred.promise + }).then(null, function(actual) { + assert.equal(actual, 'error', 'rejected via promise') + var deferred = defer() + deferred.reject('end') + return deferred.promise + }).then(null, function(actual) { + assert.equal(actual, 'end', 'rejeced via promise') + done() + }) + + deferred.reject('reason') +} + +exports['test propagation'] = function(assert, done) { + var d1 = defer(), d2 = defer(), d3 = defer() + + d1.promise.then(function(actual) { + assert.equal(actual, 'expected', 'resolves to expected value') + done() + }) + + d1.resolve(d2.promise) + d2.resolve(d3.promise) + d3.resolve('expected') +} + +exports['test chaining'] = function(assert, done) { + var boom = Error('boom'), brax = Error('braxXXx') + var deferred = defer() + + deferred.promise.then().then().then(function(actual) { + assert.equal(actual, 2, 'value propagates unchanged') + return actual + 2 + }).then(null, function(reason) { + assert.fail('should not reject') + }).then(function(actual) { + assert.equal(actual, 4, 'value propagates through if not handled') + throw boom + }).then(function(actual) { + assert.fail('exception must reject promise') + }).then().then(null, function(actual) { + assert.equal(actual, boom, 'reason propagates unchanged') + throw brax + }).then().then(null, function(actual) { + assert.equal(actual, brax, 'reason changed becase of exception') + return 'recovery' + }).then(function(actual) { + assert.equal(actual, 'recovery', 'recovered from error') + done() + }) + + deferred.resolve(2) +} + + +exports['test reject'] = function(assert, done) { + var expected = Error('boom') + + reject(expected).then(function() { + assert.fail('should reject') + }, function(actual) { + assert.equal(actual, expected, 'rejected with expected reason') + }).then(function() { + done() + }) +} + +exports['test resolve to rejected'] = function(assert, done) { + var expected = Error('boom') + var deferred = defer() + + deferred.promise.then(function() { + assert.fail('should reject') + }, function(actual) { + assert.equal(actual, expected, 'rejected with expected failure') + }).then(function() { + done() + }) + + deferred.resolve(reject(expected)) +} + +exports['test resolve'] = function(assert, done) { + var expected = 'value' + resolve(expected).then(function(actual) { + assert.equal(actual, expected, 'resolved as expected') + }).then(function() { + done() + }) +} + +exports['test resolve with prototype'] = function(assert, done) { + var seventy = resolve(70, { + subtract: function subtract(y) { + return this.then(function(x) { return x - y }) + } + }) + + seventy.subtract(17).then(function(actual) { + assert.equal(actual, 70 - 17, 'resolves to expected') + done() + }) +} + +exports['test promised with normal args'] = function(assert, done) { + var sum = promised(function(x, y) { return x + y }) + + sum(7, 8).then(function(actual) { + assert.equal(actual, 7 + 8, 'resolves as expected') + done() + }) +} + +exports['test promised with promise args'] = function(assert, done) { + var sum = promised(function(x, y) { return x + y }) + var deferred = defer() + + sum(11, deferred.promise).then(function(actual) { + assert.equal(actual, 11 + 24, 'resolved as expected') + done() + }) + + deferred.resolve(24) +} + +exports['test promised with prototype'] = function(assert, done) { + var deferred = defer() + var numeric = {} + numeric.subtract = promised(function(y) { return this - y }, numeric) + + var sum = promised(function(x, y) { return x + y }, numeric) + + sum(7, 70). + subtract(14). + subtract(deferred.promise). + subtract(5). + then(function(actual) { + assert.equal(actual, 7 + 70 - 14 - 23 - 5, 'resolved as expected') + done() + }) + + deferred.resolve(23) +} + +exports['test promised error handleing'] = function(assert, done) { + var expected = Error('boom') + var f = promised(function() { + throw expected + }) + + f().then(function() { + assert.fail('should reject') + }, function(actual) { + assert.equal(actual, expected, 'rejected as expected') + done() + }) +} + +exports['test return promise form promised'] = function(assert, done) { + var f = promised(function() { + return resolve(17) + }) + + f().then(function(actual) { + assert.equal(actual, 17, 'resolves to a promise resolution') + done() + }) +} + +exports['test promised returning failure'] = function(assert, done) { + var expected = Error('boom') + var f = promised(function() { + return reject(expected) + }) + + f().then(function() { + assert.fail('must reject') + }, function(actual) { + assert.equal(actual, expected, 'rejects with expected reason') + done() + }) +} + +exports['test promised are greedy'] = function(assert, done) { + var runs = 0 + var f = promised(function() { ++runs }) + var promise = f() + assert.equal(runs, 1, 'promised runs task right away') + done() +} + +require("test").run(exports) diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-querystring.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-querystring.js new file mode 100644 index 0000000..30b2729 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-querystring.js @@ -0,0 +1,206 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +"use strict"; + +// test using assert +var qs = require('api-utils/querystring'); + +// folding block, commented to pass gjslint +// {{{ +// [ wonkyQS, canonicalQS, obj ] +var qsTestCases = [ + ['foo=918854443121279438895193', + 'foo=918854443121279438895193', + {'foo': '918854443121279438895193'}], + ['foo=bar', 'foo=bar', {'foo': 'bar'}], + //['foo=bar&foo=quux', 'foo=bar&foo=quux', {'foo': ['bar', 'quux']}], + ['foo=1&bar=2', 'foo=1&bar=2', {'foo': '1', 'bar': '2'}], + // ['my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F', + // 'my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F', + // {'my weird field': 'q1!2"\'w$5&7/z8)?' }], + ['foo%3Dbaz=bar', 'foo%3Dbaz=bar', {'foo=baz': 'bar'}], + ['foo=baz=bar', 'foo=baz%3Dbar', {'foo': 'baz=bar'}], + /* + ['str=foo&arr=1&arr=2&arr=3&somenull=&undef=', + 'str=foo&arr=1&arr=2&arr=3&somenull=&undef=', + { 'str': 'foo', + 'arr': ['1', '2', '3'], + 'somenull': '', + 'undef': ''}], + */ + //[' foo = bar ', '%20foo%20=%20bar%20', {' foo ': ' bar '}], + // disable test that fails ['foo=%zx', 'foo=%25zx', {'foo': '%zx'}], + ['foo=%EF%BF%BD', 'foo=%EF%BF%BD', {'foo': '\ufffd' }] +]; + +// [ wonkyQS, canonicalQS, obj ] +var qsColonTestCases = [ + ['foo:bar', 'foo:bar', {'foo': 'bar'}], + //['foo:bar;foo:quux', 'foo:bar;foo:quux', {'foo': ['bar', 'quux']}], + ['foo:1&bar:2;baz:quux', + 'foo:1%26bar%3A2;baz:quux', + {'foo': '1&bar:2', 'baz': 'quux'}], + ['foo%3Abaz:bar', 'foo%3Abaz:bar', {'foo:baz': 'bar'}], + ['foo:baz:bar', 'foo:baz%3Abar', {'foo': 'baz:bar'}] +]; + +// [wonkyObj, qs, canonicalObj] +var extendedFunction = function() {}; +extendedFunction.prototype = {a: 'b'}; +var qsWeirdObjects = [ + //[{regexp: /./g}, 'regexp=', {'regexp': ''}], + //[{regexp: new RegExp('.', 'g')}, 'regexp=', {'regexp': ''}], + //[{fn: function() {}}, 'fn=', {'fn': ''}], + //[{fn: new Function('')}, 'fn=', {'fn': ''}], + //[{math: Math}, 'math=', {'math': ''}], + //[{e: extendedFunction}, 'e=', {'e': ''}], + //[{d: new Date()}, 'd=', {'d': ''}], + //[{d: Date}, 'd=', {'d': ''}], + //[{f: new Boolean(false), t: new Boolean(true)}, 'f=&t=', {'f': '', 't': ''}], + [{f: false, t: true}, 'f=false&t=true', {'f': 'false', 't': 'true'}], + //[{n: null}, 'n=', {'n': ''}], + //[{nan: NaN}, 'nan=', {'nan': ''}], + //[{inf: Infinity}, 'inf=', {'inf': ''}] +]; +// }}} + +var qsNoMungeTestCases = [ + ['', {}], + //['foo=bar&foo=baz', {'foo': ['bar', 'baz']}], + ['blah=burp', {'blah': 'burp'}], + //['gragh=1&gragh=3&goo=2', {'gragh': ['1', '3'], 'goo': '2'}], + ['frappucino=muffin&goat%5B%5D=scone&pond=moose', + {'frappucino': 'muffin', 'goat[]': 'scone', 'pond': 'moose'}], + ['trololol=yes&lololo=no', {'trololol': 'yes', 'lololo': 'no'}] +]; + +exports['test basic'] = function(assert) { + assert.strictEqual('918854443121279438895193', + qs.parse('id=918854443121279438895193').id, + 'prase id=918854443121279438895193'); +}; + +exports['test that the canonical qs is parsed properly'] = function(assert) { + qsTestCases.forEach(function(testCase) { + assert.deepEqual(testCase[2], qs.parse(testCase[0]), + 'parse ' + testCase[0]); + }); +}; + + +exports['test that the colon test cases can do the same'] = function(assert) { + qsColonTestCases.forEach(function(testCase) { + assert.deepEqual(testCase[2], qs.parse(testCase[0], ';', ':'), + 'parse ' + testCase[0] + ' -> ; :'); + }); +}; + +exports['test the weird objects, that they get parsed properly'] = function(assert) { + qsWeirdObjects.forEach(function(testCase) { + assert.deepEqual(testCase[2], qs.parse(testCase[1]), + 'parse ' + testCase[1]); + }); +}; + +exports['test non munge test cases'] = function(assert) { + qsNoMungeTestCases.forEach(function(testCase) { + //console.log(testCase[0], JSON.stringify(testCase[1]), qs.stringify(testCase[1], '&', '=', false)); + assert.deepEqual(testCase[0], qs.stringify(testCase[1], '&', '=', false), + 'stringify ' + JSON.stringify(testCase[1]) + ' -> & ='); + }); +}; + +exports['test the nested qs-in-qs case'] = function(assert) { + var f = qs.parse('a=b&q=x%3Dy%26y%3Dz'); + f.q = qs.parse(f.q); + assert.deepEqual(f, { a: 'b', q: { x: 'y', y: 'z' } }, + 'parse a=b&q=x%3Dy%26y%3Dz'); +}; + +exports['test nested in colon'] = function(assert) { + var f = qs.parse('a:b;q:x%3Ay%3By%3Az', ';', ':'); + f.q = qs.parse(f.q, ';', ':'); + assert.deepEqual(f, { a: 'b', q: { x: 'y', y: 'z' } }, + 'parse a:b;q:x%3Ay%3By%3Az -> ; :'); +}; + +exports['test stringifying'] = function(assert) { + qsTestCases.forEach(function(testCase) { + assert.equal(testCase[1], qs.stringify(testCase[2]), + 'stringify ' + JSON.stringify(testCase[2])); + }); + + qsColonTestCases.forEach(function(testCase) { + assert.equal(testCase[1], qs.stringify(testCase[2], ';', ':'), + 'stringify ' + JSON.stringify(testCase[2]) + ' -> ; :'); + }); + + qsWeirdObjects.forEach(function(testCase) { + assert.equal(testCase[1], qs.stringify(testCase[0]), + 'stringify ' + JSON.stringify(testCase[0])); + }); +}; + +exports['test stringifying nested'] = function(assert) { + var f = qs.stringify({ + a: 'b', + q: qs.stringify({ + x: 'y', + y: 'z' + }) + }); + assert.equal(f, 'a=b&q=x%3Dy%26y%3Dz', + JSON.stringify({ + a: 'b', + 'qs.stringify -> q': { + x: 'y', + y: 'z' + } + })); + + var threw = false; + try { qs.parse(undefined); } catch(error) { threw = true; } + assert.ok(!threw, "does not throws on undefined"); +}; + +exports['test nested in colon'] = function(assert) { + var f = qs.stringify({ + a: 'b', + q: qs.stringify({ + x: 'y', + y: 'z' + }, ';', ':') + }, ';', ':'); + assert.equal(f, 'a:b;q:x%3Ay%3By%3Az', + 'stringify ' + JSON.stringify({ + a: 'b', + 'qs.stringify -> q': { + x: 'y', + y: 'z' + } + }) + ' -> ; : '); + + + assert.deepEqual({}, qs.parse(), 'parse undefined'); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-registry.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-registry.js new file mode 100644 index 0000000..c5820fb --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-registry.js @@ -0,0 +1,80 @@ +/* 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'; + +exports['test:add'] = function(test) { + function Class() {} + let fixture = require('utils/registry').Registry(Class); + let isAddEmitted = false; + fixture.on('add', function(item) { + test.assert( + item instanceof Class, + 'if object added is not an instance should construct instance from it' + ); + test.assert( + fixture.has(item), + 'callback is called after instance is added' + ); + test.assert( + !isAddEmitted, + 'callback should be called for the same item only once' + ); + isAddEmitted = true; + }); + + let object = fixture.add({}); + fixture.add(object); +}; + +exports['test:remove'] = function(test) { + function Class() {} + let fixture = require('utils/registry').Registry(Class); + fixture.on('remove', function(item) { + test.assert( + item instanceof Class, + 'if object removed can be only instance of Class' + ); + test.assert( + fixture.has(item), + 'callback is called before instance is removed' + ); + test.assert( + !isRemoveEmitted, + 'callback should be called for the same item only once' + ); + isRemoveEmitted = true; + }); + + fixture.remove({}); + let object = fixture.add({}); + fixture.remove(object); + fixture.remove(object); +}; + +exports['test:items'] = function(test) { + function Class() {} + let fixture = require('utils/registry').Registry(Class), + actual, + times = 0; + + function testItem(item) { + times ++; + test.assertEqual( + actual, + item, + 'item should match actual item being added/removed' + ); + } + + actual = fixture.add({}); + + fixture.on('add', testItem); + fixture.on('remove', testItem); + + fixture.remove(actual); + fixture.remove(fixture.add(actual = new Class())); + test.assertEqual(3, times, 'should notify listeners on each call'); +} + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-require.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-require.js new file mode 100644 index 0000000..7508883 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-require.js @@ -0,0 +1,29 @@ +/* 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/. */ + +const traceback = require("traceback"); + +exports.test_no_args = function(test) { + var passed = false; + try { + var oops = require(); // leave this on line 6! + } catch(e) { + let msg = e.toString(); + test.assertEqual(msg.indexOf("Error: you must provide a module name when calling require() from "), 0); + test.assertNotEqual(msg.indexOf("test-require"), -1, msg); + // we'd also like to assert that the right filename and linenumber is in + // the stack trace, but this currently doesn't work (see bugs 679591 and + // 551604) + if (0) { + let tb = traceback.fromException(e); + let lastFrame = tb[tb.length-1]; + test.assertNotEqual(lastFrame.filename.indexOf("test-require.js"), -1, + lastFrame.filename); + test.assertEqual(lastFrame.lineNo, 6); + test.assertEqual(lastFrame.funcName, "??"); + } + passed = true; + } + test.assert(passed, 'require() with no args should raise helpful error'); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-sandbox.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-sandbox.js new file mode 100644 index 0000000..fb7a1da --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-sandbox.js @@ -0,0 +1,113 @@ +/* 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/. */ + +const { sandbox, load, evaluate } = require('api-utils/sandbox'); +const fixturesURI = module.uri.split('test-sandbox.js')[0] + 'fixtures/'; + + +exports['test basics'] = function(assert) { + let fixture = sandbox('http://example.com'); + assert.equal(evaluate(fixture, 'var a = 1;'), undefined, + 'returns expression value'); + assert.equal(evaluate(fixture, 'b = 2;'), 2, + 'returns expression value'); + assert.equal(fixture.b, 2, 'global is defined as property'); + assert.equal(fixture.a, 1, 'global is defined as property'); + assert.equal(evaluate(fixture, 'a + b;'), 3, 'returns correct sum'); +}; + +exports['test non-privileged'] = function(assert) { + let fixture = sandbox('http://example.com'); + assert.throws(function() { + evaluate(fixture, 'Compo' + 'nents.utils'); + }, 'Access to components is restricted'); + fixture.sandbox = sandbox; + assert.throws(function() { + evaluate(fixture, sandbox('http://foo.com')); + }, 'Can not call privileged code'); +}; + +exports['test injection'] = function(assert) { + let fixture = sandbox(); + fixture.hi = function(name) 'Hi ' + name + assert.equal(evaluate(fixture, 'hi("sandbox");'), 'Hi sandbox', + 'injected functions are callable'); +}; + +exports['test exceptions'] = function(assert) { + let fixture = sandbox(); + try { + evaluate(fixture, '!' + function() { + var message = 'boom'; + throw Error(message); + } + '();'); + } + catch (error) { + assert.equal(error.fileName, '', 'no fileName reported'); + assert.equal(error.lineNumber, 3, 'reports correct line number'); + } + + try { + evaluate(fixture, '!' + function() { + var message = 'boom'; + throw Error(message); + } + '();', 'foo.js'); + } + catch (error) { + assert.equal(error.fileName, 'foo.js', 'correct fileName reported'); + assert.equal(error.lineNumber, 3, 'reports correct line number'); + } + + try { + evaluate(fixture, '!' + function() { + var message = 'boom'; + throw Error(message); + } + '();', 'foo.js', 2); + } + catch (error) { + assert.equal(error.fileName, 'foo.js', 'correct fileName reported'); + assert.equal(error.lineNumber, 4, 'line number was opted'); + } +}; + +exports['test opt version'] = function(assert) { + let fixture = sandbox(); + assert.throws(function() { + evaluate(fixture, 'let a = 2;', 'test.js', 1, '1.5'); + }, 'No let in js 1.5'); +}; + +exports['test load'] = function(assert) { + let fixture = sandbox(); + load(fixture, fixturesURI + 'sandbox-normal.js'); + assert.equal(fixture.a, 1, 'global variable defined'); + assert.equal(fixture.b, 2, 'global via `this` property was set'); + assert.equal(fixture.f(), 4, 'function was defined'); +}; + +exports['test load with data: URL'] = function(assert) { + let code = "var a = 1; this.b = 2; function f() 4"; + let fixture = sandbox(); + load(fixture, "data:," + encodeURIComponent(code)); + + assert.equal(fixture.a, 1, 'global variable defined'); + assert.equal(fixture.b, 2, 'global via `this` property was set'); + assert.equal(fixture.f(), 4, 'function was defined'); +}; + +exports['test load script with complex char'] = function(assert) { + let fixture = sandbox(); + load(fixture, fixturesURI + 'sandbox-complex-character.js'); + assert.equal(fixture.chars, 'გამარჯობა', 'complex chars were loaded correctly'); +}; + +exports['test load script with data: URL and complex char'] = function(assert) { + let code = "var chars = 'გამარჯობა';"; + let fixture = sandbox(); + load(fixture, "data:," + encodeURIComponent(code)); + + assert.equal(fixture.chars, 'გამარჯობა', 'complex chars were loaded correctly'); +}; + +require('test').run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-self.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-self.js new file mode 100644 index 0000000..2f0e093 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-self.js @@ -0,0 +1,54 @@ +/* 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, Cu, Cm, components} = require('chrome'); +Cu.import("resource://gre/modules/AddonManager.jsm", this); + +exports.testSelf = function(test) { + var self = require("self"); + + var source = self.data.load("test-content-symbiont.js"); + test.assert(source.match(/test-content-symbiont/), "self.data.load() works"); + + // Likewise, we can't assert anything about the full URL, because that + // depends on self.id . We can only assert that it ends in the right + // thing. + var url = self.data.url("test-content-symbiont.js"); + test.assertEqual(typeof(url), "string", "self.data.url('x') returns string"); + test.assertEqual(/\/test-content-symbiont\.js$/.test(url), true); + + // Make sure 'undefined' is not in url when no string is provided. + url = self.data.url(); + test.assertEqual(typeof(url), "string", "self.data.url() returns string"); + test.assertEqual(/\/undefined$/.test(url), false); + + // When tests are run on just the api-utils package, self.name is + // api-utils. When they're run as 'cfx testall', self.name is testpkgs. + test.assert((self.name == "api-utils") || (self.name == "testpkgs"), + "self.name is api-utils or testpkgs"); +}; + +exports.testSelfID = function(test) { + test.waitUntilDone(); + + var self = require("self"); + // We can't assert anything about the ID inside the unit test right now, + // because the ID we get depends upon how the test was invoked. The idea + // is that it is supposed to come from the main top-level package's + // package.json file, from the "id" key. + test.assertEqual(typeof(self.id), "string", "self.id is a string"); + test.assert(self.id.length > 0); + + AddonManager.getAddonByID(self.id, function(addon) { + if (!addon) { + test.fail("did not find addon with self.id"); + } + else { + test.pass("found addon with self.id"); + } + test.done(); + }); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-set-exports.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-set-exports.js new file mode 100644 index 0000000..5221237 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-set-exports.js @@ -0,0 +1,35 @@ +/* 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/. */ + +let four = require("./modules/exportsEquals"); +exports.testExportsEquals = function(test) { + test.assertEqual(four, 4); +} + +/* TODO: Discuss idea of dropping support for this feature that was alternative + to `module.exports = ..` that failed. +let five = require("./modules/setExports"); +exports.testSetExports = function(test) { + test.assertEqual(five, 5); +} + +exports.testDupeSetExports = function(test) { + var passed = false; + try { + var dupe = require('./modules/dupeSetExports'); + } catch(e) { + passed = /define\(\) was used, so module\.exports= and module\.setExports\(\) may not be used/.test(e.toString()); + } + test.assertEqual(passed, true, 'define() or setExports(), not both'); +} +*/ + +exports.testModule = function(test) { + // module.id is not cast in stone yet. In the future, it may include the + // package name, or may possibly be a/ URL of some sort. For now, it's a + // URL that starts with resource: and ends with this module name, but the + // part in between varies depending upon how the test is run. + var found = /test-set-exports$/.test(module.id); + test.assertEqual(found, true, module.id+" ends with test-set-exports.js"); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-tab-browser.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-tab-browser.js new file mode 100644 index 0000000..31c5656 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-tab-browser.js @@ -0,0 +1,493 @@ +/* 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/. */ + +var timer = require("timer"); +var {Cc,Ci} = require("chrome"); + +function onBrowserLoad(callback, event) { + if (event.target && event.target.defaultView == this) { + this.removeEventListener("load", onBrowserLoad, true); + let browsers = this.document.getElementsByTagName("tabbrowser"); + try { + require("timer").setTimeout(function (window) { + callback(window, browsers[0]); + }, 10, this); + } catch (e) { console.exception(e); } + } +} +// Utility function to open a new browser window. +function openBrowserWindow(callback, url) { + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + let urlString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + urlString.data = url; + let window = ww.openWindow(null, "chrome://browser/content/browser.xul", + "_blank", "chrome,all,dialog=no", urlString); + if (callback) + window.addEventListener("load", onBrowserLoad.bind(window, callback), true); + + return window; +} + +// 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); +} + +// Helper for opening two windows at once +function openTwoWindows(callback) { + openBrowserWindow(function (window1) { + openBrowserWindow(function (window2) { + callback(window1, window2); + }); + }); +} + +// Helper for closing two windows at once +function closeTwoWindows(window1, window2, callback) { + closeBrowserWindow(window1, function() { + closeBrowserWindow(window2, callback); + }); +} + +exports.testAddTab = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(window, browser) { + const tabBrowser = require("tab-browser"); + + let cache = []; + let windowUtils = require("window-utils"); + new windowUtils.WindowTracker({ + onTrack: function(win) { + cache.push(win); + }, + onUntrack: function(win) { + cache.splice(cache.indexOf(win), 1) + } + }); + let startWindowCount = cache.length; + + // Test 1: add a tab + let firstUrl = "data:text/html,one"; + tabBrowser.addTab(firstUrl, { + onLoad: function(e) { + let win1 = cache[startWindowCount - 1]; + test.assertEqual(win1.content.location, firstUrl, "URL of new tab in first window matches"); + + // Test 2: add a tab in a new window + let secondUrl = "data:text/html,two"; + tabBrowser.addTab(secondUrl, { + inNewWindow: true, + onLoad: function(e) { + test.assertEqual(cache.length, startWindowCount + 1, "a new window was opened"); + let win2 = cache[startWindowCount]; + let gBrowser = win2.gBrowser; + gBrowser.addEventListener("DOMContentLoaded", function onLoad(e) { + gBrowser.removeEventListener("DOMContentLoaded", onLoad, false); + test.assertEqual(win2.content.location, secondUrl, "URL of new tab in the new window matches"); + + closeBrowserWindow(win2, function() { + closeBrowserWindow(win1, function() { + test.done(); + }); + }); + }, false); + } + }); + } + }); + }); +}; + +exports.testTrackerWithDelegate = function(test) { + test.waitUntilDone(); + const tabBrowser = require("tab-browser"); + + var delegate = { + state: "initializing", + onTrack: function onTrack(browser) { + if (this.state == "initializing") { + this.state = "waiting for browser window to open"; + } + else if (this.state == "waiting for browser window to open") { + this.state = "waiting for browser window to close"; + require("timer").setTimeout(function() { + closeBrowserWindow(browser.ownerDocument.defaultView, function() { + test.assertEqual(delegate.state, "deinitializing"); + tb.unload(); + test.done(); + }); + }, 0); + } + else + test.fail("invalid state"); + }, + onUntrack: function onUntrack(browser) { + if (this.state == "waiting for browser window to close") { + test.pass("proper state in onUntrack"); + this.state = "deinitializing"; + } + else if (this.state != "deinitializing") + test.fail("invalid state"); + } + }; + var tb = new tabBrowser.Tracker(delegate); + + delegate.state = "waiting for browser window to open"; + + openBrowserWindow(); +}; + +exports.testWhenContentLoaded = function(test) { + test.waitUntilDone(); + const tabBrowser = require("tab-browser"); + + var tracker = tabBrowser.whenContentLoaded( + function(window) { + var item = window.document.getElementById("foo"); + test.assertEqual(item.textContent, "bar", + "whenContentLoaded() works."); + tracker.unload(); + closeBrowserWindow(activeWindow(), function() { + test.done(); + }); + }); + + openBrowserWindow(function(browserWindow, browser) { + var html = '<div id="foo">bar</div>'; + browser.addTab("data:text/html," + html); + }); +}; + +exports.testTrackerWithoutDelegate = function(test) { + test.waitUntilDone(); + const tabBrowser = require("tab-browser"); + + openBrowserWindow(function(browserWindow, browser) { + var tb = new tabBrowser.Tracker(); + + if (tb.length == 0) + test.fail("expect at least one tab browser to exist."); + + for (var i = 0; i < tb.length; i++) + test.assertEqual(tb.get(i).nodeName, "tabbrowser", + "get() method and length prop should work"); + for (var b in tb) + test.assertEqual(b.nodeName, "tabbrowser", + "iterator should work"); + + var matches = [b for (b in tb) + if (b == browser)]; + test.assertEqual(matches.length, 1, + "New browser should be in tracker."); + tb.unload(); + + closeBrowserWindow(browserWindow, function() { + test.done(); + }); + }); +}; + +exports.testTabTracker = function(test) { + test.waitUntilDone(); + const tabBrowser = require("tab-browser"); + + openBrowserWindow(function(browserWindow, browser) { + var delegate = { + tracked: 0, + onTrack: function(tab) { + this.tracked++; + }, + onUntrack: function(tab) { + this.tracked--; + } + }; + + let tabTracker = tabBrowser.TabTracker(delegate); + + let tracked = delegate.tracked; + let url1 = "data:text/html,1"; + let url2 = "data:text/html,2"; + let url3 = "data:text/html,3"; + let tabCount = 0; + + function tabLoadListener(e) { + let loadedURL = e.target.defaultView.location; + if (loadedURL == url1) + tabCount++; + else if (loadedURL == url2) + tabCount++; + else if (loadedURL == url3) + tabCount++; + + if (tabCount == 3) { + test.assertEqual(delegate.tracked, tracked + 3, "delegate tracked tabs matched count"); + tabTracker.unload(); + closeBrowserWindow(browserWindow, function() { + require("timer").setTimeout(function() test.done(), 0); + }); + } + } + + tabBrowser.addTab(url1, { + onLoad: tabLoadListener + }); + tabBrowser.addTab(url2, { + onLoad: tabLoadListener + }); + tabBrowser.addTab(url3, { + onLoad: tabLoadListener + }); + }); +}; + +exports.testActiveTab = function(test) { + test.waitUntilDone(); + openBrowserWindow(function(browserWindow, browser) { + const tabBrowser = require("tab-browser"); + const TabModule = require("tab-browser").TabModule; + let tm = new TabModule(browserWindow); + test.assertEqual(tm.length, 1); + let url1 = "data:text/html,foo"; + let url2 = "data:text/html,bar"; + + function tabURL(tab) tab.ownerDocument.defaultView.content.location.toString() + + tabBrowser.addTab(url1, { + onLoad: function(e) { + // make sure we're running in the right window. + test.assertEqual(tabBrowser.activeTab.ownerDocument.defaultView, browserWindow, "active window matches"); + browserWindow.focus(); + + test.assertEqual(tabURL(tabBrowser.activeTab), url1, "url1 matches"); + let tabIndex = browser.getBrowserIndexForDocument(e.target); + let tabAtIndex = browser.tabContainer.getItemAtIndex(tabIndex); + test.assertEqual(tabAtIndex, tabBrowser.activeTab, "activeTab element matches"); + + tabBrowser.addTab(url2, { + inBackground: true, + onLoad: function() { + test.assertEqual(tabURL(tabBrowser.activeTab), url1, "url1 still matches"); + let tabAtIndex = browser.tabContainer.getItemAtIndex(tabIndex); + test.assertEqual(tabAtIndex, tabBrowser.activeTab, "activeTab element matches"); + closeBrowserWindow(browserWindow, function() { + test.done() + }); + } + }); + } + }); + }); +}; + +// TabModule tests +exports.testEventsAndLengthStayInModule = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openTwoWindows(function(window1, window2) { + let tm1 = new TabModule(window1); + let tm2 = new TabModule(window2); + + let counter1 = 0, counter2 = 0; + let counterTabs = 0; + + function onOpenListener() { + ++counterTabs; + if (counterTabs < 5) + return; + test.assertEqual(counter1, 2, "Correct number of events fired from window 1"); + test.assertEqual(counter2, 3, "Correct number of events fired from window 2"); + test.assertEqual(counterTabs, 5, "Correct number of events fired from all windows"); + test.assertEqual(tm1.length, 3, "Correct number of tabs in window 1"); + test.assertEqual(tm2.length, 4, "Correct number of tabs in window 2"); + closeTwoWindows(window1, window2, function() test.done()); + } + + tm1.onOpen = function() ++counter1 && onOpenListener(); + tm2.onOpen = function() ++counter2 && onOpenListener(); + + let url = "data:text/html,default"; + tm1.open(url); + tm1.open(url); + + tm2.open(url); + tm2.open(url); + tm2.open(url); + }); +} + +exports.testTabModuleActiveTab_getterAndSetter = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openTwoWindows(function(window1, window2) { + let tm1 = new TabModule(window1); + let tm2 = new TabModule(window2); + + let tab1 = null; + tm1.open({ + url: "data:text/html,<title>window1,tab1</title>", + onOpen: function(tab) tab1 = tab, + }); + tm1.open("data:text/html,<title>window1,tab2</title>"); + + tm1.onActivate = function onActivate() { + tm1.onActivate.remove(onActivate); + require("timer").setTimeout(function() { + test.assertEqual(tm1.activeTab.title, "window1,tab1", "activeTab setter works"); + closeTwoWindows(window1, window2, function() test.done()); + }, 1000); + } + + tm2.open("data:text/html,<title>window2,tab1</title>"); + tm2.open({ + url: "data:text/html,<title>window2,tab2</title>", + onOpen: function(tab4) { + test.assertEqual(tm1.activeTab.title, "window1,tab2", "Correct active tab on window 1"); + test.assertEqual(tm2.activeTab.title, "window2,tab2", "Correct active tab on window 2"); + + tm1.activeTab = tab1; + tm1.activeTab = tab4; // Setting activeTab from another window should have no effect + } + }); + }); +} + +// test tabs iterator +exports.testTabModuleTabsIterator = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openBrowserWindow(function(window) { + let tm1 = new TabModule(window); + let url = "data:text/html,default"; + tm1.open(url); + tm1.open(url); + tm1.open({ + url: url, + onOpen: function(tab) { + let count = 0; + for each (let t in tm1) count++; + test.assertEqual(count, 4, "iterated tab count matches"); + test.assertEqual(count, tm1.length, "length tab count matches"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// inNewWindow parameter is ignored on single-window modules +exports.testTabModuleCantOpenInNewWindow = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openBrowserWindow(function(window) { + let tm = new TabModule(window); + let url = "data:text/html,default"; + tm.open({ + url: url, + inNewWindow: true, + onOpen: function() { + test.assertEqual(tm.length, 2, "Tab was open on same window"); + closeBrowserWindow(window, function() test.done()); + } + }); + }); +}; + +// Test that having two modules attached to the same +// window won't duplicate events fired on each module +exports.testModuleListenersDontInteract = function(test) { + test.waitUntilDone(); + let TabModule = require("tab-browser").TabModule; + + openBrowserWindow(function(window) { + let tm1 = new TabModule(window); + let tm2 = new TabModule(window); + + let url = "data:text/html,foo"; + let eventCount = 0, eventModule1 = 0, eventModule2 = 0; + + + let listener1 = function() { + // this should be called twice: when tab is open and when + // the url location is changed + eventCount++; + eventModule1++; + } + tm1.onReady = listener1; + + tm2.open({ + url: "about:blank", + onOpen: function(tab) { + // add listener via property assignment + let listener2 = function() { + eventCount++; + eventModule2++; + }; + tab.onReady = listener2; + + // add listener via collection add + let listener3 = function() { + eventCount++; + eventModule2++; + }; + tab.onReady.add(listener3); + + tab.location = url; + + test.waitUntilEqual(function () eventCount, 4, + "Correct global number of events") + .then(function () { + test.assertEqual(eventModule1, 2, + "Correct number of events on module 1"); + test.assertEqual(eventModule2, 2, + "Correct number of events on module 2"); + + tm1.onReady.remove(listener1); + tab.onReady.remove(listener2); + tab.onReady.remove(listener3); + closeBrowserWindow(window, function() test.done()); + }); + } + }); + }); +}; + +/******************* helpers *********************/ + +// Helper for getting the active window +function activeWindow() { + return Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator). + getMostRecentWindow("navigator:browser"); +}; + +// 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("tab-browser"); +} +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("the tab-browser module does not support this application."); + }; +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-tab-observer.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-tab-observer.js new file mode 100644 index 0000000..3a506fa --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-tab-observer.js @@ -0,0 +1,39 @@ +/* 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 { openTab, closeTab } = require("api-utils/tabs/utils"); +const { Loader } = require("./helpers"); +const { setTimeout } = require("timer"); + +exports["test unload tab observer"] = function(assert, done) { + let loader = Loader(module); + + let window = loader.require("api-utils/window-utils").activeBrowserWindow; + let observer = loader.require("api-utils/tabs/observer").observer; + let opened = 0; + let closed = 0; + + observer.on("open", function onOpen(window) { opened++; }); + observer.on("close", function onClose(window) { closed++; }); + + // Open and close tab to trigger observers. + closeTab(openTab(window, "data:text/html,tab-1")); + + // Unload the module so that all listeners set by observer are removed. + loader.unload(); + + // Open and close tab once again. + closeTab(openTab(window, "data:text/html,tab-2")); + + // Enqueuing asserts to make sure that assertion is not performed early. + setTimeout(function () { + assert.equal(1, opened, "observer open was called before unload only"); + assert.equal(1, closed, "observer close was called before unload only"); + done(); + }, 0); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-tab.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-tab.js new file mode 100644 index 0000000..2324fa7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-tab.js @@ -0,0 +1,110 @@ +/* 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/. */ + +const tabAPI = require("tabs/tab"); +const tabs = require("tabs"); // From addon-kit +const windowUtils = require("window-utils"); + +// The primary test tab +var primaryTab; + +// We have an auxiliary tab to test background tabs. +var auxTab; + +// The window for the outer iframe in the primary test page +var iframeWin; + +exports.testGetTabForWindow = function(test) { + test.waitUntilDone(); + + test.assertEqual(tabAPI.getTabForWindow(windowUtils.activeWindow), null, + "getTabForWindow return null on topwindow"); + test.assertEqual(tabAPI.getTabForWindow(windowUtils.activeBrowserWindow), null, + "getTabForWindow return null on topwindow"); + + let subSubDocument = encodeURIComponent( + 'Sub iframe<br/>'+ + '<iframe id="sub-sub-iframe" src="data:text/html,SubSubIframe" />'); + let subDocument = encodeURIComponent( + 'Iframe<br/>'+ + '<iframe id="sub-iframe" src="data:text/html,'+subSubDocument+'" />'); + let url = 'data:text/html,' + encodeURIComponent( + 'Content<br/><iframe id="iframe" src="data:text/html,'+subDocument+'" />'); + + // Open up a new tab in the background. + // + // This lets us test whether GetTabForWindow works even when the tab in + // question is not active. + tabs.open({ + inBackground: true, + url: "about:mozilla", + onReady: function(tab) { auxTab = tab; step2(url, test);}, + onActivate: function(tab) { step3(test); } + }); +} + +function step2(url, test) { + + tabs.open({ + url: url, + onReady: function(tab) { + primaryTab = tab; + let window = windowUtils.activeBrowserWindow.content; + + let matchedTab = tabAPI.getTabForWindow(window); + test.assertEqual(matchedTab, tab, + "We are able to find the tab with his content window object"); + + let timer = require("timer"); + function waitForFrames() { + let iframe = window.document.getElementById("iframe"); + if (!iframe) { + timer.setTimeout(waitForFrames, 100); + return; + } + iframeWin = iframe.contentWindow; + let subIframe = iframeWin.document.getElementById("sub-iframe"); + if (!subIframe) { + timer.setTimeout(waitForFrames, 100); + return; + } + let subIframeWin = subIframe.contentWindow; + let subSubIframe = subIframeWin.document.getElementById("sub-sub-iframe"); + if (!subSubIframe) { + timer.setTimeout(waitForFrames, 100); + return; + } + let subSubIframeWin = subSubIframe.contentWindow; + + matchedTab = tabAPI.getTabForWindow(iframeWin); + test.assertEqual(matchedTab, tab, + "We are able to find the tab with an iframe window object"); + + matchedTab = tabAPI.getTabForWindow(subIframeWin); + test.assertEqual(matchedTab, tab, + "We are able to find the tab with a sub-iframe window object"); + + matchedTab = tabAPI.getTabForWindow(subSubIframeWin); + test.assertEqual(matchedTab, tab, + "We are able to find the tab with a sub-sub-iframe window object"); + + // Put our primary tab in the background and test again. + // The onActivate listener will take us to step3. + auxTab.activate(); + } + waitForFrames(); + } + }); +} + +function step3(test) { + + let matchedTab = tabAPI.getTabForWindow(iframeWin); + test.assertEqual(matchedTab, primaryTab, + "We get the correct tab even when it's in the background"); + + primaryTab.close(function () { + auxTab.close(function () { test.done();}); + }); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-text-streams.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-text-streams.js new file mode 100644 index 0000000..f5aec98 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-text-streams.js @@ -0,0 +1,156 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et filetype=javascript + * 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/. */ + +const file = require("file"); +const { pathFor } = require("api-utils/system"); +const { Loader } = require("./helpers"); + +const STREAM_CLOSED_ERROR = "The stream is closed and cannot be used."; + +// This should match the constant of the same name in text-streams.js. +const BUFFER_BYTE_LEN = 0x8000; + +exports.testWriteRead = function (test) { + let fname = dataFileFilename(); + + // Write a small string less than the stream's buffer size... + let str = "exports.testWriteRead data!"; + let stream = file.open(fname, "w"); + test.assert(!stream.closed, "stream.closed after open should be false"); + stream.write(str); + stream.close(); + test.assert(stream.closed, "stream.closed after close should be true"); + test.assertRaises(function () stream.close(), + STREAM_CLOSED_ERROR, + "stream.close after already closed should raise error"); + test.assertRaises(function () stream.write("This shouldn't be written!"), + STREAM_CLOSED_ERROR, + "stream.write after close should raise error"); + + // ... and read it. + stream = file.open(fname); + test.assert(!stream.closed, "stream.closed after open should be false"); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + test.assertEqual(stream.read(), "", + "stream.read at EOS should return empty string"); + stream.close(); + test.assert(stream.closed, "stream.closed after close should be true"); + test.assertRaises(function () stream.close(), + STREAM_CLOSED_ERROR, + "stream.close after already closed should raise error"); + test.assertRaises(function () stream.read(), + STREAM_CLOSED_ERROR, + "stream.read after close should raise error"); + + // Write a big string many times the size of the stream's buffer and read it. + // Since it comes after the previous test, this also ensures that the file is + // truncated when it's opened for writing. + str = ""; + let bufLen = BUFFER_BYTE_LEN; + let fileSize = bufLen * 10; + for (let i = 0; i < fileSize; i++) + str += i % 10; + stream = file.open(fname, "w"); + stream.write(str); + stream.close(); + stream = file.open(fname); + test.assertEqual(stream.read(), str, + "stream.read should return string written"); + stream.close(); + + // The same, but write and read in chunks. + stream = file.open(fname, "w"); + let i = 0; + while (i < str.length) { + // Use a chunk length that spans buffers. + let chunk = str.substr(i, bufLen + 1); + stream.write(chunk); + i += bufLen + 1; + } + stream.close(); + stream = file.open(fname); + let readStr = ""; + bufLen = BUFFER_BYTE_LEN; + let readLen = bufLen + 1; + do { + var frag = stream.read(readLen); + readStr += frag; + } while (frag); + stream.close(); + test.assertEqual(readStr, str, + "stream.write and read in chunks should work as expected"); + + // Read the same file, passing in strange numbers of bytes to read. + stream = file.open(fname); + test.assertEqual(stream.read(fileSize * 100), str, + "stream.read with big byte length should return string " + + "written"); + stream.close(); + + stream = file.open(fname); + test.assertEqual(stream.read(0), "", + "string.read with zero byte length should return empty " + + "string"); + stream.close(); + + stream = file.open(fname); + test.assertEqual(stream.read(-1), "", + "string.read with negative byte length should return " + + "empty string"); + stream.close(); + + file.remove(fname); +}; + +exports.testWriteAsync = function (test) { + test.waitUntilDone(); + + let fname = dataFileFilename(); + let str = "exports.testWriteAsync data!"; + let stream = file.open(fname, "w"); + test.assert(!stream.closed, "stream.closed after open should be false"); + + // Write. + stream.writeAsync(str, function (err) { + test.assertEqual(this, stream, "|this| should be the stream object"); + test.assertEqual(err, undefined, + "stream.writeAsync should not cause error"); + test.assert(stream.closed, "stream.closed after write should be true"); + test.assertRaises(function () stream.close(), + STREAM_CLOSED_ERROR, + "stream.close after already closed should raise error"); + test.assertRaises(function () stream.writeAsync("This shouldn't work!"), + STREAM_CLOSED_ERROR, + "stream.writeAsync after close should raise error"); + + // Read. + stream = file.open(fname, "r"); + test.assert(!stream.closed, "stream.closed after open should be false"); + let readStr = stream.read(); + test.assertEqual(readStr, str, + "string.read should yield string written"); + stream.close(); + file.remove(fname); + test.done(); + }); +}; + +exports.testUnload = function (test) { + let loader = Loader(module); + let file = loader.require("file"); + + let filename = dataFileFilename("temp"); + let stream = file.open(filename, "w"); + + loader.unload(); + test.assert(stream.closed, "stream should be closed after module unload"); +}; + +// Returns the name of a file that should be used to test writing and reading. +function dataFileFilename() { + return file.join(pathFor("ProfD"), "test-text-streams-data"); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-timer.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-timer.js new file mode 100644 index 0000000..bf6917c --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-timer.js @@ -0,0 +1,131 @@ +/* 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/. */ + +var timer = require("timer"); +const { Loader } = require("./helpers"); + +exports.testSetTimeout = function(test) { + timer.setTimeout(function() { + test.pass("testSetTimeout passed"); + test.done(); + }, 1); + test.waitUntilDone(); +}; + +exports.testParamedSetTimeout = function(test) { + let params = [1, 'foo', { bar: 'test' }, null, undefined]; + timer.setTimeout.apply(null, [function() { + test.assertEqual(arguments.length, params.length); + for (let i = 0, ii = params.length; i < ii; i++) + test.assertEqual(params[i], arguments[i]); + test.done(); + }, 1].concat(params)); + test.waitUntilDone(); +}; + +exports.testClearTimeout = function(test) { + var myFunc = function myFunc() { + test.fail("myFunc() should not be called in testClearTimeout"); + }; + var id = timer.setTimeout(myFunc, 1); + timer.setTimeout(function() { + test.pass("testClearTimeout passed"); + test.done(); + }, 2); + timer.clearTimeout(id); + test.waitUntilDone(); +}; + +exports.testParamedClearTimeout = function(test) { + let params = [1, 'foo', { bar: 'test' }, null, undefined]; + var myFunc = function myFunc() { + test.fail("myFunc() should not be called in testClearTimeout"); + }; + var id = timer.setTimeout(myFunc, 1); + timer.setTimeout.apply(null, [function() { + test.assertEqual(arguments.length, params.length); + for (let i = 0, ii = params.length; i < ii; i++) + test.assertEqual(params[i], arguments[i]); + test.done(); + }, 1].concat(params)); + timer.clearTimeout(id); + test.waitUntilDone(); +}; + +exports.testSetInterval = function (test) { + var count = 0; + var id = timer.setInterval(function () { + count++; + if (count >= 5) { + timer.clearInterval(id); + test.pass("testSetInterval passed"); + test.done(); + } + }, 1); + test.waitUntilDone(); +}; + +exports.testParamedSetInerval = function(test) { + let params = [1, 'foo', { bar: 'test' }, null, undefined]; + let count = 0; + let id = timer.setInterval.apply(null, [function() { + count ++; + if (count < 5) { + test.assertEqual(arguments.length, params.length); + for (let i = 0, ii = params.length; i < ii; i++) + test.assertEqual(params[i], arguments[i]); + } else { + timer.clearInterval(id); + test.done(); + } + }, 1].concat(params)); + test.waitUntilDone(); +}; + +exports.testClearInterval = function (test) { + timer.clearInterval(timer.setInterval(function () { + test.fail("setInterval callback should not be called"); + }, 1)); + var id = timer.setInterval(function () { + timer.clearInterval(id); + test.pass("testClearInterval passed"); + test.done(); + }, 2); + test.waitUntilDone(); +}; + +exports.testParamedClearInterval = function(test) { + timer.clearInterval(timer.setInterval(function () { + test.fail("setInterval callback should not be called"); + }, 1, timer, {}, null)); + + let id = timer.setInterval(function() { + timer.clearInterval(id); + test.assertEqual(3, arguments.length); + test.done(); + }, 2, undefined, 'test', {}); + test.waitUntilDone(); +}; + + +exports.testUnload = function(test) { + var loader = Loader(module); + var sbtimer = loader.require("timer"); + + var myFunc = function myFunc() { + test.fail("myFunc() should not be called in testUnload"); + }; + + sbtimer.setTimeout(myFunc, 1); + sbtimer.setTimeout(myFunc, 1, 'foo', 4, {}, undefined); + sbtimer.setInterval(myFunc, 1); + sbtimer.setInterval(myFunc, 1, {}, null, 'bar', undefined, 87); + loader.unload(); + timer.setTimeout(function() { + test.pass("timer testUnload passed"); + test.done(); + }, 2); + test.waitUntilDone(); +}; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-traceback.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-traceback.js new file mode 100644 index 0000000..e4d1737 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-traceback.js @@ -0,0 +1,118 @@ +/* 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/. */ + +var traceback = require("traceback"); +var {Cc,Ci,Cr,Cu} = require("chrome"); + +function throwNsIException() { + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + ios.newURI("i'm a malformed URI", null, null); +} + +function throwError() { + throw new Error("foob"); +} + +exports.testFormatDoesNotFetchRemoteFiles = function(test) { + var observers = require("observer-service"); + ["http", "https"].forEach( + function(scheme) { + var httpRequests = 0; + function onHttp() { + httpRequests++; + } + + observers.add("http-on-modify-request", onHttp); + + try { + var tb = [{filename: scheme + "://www.mozilla.org/", + lineNo: 1, + funcName: "blah"}]; + traceback.format(tb); + } catch (e) { + test.exception(e); + } + + observers.remove("http-on-modify-request", onHttp); + + test.assertEqual(httpRequests, 0, + "traceback.format() does not make " + + scheme + " request"); + }); +}; + +exports.testFromExceptionWithString = function(test) { + try { + throw "foob"; + test.fail("an exception should've been thrown"); + } catch (e if e == "foob") { + var tb = traceback.fromException(e); + test.assertEqual(tb.length, 0); + } +}; + +exports.testFormatWithString = function(test) { + // This can happen if e.g. a thrown exception was + // a string instead of an Error instance. + test.assertEqual(traceback.format("blah"), + "Traceback (most recent call last):"); +}; + +exports.testFromExceptionWithError = function(test) { + try { + throwError(); + test.fail("an exception should've been thrown"); + } catch (e if e instanceof Error) { + var tb = traceback.fromException(e); + var xulApp = require("xul-app"); + test.assertEqual(tb.slice(-1)[0].funcName, "throwError"); + } +}; + +exports.testFromExceptionWithNsIException = function(test) { + try { + throwNsIException(); + test.fail("an exception should've been thrown"); + } catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) { + var tb = traceback.fromException(e); + test.assertEqual(tb.slice(-1)[0].funcName, + "throwNsIException"); + } +}; + +exports.testFormat = function(test) { + function getTraceback() { + return traceback.format(); + } + + var formatted = getTraceback(); + test.assertEqual(typeof(formatted), "string"); + var lines = formatted.split("\n"); + test.assertEqual(lines.slice(-2)[0].indexOf("getTraceback") > 0, + true, + "formatted traceback should include function name"); + test.assertEqual(lines.slice(-1)[0].trim(), + "return traceback.format();", + "formatted traceback should include source code"); +}; + +exports.testExceptionsWithEmptyStacksAreLogged = function(test) { + // Ensures that our fix to bug 550368 works. + var sandbox = Cu.Sandbox("http://www.foo.com"); + var excRaised = false; + try { + Cu.evalInSandbox("returns 1 + 2;", sandbox, "1.8", + "blah.js", 25); + } catch (e) { + excRaised = true; + var stack = traceback.fromException(e); + test.assertEqual(stack.length, 1, "stack should have one frame"); + test.assert(stack[0].filename, "blah.js", "frame should have filename"); + test.assert(stack[0].lineNo, 25, "frame should have line no"); + test.assertEqual(stack[0].funcName, null, "frame should have null function name"); + } + if (!excRaised) + test.fail("Exception should have been raised."); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-traits-core.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-traits-core.js new file mode 100644 index 0000000..c2bf4df --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-traits-core.js @@ -0,0 +1,838 @@ +/* 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 ERR_CONFLICT = 'Remaining conflicting property: ', + ERR_REQUIRED = 'Missing required property: '; + +function assertSametrait(test, trait1, trait2) { + let names1 = Object.getOwnPropertyNames(trait1), + names2 = Object.getOwnPropertyNames(trait2); + + test.assertEqual( + names1.length, + names2.length, + 'equal traits must have same amount of properties' + ); + + for (let i = 0; i < names1.length; i++) { + let name = names1[i]; + test.assertNotEqual( + -1, + names2.indexOf(name), + 'equal traits must contain same named properties: ' + name + ); + assertSameDescriptor(test, name, trait1[name], trait2[name]); + } +} + +function assertSameDescriptor(test, name, desc1, desc2) { + if (desc1.conflict || desc2.conflict) { + test.assertEqual( + desc1.conflict, + desc2.conflict, + 'if one of same descriptors has `conflict` another must have it: ' + + name + ); + } else if (desc1.required || desc2.required) { + test.assertEqual( + desc1.required, + desc2.required, + 'if one of same descriptors is has `required` another must have it: ' + + name + ); + } else { + test.assertEqual( + desc1.get, + desc2.get, + 'get must be the same on both descriptors: ' + name + ); + test.assertEqual( + desc1.set, + desc2.set, + 'set must be the same on both descriptors: ' + name + ); + test.assertEqual( + desc1.value, + desc2.value, + 'value must be the same on both descriptors: ' + name + ); + test.assertEqual( + desc1.enumerable, + desc2.enumerable, + 'enumerable must be the same on both descriptors: ' + name + ); + test.assertEqual( + desc1.required, + desc2.required, + 'value must be the same on both descriptors: ' + name + ); + } +} + +function Data(value, enumerable, confligurable, writable) { + return { + value: value, + enumerable: false !== enumerable, + confligurable: false !== confligurable, + writable: false !== writable + }; +} + +function Method(method, enumerable, confligurable, writable) { + return { + value: method, + enumerable: false !== enumerable, + confligurable: false !== confligurable, + writable: false !== writable + }; +} + +function Accessor(get, set, enumerable, confligurable) { + return { + get: get, + set: set, + enumerable: false !== enumerable, + confligurable: false !== confligurable, + }; +} + +function Required(name) { + function required() { throw new Error(ERR_REQUIRED + name) } + return { + get: required, + set: required, + required: true + }; +} + +function Conflict(name) { + function conflict() { throw new Error(ERR_CONFLICT + name) } + return { + get: conflict, + set: conflict, + conflict: true + }; +} + +function testMethod() {}; + +const { trait, compose, resolve, required, override, create } = + require('traits/core'); + + +exports['test:empty trait'] = function(test) { + assertSametrait( + test, + trait({}), + {} + ); +}; + +exports['test:simple trait'] = function(test) { + assertSametrait( + test, + trait({ + a: 0, + b: testMethod + }), + { + a: Data(0, true, true, true), + b: Method(testMethod, true, true, true) + } + ); +}; + +exports['test:simple trait with required prop'] = function(test) { + assertSametrait( + test, + trait({ + a: required, + b: 1 + }), + { + a: Required('a'), + b: Data(1) + } + ); +}; + +exports['test:ordering of trait properties is irrelevant'] = function(test) { + assertSametrait(test, + trait({ a: 0, b: 1, c: required }), + trait({ b: 1, c: required, a: 0 }) + ); +}; + +exports['test:trait with accessor property'] = function(test) { + let record = { get a() {}, set a(v) {} }; + let get = Object.getOwnPropertyDescriptor(record,'a').get; + let set = Object.getOwnPropertyDescriptor(record,'a').set; + assertSametrait(test, + trait(record), + { a: Accessor(get, set ) } + ); +}; + +exports['test:simple composition'] = function(test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1 }), + trait({ c: 2, d: testMethod }) + ), + { + a: Data(0), + b: Data(1), + c: Data(2), + d: Method(testMethod) + } + ); +}; + +exports['test:composition with conflict'] = function(test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1 }), + trait({ a: 2, c: testMethod }) + ), + { + a: Conflict('a'), + b: Data(1), + c: Method(testMethod) + } + ); +}; + +exports['test:composition of identical props does not cause conflict'] = +function(test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1 }), + trait({ a: 0, c: testMethod }) + ), + { + a: Data(0), + b: Data(1), + c: Method(testMethod) } + ) +}; + +exports['test:composition with identical required props'] = +function(test) { + assertSametrait(test, + compose( + trait({ a: required, b: 1 }), + trait({ a: required, c: testMethod }) + ), + { + a: Required(), + b: Data(1), + c: Method(testMethod) + } + ); +}; + +exports['test:composition satisfying a required prop'] = function (test) { + assertSametrait(test, + compose( + trait({ a: required, b: 1 }), + trait({ a: testMethod }) + ), + { + a: Method(testMethod), + b: Data(1) + } + ); +}; + +exports['test:compose is neutral wrt conflicts'] = function (test) { + assertSametrait(test, + compose( + compose( + trait({ a: 1 }), + trait({ a: 2 }) + ), + trait({ b: 0 }) + ), + { + a: Conflict('a'), + b: Data(0) + } + ); +}; + +exports['test:conflicting prop overrides required prop'] = function (test) { + assertSametrait(test, + compose( + compose( + trait({ a: 1 }), + trait({ a: 2 }) + ), + trait({ a: required }) + ), + { + a: Conflict('a') + } + ); +}; + +exports['test:compose is commutative'] = function (test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1 }), + trait({ c: 2, d: testMethod }) + ), + compose( + trait({ c: 2, d: testMethod }), + trait({ a: 0, b: 1 }) + ) + ); +}; + +exports['test:compose is commutative, also for required/conflicting props'] = +function (test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1, c: 3, e: required }), + trait({ c: 2, d: testMethod }) + ), + compose( + trait({ c: 2, d: testMethod }), + trait({ a: 0, b: 1, c: 3, e: required }) + ) + ); +}; +exports['test:compose is associative'] = function (test) { + assertSametrait(test, + compose( + trait({ a: 0, b: 1, c: 3, d: required }), + compose( + trait({ c: 3, d: required }), + trait({ c: 2, d: testMethod, e: 'foo' }) + ) + ), + compose( + compose( + trait({ a: 0, b: 1, c: 3, d: required }), + trait({ c: 3, d: required }) + ), + trait({ c: 2, d: testMethod, e: 'foo' }) + ) + ); +}; + +exports['test:diamond import of same prop does not generate conflict'] = +function (test) { + assertSametrait(test, + compose( + compose( + trait({ b: 2 }), + trait({ a: 1 }) + ), + compose( + trait({ c: 3 }), + trait({ a: 1 }) + ), + trait({ d: 4 }) + ), + { + a: Data(1), + b: Data(2), + c: Data(3), + d: Data(4) + } + ); +}; + +exports['test:resolve with empty resolutions has no effect'] = +function (test) { + assertSametrait(test, resolve({}, trait({ + a: 1, + b: required, + c: testMethod + })), { + a: Data(1), + b: Required(), + c: Method(testMethod) + }); +}; + +exports['test:resolve: renaming'] = function (test) { + assertSametrait(test, + resolve( + { a: 'A', c: 'C' }, + trait({ a: 1, b: required, c: testMethod }) + ), + { + A: Data(1), + b: Required(), + C: Method(testMethod), + a: Required(), + c: Required() + } + ); +}; + +exports['test:resolve: renaming to conflicting name causes conflict, order 1'] += function (test) { + assertSametrait(test, + resolve( + { a: 'b'}, + trait({ a: 1, b: 2 }) + ), + { + b: Conflict('b'), + a: Required() + } + ); +}; + +exports['test:resolve: renaming to conflicting name causes conflict, order 2'] += function (test) { + assertSametrait(test, + resolve( + { a: 'b' }, + trait({ b: 2, a: 1 }) + ), + { + b: Conflict('b'), + a: Required() + } + ); +}; + +exports['test:resolve: simple exclusion'] = function (test) { + assertSametrait(test, + resolve( + { a: undefined }, + trait({ a: 1, b: 2 }) + ), + { + a: Required(), + b: Data(2) + } + ); +}; + +exports['test:resolve: exclusion to "empty" trait'] = function (test) { + assertSametrait(test, + resolve( + { a: undefined, b: undefined }, + trait({ a: 1, b: 2 }) + ), + { + a: Required(), + b: Required() + } + ); +}; + +exports['test:resolve: exclusion and renaming of disjoint props'] = +function (test) { + assertSametrait(test, + resolve( + { a: undefined, b: 'c' }, + trait({ a: 1, b: 2 }) + ), + { + a: Required(), + c: Data(2), + b: Required() + } + ); +}; + +exports['test:resolve: exclusion and renaming of overlapping props'] = +function (test) { + assertSametrait(test, + resolve( + { a: undefined, b: 'a' }, + trait({ a: 1, b: 2 }) + ), + { + a: Data(2), + b: Required() + } + ); +}; + +exports['test:resolve: renaming to a common alias causes conflict'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'c', b: 'c' }, + trait({ a: 1, b: 2 }) + ), + { + c: Conflict('c'), + a: Required(), + b: Required() + } + ); +}; + +exports['test:resolve: renaming overrides required target'] = +function (test) { + assertSametrait(test, + resolve( + { b: 'a' }, + trait({ a: required, b: 2 }) + ), + { + a: Data(2), + b: Required() + } + ); +}; + +exports['test:resolve: renaming required properties has no effect'] = +function (test) { + assertSametrait(test, + resolve( + { b: 'a' }, + trait({ a: 2, b: required }) + ), + { + a: Data(2), + b: Required() + } + ); +}; + +exports['test:resolve: renaming of non-existent props has no effect'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'c', d: 'c' }, + trait({ a: 1, b: 2 }) + ), + { + c: Data(1), + b: Data(2), + a: Required() + } + ); +}; + +exports['test:resolve: exclusion of non-existent props has no effect'] = +function (test) { + assertSametrait(test, + resolve( + { b: undefined }, + trait({ a: 1 }) + ), + { + a: Data(1) + } + ); +}; + +exports['test:resolve is neutral w.r.t. required properties'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'c', b: undefined }, + trait({ a: required, b: required, c: 'foo', d: 1 }) + ), + { + a: Required(), + b: Required(), + c: Data('foo'), + d: Data(1) + } + ); +}; + +exports['test:resolve supports swapping of property names, ordering 1'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'b', b: 'a' }, + trait({ a: 1, b: 2 }) + ), + { + a: Data(2), + b: Data(1) + } + ); +}; + +exports['test:resolve supports swapping of property names, ordering 2'] = +function (test) { + assertSametrait(test, + resolve( + { b: 'a', a: 'b' }, + trait({ a: 1, b: 2 }) + ), + { + a: Data(2), + b: Data(1) + } + ); +}; + +exports['test:resolve supports swapping of property names, ordering 3'] = +function (test) { + assertSametrait(test, + resolve( + { b: 'a', a: 'b' }, + trait({ b: 2, a: 1 }) + ), + { + a: Data(2), + b: Data(1) + } + ); +}; + +exports['test:resolve supports swapping of property names, ordering 4'] = +function (test) { + assertSametrait(test, + resolve( + { a: 'b', b: 'a' }, + trait({ b: 2, a: 1 }) + ), + { + a: Data(2), + b: Data(1) + } + ); +}; + +exports['test:override of mutually exclusive traits'] = function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ c: 3, d: testMethod }) + ), + { + a: Data(1), + b: Data(2), + c: Data(3), + d: Method(testMethod) + } + ); +}; + +exports['test:override of mutually exclusive traits is compose'] = +function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ c: 3, d: testMethod }) + ), + compose( + trait({ d: testMethod, c: 3 }), + trait({ b: 2, a: 1 }) + ) + ); +}; + +exports['test:override of overlapping traits'] = function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ a: 3, c: testMethod }) + ), + { + a: Data(1), + b: Data(2), + c: Method(testMethod) + } + ); +}; + +exports['test:three-way override of overlapping traits'] = function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ b: 4, c: 3 }), + trait({ a: 3, c: testMethod, d: 5 }) + ), + { + a: Data(1), + b: Data(2), + c: Data(3), + d: Data(5) + } + ); +}; + +exports['test:override replaces required properties'] = function (test) { + assertSametrait(test, + override( + trait({ a: required, b: 2 }), + trait({ a: 1, c: testMethod }) + ), + { + a: Data(1), + b: Data(2), + c: Method(testMethod) + } + ); +}; + +exports['test:override is not commutative'] = function (test) { + assertSametrait(test, + override( + trait({ a: 1, b: 2 }), + trait({ a: 3, c: 4 }) + ), + { + a: Data(1), + b: Data(2), + c: Data(4) + } + ); + + assertSametrait(test, + override( + trait({ a: 3, c: 4 }), + trait({ a: 1, b: 2 }) + ), + { + a: Data(3), + b: Data(2), + c: Data(4) + } + ); +}; + +exports['test:override is associative'] = function (test) { + assertSametrait(test, + override( + override( + trait({ a: 1, b: 2 }), + trait({ a: 3, c: 4, d: 5 }) + ), + trait({ a: 6, c: 7, e: 8 }) + ), + override( + trait({ a: 1, b: 2 }), + override( + trait({ a: 3, c: 4, d: 5 }), + trait({ a: 6, c: 7, e: 8 }) + ) + ) + ); +}; + +exports['test:create simple'] = function(test) { + let o1 = create( + Object.prototype, + trait({ a: 1, b: function() { return this.a; } }) + ); + + test.assertEqual( + Object.prototype, + Object.getPrototypeOf(o1), + 'o1 prototype' + ); + test.assertEqual(1, o1.a, 'o1.a'); + test.assertEqual(1, o1.b(), 'o1.b()'); + test.assertEqual( + 2, + Object.getOwnPropertyNames(o1).length, + 'Object.keys(o1).length === 2' + ); +}; + +exports['test:create with Array.prototype'] = function(test) { + let o2 = create(Array.prototype, trait({})); + test.assertEqual( + Array.prototype, + Object.getPrototypeOf(o2), + "o2 prototype" + ); +}; + +exports['test:exception for incomplete required properties'] = +function(test) { + try { + create(Object.prototype, trait({ foo: required })); + test.fail('expected create to complain about missing required props'); + } catch(e) { + test.assertEqual( + 'Error: Missing required property: foo', + e.toString(), + 'required prop error' + ); + } +}; + +exports['test:exception for unresolved conflicts'] = function(test) { + try { + create({}, compose(trait({ a: 0 }), trait({ a: 1 }))); + test.fail('expected create to complain about unresolved conflicts'); + } catch(e) { + test.assertEqual( + 'Error: Remaining conflicting property: a', + e.toString(), + 'conflicting prop error' + ); + } +}; + +exports['test:verify that required properties are present but undefined'] = +function(test) { + try { + let o4 = Object.create(Object.prototype, trait({ foo: required })); + test.assertEqual(true, 'foo' in o4, 'required property present'); + try { + let foo = o4.foo; + test.fail('access to required property must throw'); + } catch(e) { + test.assertEqual( + 'Error: Missing required property: foo', + e.toString(), + 'required prop error' + ) + } + } catch(e) { + test.fail('did not expect create to complain about required props'); + } +}; + +exports['test:verify that conflicting properties are present'] = +function(test) { + try { + let o5 = Object.create( + Object.prototype, + compose(trait({ a: 0 }), trait({ a: 1 })) + ); + test.assertEqual(true, 'a' in o5, 'conflicting property present'); + try { + let a = o5.a; // accessors or data prop + test.fail('expected conflicting prop to cause exception'); + } catch (e) { + test.assertEqual( + 'Error: Remaining conflicting property: a', + e.toString(), + 'conflicting prop access error' + ); + } + } catch(e) { + test.fail('did not expect create to complain about conflicting props'); + } +}; + +exports['test diamond with conflicts'] = function(test) { + function makeT1(x) trait({ m: function() { return x; } }) + function makeT2(x) compose(trait({ t2: 'foo' }), makeT1(x)) + function makeT3(x) compose(trait({ t3: 'bar' }), makeT1(x)) + + let T4 = compose(makeT2(5), makeT3(5)); + try { + let o = create(Object.prototype, T4); + test.fail('expected diamond prop to cause exception'); + } catch(e) { + test.assertEqual( + 'Error: Remaining conflicting property: m', + e.toString(), + 'diamond prop conflict' + ); + } +}; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-traits.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-traits.js new file mode 100644 index 0000000..8b88f94 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-traits.js @@ -0,0 +1,398 @@ +/* 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 { Trait } = require('traits'); + +exports['test:simple compose'] = function(test) { + let List = Trait.compose({ + _list: null, + constructor: function List() { + this._list = []; + }, + list: function list() this._list.slice(0), + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + test.assertNotEqual(undefined, List, 'should not be undefined'); + test.assertEqual('function', typeof List, 'type should be function'); + test.assertEqual( + Trait.compose, + List.compose, + 'should inherit static compose' + ); + test.assertEqual( + Trait.override, + List.override, + 'should inherit static override' + ); + test.assertEqual( + Trait.required, + List.required, + 'should inherit static required' + ); + test.assertEqual( + Trait.resolve, + List.resolve, + 'should inherit static resolve' + ); + + test.assert( + !('_list' in List.prototype), + 'should not expose private API' + ); +} +exports['test: compose trait instance and create instance'] = function(test) { + let List = Trait.compose({ + constructor: function List(options) { + this._list = []; + this._public.publicMember = options.publicMember; + }, + _privateMember: true, + get privateMember() this._privateMember, + get list() this._list.slice(0), + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list + let index = list.indexOf(item) + if (0 <= index) list.slice(index, 1) + } + }); + let list = List({ publicMember: true }); + + test.assertEqual('object', typeof list, 'should return an object') + test.assertEqual( + true, + list instanceof List, + 'should be instance of a List' + ); + + test.assertEqual( + undefined, + list._privateMember, + 'instance should not expose private API' + ); + + test.assertEqual( + true, + list.privateMember, + 'privates are accessible by public API' + ); + + list._privateMember = false; + + test.assertEqual( + true, + list.privateMember, + 'property changes on instance must not affect privates' + ); + + test.assert( + !('_list' in list), + 'instance should not expose private members' + ); + + test.assertEqual( + true, + list.publicMember, + 'public members are exposed' + ) + test.assertEqual( + 'function', + typeof list.add, + 'should be function' + ) + test.assertEqual( + 'function', + typeof list.remove, + 'should be function' + ); + + list.add(1); + test.assertEqual( + 1, + list.list[0], + 'exposed public API should be able of modifying privates' + ) +}; + + +exports['test:instances must not be hackable'] = function(test) { + let SECRET = 'There is no secret!', + secret = null; + + let Class = Trait.compose({ + _secret: null, + protect: function(data) this._secret = data + }); + + let i1 = Class(); + i1.protect(SECRET); + + test.assertEqual( + undefined, + (function() this._secret).call(i1), + 'call / apply can\'t access private state' + ); + + let proto = Object.getPrototypeOf(i1); + try { + proto.reveal = function() this._secret; + secret = i1.reveal(); + } catch(e) {} + test.assertNotEqual( + SECRET, + secret, + 'public __proto__ changes should not affect privates' + ); + secret = null; + + let Class2 = Trait.compose({ + _secret: null, + protect: function(data) this._secret = data + }); + let i2 = Class2(); + i2.protect(SECRET); + try { + Object.prototype.reveal = function() this._secret; + secret = i2.reveal(); + } catch(e) {} + test.assertNotEqual( + SECRET, + secret, + 'Object.prototype changes must not affect instances' + ); +} + +exports['test:instanceof'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = [] + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + test.assert(List() instanceof List, 'Must be instance of List'); + test.assert(new List() instanceof List, 'Must be instance of List'); +}; + +exports['test:privates are unaccessible'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + let list = List(); + test.assert(!('_list' in list), 'no privates on instance'); + test.assert( + !('_list' in List.prototype), + 'no privates on prototype' + ); +}; + +exports['test:public API can access private API'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + let list = List(); + + list.add('test'); + + test.assertEqual( + 1, + list.length, + 'should be able to add element and access it from public getter' + ); +}; + +exports['test:required'] = function(test) { + const Enumerable = Trait.compose({ + list: Trait.required, + forEach: function forEach(consumer) { + return this.list.forEach(consumer); + } + }); + + try { + let i = Enumerable(); + test.fail('should throw when creating instance with required properties'); + } catch(e) { + test.assertEqual( + 'Error: Missing required property: list', + e.toString(), + 'required prop error' + ); + } +}; + +exports['test:compose with required'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + const Enumerable = Trait.compose({ + list: Trait.required, + forEach: function forEach(consumer) { + return this.list.forEach(consumer); + } + }); + + const EnumerableList = Enumerable.compose({ + get list() this._list.slice(0) + }, List); + + let array = [1,2, 'ab'] + let l = EnumerableList(array); + array.forEach(function(element) l.add(element)); + let number = 0; + l.forEach(function(element, index) { + number ++; + test.assertEqual(array[index], element, 'should mach array element') + }); + test.assertEqual( + array.length, + number, + 'should perform as many asserts as elements in array' + ); +}; + +exports['test:resolve'] = function(test) { + const List = Trait.compose({ + // private API: + _list: null, + // public API + constructor: function List() { + this._list = []; + }, + get length() this._list.length, + add: function add(item) this._list.push(item), + remove: function remove(item) { + let list = this._list; + let index = list.indexOf(item); + if (0 <= index) list.slice(index, 1); + } + }); + + const Range = List.resolve({ + constructor: null, + add: '_add', + }).compose({ + min: null, + max: null, + get list() this._list.slice(0), + constructor: function Range(min, max) { + this.min = min; + this.max = max; + this._list = []; + }, + add: function(item) { + if (item <= this.max && item >= this.min) + this._add(item) + } + }); + + let r = Range(0, 10); + + test.assertEqual( + 0, + r.min, + 'constructor must have set min' + ); + test.assertEqual( + 10, + r.max, + 'constructor must have set max' + ); + + test.assertEqual( + 0, + r.length, + 'should not contain any elements' + ); + + r.add(5); + + test.assertEqual( + 1, + r.length, + 'should add `5` to list' + ); + + r.add(12); + + test.assertEqual( + 1, + r.length, + 'should not add `12` to list' + ); +}; + +exports['test:custom iterator'] = function(test) { + let Sub = Trait.compose({ + foo: "foo", + bar: "bar", + baz: "baz", + __iterator__: function() { + yield 1; + yield 2; + yield 3; + } + }); + + let (i = 0, sub = Sub()) { + for (let item in sub) + test.assertEqual(++i, item, "iterated item has the right value"); + }; +}; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-type.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-type.js new file mode 100644 index 0000000..d5b5775 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-type.js @@ -0,0 +1,92 @@ +/* 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" + +var utils = require("type"); + +exports["test function"] = function (assert) { + assert.ok(utils.isFunction(function(){}), "value is function"); + assert.ok(utils.isFunction(Object), "Object is function"); + assert.ok(utils.isFunction(new Function("")), "Genertaed value is function"); + assert.ok(!utils.isFunction({}), "object is not a function"); + assert.ok(!utils.isFunction(4), "number is not a function"); +}; + +exports["test atoms"] = function (assert) { + assert.ok(utils.isPrimitive(2), "number is primitive"); + assert.ok(utils.isPrimitive(NaN), "`NaN` is primitve"); + assert.ok(utils.isPrimitive(undefined), "`undefined` is primitive"); + assert.ok(utils.isPrimitive(null), "`null` is primitive"); + assert.ok(utils.isPrimitive(Infinity), "`Infinity` is primitive"); + assert.ok(utils.isPrimitive("foo"), "strings are primitive"); + assert.ok(utils.isPrimitive(true) && utils.isPrimitive(false), + "booleans are primitive"); +}; + +exports["test object"] = function (assert) { + assert.ok(utils.isObject({}), "`{}` is object"); + assert.ok(!utils.isObject(null), "`null` is not an object"); + assert.ok(!utils.isObject(Object), "functions is not an object"); +}; + +exports["test flat objects"] = function (assert) { + assert.ok(utils.isFlat({}), "`{}` is a flat object"); + assert.ok(!utils.isFlat([]), "`[]` is not a flat object"); + assert.ok(!utils.isFlat(new function() {}), "derived objects are not flat"); + assert.ok(utils.isFlat(Object.prototype), "Object.prototype is flat"); +}; + +exports["test json atoms"] = function (assert) { + assert.ok(utils.isJSON(null), "`null` is JSON"); + assert.ok(utils.isJSON(undefined), "`undefined` is JSON"); + assert.ok(utils.isJSON(NaN), "`NaN` is JSON"); + assert.ok(utils.isJSON(Infinity), "`Infinity` is JSON"); + assert.ok(utils.isJSON(true) && utils.isJSON(false), "booleans are JSON"); + assert.ok(utils.isJSON(4), utils.isJSON(0), "numbers are JSON"); + assert.ok(utils.isJSON("foo bar"), "strings are JSON"); +}; + +exports["test instanceOf"] = function (assert) { + assert.ok(utils.instanceOf(assert, Object), + "assert is object from other sandbox"); + assert.ok(utils.instanceOf(new Date(), Date), "instance of date"); + assert.ok(!utils.instanceOf(null, Object), "null is not an instance"); +}; + +exports["test json"] = function (assert) { + assert.ok(!utils.isJSON(function(){}), "functions are not json"); + assert.ok(utils.isJSON({}), "`{}` is JSON"); + assert.ok(utils.isJSON({ + a: "foo", + b: 3, + c: undefined, + d: null, + e: { + f: { + g: "bar", + p: [{}, "oueou", 56] + }, + q: { nan: NaN, infinity: Infinity }, + "non standard name": "still works" + } + }), "JSON can contain nested objects"); + + var foo = {}; + var bar = { foo: foo }; + foo.bar = bar; + assert.ok(!utils.isJSON(foo), "recursive objects are not json"); + + + assert.ok(!utils.isJSON({ get foo() { return 5 } }), + "json can not have getter"); + + assert.ok(!utils.isJSON({ foo: "bar", baz: function () {} }), + "json can not contain functions"); + + assert.ok(!utils.isJSON(Object.create({})), + "json must be direct descendant of `Object.prototype`"); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-unit-test.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-unit-test.js new file mode 100644 index 0000000..8ee99a4 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-unit-test.js @@ -0,0 +1,251 @@ +/* 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/. */ + +const timer = require("timer"); +const { Loader } = require("./helpers"); + +var setupCalled = false, teardownCalled = false; + +exports.setup = function() { + setupCalled = true; +}; + +exports.teardown = function() { + teardownCalled = true; + setupCalled = false; +}; + +// Important note - unit tests are run in alphabetical order. The following +// unit tests for setup/teardown are order dependent, sometimes the result of +// one test is checked in the next test (testing for teardown does this). When +// tests are cohesively a single unit, they are named <test_name> - partN where +// N is their order in the sequence. Secondly, because these tests should be +// run before all others, they start with an A. +exports.testASetupTeardownSyncTestPart1 = function(test) { + test.assertEqual(true, setupCalled, 'setup function was called before this'); + test.assertEqual(false, teardownCalled, 'teardown function was not called before this'); +}; + +exports.testASetupTeardownSyncTestPart2 = function(test) { + test.assertEqual(true, setupCalled, 'setup was re-called before this'); + test.assertEqual(true, teardownCalled, 'teardown was called after first function'); +}; + +exports.testATeardownAsyncTestPart1 = function(test) { + teardownCalled = false; + + timer.setTimeout(function() { + test.assertEqual(false, teardownCalled, "teardown not called until done"); + test.done(); + }, 200); + test.waitUntilDone(); +}; + +exports.testATeardownAsyncTestPart2 = function(test) { + test.assertEqual(true, teardownCalled, "teardown called after done"); +}; + +exports.testWaitUntilInstant = function(test) { + test.waitUntilDone(); + + test.waitUntil(function () true, "waitUntil with instant true pass") + .then(function () test.done()); +} + +exports.testWaitUntil = function(test) { + test.waitUntilDone(); + let succeed = false; + + test.waitUntil(function () succeed, "waitUntil pass") + .then(function () test.done()); + + timer.setTimeout(function () { + succeed = true; + }, 200); +} + +exports.testWaitUntilEqual = function(test) { + test.waitUntilDone(); + let succeed = false; + + test.waitUntilEqual("foo", function () succeed ? "foo" : "bar", + "waitUntilEqual pass") + .then(function () test.done()); + + timer.setTimeout(function () { + succeed = true; + }, 200); +} + +exports.testWaitUntilNotEqual = function(test) { + test.waitUntilDone(); + let succeed = false; + + test.waitUntilNotEqual("foo", function () succeed ? "bar" : "foo", + "waitUntilNotEqual pass") + .then(function () test.done()); + + timer.setTimeout(function () { + succeed = true; + }, 200); +} + +exports.testWaitUntilMatches = function(test) { + test.waitUntilDone(); + let succeed = false; + + test.waitUntilMatches(function () succeed ? "foo" : "bar", + /foo/, "waitUntilEqual pass") + .then(function () test.done()); + + timer.setTimeout(function () { + succeed = true; + }, 200); +} + +exports.testWaitUntilErrorInCallback = function(test) { + test.waitUntilDone(); + + test.expectFail(function() { + test.waitUntil(function () {throw "oops"}, "waitUntil pass") + .then(function () test.done()); + }); +} + +exports.testWaitUntilTimeoutInCallback = function(test) { + test.waitUntilDone(1000); + + let runner = new (require("unit-test").TestRunner)({ + console: { + calls: 0, + error: function(msg) { + this.calls++; + if (this.calls == 1) + test.assertEqual(arguments[0], "TEST FAILED: wait4ever (timed out)"); + else if (this.calls == 2) { + test.assertEqual(arguments[0], "test assertion never became true:\n"); + test.assertEqual(arguments[1], "assertion failed, value is false\n"); + // We could additionally check that arguments[1] contains the correct + // stack, but it would be difficult to do so given that it contains + // resource: URLs with a randomly generated string embedded in them + // (the ID of the test addon created to run the tests). And in any + // case, checking the arguments seems sufficient. + + test.done(); + } + else { + test.fail("We got unexpected console.error() calls from waitUntil" + + " assertion callback: '" + arguments[1] + "'"); + } + }, + trace: function () {} + } + }); + + runner.start({ + test: { + name: "wait4ever", + testFunction: function(test) { + test.waitUntilDone(100); + test.waitUntil(function() false); + } + }, + onDone: function() {} + }); +}; + +exports.testExpectFail = function(test) { + test.expectFail(function() { + test.fail('expectFail masking .fail'); + }); + + test.expectFail(function() { + test.assert(false, 'expectFail masking .assert'); + }); + + test.assert(true, 'assert should pass with no expectFail'); +/* + test.expectFail(function() { + test.expectFail(function() { + test.fail('this should blow up'); + }); + }); +*/ +}; + +exports.testAssertFunction = function(test) { + test.assertFunction(function() {}, 'assertFunction with function'); + test.expectFail(function() { + test.assertFunction(null, 'assertFunction with non-function'); + }); +}; + +exports.testAssertUndefined = function(test) { + test.assertUndefined(undefined, 'assertUndefined with undefined'); + test.expectFail(function() { + test.assertUndefined(null, 'assertUndefined with null'); + }); + test.expectFail(function() { + test.assertUndefined(false, 'assertUndefined with false'); + }); + test.expectFail(function() { + test.assertUndefined(0, 'assertUndefined with 0'); + }); +}; + +exports.testAssertNotUndefined = function(test) { + test.expectFail(function() { + test.assertNotUndefined(undefined, 'assertNotUndefined with undefined'); + }); + test.assertNotUndefined(null, 'assertNotUndefined with null'); + test.assertNotUndefined(false, 'assertNotUndefined with false'); + test.assertNotUndefined(0, 'assertNotUndefined with 0'); +}; + +exports.testAssertNull = function(test) { + test.assertNull(null, 'assertNull with null'); + test.expectFail(function() { + test.assertNull(undefined, 'assertNull with undefined'); + }); + test.expectFail(function() { + test.assertNull(false, 'assertNull with false'); + }); + test.expectFail(function() { + test.assertNull(0, 'assertNull with 0'); + }); +}; + +exports.testAssertNotNull = function(test) { + test.assertNotNull(undefined, 'assertNotNull with undefined'); + test.assertNotNull(false, 'assertNotNull with false'); + test.assertNotNull(0, 'assertNotNull with 0'); + + test.expectFail(function() { + test.assertNotNull(null, 'testAssertNotNull with null'); + }); +}; + +exports.testAssertObject = function(test) { + test.assertObject({}, 'assertObject with {}' ); + test.assertObject(new Object(), 'assertObject with new Object'); + test.expectFail(function() { + test.assertObject('fail', 'assertObject with string'); + }); +}; + +exports.testAssertString = function(test) { + test.assertString('', 'assertString with ""'); + test.assertString(new String(), 'assertString with new String'); +}; + +exports.testAssertArray = function(test) { + test.assertArray([], 'assertArray with []'); + test.assertArray(new Array(), 'assertArray with new Array'); +}; + +exports.testNumber = function(test) { + test.assertNumber(1, 'assertNumber with 1'); + test.assertNumber(new Number('2'), 'assertNumber with new Number("2")' ); +}; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-unload.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-unload.js new file mode 100644 index 0000000..e05b4e0 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-unload.js @@ -0,0 +1,161 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +var unload = require("unload"); +var { Loader } = require("./helpers"); + +exports.testUnloading = function(test) { + var loader = Loader(module); + var ul = loader.require("unload"); + var unloadCalled = 0; + var errorsReported = 0; + function unload() { + unloadCalled++; + throw new Error("error"); + } + ul.when(unload); + + // This should be ignored, as we already registered it + ul.when(unload); + + function unload2() { unloadCalled++; } + ul.when(unload2); + loader.unload(undefined, function onError() { errorsReported++; }); + test.assertEqual(unloadCalled, 2, + "Unloader functions are called on unload."); + test.assertEqual(errorsReported, 1, + "One unload handler threw exception"); +}; + +exports.testEnsure = function(test) { + test.assertRaises(function() { unload.ensure({}); }, + "object has no 'unload' property", + "passing obj with no unload prop should fail"); + test.assertRaises(function() { unload.ensure({}, "destroy"); }, + "object has no 'destroy' property", + "passing obj with no custom unload prop should fail"); + + var called = 0; + var obj = {unload: function() { called++; }}; + + unload.ensure(obj); + obj.unload(); + test.assertEqual(called, 1, + "unload() should be called"); + obj.unload(); + test.assertEqual(called, 1, + "unload() should be called only once"); +}; + +/** + * Check that destructors are called only once with Traits. + * - check that public API is calling the destructor and unregister it, + * - check that composed traits with multiple ensure calls, leads to only + * one destructor call. + */ +exports.testEnsureWithTraits = function(test) { + + let { Trait } = require("traits"); + let loader = Loader(module); + let ul = loader.require("unload"); + + let called = 0; + let composedCalled = 0; + let composedTrait = Trait.compose({ + constructor: function () { + // We have to give "public interface" of this trait, as we want to + // call public `unload` method and ensure that we call it only once, + // either when we call this public function manually or on add-on unload + ul.ensure(this._public); + }, + unload: function unload() { + composedCalled++; + } + }); + let obj = Trait.compose( + composedTrait.resolve({ + constructor: "_constructor", + unload : "_unload" + }), { + constructor: function constructor() { + // Same thing applies here, we need to pass public interface + ul.ensure(this._public); + this._constructor(); + }, + unload: function unload() { + called++; + this._unload(); + } + })(); + + obj.unload(); + test.assertEqual(called, 1, + "unload() should be called"); + + test.assertEqual(composedCalled, 1, + "composed object unload() should be called"); + + obj.unload(); + test.assertEqual(called, 1, + "unload() should be called only once"); + test.assertEqual(composedCalled, 1, + "composed object unload() should be called only once"); + + loader.unload(); + test.assertEqual(called, 1, + "unload() should be called only once, after addon unload"); + test.assertEqual(composedCalled, 1, + "composed object unload() should be called only once, " + + "after addon unload"); +}; + +exports.testEnsureWithTraitsPrivate = function(test) { + + let { Trait } = require("traits"); + let loader = Loader(module); + let ul = loader.require("unload"); + + let called = 0; + let privateObj = null; + let obj = Trait.compose({ + constructor: function constructor() { + // This time wa don't have to give public interface, + // as we want to call a private method: + ul.ensure(this, "_unload"); + privateObj = this; + }, + _unload: function unload() { + called++; + this._unload(); + } + })(); + + loader.unload(); + test.assertEqual(called, 1, + "unload() should be called"); + + privateObj._unload(); + test.assertEqual(called, 1, + "_unload() should be called only once, after addon unload"); +}; + +exports.testReason = function (test) { + var reason = "Reason doesn't actually have to be anything in particular."; + var loader = Loader(module); + var ul = loader.require("unload"); + ul.when(function (rsn) { + test.assertEqual(rsn, reason, + "when() reason should be reason given to loader"); + }); + var obj = { + unload: function (rsn) { + test.assertEqual(rsn, reason, + "ensure() reason should be reason given to loader"); + } + }; + ul.ensure(obj); + loader.unload(reason); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-url.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-url.js new file mode 100644 index 0000000..f217400 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-url.js @@ -0,0 +1,204 @@ +/* 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/. */ + +var url = require("url"); + +exports.testResolve = function(test) { + test.assertEqual(url.URL("bar", "http://www.foo.com/").toString(), + "http://www.foo.com/bar"); + + test.assertEqual(url.URL("bar", "http://www.foo.com"), + "http://www.foo.com/bar"); + + test.assertEqual(url.URL("http://bar.com/", "http://foo.com/"), + "http://bar.com/", + "relative should override base"); + + test.assertRaises(function() { url.URL("blah"); }, + "malformed URI: blah", + "url.resolve() should throw malformed URI on base"); + + test.assertRaises(function() { url.URL("chrome://global"); }, + "invalid URI: chrome://global", + "url.resolve() should throw invalid URI on base"); + + test.assertRaises(function() { url.URL("chrome://foo/bar"); }, + "invalid URI: chrome://foo/bar", + "url.resolve() should throw on bad chrome URI"); + + test.assertEqual(url.URL("", "http://www.foo.com"), + "http://www.foo.com/", + "url.resolve() should add slash to end of domain"); +}; + +exports.testParseHttp = function(test) { + var info = url.URL("http://foo.com/bar"); + test.assertEqual(info.scheme, "http"); + test.assertEqual(info.host, "foo.com"); + test.assertEqual(info.port, null); + test.assertEqual(info.userPass, null); + test.assertEqual(info.path, "/bar"); +}; + +exports.testParseHttpWithPort = function(test) { + var info = url.URL("http://foo.com:5/bar"); + test.assertEqual(info.port, 5); +}; + +exports.testParseChrome = function(test) { + var info = url.URL("chrome://global/content/blah"); + test.assertEqual(info.scheme, "chrome"); + test.assertEqual(info.host, "global"); + test.assertEqual(info.port, null); + test.assertEqual(info.userPass, null); + test.assertEqual(info.path, "/content/blah"); +}; + +exports.testParseAbout = function(test) { + var info = url.URL("about:boop"); + test.assertEqual(info.scheme, "about"); + test.assertEqual(info.host, null); + test.assertEqual(info.port, null); + test.assertEqual(info.userPass, null); + test.assertEqual(info.path, "boop"); +}; + +exports.testParseFTP = function(test) { + var info = url.URL("ftp://1.2.3.4/foo"); + test.assertEqual(info.scheme, "ftp"); + test.assertEqual(info.host, "1.2.3.4"); + test.assertEqual(info.port, null); + test.assertEqual(info.userPass, null); + test.assertEqual(info.path, "/foo"); +}; + +exports.testParseFTPWithUserPass = function(test) { + var info = url.URL("ftp://user:pass@1.2.3.4/foo"); + test.assertEqual(info.userPass, "user:pass"); +}; + +// rootURI is jar:file://...!/ if we're packed, and file://.../ if we're +// unpacked. url.toFilename() is not required to work for the contents of +// packed XPIs +var unpacked = (require("@packaging").rootURI.indexOf("file:") == 0); + +exports.testToFilename = function(test) { + test.assertRaises( + function() { url.toFilename("resource://nonexistent"); }, + "resource does not exist: resource://nonexistent/", + "url.toFilename() on nonexistent resources should throw" + ); + + if (unpacked) + test.assertMatches(url.toFilename(module.uri), + /.*test-url\.js$/, + "url.toFilename() on resource: URIs should work"); + else + test.assertRaises( + function() { url.toFilename(module.uri); }, + "cannot map to filename: "+module.uri, + "url.toFilename() can fail for packed XPIs"); + + test.assertRaises( + function() { url.toFilename("http://foo.com/"); }, + "cannot map to filename: http://foo.com/", + "url.toFilename() on http: URIs should raise error" + ); + + try { + test.assertMatches( + url.toFilename("chrome://global/content/console.xul"), + /.*console\.xul$/, + "url.toFilename() w/ console.xul works when it maps to filesystem" + ); + } catch (e) { + if (/chrome url isn\'t on filesystem/.test(e.message)) + test.pass("accessing console.xul in jar raises exception"); + else + test.fail("accessing console.xul raises " + e); + } + + // TODO: Are there any chrome URLs that we're certain exist on the + // filesystem? + // test.assertMatches(url.toFilename("chrome://myapp/content/main.js"), + // /.*main\.js$/); +}; + +exports.testFromFilename = function(test) { + var profileDirName = require("system").pathFor("ProfD"); + var fileUrl = url.fromFilename(profileDirName); + test.assertEqual(url.URL(fileUrl).scheme, 'file', + 'url.toFilename() should return a file: url'); + test.assertEqual(url.fromFilename(url.toFilename(fileUrl)), + fileUrl); +}; + +exports.testURL = function(test) { + let URL = url.URL; + test.assert(URL("h:foo") instanceof URL, "instance is of correct type"); + test.assertRaises(function() URL(), + "malformed URI: undefined", + "url.URL should throw on undefined"); + test.assertRaises(function() URL(""), + "malformed URI: ", + "url.URL should throw on empty string"); + test.assertRaises(function() URL("foo"), + "malformed URI: foo", + "url.URL should throw on invalid URI"); + test.assert(URL("h:foo").scheme, "has scheme"); + test.assertEqual(URL("h:foo").toString(), + "h:foo", + "toString should roundtrip"); + // test relative + base + test.assertEqual(URL("mypath", "http://foo").toString(), + "http://foo/mypath", + "relative URL resolved to base"); + // test relative + no base + test.assertRaises(function() URL("path").toString(), + "malformed URI: path", + "no base for relative URI should throw"); + + let a = URL("h:foo"); + let b = URL(a); + test.assertEqual(b.toString(), + "h:foo", + "a URL can be initialized from another URL"); + test.assertNotStrictEqual(a, b, + "a URL initialized from another URL is not the same object"); + test.assert(a == "h:foo", + "toString is implicit when a URL is compared to a string via =="); + test.assertStrictEqual(a + "", "h:foo", + "toString is implicit when a URL is concatenated to a string"); +}; + +exports.testStringInterface = function(test) { + let URL = url.URL; + var EM = "about:addons"; + var a = URL(EM); + + // make sure the standard URL properties are enumerable and not the String interface bits + test.assertEqual(Object.keys(a), "scheme,userPass,host,port,path", "enumerable key list check for URL."); + test.assertEqual( + JSON.stringify(a), + "{\"scheme\":\"about\",\"userPass\":null,\"host\":null,\"port\":null,\"path\":\"addons\"}", + "JSON.stringify should return a object with correct props and vals."); + + // make sure that the String interface exists and works as expected + test.assertEqual(a.indexOf(":"), EM.indexOf(":"), "indexOf on URL works"); + test.assertEqual(a.valueOf(), EM.valueOf(), "valueOf on URL works."); + test.assertEqual(a.toSource(), EM.toSource(), "toSource on URL works."); + test.assertEqual(a.lastIndexOf("a"), EM.lastIndexOf("a"), "lastIndexOf on URL works."); + test.assertEqual(a.match("t:").toString(), EM.match("t:").toString(), "match on URL works."); + test.assertEqual(a.toUpperCase(), EM.toUpperCase(), "toUpperCase on URL works."); + test.assertEqual(a.toLowerCase(), EM.toLowerCase(), "toLowerCase on URL works."); + test.assertEqual(a.split(":").toString(), EM.split(":").toString(), "split on URL works."); + test.assertEqual(a.charAt(2), EM.charAt(2), "charAt on URL works."); + test.assertEqual(a.charCodeAt(2), EM.charCodeAt(2), "charCodeAt on URL works."); + test.assertEqual(a.concat(EM), EM.concat(a), "concat on URL works."); + test.assertEqual(a.substr(2,3), EM.substr(2,3), "substr on URL works."); + test.assertEqual(a.substring(2,3), EM.substring(2,3), "substring on URL works."); + test.assertEqual(a.trim(), EM.trim(), "trim on URL works."); + test.assertEqual(a.trimRight(), EM.trimRight(), "trimRight on URL works."); + test.assertEqual(a.trimLeft(), EM.trimLeft(), "trimLeft on URL works."); +} diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-uuid.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-uuid.js new file mode 100644 index 0000000..5670023 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-uuid.js @@ -0,0 +1,27 @@ +/* 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 { uuid } = require('api-utils/uuid'); + +exports['test generate uuid'] = function(assert) { + let signature = /{[0-9a-f\-]+}/ + let first = String(uuid()); + let second = String(uuid()); + + assert.ok(signature.test(first), 'first guid has a correct signature'); + assert.ok(signature.test(second), 'second guid has a correct signature'); + assert.notEqual(first, second, 'guid generates new guid on each call'); +}; + +exports['test parse uuid'] = function(assert) { + let firefoxUUID = '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}'; + let actual = uuid(firefoxUUID); + + assert.equal(actual.number, firefoxUUID, 'uuid parsed given string'); + assert.equal(String(actual), firefoxUUID, 'serializes to the same value'); +}; + +require('test').run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-loader.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-loader.js new file mode 100644 index 0000000..0a5ab84 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-loader.js @@ -0,0 +1,120 @@ +/* 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 { WindowLoader } = require('windows/loader'), + { Trait } = require('traits'); + +const Loader = Trait.compose( + WindowLoader, + { + constructor: function Loader(options) { + this._onLoad = options.onLoad; + this._onUnload = options.onUnload; + if ('window' in options) + this._window = options.window; + this._load(); + this.window = this._window; + }, + window: null, + _onLoad: null, + _onUnload: null, + _tabOptions: [] + } +); + +exports['test compositions with missing required properties'] = function(test) { + test.assertRaises( + function() WindowLoader.compose({})(), + 'Missing required property: _onLoad', + 'should throw missing required property exception' + ); + test.assertRaises( + function() WindowLoader.compose({ _onLoad: null, _tabOptions: null })(), + 'Missing required property: _onUnload', + 'should throw missing required property `_onUnload`' + ); + test.assertRaises( + function() WindowLoader.compose({ _onUnload: null, _tabOptions: null })(), + 'Missing required property: _onLoad', + 'should throw missing required property `_onLoad`' + ); + test.assertRaises( + function() WindowLoader.compose({ _onUnload: null, _onLoad: null })(), + 'Missing required property: _tabOptions', + 'should throw missing required property `_tabOptions`' + ); +}; + +exports['test `load` events'] = function(test) { + test.waitUntilDone(); + let onLoadCalled = false; + Loader({ + onLoad: function(window) { + onLoadCalled = true; + test.assertEqual( + window, this._window, 'windows should match' + ); + test.assertEqual( + window.document.readyState, 'complete', 'window must be fully loaded' + ); + window.close(); + }, + onUnload: function(window) { + test.assertEqual( + window, this._window, 'windows should match' + ); + test.assertEqual( + window.document.readyState, 'complete', 'window must be fully loaded' + ); + test.assert(onLoadCalled, 'load callback is supposed to be called'); + test.done(); + } + }); +}; + +exports['test removeing listeners'] = function(test) { + test.waitUntilDone(); + Loader({ + onLoad: function(window) { + test.assertEqual( + window, this._window, 'windows should match' + ); + window.close(); + }, + onUnload: function(window) { + test.done(); + } + }); +}; + +exports['test create loader from opened window'] = function(test) { + test.waitUntilDone(); + let onUnloadCalled = false; + Loader({ + onLoad: function(window) { + test.assertEqual( + window, this._window, 'windows should match' + ); + test.assertEqual( + window.document.readyState, 'complete', 'window must be fully loaded' + ); + Loader({ + window: window, + onLoad: function(win) { + test.assertEqual(win, window, 'windows should match'); + window.close(); + }, + onUnload: function(window) { + test.assert(onUnloadCalled, 'first handler should be called already'); + test.done(); + } + }); + }, + onUnload: function(window) { + onUnloadCalled = true; + } + }); +}; + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-observer.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-observer.js new file mode 100644 index 0000000..687df04 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-observer.js @@ -0,0 +1,48 @@ +/* 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 { Loader } = require("./helpers"); + +exports["test unload window observer"] = function(assert, done) { + // Hacky way to be able to create unloadable modules via makeSandboxedLoader. + let loader = Loader(module); + + let utils = loader.require("api-utils/window-utils"); + let { isBrowser, activeBrowserWindow: activeWindow } = utils; + let observer = loader.require("api-utils/windows/observer").observer; + let opened = 0; + let closed = 0; + + observer.on("open", function onOpen(window) { + // Ignoring non-browser windows + if (isBrowser(window)) + opened++; + }); + observer.on("close", function onClose(window) { + // Ignore non-browser windows & already opened `activeWindow` (unload will + // emit close on it even though it is not actually closed). + if (isBrowser(window) && window !== activeWindow) + closed++; + }); + + // Open window and close it to trigger observers. + activeWindow.open().close(); + + // Unload the module so that all listeners set by observer are removed. + loader.unload(); + + // Open and close window once again. + activeWindow.open().close(); + + // Enqueuing asserts to make sure that assertion is not performed early. + require("timer").setTimeout(function () { + assert.equal(1, opened, "observer open was called before unload only"); + assert.equal(1, closed, "observer close was called before unload only"); + done(); + }, 0); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-utils.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-utils.js new file mode 100644 index 0000000..9985458 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-utils.js @@ -0,0 +1,321 @@ +/* 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"; + +var windowUtils = require("api-utils/window-utils"); +var timer = require("api-utils/timer"); +var { Cc, Ci } = require("chrome"); +var { Loader } = require("./helpers"); + +function toArray(iterator) { + let array = []; + for each (let item in iterator()) + array.push(item); + return array; +} + +function makeEmptyWindow() { + var xulNs = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var blankXul = ('<?xml version="1.0"?>' + + '<?xml-stylesheet href="chrome://global/skin/" ' + + ' type="text/css"?>' + + '<window xmlns="' + xulNs + '" windowtype="test:window">' + + '</window>'); + var url = "data:application/vnd.mozilla.xul+xml," + escape(blankXul); + var features = ["chrome", "width=10", "height=10"]; + + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Ci.nsIWindowWatcher); + return ww.openWindow(null, url, null, features.join(","), null); +} + +exports['test close on unload'] = function(assert) { + var timesClosed = 0; + var fakeWindow = { + _listeners: [], + addEventListener: function(name, func, bool) { + this._listeners.push(func); + }, + removeEventListener: function(name, func, bool) { + var index = this._listeners.indexOf(func); + if (index == -1) + throw new Error("event listener not found"); + this._listeners.splice(index, 1); + }, + close: function() { + timesClosed++; + this._listeners.forEach( + function(func) { + func({target: fakeWindow.document}); + }); + }, + document: { + get defaultView() { return fakeWindow; } + } + }; + + let loader = Loader(module); + loader.require("window-utils").closeOnUnload(fakeWindow); + assert.equal(fakeWindow._listeners.length, 1, + "unload listener added on closeOnUnload()"); + assert.equal(timesClosed, 0, + "window not closed when registered."); + loader.require("unload").send(); + assert.equal(timesClosed, 1, + "window closed on module unload."); + assert.equal(fakeWindow._listeners.length, 0, + "unload event listener removed on module unload"); + + timesClosed = 0; + loader.require("window-utils").closeOnUnload(fakeWindow); + assert.equal(timesClosed, 0, + "window not closed when registered."); + fakeWindow.close(); + assert.equal(timesClosed, 1, + "window closed when close() called."); + assert.equal(fakeWindow._listeners.length, 0, + "unload event listener removed on window close"); + loader.require("unload").send(); + assert.equal(timesClosed, 1, + "window not closed again on module unload."); + loader.unload(); +}; + +exports['test window watcher'] = function(assert, done) { + var myWindow; + var finished = false; + + var delegate = { + onTrack: function(window) { + if (window == myWindow) { + assert.pass("onTrack() called with our test window"); + timer.setTimeout(function() { myWindow.close(); }, 1); + } + }, + onUntrack: function(window) { + if (window == myWindow) { + assert.pass("onUntrack() called with our test window"); + timer.setTimeout(function() { + if (!finished) { + finished = true; + myWindow = null; + wt.unload(); + done(); + } else + assert.fail("finishTest() called multiple times."); + }, 1); + } + } + }; + + // test bug 638007 (new is optional), using new + var wt = new windowUtils.WindowTracker(delegate); + myWindow = makeEmptyWindow(); +}; + +exports['test window watcher untracker'] = function(assert, done) { + var myWindow; + var tracks = 0; + var unloadCalled = false; + + var delegate = { + onTrack: function(window) { + tracks = tracks + 1; + if (window == myWindow) { + assert.pass("onTrack() called with our test window"); + timer.setTimeout(function() { + myWindow.close(); + }, 1); + } + }, + onUntrack: function(window) { + tracks = tracks - 1; + if (window == myWindow && !unloadCalled) { + unloadCalled = true; + timer.setTimeout(function() { + wt.unload(); + }, 1); + } + if (0 > tracks) { + assert.fail("WindowTracker onUntrack was called more times than onTrack.."); + } + else if (0 == tracks) { + timer.setTimeout(function() { + myWindow = null; + done(); + }, 1); + } + } + }; + + // test bug 638007 (new is optional), not using new + var wt = windowUtils.WindowTracker(delegate); + myWindow = makeEmptyWindow(); +}; + +// test that _unregWindow calls _unregLoadingWindow +exports['test window watcher unregs 4 loading wins'] = function(assert, done) { + var myWindow; + var finished = false; + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); + var counter = 0; + + var delegate = { + onTrack: function(window) { + var type = window.document.documentElement.getAttribute("windowtype"); + if (type == "test:window") + assert.fail("onTrack shouldn't have been executed."); + } + }; + var wt = new windowUtils.WindowTracker(delegate); + + // make a new window + myWindow = makeEmptyWindow(); + + // make sure that the window hasn't loaded yet + assert.notEqual( + myWindow.document.readyState, + "complete", + "window hasn't loaded yet."); + + // unload WindowTracker + wt.unload(); + + // make sure that the window still hasn't loaded, which means that the onTrack + // would have been removed successfully assuming that it doesn't execute. + assert.notEqual( + myWindow.document.readyState, + "complete", + "window still hasn't loaded yet."); + + // wait for the window to load and then close it. onTrack wouldn't be called + // until the window loads, so we must let it load before closing it to be + // certain that onTrack was removed. + myWindow.addEventListener("load", function() { + // allow all of the load handles to execute before closing + myWindow.setTimeout(function() { + myWindow.addEventListener("unload", function() { + // once the window unloads test is done + done(); + }, false); + myWindow.close(); + }, 0); + }, false); +} + +exports['test window watcher without untracker'] = function(assert, done) { + var myWindow; + var finished = false; + + var delegate = { + onTrack: function(window) { + if (window == myWindow) { + assert.pass("onTrack() called with our test window"); + timer.setTimeout(function() { + myWindow.close(); + + if (!finished) { + finished = true; + myWindow = null; + wt.unload(); + done(); + } else { + assert.fail("onTrack() called multiple times."); + } + }, 1); + } + } + }; + + var wt = new windowUtils.WindowTracker(delegate); + myWindow = makeEmptyWindow(); +}; + +exports['test active window'] = function(assert, done) { + let testRunnerWindow = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow("test:runner"); + let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); + + assert.equal(windowUtils.activeBrowserWindow, browserWindow, + "Browser window is the active browser window."); + + + let testSteps = [ + function() { + windowUtils.activeWindow = browserWindow; + continueAfterFocus(browserWindow); + }, + function() { + assert.equal(windowUtils.activeWindow, browserWindow, + "Correct active window [1]"); + continueAfterFocus(windowUtils.activeWindow = testRunnerWindow); + }, + function() { + assert.equal(windowUtils.activeWindow, testRunnerWindow, + "Correct active window [2]"); + assert.equal(windowUtils.activeBrowserWindow, browserWindow, + "Correct active browser window [3]"); + continueAfterFocus(windowUtils.activeWindow = browserWindow); + }, + function() { + assert.equal(windowUtils.activeWindow, browserWindow, + "Correct active window [4]"); + continueAfterFocus(windowUtils.activeWindow = testRunnerWindow); + }, + function() { + assert.equal(windowUtils.activeWindow, testRunnerWindow, + "Correct active window [5]"); + assert.equal(windowUtils.activeBrowserWindow, browserWindow, + "Correct active browser window [6]"); + testRunnerWindow = null; + browserWindow = null; + done(); + } + ]; + + let nextTest = function() { + let func = testSteps.shift(); + if (func) { + func(); + } + } + + function continueAfterFocus(targetWindow) { + + // Based on SimpleTest.waitForFocus + var fm = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + + var childTargetWindow = {}; + fm.getFocusedElementForWindow(targetWindow, true, childTargetWindow); + childTargetWindow = childTargetWindow.value; + + var focusedChildWindow = {}; + if (fm.activeWindow) { + fm.getFocusedElementForWindow(fm.activeWindow, true, focusedChildWindow); + focusedChildWindow = focusedChildWindow.value; + } + + var focused = (focusedChildWindow == childTargetWindow); + if (focused) { + nextTest(); + } else { + childTargetWindow.addEventListener("focus", function focusListener() { + childTargetWindow.removeEventListener("focus", focusListener, true); + nextTest(); + }, true); + } + + } + + nextTest(); +}; + +require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-utils2.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-utils2.js new file mode 100644 index 0000000..62f34f9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-window-utils2.js @@ -0,0 +1,67 @@ +/* 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 { Ci } = require('chrome'); +const { open, backgroundify, + getXULWindow, getBaseWindow } = require('api-utils/window/utils'); +const windowUtils = require('api-utils/window-utils'); + +function windows(iterator) { + let array = []; + for each (let item in windowUtils.windowIterator()) + array.push(item); + return array; +} + +exports['test get nsIBaseWindow from nsIDomWindow'] = function(assert) { + let active = windowUtils.activeBrowserWindow; + + assert.ok(!(active instanceof Ci.nsIBaseWindow), + 'active window is not nsIBaseWindow'); + + assert.ok(getBaseWindow(active) instanceof Ci.nsIBaseWindow, + 'base returns nsIBaseWindow'); +}; + +exports['test get nsIXULWindow from nsIDomWindow'] = function(assert) { + let active = windowUtils.activeBrowserWindow; + assert.ok(!(active instanceof Ci.nsIXULWindow), + 'active window is not nsIXULWindow'); + assert.ok(getXULWindow(active) instanceof Ci.nsIXULWindow, + 'base returns nsIXULWindow'); +}; + +exports['test top window creation'] = function(assert) { + let window = open('data:text/html,Hello top window'); + assert.ok(~windows().indexOf(window), 'window was opened'); + window.close(); +}; + +exports['test new top window with options'] = function(assert) { + let window = open('data:text/html,Hi custom top window', { + name: 'test', + features: { height: 100, width: 200, toolbar: true } + }); + assert.ok(~windows().indexOf(window), 'window was opened'); + assert.equal(window.name, 'test', 'name was set'); + assert.equal(window.innerHeight, 100, 'height is set'); + assert.equal(window.innerWidth, 200, 'height is set'); + assert.equal(window.toolbar.visible, true, 'toolbar was set'); + window.close(); +}; + +exports['test backgroundify'] = function(assert) { + let window = open('data:text/html,backgroundy'); + assert.ok(~windows().indexOf(window), + 'window is in the list of windows'); + let backgroundy = backgroundify(window); + assert.equal(backgroundy, window, 'backgroundify returs give window back'); + assert.ok(!~windows().indexOf(window), + 'backgroundifyied window is in the list of windows'); + window.close(); +}; + +require('test').run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-xhr.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-xhr.js new file mode 100644 index 0000000..ad47e54 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-xhr.js @@ -0,0 +1,75 @@ +/* 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/. */ + +var xhr = require("xhr"); +var timer = require("timer"); +var { Loader } = require("./helpers"); +var xulApp = require("xul-app"); + +/* Test is intentionally disabled until platform bug 707256 is fixed. +exports.testAbortedXhr = function(test) { + var req = new xhr.XMLHttpRequest(); + test.assertEqual(xhr.getRequestCount(), 1); + req.abort(); + test.assertEqual(xhr.getRequestCount(), 0); +}; +*/ + +exports.testLocalXhr = function(test) { + var req = new xhr.XMLHttpRequest(); + req.overrideMimeType("text/plain"); + req.open("GET", module.uri); + req.onreadystatechange = function() { + if (req.readyState == 4 && req.status == 0) { + test.assertMatches(req.responseText, + /onreadystatechange/, + "XMLHttpRequest should get local files"); + timer.setTimeout( + function() { test.assertEqual(xhr.getRequestCount(), 0); + test.done(); }, + 0 + ); + } + }; + req.send(null); + test.assertEqual(xhr.getRequestCount(), 1); + test.waitUntilDone(4000); +}; + +exports.testUnload = function(test) { + var loader = Loader(module); + var sbxhr = loader.require("xhr"); + var req = new sbxhr.XMLHttpRequest(); + req.overrideMimeType("text/plain"); + req.open("GET", module.uri); + req.send(null); + test.assertEqual(sbxhr.getRequestCount(), 1); + loader.unload(); + test.assertEqual(sbxhr.getRequestCount(), 0); +}; + +exports.testResponseHeaders = function(test) { + var req = new xhr.XMLHttpRequest(); + req.overrideMimeType("text/plain"); + req.open("GET", module.uri); + req.onreadystatechange = function() { + if (req.readyState == 4 && req.status == 0) { + var headers = req.getAllResponseHeaders(); + if (xulApp.versionInRange(xulApp.platformVersion, "13.0a1", "*")) { + // Now that bug 608939 is FIXED, headers works correctly on files: + test.assertEqual(headers, "Content-Type: text/plain\n", + "XHR's headers are valid"); + } + else { + test.assert(headers === null || headers === "", + "XHR's headers are empty"); + } + test.done(); + } + }; + req.send(null); + test.assertEqual(xhr.getRequestCount(), 1); + test.waitUntilDone(4000); +} + diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-xpcom.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-xpcom.js new file mode 100644 index 0000000..eda3565 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-xpcom.js @@ -0,0 +1,217 @@ +/* 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/. */ + +const xpcom = require("api-utils/xpcom"); +const { Cc, Ci, Cm, Cr } = require("chrome"); +const { isCIDRegistered } = Cm.QueryInterface(Ci.nsIComponentRegistrar); +const { Loader } = require("./helpers"); + +exports['test Unknown implements nsISupports'] = function(assert) { + let actual = xpcom.Unknown.new(); + assert.equal(actual.QueryInterface(Ci.nsISupports), + actual, + 'component implements nsISupports'); +}; + +exports['test implement xpcom interfaces'] = function(assert) { + let component = xpcom.Unknown.extend({ + interfaces: [ 'nsIWeakReference' ], + QueryReferent: function() {} + }) + + assert.equal(component.QueryInterface(Ci.nsISupports), + component, + 'component implements nsISupports'); + assert.equal(component.QueryInterface(Ci.nsIWeakReference), + component, + 'component implements specified interface'); + + assert.throws(function() { + component.QueryInterface(Ci.nsIObserver); + }, "component does not implements interface"); + + let actual = component.extend({ + interfaces: [ 'nsIObserver', 'nsIRequestObserver' ], + observe: function() {}, + onStartRequest: function() {}, + onStopRequest: function() {} + }); + + assert.equal(actual.QueryInterface(Ci.nsISupports), + actual, + 'derived component implements nsISupports'); + assert.equal(actual.QueryInterface(Ci.nsIWeakReference), + actual, + 'derived component implements supers interface'); + assert.equal(actual.QueryInterface(Ci.nsIObserver), + actual.QueryInterface(Ci.nsIRequestObserver), + 'derived component implements specified interfaces'); +}; + +exports['test implement factory without contract'] = function(assert) { + let actual = xpcom.Factory.new({ + component: xpcom.Unknown.extend({ + get wrappedJSObject() this, + }) + }); + + assert.ok(isCIDRegistered(actual.id), 'factory is regiseterd'); + xpcom.unregister(actual); + assert.ok(!isCIDRegistered(actual.id), 'factory is unregistered'); +}; + +exports['test implement xpcom factory'] = function(assert) { + let Component = xpcom.Unknown.extend({ + interfaces: [ 'nsIObserver' ], + get wrappedJSObject() this, + observe: function() {} + }); + + let factory = xpcom.Factory.new({ + register: false, + contract: '@jetpack/test/factory;1', + component: Component + }); + + assert.ok(!isCIDRegistered(factory.id), 'factory is not registered'); + xpcom.register(factory); + assert.ok(isCIDRegistered(factory.id), 'factory is registered'); + + let actual = Cc[factory.contract].createInstance(Ci.nsIObserver); + + assert.ok(Component.isPrototypeOf(actual.wrappedJSObject), + "createInstance returnes wrapped factory instances"); + + assert.notEqual(Cc[factory.contract].createInstance(Ci.nsIObserver), + Cc[factory.contract].createInstance(Ci.nsIObserver), + "createInstance returns new instance each time"); +}; + +exports['test implement xpcom service'] = function(assert) { + let actual = xpcom.Service.new({ + contract: '@jetpack/test/service;1', + register: false, + component: xpcom.Unknown.extend({ + get wrappedJSObject() this, + interfaces: [ 'nsIObserver'], + observe: function() {}, + name: 'my-service' + }) + }); + + assert.ok(!isCIDRegistered(actual.id), 'component is not registered'); + xpcom.register(actual); + assert.ok(isCIDRegistered(actual.id), 'service is regiseterd'); + assert.ok(Cc[actual.contract].getService(Ci.nsIObserver).observe, + 'service can be accessed via get service'); + assert.equal(Cc[actual.contract].getService(Ci.nsIObserver).wrappedJSObject, + actual.component, + 'wrappedJSObject is an actual component'); + xpcom.unregister(actual); + assert.ok(!isCIDRegistered(actual.id), 'service is unregistered'); +}; + + +function testRegister(assert, text) { + + const service = xpcom.Service.new({ + description: 'test about:boop page', + contract: '@mozilla.org/network/protocol/about;1?what=boop', + register: false, + component: xpcom.Unknown.extend({ + get wrappedJSObject() this, + interfaces: [ 'nsIAboutModule' ], + newChannel : function(aURI) { + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + var channel = ios.newChannel( + "data:text/plain," + text, + null, + null + ); + + channel.originalURI = aURI; + return channel; + }, + getURIFlags: function(aURI) { + return Ci.nsIAboutModule.ALLOW_SCRIPT; + } + }) + }); + + xpcom.register(service); + + assert.equal(isCIDRegistered(service.id), true); + + var aboutFactory = xpcom.factoryByContract(service.contract); + var about = aboutFactory.createInstance(Ci.nsIAboutModule); + + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + assert.equal( + about.getURIFlags(ios.newURI("http://foo.com", null, null)), + Ci.nsIAboutModule.ALLOW_SCRIPT + ); + + var aboutURI = ios.newURI("about:boop", null, null); + var channel = ios.newChannelFromURI(aboutURI); + var iStream = channel.open(); + var siStream = Cc['@mozilla.org/scriptableinputstream;1'] + .createInstance(Ci.nsIScriptableInputStream); + siStream.init(iStream); + var data = new String(); + data += siStream.read(-1); + siStream.close(); + iStream.close(); + assert.equal(data, text); + + xpcom.unregister(service); + assert.equal(isCIDRegistered(service.id), false); +} + +exports["test register"] = function(assert) { + testRegister(assert, "hai2u"); +}; + +exports["test re-register"] = function(assert) { + testRegister(assert, "hai2u again"); +}; + +exports["test unload"] = function(assert) { + let loader = Loader(module); + let sbxpcom = loader.require("xpcom"); + + let auto = sbxpcom.Factory.new({ + contract: "@mozilla.org/test/auto-unload;1", + description: "test auto", + component: sbxpcom.Unknown.extend({ name: 'auto' }) + }); + + let manual = sbxpcom.Factory.new({ + contract: "@mozilla.org/test/manual-unload;1", + description: "test manual", + register: false, + unregister: false, + component: sbxpcom.Unknown.extend({ name: 'manual' }) + }); + + assert.equal(isCIDRegistered(auto.id), true, 'component registered'); + assert.equal(isCIDRegistered(manual.id), false, 'component not registered'); + + sbxpcom.register(manual) + assert.equal(isCIDRegistered(manual.id), true, + 'component was automatically registered on first instance'); + loader.unload(); + + assert.equal(isCIDRegistered(auto.id), false, + 'component was atumatically unregistered on unload'); + assert.equal(isCIDRegistered(manual.id), true, + 'component was not automatically unregistered on unload'); + sbxpcom.unregister(manual); + assert.equal(isCIDRegistered(manual.id), false, + 'component was manually unregistered on unload'); +}; + +require("test").run(exports) diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/test-xul-app.js b/tools/addon-sdk-1.7/packages/api-utils/tests/test-xul-app.js new file mode 100644 index 0000000..58aa29e --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/test-xul-app.js @@ -0,0 +1,45 @@ +/* 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/. */ + +var xulApp = require("xul-app"); + +exports.testXulApp = function(test) { + test.assertEqual(typeof(xulApp.ID), "string", + "ID is a string"); + test.assertEqual(typeof(xulApp.name), "string", + "name is a string"); + test.assertEqual(typeof(xulApp.version), "string", + "version is a string"); + test.assertEqual(typeof(xulApp.platformVersion), "string", + "platformVersion is a string"); + + test.assertRaises(function() { xulApp.is("blargy"); }, + "Unkown Mozilla Application: blargy", + "is() throws error on bad app name"); + test.assertRaises(function() { xulApp.isOneOf(["blargy"]); }, + "Unkown Mozilla Application: blargy", + "isOneOf() throws error on bad app name"); + + function testSupport(name) { + var item = xulApp.is(name); + test.assert(item === true || item === false, + "is('" + name + "') is true or false."); + } + + var apps = ["Firefox", "Mozilla", "Sunbird", "SeaMonkey", + "Fennec", "Thunderbird"]; + + apps.forEach(function(name) { testSupport(name); }); + + test.assert(xulApp.isOneOf(apps) == true || + xulApp.isOneOf(apps) == false, + "isOneOf() returns true or false."); + + test.assertEqual(xulApp.versionInRange(xulApp.platformVersion, "1.9", "*"), + true, "platformVersion in range [1.9, *)"); + test.assertEqual(xulApp.versionInRange("3.6.4", "3.6.4", "3.6.*"), + true, "3.6.4 in [3.6.4, 3.6.*)"); + test.assertEqual(xulApp.versionInRange("1.9.3", "1.9.2", "1.9.3"), + false, "1.9.3 not in [1.9.2, 1.9.3)"); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/traits/assert.js b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/assert.js new file mode 100644 index 0000000..7c385e4 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/assert.js @@ -0,0 +1,98 @@ +/* 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"; + +var BaseAssert = require("test/assert").Assert; + +/** + * Whether or not given property descriptors are equivalent. They are + * equivalent either if both are marked as "conflict" or "required" property + * or if all the properties of descriptors are equal. + * @param {Object} actual + * @param {Object} expected + */ +function equivalentDescriptors(actual, expected) { + return (actual.conflict && expected.conflict) || + (actual.required && expected.required) || + equalDescriptors(actual, expected); +} + +function equalDescriptors(actual, expected) { + return actual.get === expected.get && + actual.set === expected.set && + actual.value === expected.value && + !!actual.enumerable === !!expected.enumerable && + !!actual.configurable === !!expected.configurable && + !!actual.writable === !!expected.writable; +} + +/** + * Whether or not given `target` array contains all the element + * from a given `source` array. + */ +function containsSet(source, target) { + return source.some(function(element) { + return 0 > target.indexOf(element); + }); +} + +/** + * Whether or not given two arrays contain all elements from another. + */ +function equivalentSets(source, target) { + return containsSet(source, target) && containsSet(target, source); +} + +/** + * Finds name of the property from `source` property descriptor map, that + * is not equivalent of the name named property in the `target` property + * descriptor map. If not found `null` is returned instead. + */ +function findNonEquivalentPropertyName(source, target) { + var value = null; + Object.getOwnPropertyNames(source).some(function(key) { + var areEquivalent = false; + if (!equivalentDescriptors(source[key], target[key])) { + value = key; + areEquivalent = true; + } + return areEquivalent; + }); + return value; +} + +var AssertDescriptor = { + equalTraits: { + value: function equivalentTraits(actual, expected, message) { + var difference; + var actualKeys = Object.getOwnPropertyNames(actual); + var expectedKeys = Object.getOwnPropertyNames(expected); + + if (equivalentSets(actualKeys, expectedKeys)) { + this.fail({ + operator: "equalTraits", + message: "Traits define different properties", + actual: actualKeys.sort().join(","), + expected: expectedKeys.sort().join(","), + }); + } + else if ((difference = findNonEquivalentPropertyName(actual, expected))) { + this.fail({ + operator: "equalTraits", + message: "Traits define non-equivalent property `" + difference + "`", + actual: actual[difference], + expected: expected[difference] + }); + } + else { + this.pass(message || "Traits are equivalent."); + } + } + } +}; + +exports.Assert = function Assert() { + return Object.create(BaseAssert.apply(null, arguments), AssertDescriptor); +}; diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/traits/descriptor-tests.js b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/descriptor-tests.js new file mode 100644 index 0000000..b775ff4 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/descriptor-tests.js @@ -0,0 +1,335 @@ +/* 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"; + +var Trait = require("light-traits").Trait; +var utils = require("./utils"); +var Data = utils.Data; +var Method = utils.Method; +var Accessor = utils.Accessor; +var Required = utils.Required; +var Conflict = utils.Conflict; + +function method() {} + +exports.Assert = require("./assert").Assert +exports["test simple composition"] = function(assert) { + var actual = Trait.compose( + Trait({ a: 0, b: 1 }), + { c: { value: 2 }, d: { value: method, enumerable: true } } + ); + + var expected = { + a: Data(0), + b: Data(1), + c: Data(2, false, false, false), + d: Method(method, true, false, false) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test composition with conflict"] = function(assert) { + var actual = Trait.compose( + Trait({ a: 0, b: 1 }), + { + a: { + value: 2, + writable: true, + configurable: true, + enumerable: true + }, + c: { + value: method, + configurable: true + } + } + ); + + var expected = { + a: Conflict("a"), + b: Data(1), + c: Method(method, false, true, false) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test identical props does not cause conflict"] = function(assert) { + var actual = Trait.compose( + { + a: { + value: 0, + writable: true, + configurable: true, + enumerable: true + }, + b: { + value: 1 + } + }, + Trait({ + a: 0, + c: method + }) + ); + + var expected = { + a: Data(0), + b: Data(1, false, false, false), + c: Method(method) + } + + assert.equalTraits(actual, expected); +}; + +exports["test composition with identical required props"] = function(assert) { + var actual = Trait.compose( + Trait({ a: Trait.required, b: 1 }), + { a: { required: true }, c: { value: method } } + ); + + var expected = { + a: Required(), + b: Data(1), + c: Method(method, false, false, false) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test composition satisfying a required prop"] = function(assert) { + var actual = Trait.compose( + Trait({ a: Trait.required, b: 1 }), + { a: { value: method, enumerable: true } } + ); + + var expected = { + a: Method(method, true, false, false), + b: Data(1) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test compose is neutral wrt conflicts"] = function(assert) { + var actual = Trait.compose( + Trait({ a: { value: 1 } }, Trait({ a: 2 })), + { b: { value: 0, writable: true, configurable: true, enumerable: false } } + ); + + var expected = { a: Conflict("a"), b: Data(0, false) }; + + assert.equalTraits(actual, expected); +}; + +exports["test conflicting prop overrides Trait.required"] = function(assert) { + var actual = Trait.compose( + Trait.compose( + Trait({ a: 1 }), + { a: { value: 2 } } + ), + { a: { value: Trait.required } } + ); + + var expected = { a: Conflict("a") }; + + assert.equalTraits(actual, expected); +}; + +exports["test compose is commutative"] = function(assert) { + var actual = Trait.compose( + Trait({ a: 0, b: 1 }), + { c: { value: 2 }, d: { value: method } } + ); + + var expected = Trait.compose( + { c: { value: 2 }, d: { value: method } }, + Trait({ a: 0, b: 1 }) + ); + + assert.equalTraits(actual, expected); +} + +exports["test compose is commutative, also for required/conflicting props"] = function(assert) { + var actual = Trait.compose( + { + a: { value: 0 }, + b: { value: 1 }, + c: { value: 3 }, + e: { value: Trait.required } + }, + { + c: { value: 2 }, + d: { get: method } + } + ); + + var expected = Trait.compose( + Trait({ c: 3 }), + { + c: { value: 2 }, + d: { get: method }, + a: { value: 0 }, + b: { value: 1 }, + e: { value: Trait.required }, + } + ); + + assert.equalTraits(actual, expected); +}; + +exports["test compose is associative"] = function(assert) { + var actual = Trait.compose( + { + a: { value: 0 }, + b: { value: 1 }, + c: { value: 3 }, + d: { value: Trait.required } + }, + Trait.compose( + { c: { value: 3 }, d: { value: Trait.required } }, + { c: { value: 2 }, d: { value: method }, e: { value: "foo" } } + ) + ); + + var expected = Trait.compose( + Trait.compose( + { + a: { value: 0 }, + b: { value: 1 }, + c: { value: 3 }, + d: { value: Trait.required } + }, + { + c: { value: 3 }, + d: { value: Trait.required } + } + ), + { + c: { value: 2 }, + d: { value: method }, + e: { value: "foo" } + } + ); + + assert.equalTraits(actual, expected); +}; + +exports["test diamond import of same prop do not conflict"] = function(assert) { + var actual = Trait.compose( + Trait.compose( + { b: { value: 2 } }, + { a: { value: 1, enumerable: true, configurable: true, writable: true } } + ), + Trait.compose( + { c: { value: 3 } }, + Trait({ a: 1 }) + ), + Trait({ d: 4 }) + ); + + var expected = { + a: Data(1), + b: Data(2, false, false, false), + c: Data(3, false, false, false), + d: Data(4) + }; + + assert.equalTraits(actual, expected); +}; + +exports["test create simple"] = function(assert) { + var o1 = Trait.compose( + Trait({ a: 1 }), + { + b: { + value: function() { + return this.a; + } + } + } + ).create(Object.prototype); + + assert.equal(Object.getPrototypeOf(o1), Object.prototype, "o1 prototype"); + assert.equal(1, o1.a, "o1.a"); + assert.equal(1, o1.b(), "o1.b()"); + assert.equal(Object.keys(o1).length, 1, "Object.keys(o1).length === 2"); +}; + +exports["test create with Array.prototype"] = function(assert) { + var o2 = Trait.compose({}, {}).create(Array.prototype); + assert.equal(Object.getPrototypeOf(o2), Array.prototype, "o2 prototype"); +}; + +exports["test exception for incomplete required properties"] = function(assert) { + assert.throws(function() { + Trait({ foo: Trait.required }).create(Object.prototype) + }, /Missing required property: `foo`/, "required prop error"); +} + +exports["test exception for unresolved conflicts"] = function(assert) { + assert.throws(function() { + Trait(Trait({ a: 0 }), Trait({ a: 1 })).create({}) + }, /Remaining conflicting property: `a`/, "conflicting prop error"); +} + +exports["test conflicting properties are present"] = function(assert) { + var o5 = Object.create(Object.prototype, Trait.compose( + { a: { value: 0 } }, + { a: { value: 1 } } + )); + + assert.ok("a" in o5, "conflicting property present"); + assert.throws(function() { + o5.a + }, /Remaining conflicting property: `a`/, "conflicting prop access error"); +}; + +exports["test diamond with conflicts"] = function(assert) { + function makeT1(x) { + return { + m: { + value: function() { + return x + } + } + }; + }; + + function makeT2(x) { + return Trait.compose( + Trait({ t2: "foo" }), + makeT1(x) + ); + }; + + function makeT3(x) { + return Trait.compose( + { + t3: { value: "bar" } + }, + makeT1(x) + ); + }; + + var T4 = Trait.compose(makeT2(5), makeT3(5)); + + assert.throws(function() { + T4.create(Object.prototype); + }, /Remaining conflicting property: `m`/, "diamond prop conflict"); +}; + +exports["test providing requirements through proto"] = function(assert) { + var t = Trait.compose( + {}, + { required: { required: true } } + ).create({ required: "test" }); + + assert.equal(t.required, "test", "property from proto is inherited"); +}; + +if (module == require.main) + require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/traits/inheritance-tests.js b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/inheritance-tests.js new file mode 100644 index 0000000..3b311f9 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/inheritance-tests.js @@ -0,0 +1,104 @@ +/* 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"; + +var Trait = require("light-traits").Trait; + +exports["test custom constructor and inherited toString"] = function(assert) { + function Type() { + return Object.create(Type.prototype); + } + Type.prototype = Trait({ + method: function method() { + return 2; + } + }).create(Object.freeze(Type.prototype)); + + var fixture = Type(); + + assert.equal(fixture.constructor, Type, "must override constructor"); + assert.equal(fixture.toString(), "[object Type]", "must inherit toString"); +}; + +exports["test custom toString and inherited constructor"] = function(assert) { + function Type() { + return Object.create(Type.prototype); + } + Type.prototype = Trait({ + toString: function toString() { + return "<toString>"; + } + }).create(); + + var fixture = Type(); + + assert.equal(fixture.constructor, Trait, "must inherit constructor Trait"); + assert.equal(fixture.toString(), "<toString>", "Must override toString"); +}; + +exports["test custom toString and constructor"] = function(assert) { + function Type() { + return TypeTrait.create(Type.prototype); + } + Object.freeze(Type.prototype); + var TypeTrait = Trait({ + toString: function toString() { + return "<toString>"; + } + }); + + var fixture = Type(); + + assert.equal(fixture.constructor, Type, "constructor is provided to create"); + assert.equal(fixture.toString(), "<toString>", "toString was overridden"); +}; + +exports["test resolve constructor"] = function (assert) { + function Type() {} + var T1 = Trait({ constructor: Type }).resolve({ constructor: '_foo' }); + var f1 = T1.create(); + + assert.equal(f1._foo, Type, "constructor was resolved"); + assert.equal(f1.constructor, Trait, "constructor of prototype is inherited"); + assert.equal(f1.toString(), "[object Trait]", "toString is inherited"); +}; + +exports["test compose read-only"] = function (assert) { + function Type() {} + Type.prototype = Trait.compose(Trait({}), { + constructor: { value: Type }, + a: { value: "b", enumerable: true } + }).resolve({ a: "b" }).create({ a: "a" }); + + var f1 = new Type(); + + assert.equal(Object.getPrototypeOf(f1), Type.prototype, "inherits correctly"); + assert.equal(f1.constructor, Type, "constructor was overridden"); + assert.equal(f1.toString(), "[object Type]", "toString was inherited"); + assert.equal(f1.a, "a", "property a was resolved"); + assert.equal(f1.b, "b", "property a was renamed to b"); + assert.ok(!Object.getOwnPropertyDescriptor(Type.prototype, "a"), + "a is not on the prototype of the instance"); + + var proto = Object.getPrototypeOf(Type.prototype); + var dc = Object.getOwnPropertyDescriptor(Type.prototype, "constructor"); + var db = Object.getOwnPropertyDescriptor(Type.prototype, "b"); + var da = Object.getOwnPropertyDescriptor(proto, "a"); + + assert.ok(!dc.writable, "constructor is not writable"); + assert.ok(!dc.enumerable, "constructor is not enumerable"); + assert.ok(dc.configurable, "constructor inherits configurability"); + + assert.ok(!db.writable, "a -> b is not writable"); + assert.ok(db.enumerable, "a -> b is enumerable"); + assert.ok(!db.configurable, "a -> b is not configurable"); + + assert.ok(da.writable, "a is writable"); + assert.ok(da.enumerable, "a is enumerable"); + assert.ok(da.configurable, "a is configurable"); +}; + +if (require.main == module) + require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/traits/object-tests.js b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/object-tests.js new file mode 100644 index 0000000..6a94ff3 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/object-tests.js @@ -0,0 +1,321 @@ +/* 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"; + +var Trait = require("light-traits").Trait; +var utils = require("./utils"); +var Data = utils.Data; +var Method = utils.Method; +var Accessor = utils.Accessor; +var Required = utils.Required; +var Conflict = utils.Conflict; + +function method() {} + +exports.Assert = require("./assert").Assert; + +exports["test empty trait"] = function (assert) { + assert.equalTraits(Trait({}), {}); +}; + +exports["test simple trait"] = function (assert) { + var expected = { + a: Data(0, true, true, true), + b: Method(method, true, true, true) + }; + + assert.equalTraits(Trait({ a: 0, b: method }), expected); +}; + +exports["test simple trait with Trait.required property"] = function (assert) { + var actual = Trait({ a: Trait.required, b: 1 }); + var expected = { a: Required("a"), b: Data(1) }; + + assert.equalTraits(actual, expected); +}; + +exports["test ordering of trait properties is irrelevant"] = function (assert) { + var actual = Trait({ a: 0, b: 1, c: Trait.required }); + var expected = Trait({ b: 1, c: Trait.required, a: 0 }); + + assert.equalTraits(actual, expected); +}; + +exports["test trait with accessor property"] = function (assert) { + var record = { get a() {}, set a(v) {} }; + var get = Object.getOwnPropertyDescriptor(record, "a").get; + var set = Object.getOwnPropertyDescriptor(record, "a").set; + + assert.equalTraits(Trait(record), { a: Accessor(get, set) }); +}; + +exports["test simple composition"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1 }), Trait({ c: 2, d: method })); + var expected = { a: Data(0), b: Data(1), c: Data(2), d: Method(method) }; + + assert.equalTraits(actual, expected); +}; + +exports["test composition with conflict"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1 }), Trait({ a: 2, c: method })); + var expected = { a: Conflict("a"), b: Data(1), c: Method(method) }; + + assert.equalTraits(actual, expected); +}; + +exports["test composition of identical props does not cause conflict"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1 }), Trait({ a: 0, c: method })); + + assert.equalTraits(actual, { a: Data(0), b: Data(1), c: Method(method) }); +}; + +exports["test composition with identical Trait.required props"] = function (assert) { + var actual = Trait.compose(Trait({ a: Trait.required, b: 1 }), + Trait({ a: Trait.required, c: method })); + + assert.equalTraits(actual, { a: Required(), b: Data(1), c: Method(method) }); +}; + +exports["test composition satisfying a Trait.required prop"] = function (assert) { + var actual = Trait.compose(Trait({ a: Trait.required, b: 1 }), + Trait({ a: method })); + + assert.equalTraits(actual, { a: Method(method), b: Data(1) }); +}; + +exports["test compose is neutral wrt conflicts"] = function (assert) { + var actual = Trait.compose(Trait.compose(Trait({ a: 1 }), Trait({ a: 2 })), + Trait({ b: 0 })); + + assert.equalTraits(actual, { a: Conflict("a"), b: Data(0) }); +}; + +exports["test conflicting prop overrides Trait.required prop"] = function (assert) { + var actual = Trait.compose(Trait.compose(Trait({ a: 1 }), + Trait({ a: 2 })), + Trait({ a: Trait.required })); + + assert.equalTraits(actual, { a: Conflict("a") }); +}; + +exports["test compose is commutative"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1 }), Trait({ c: 2, d: method })); + var expected = Trait.compose(Trait({ c: 2, d: method }), + Trait({ a: 0, b: 1 })); + + assert.equalTraits(actual, expected); +}; + +exports["test compose is commutative, also for Trait.required/conflicting props"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1, c: 3, e: Trait.required }), + Trait({ c: 2, d: method })); + + var expected = Trait.compose(Trait({ c: 2, d: method }), + Trait({ a: 0, b: 1, c: 3, e: Trait.required })); + + assert.equalTraits(actual, expected); +}; + +exports["test compose is associative"] = function (assert) { + var actual = Trait.compose(Trait({ a: 0, b: 1, c: 3, d: Trait.required }), + Trait.compose(Trait({ c: 3, d: Trait.required }), + Trait({ c: 2, d: method, + e: "foo" }))); + + var expected = Trait.compose( + Trait.compose(Trait({ a: 0, b: 1, c: 3, d: Trait.required }), + Trait({ c: 3, d: Trait.required })), + Trait({ c: 2, d: method, e: "foo" })); + + assert.equalTraits(actual, expected); +}; + +exports["test diamond import of same prop does not generate conflict"] = function (assert) { + var actual = Trait.compose(Trait.compose(Trait({ b: 2 }), Trait({ a: 1 })), + Trait.compose(Trait({ c: 3 }), Trait({ a: 1 })), + Trait({ d: 4 })); + var expected = { a: Data(1), b: Data(2), c: Data(3), d: Data(4) }; + + assert.equalTraits(actual, expected); +}; + +exports["test resolve with empty resolutions has no effect"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: Trait.required, c: method }).resolve({}), + { a: Data(1), b: Required(), c: Method(method) }); +}; + +exports["test resolve: renaming"] = function (assert) { + var actual = Trait({ a: 1, b: Trait.required, c: method }); + + assert.equalTraits(actual.resolve({ a: "A", c: "C" }), + { A: Data(1), b: Required(), C: Method(method), + a: Required(), c: Required() }); +}; + +exports["test resolve: renaming to conflicting name causes conflict, order 1"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: "b" }), + { b: Conflict("b"), a: Required() }); +}; + +exports["test resolve: renaming to conflicting name causes conflict, order 2"] = function (assert) { + assert.equalTraits(Trait({ b: 2, a: 1 }).resolve({ a: "b" }), + { b: Conflict("b"), a: Required() }); +}; + +exports["test resolve: simple exclusion"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: undefined }), + { a: Required(), b: Data(2) }); +}; + +exports["test resolve: exclusion to empty trait"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: null, b: undefined }), + { a: Required(), b: Required() }); +}; + +exports["test resolve: exclusion and renaming of disjoint props"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: undefined, b: "c" }), + { a: Required(), c: Data(2), b: Required() }); +}; + +exports["test resolve: exclusion and renaming of overlapping props"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: undefined, b: "a" }), + { a: Data(2), b: Required() }); +}; + +exports["test resolve: renaming to a common alias causes conflict"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: "c", b: "c" }), + { c: Conflict("c"), a: Required(), b: Required() }); +}; + +exports["test resolve: renaming overrides Trait.required target"] = function (assert) { + assert.equalTraits(Trait({ a: Trait.required, b: 2 }).resolve({ b: "a" }), + { a: Data(2), b: Required() }); +}; + +exports["test resolve: renaming Trait.required properties has no effect"] = function (assert) { + assert.equalTraits(Trait({ a: 2, b: Trait.required }).resolve({ b: "a" }), + { a: Data(2), b: Required() }); +}; + +exports["test resolve: renaming of non-existent props has no effect"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: "c", d: "c" }), + { c: Data(1), b: Data(2), a: Required() }); +}; + +exports["test resolve: exclusion of non-existent props has no effect"] = function (assert) { + assert.equalTraits(Trait({ a: 1 }).resolve({ b: undefined }), { a: Data(1) }); +}; + +exports["test resolve is neutral w.r.t. Trait.required properties"] = function (assert) { + var actual = Trait({ a: Trait.required, b: Trait.required, c: "foo", d: 1 }); + var expected = { a: Required(), b: Required(), c: Data("foo"), d: Data(1) }; + assert.equalTraits(actual.resolve({ a: "c", b: undefined }), expected); +}; + +exports["test resolve supports swapping of property names, ordering 1"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ a: "b", b: "a" }), + { a: Data(2), b: Data(1) }); +}; + +exports["test resolve supports swapping of property names, ordering 2"] = function (assert) { + assert.equalTraits(Trait({ a: 1, b: 2 }).resolve({ b: "a", a: "b" }), + { a: Data(2), b: Data(1) }); +}; + +exports["test resolve supports swapping of property names, ordering 3"] = function (assert) { + assert.equalTraits(Trait({ b: 2, a: 1 }).resolve({ b: "a", a: "b" }), + { a: Data(2), b: Data(1) }); +}; + +exports["test resolve supports swapping of property names, ordering 4"] = function (assert) { + assert.equalTraits(Trait({ b: 2, a: 1 }).resolve({ a: "b", b: "a" }), + { a: Data(2), b: Data(1) }); +}; + +exports["test create simple"] = function (assert) { + var o1 = Trait({ + a: 1, + b: function () { + return this.a; + } + }).create(Object.prototype); + + assert.equal(Object.getPrototypeOf(o1), Object.prototype, "o1 prototype"); + assert.equal(1, o1.a, "o1.a"); + assert.equal(1, o1.b(), "o1.b()"); + assert.equal(Object.keys(o1).length, 2, "Object.keys(o1).length === 2"); +}; + +exports["test create with Array.prototype"] = function (assert) { + var o2 = Trait({}).create(Array.prototype); + assert.equal(Object.getPrototypeOf(o2), Array.prototype, "o2 prototype"); +}; + +exports["test exception for incomplete required properties"] = function (assert) { + assert.throws(function () { + Trait({ foo: Trait.required }).create(Object.prototype); + }, /Missing required property: `foo`/, "required prop error"); +}; + +exports["test exception for unresolved conflicts"] = function (assert) { + assert.throws(function () { + Trait.compose(Trait({ a: 0 }), Trait({ a: 1 })).create({}); + }, /Remaining conflicting property: `a`/, "conflicting prop error"); +}; + +exports["test verify that required properties are present but undefined"] = function (assert) { + var o4 = Object.create(Object.prototype, Trait({ foo: Trait.required })); + + assert.ok("foo" in o4, "required property present"); + assert.throws(function () { + o4.foo; + }, /Missing required property: `foo`/, "required prop error"); +}; + +exports["test verify that conflicting properties are present"] = function (assert) { + var o5 = Object.create(Object.prototype, Trait.compose(Trait({ a: 0 }), + Trait({ a: 1 }))); + + assert.ok("a" in o5, "conflicting property present"); + assert.throws(function () { + o5.a; + }, /Remaining conflicting property: `a`/, "conflicting prop access error"); +}; + +exports["test diamond with conflicts"] = function (assert) { + function makeT1(x) { + return Trait({ + m: function () { + return x + } + }) + }; + + function makeT2(x) { + return Trait.compose(Trait({ + t2: "foo" + }), makeT1(x)); + }; + + function makeT3(x) { + return Trait.compose(Trait({ + t3: "bar" + }), makeT1(x)); + }; + + var T4 = Trait.compose(makeT2(5), makeT3(5)); + + assert.throws(function () { + T4.create(Object.prototype); + }, /Remaining conflicting property: `m`/, "diamond prop conflict"); +}; + +exports["test providing requirements through proto"] = function (assert) { + var t = Trait({ required: Trait.required }).create({ required: "test" }); + assert.equal(t.required, "test", "property from proto is inherited"); +}; + +if (module == require.main) + require("test").run(exports); diff --git a/tools/addon-sdk-1.7/packages/api-utils/tests/traits/utils.js b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/utils.js new file mode 100644 index 0000000..8426af7 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/api-utils/tests/traits/utils.js @@ -0,0 +1,56 @@ +/* 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"; + +var ERR_CONFLICT = "Remaining conflicting property: "; +var ERR_REQUIRED = "Missing required property: "; + +exports.Data = function Data(value, enumerable, configurable, writable) { + return ({ + value: value, + enumerable: enumerable !== false, + configurable: configurable !== false, + writable: writable !== false + }); +}; + +exports.Method = function Method(method, enumerable, configurable, writable) { + return ({ + value: method, + enumerable: enumerable !== false, + configurable: configurable !== false, + writable: writable !== false + }); +}; + +exports.Accessor = function Accessor(get, set, enumerable, configurable) { + return ({ + get: get, + set: set, + enumerable: enumerable !== false, + configurable: configurable !== false + }); +}; + +exports.Required = function Required(name) { + function required() { throw new Error(ERR_REQUIRED + name) } + + return ({ + get: required, + set: required, + required: true + }); +}; + +exports.Conflict = function Conflict(name) { + function conflict() { throw new Error(ERR_CONFLICT + name) } + + return ({ + get: conflict, + set: conflict, + conflict: true + }); +}; + diff --git a/tools/addon-sdk-1.7/packages/test-harness/README.md b/tools/addon-sdk-1.7/packages/test-harness/README.md new file mode 100644 index 0000000..b22824b --- /dev/null +++ b/tools/addon-sdk-1.7/packages/test-harness/README.md @@ -0,0 +1,12 @@ +<!-- 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/. --> + +<span class="aside"> +For more information on testing in the Add-on SDK, see the +[Unit Testing](dev-guide/tutorials/unit-testing.html) +tutorial. +</span> + +This package contains a program that finds and runs tests. It is +automatically used whenever the `cfx test` command is executed. diff --git a/tools/addon-sdk-1.7/packages/test-harness/docs/harness.md b/tools/addon-sdk-1.7/packages/test-harness/docs/harness.md new file mode 100644 index 0000000..45218ec --- /dev/null +++ b/tools/addon-sdk-1.7/packages/test-harness/docs/harness.md @@ -0,0 +1,6 @@ +<!-- 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/. --> + +This module contains the bulk of the test harness setup and execution +implementation. diff --git a/tools/addon-sdk-1.7/packages/test-harness/docs/run-tests.md b/tools/addon-sdk-1.7/packages/test-harness/docs/run-tests.md new file mode 100644 index 0000000..155b809 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/test-harness/docs/run-tests.md @@ -0,0 +1,13 @@ +<!-- 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/. --> + +<span class="aside"> +For more information on testing in the Add-on SDK, see the +[Unit Testing](dev-guide/tutorials/unit-testing.html) +tutorial. +</span> + +This module contains the package's main program, which does a +bit of high-level setup and then delegates test finding and running to +the `harness` module. diff --git a/tools/addon-sdk-1.7/packages/test-harness/lib/harness.js b/tools/addon-sdk-1.7/packages/test-harness/lib/harness.js new file mode 100644 index 0000000..e1b1105 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/test-harness/lib/harness.js @@ -0,0 +1,324 @@ +/* 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("@loader") +const memory = require('api-utils/memory'); + +var cService = Cc['@mozilla.org/consoleservice;1'].getService() + .QueryInterface(Ci.nsIConsoleService); + +// Cuddlefish loader for the sandbox in which we load and +// execute tests. +var sandbox; + +// Function to call when we're done running tests. +var onDone; + +// Function to print text to a console, w/o CR at the end. +var print; + +// How many more times to run all tests. +var iterationsLeft; + +// Only tests in files whose names match this regexp filter will be run. +var filter; + +// Whether to report memory profiling information. +var profileMemory; + +// Whether we should stop as soon as a test reports a failure. +var stopOnError; + +// Combined information from all test runs. +var results = { + passed: 0, + failed: 0, + testRuns: [] +}; + +// JSON serialization of last memory usage stats; we keep it stringified +// so we don't actually change the memory usage stats (in terms of objects) +// of the JSRuntime we're profiling. +var lastMemoryUsage; + +function analyzeRawProfilingData(data) { + var graph = data.graph; + var shapes = {}; + + // Convert keys in the graph from strings to ints. + // TODO: Can we get rid of this ridiculousness? + var newGraph = {}; + for (id in graph) { + newGraph[parseInt(id)] = graph[id]; + } + graph = newGraph; + + var modules = 0; + var moduleIds = []; + var moduleObjs = {UNKNOWN: 0}; + for (let name in data.namedObjects) { + moduleObjs[name] = 0; + moduleIds[data.namedObjects[name]] = name; + modules++; + } + + var count = 0; + for (id in graph) { + var parent = graph[id].parent; + while (parent) { + if (parent in moduleIds) { + var name = moduleIds[parent]; + moduleObjs[name]++; + break; + } + if (!(parent in graph)) { + moduleObjs.UNKNOWN++; + break; + } + parent = graph[parent].parent; + } + count++; + } + + print("\nobject count is " + count + " in " + modules + " modules" + + " (" + data.totalObjectCount + " across entire JS runtime)\n"); + if (lastMemoryUsage) { + var last = JSON.parse(lastMemoryUsage); + var diff = { + moduleObjs: dictDiff(last.moduleObjs, moduleObjs), + totalObjectClasses: dictDiff(last.totalObjectClasses, + data.totalObjectClasses) + }; + + for (let name in diff.moduleObjs) + print(" " + diff.moduleObjs[name] + " in " + name + "\n"); + for (let name in diff.totalObjectClasses) + print(" " + diff.totalObjectClasses[name] + " instances of " + + name + "\n"); + } + lastMemoryUsage = JSON.stringify( + {moduleObjs: moduleObjs, + totalObjectClasses: data.totalObjectClasses} + ); +} + +function dictDiff(last, curr) { + var diff = {}; + + for (let name in last) { + var result = (curr[name] || 0) - last[name]; + if (result) + diff[name] = (result > 0 ? "+" : "") + result; + } + for (let name in curr) { + var result = curr[name] - (last[name] || 0); + if (result) + diff[name] = (result > 0 ? "+" : "") + result; + } + return diff; +} + +function reportMemoryUsage() { + memory.gc(); + + var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager); + var reporters = mgr.enumerateReporters(); + if (reporters.hasMoreElements()) + print("\n"); + while (reporters.hasMoreElements()) { + var reporter = reporters.getNext(); + reporter.QueryInterface(Ci.nsIMemoryReporter); + print(reporter.description + ": " + reporter.memoryUsed + "\n"); + } + + var weakrefs = [info.weakref.get() + for each (info in memory.getObjects())]; + weakrefs = [weakref for each (weakref in weakrefs) if (weakref)]; + print("Tracked memory objects in testing sandbox: " + + weakrefs.length + "\n"); +} + +var gWeakrefInfo; + +function showResults() { + memory.gc(); + + if (gWeakrefInfo) { + gWeakrefInfo.forEach( + function(info) { + var ref = info.weakref.get(); + if (ref !== null) { + var data = ref.__url__ ? ref.__url__ : ref; + var warning = data == "[object Object]" + ? "[object " + data.constructor.name + "(" + + [p for (p in data)].join(", ") + ")]" + : data; + console.warn("LEAK", warning, info.bin); + } + } + ); + } + + print("\n"); + var total = results.passed + results.failed; + print(results.passed + " of " + total + " tests passed.\n"); + onDone(results); +} + +function cleanup() { + try { + for (let name in sandbox.modules) + memory.track(sandbox.modules[name], + "module global scope: " + name); + memory.track(sandbox, "Cuddlefish Loader"); + + if (profileMemory) { + gWeakrefInfo = [{ weakref: info.weakref, bin: info.bin } + for each (info in memory.getObjects())]; + } + + sandbox.unload(); + + if (sandbox.globals.console.errorsLogged && !results.failed) { + results.failed++; + console.error("warnings and/or errors were logged."); + } + + if (consoleListener.errorsLogged && !results.failed) { + console.warn(consoleListener.errorsLogged + " " + + "warnings or errors were logged to the " + + "platform's nsIConsoleService, which could " + + "be of no consequence; however, they could also " + + "be indicative of aberrant behavior."); + } + + consoleListener.errorsLogged = 0; + sandbox = null; + + memory.gc(); + } catch (e) { + results.failed++; + console.error("unload.send() threw an exception."); + console.exception(e); + }; + + require("api-utils/timer").setTimeout(showResults, 1); +} + +function nextIteration(tests) { + if (tests) { + results.passed += tests.passed; + results.failed += tests.failed; + + if (profileMemory) + reportMemoryUsage(); + + let testRun = []; + for each (let test in tests.testRunSummary) { + let testCopy = {}; + for (let info in test) { + testCopy[info] = test[info]; + } + testRun.push(testCopy); + } + + results.testRuns.push(testRun); + iterationsLeft--; + } + + if (iterationsLeft && (!stopOnError || results.failed == 0)) { + let require = Loader.require.bind(sandbox, module.path); + require("api-utils/unit-test").findAndRunTests({ + testOutOfProcess: require('@packaging').enableE10s, + testInProcess: true, + stopOnError: stopOnError, + filter: filter, + onDone: nextIteration + }); + } + else { + require("api-utils/timer").setTimeout(cleanup, 0); + } +} + +var POINTLESS_ERRORS = [ + "Invalid chrome URI:", + "OpenGL LayerManager Initialized Succesfully." +]; + +var consoleListener = { + errorsLogged: 0, + observe: function(object) { + if (!(object instanceof Ci.nsIScriptError)) + return; + this.errorsLogged++; + var message = object.QueryInterface(Ci.nsIConsoleMessage).message; + var pointless = [err for each (err in POINTLESS_ERRORS) + if (message.indexOf(err) == 0)]; + if (pointless.length == 0 && message) + print("console: " + message + "\n"); + } +}; + +function TestRunnerConsole(base, options) { + this.__proto__ = { + errorsLogged: 0, + warn: function warn() { + this.errorsLogged++; + base.warn.apply(base, arguments); + }, + error: function error() { + this.errorsLogged++; + base.error.apply(base, arguments); + }, + info: function info(first) { + if (options.verbose) + base.info.apply(base, arguments); + else + if (first == "pass:") + print("."); + }, + __proto__: base + }; +} + +var runTests = exports.runTests = function runTests(options) { + iterationsLeft = options.iterations; + filter = options.filter; + profileMemory = options.profileMemory; + stopOnError = options.stopOnError; + onDone = options.onDone; + print = options.print; + + try { + cService.registerListener(consoleListener); + + var ptc = require("api-utils/plain-text-console"); + var url = require("api-utils/url"); + var system = require("api-utils/system"); + + print("Running tests on " + system.name + " " + system.version + + "/Gecko " + system.platformVersion + " (" + + system.id + ") under " + + system.platform + "/" + system.architecture + ".\n"); + + sandbox = Loader.new(require("@packaging")); + Object.defineProperty(sandbox.globals, 'console', { + value: new TestRunnerConsole(new ptc.PlainTextConsole(print), options) + }); + + nextIteration(); + } catch (e) { + print(require("api-utils/traceback").format(e) + "\n" + e + "\n"); + onDone({passed: 0, failed: 1}); + } +}; + +require("api-utils/unload").when(function() { + cService.unregisterListener(consoleListener); +}); diff --git a/tools/addon-sdk-1.7/packages/test-harness/lib/run-tests.js b/tools/addon-sdk-1.7/packages/test-harness/lib/run-tests.js new file mode 100644 index 0000000..4eb8570 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/test-harness/lib/run-tests.js @@ -0,0 +1,101 @@ +/* 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"; + +var obsvc = require("api-utils/observer-service"); +var system = require("api-utils/system"); +var options = require('@packaging'); +var {Cc,Ci} = require("chrome"); + +function runTests(iterations, filter, profileMemory, stopOnError, verbose, exit, print) { + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Ci.nsIWindowWatcher); + + let ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + let msg = 'Running tests...'; + let markup = '<?xml version="1.0"?><window xmlns="' + ns + + '" windowtype="test:runner"><label>' + msg + '</label></window>'; + let url = "data:application/vnd.mozilla.xul+xml," + escape(markup); + + + var window = ww.openWindow(null, url, "harness", "centerscreen", null); + + var harness = require("./harness"); + + function onDone(tests) { + window.close(); + if (tests.failed == 0) { + if (tests.passed === 0) + print("No tests were run\n"); + exit(0); + } else { + printFailedTests(tests, verbose, print); + exit(1); + } + }; + + // We have to wait for this window to be fully loaded *and* focused + // in order to avoid it to mess with our various window/focus tests. + // We are first waiting for our window to be fully loaded before ensuring + // that it will take the focus, and then we wait for it to be focused. + window.addEventListener("load", function onload() { + window.removeEventListener("load", onload, true); + + window.addEventListener("focus", function onfocus() { + window.removeEventListener("focus", onfocus, true); + // Finally, we have to run test on next cycle, otherwise XPCOM components + // are not correctly updated. + // For ex: nsIFocusManager.getFocusedElementForWindow may throw + // NS_ERROR_ILLEGAL_VALUE exception. + require("timer").setTimeout(function () { + harness.runTests({iterations: iterations, + filter: filter, + profileMemory: profileMemory, + stopOnError: stopOnError, + verbose: verbose, + print: print, + onDone: onDone}); + }, 0); + }, true); + window.focus(); + }, true); +} + +function printFailedTests(tests, verbose, print) { + if (!verbose) + return; + + let iterationNumber = 0; + let singleIteration = tests.testRuns.length == 1; + let padding = singleIteration ? "" : " "; + + print("\nThe following tests failed:\n"); + + for each (let testRun in tests.testRuns) { + iterationNumber++; + + if (!singleIteration) + print(" Iteration " + iterationNumber + ":\n"); + + for each (let test in testRun) { + if (test.failed > 0) { + print(padding + " " + test.name + ": " + test.errors +"\n"); + } + } + print("\n"); + } +} + +exports.main = function main() { + var testsStarted = false; + + if (!testsStarted) { + testsStarted = true; + runTests(options.iterations, options.filter, + options.profileMemory, options.stopOnError, options.verbose, + system.exit, + dump); + } +}; diff --git a/tools/addon-sdk-1.7/packages/test-harness/package.json b/tools/addon-sdk-1.7/packages/test-harness/package.json new file mode 100644 index 0000000..b44fb06 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/test-harness/package.json @@ -0,0 +1,8 @@ +{ + "name": "test-harness", + "description": "A harness for running Jetpack tests.", + "author": "Atul Varma (http://toolness.com/)", + "keywords": ["jetpack-low-level"], + "license": "MPL 2.0", + "dependencies": ["api-utils"] +} diff --git a/tools/addon-sdk-1.7/packages/test-harness/tests/test-packaging.js b/tools/addon-sdk-1.7/packages/test-harness/tests/test-packaging.js new file mode 100644 index 0000000..0628af2 --- /dev/null +++ b/tools/addon-sdk-1.7/packages/test-harness/tests/test-packaging.js @@ -0,0 +1,18 @@ +/* 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/. */ + +var url = require("url"); +var file = require("file"); +var {Cm,Ci} = require("chrome"); +var options = require("@packaging"); + +exports.testPackaging = function(test) { + test.assertEqual(options.main, + 'test-harness/run-tests', + "main program should be the test harness"); + + test.assertEqual(options.metadata['test-harness'].author, + 'Atul Varma (http://toolness.com/)', + "packaging metadata should be available"); +}; |